epicyon/theme.py

911 lines
33 KiB
Python
Raw Normal View History

2020-04-04 12:03:28 +00:00
__filename__ = "theme.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
2021-01-26 10:07:42 +00:00
__version__ = "1.2.0"
2020-04-04 12:03:28 +00:00
__maintainer__ = "Bob Mottram"
2021-09-10 16:14:50 +00:00
__email__ = "bob@libreserver.org"
2020-04-04 12:03:28 +00:00
__status__ = "Production"
2021-06-26 11:16:41 +00:00
__module_group__ = "Web Interface"
2019-11-23 13:04:11 +00:00
import os
2021-06-25 18:02:05 +00:00
from utils import isAccountDir
2019-11-23 13:04:11 +00:00
from utils import loadJson
from utils import saveJson
2020-11-21 11:54:29 +00:00
from utils import getImageExtensions
2021-05-29 11:04:03 +00:00
from utils import copytree
2021-07-13 21:59:53 +00:00
from utils import acctDir
from utils import dangerousSVG
2021-10-22 09:10:23 +00:00
from utils import localActorUrl
2020-05-28 21:30:40 +00:00
from shutil import copyfile
2021-05-28 21:39:34 +00:00
from shutil import make_archive
2021-05-29 11:04:03 +00:00
from shutil import unpack_archive
2021-05-30 11:36:20 +00:00
from shutil import rmtree
2020-11-15 11:01:05 +00:00
from content import dangerousCSS
2020-11-15 10:33:11 +00:00
2021-12-25 16:17:53 +00:00
def importTheme(base_dir: str, filename: str) -> bool:
2021-05-29 11:04:03 +00:00
"""Imports a theme
"""
if not os.path.isfile(filename):
return False
2021-12-25 16:17:53 +00:00
tempThemeDir = base_dir + '/imports/files'
2021-05-30 11:39:09 +00:00
if os.path.isdir(tempThemeDir):
2021-10-29 18:48:15 +00:00
rmtree(tempThemeDir, ignore_errors=False, onerror=None)
2021-05-30 11:39:09 +00:00
os.mkdir(tempThemeDir)
2021-05-29 11:04:03 +00:00
unpack_archive(filename, tempThemeDir, 'zip')
essentialThemeFiles = ('name.txt', 'theme.json')
for themeFile in essentialThemeFiles:
if not os.path.isfile(tempThemeDir + '/' + themeFile):
print('WARN: ' + themeFile +
' missing from imported theme')
return False
newThemeName = None
with open(tempThemeDir + '/name.txt', 'r') as fp:
newThemeName = fp.read().replace('\n', '').replace('\r', '')
2021-05-29 11:04:03 +00:00
if len(newThemeName) > 20:
print('WARN: Imported theme name is too long')
return False
if len(newThemeName) < 2:
print('WARN: Imported theme name is too short')
return False
newThemeName = newThemeName.lower()
forbiddenChars = (
' ', ';', '/', '\\', '?', '!', '#', '@',
':', '%', '&', '"', '+', '<', '>', '$'
)
for ch in forbiddenChars:
if ch in newThemeName:
print('WARN: theme name contains forbidden character')
return False
if not newThemeName:
return False
# if the theme name in the default themes list?
2021-12-25 16:17:53 +00:00
defaultThemesFilename = base_dir + '/defaultthemes.txt'
if os.path.isfile(defaultThemesFilename):
if newThemeName.title() + '\n' in open(defaultThemesFilename).read():
newThemeName = newThemeName + '2'
2021-12-25 16:17:53 +00:00
themeDir = base_dir + '/theme/' + newThemeName
2021-05-29 11:04:03 +00:00
if not os.path.isdir(themeDir):
os.mkdir(themeDir)
copytree(tempThemeDir, themeDir)
2021-05-30 11:34:30 +00:00
if os.path.isdir(tempThemeDir):
2021-10-29 18:48:15 +00:00
rmtree(tempThemeDir, ignore_errors=False, onerror=None)
2021-09-13 18:50:02 +00:00
if scanThemesForScripts(themeDir):
2021-10-29 18:48:15 +00:00
rmtree(themeDir, ignore_errors=False, onerror=None)
2021-09-13 18:50:02 +00:00
return False
2021-05-29 11:04:03 +00:00
return os.path.isfile(themeDir + '/theme.json')
2021-12-25 16:17:53 +00:00
def exportTheme(base_dir: str, theme: str) -> bool:
2021-05-28 21:39:34 +00:00
"""Exports a theme as a zip file
"""
2021-12-25 16:17:53 +00:00
themeDir = base_dir + '/theme/' + theme
2021-05-28 22:00:46 +00:00
if not os.path.isfile(themeDir + '/theme.json'):
2021-05-28 21:39:34 +00:00
return False
2021-12-25 16:17:53 +00:00
if not os.path.isdir(base_dir + '/exports'):
os.mkdir(base_dir + '/exports')
exportFilename = base_dir + '/exports/' + theme + '.zip'
2021-05-28 21:39:34 +00:00
if os.path.isfile(exportFilename):
try:
os.remove(exportFilename)
2021-11-25 18:42:38 +00:00
except OSError:
2021-10-29 18:48:15 +00:00
print('EX: exportTheme unable to delete ' + str(exportFilename))
2021-05-28 21:39:34 +00:00
try:
2021-12-25 16:17:53 +00:00
make_archive(base_dir + '/exports/' + theme, 'zip', themeDir)
2021-05-28 21:39:34 +00:00
except BaseException:
2021-10-29 18:48:15 +00:00
print('EX: exportTheme unable to archive ' +
2021-12-25 16:17:53 +00:00
base_dir + '/exports/' + str(theme))
2021-05-28 21:39:34 +00:00
pass
return os.path.isfile(exportFilename)
def _getThemeFiles() -> []:
2021-02-07 15:22:06 +00:00
"""Gets the list of theme style sheets
"""
2020-07-11 09:30:07 +00:00
return ('epicyon.css', 'login.css', 'follow.css',
2020-07-25 22:12:59 +00:00
'suspended.css', 'calendar.css', 'blog.css',
2021-02-25 12:42:05 +00:00
'options.css', 'search.css', 'links.css',
2021-10-19 20:08:24 +00:00
'welcome.css', 'graph.css')
2020-07-11 09:30:07 +00:00
2021-12-25 23:35:50 +00:00
def isNewsThemeName(base_dir: str, theme_name: str) -> bool:
2021-02-27 11:44:50 +00:00
"""Returns true if the given theme is a news instance
"""
2021-12-25 23:35:50 +00:00
themeDir = base_dir + '/theme/' + theme_name
2021-02-27 11:44:50 +00:00
if os.path.isfile(themeDir + '/is_news_instance'):
return True
return False
2021-12-25 16:17:53 +00:00
def getThemesList(base_dir: str) -> []:
2020-05-28 09:11:21 +00:00
"""Returns the list of available themes
Note that these should be capitalized, since they're
also used to create the web interface dropdown list
and to lookup function names
"""
2020-11-14 15:28:29 +00:00
themes = []
2021-12-25 16:17:53 +00:00
for subdir, dirs, files in os.walk(base_dir + '/theme'):
2021-12-25 23:35:50 +00:00
for theme_name in dirs:
if '~' not in theme_name and \
theme_name != 'icons' and theme_name != 'fonts':
themes.append(theme_name.title())
2020-11-14 15:37:20 +00:00
break
2020-11-16 15:11:11 +00:00
themes.sort()
2020-11-14 15:28:29 +00:00
print('Themes available: ' + str(themes))
return themes
2020-05-28 09:11:21 +00:00
2021-12-25 23:35:50 +00:00
def _copyThemeHelpFiles(base_dir: str, theme_name: str,
2021-12-25 23:03:28 +00:00
system_language: str) -> None:
"""Copies any theme specific help files from the welcome subdirectory
"""
2021-12-25 23:03:28 +00:00
if not system_language:
system_language = 'en'
2021-12-25 23:35:50 +00:00
themeDir = base_dir + '/theme/' + theme_name + '/welcome'
if not os.path.isdir(themeDir):
2021-12-25 16:17:53 +00:00
themeDir = base_dir + '/defaultwelcome'
for subdir, dirs, files in os.walk(themeDir):
for helpMarkdownFile in files:
2021-12-25 23:03:28 +00:00
if not helpMarkdownFile.endswith('_' + system_language + '.md'):
2021-02-27 14:39:35 +00:00
continue
destHelpMarkdownFile = \
2021-12-25 23:03:28 +00:00
helpMarkdownFile.replace('_' + system_language + '.md', '.md')
2021-02-27 14:44:47 +00:00
if destHelpMarkdownFile == 'profile.md' or \
destHelpMarkdownFile == 'final.md':
destHelpMarkdownFile = 'welcome_' + destHelpMarkdownFile
2021-12-25 16:17:53 +00:00
if os.path.isdir(base_dir + '/accounts'):
2021-03-03 13:39:39 +00:00
copyfile(themeDir + '/' + helpMarkdownFile,
2021-12-25 16:17:53 +00:00
base_dir + '/accounts/' + destHelpMarkdownFile)
break
2021-12-25 16:17:53 +00:00
def _setThemeInConfig(base_dir: str, name: str) -> bool:
2021-02-07 15:22:06 +00:00
"""Sets the theme with the given name within config.json
"""
2021-12-25 16:17:53 +00:00
configFilename = base_dir + '/config.json'
2019-11-23 13:04:11 +00:00
if not os.path.isfile(configFilename):
return False
2020-04-04 12:03:28 +00:00
configJson = loadJson(configFilename, 0)
2019-11-23 13:04:11 +00:00
if not configJson:
return False
2020-04-04 12:03:28 +00:00
configJson['theme'] = name
return saveJson(configJson, configFilename)
2019-11-23 13:04:11 +00:00
2021-12-25 16:17:53 +00:00
def _setNewswirePublishAsIcon(base_dir: str, useIcon: bool) -> bool:
"""Shows the newswire publish action as an icon or a button
"""
2021-12-25 16:17:53 +00:00
configFilename = base_dir + '/config.json'
if not os.path.isfile(configFilename):
return False
configJson = loadJson(configFilename, 0)
if not configJson:
return False
2021-12-25 19:34:20 +00:00
configJson['show_publish_as_icon'] = useIcon
return saveJson(configJson, configFilename)
2021-12-25 16:17:53 +00:00
def _setIconsAsButtons(base_dir: str, useButtons: bool) -> bool:
2020-10-25 20:38:01 +00:00
"""Whether to show icons in the header (inbox, outbox, etc)
as buttons
"""
2021-12-25 16:17:53 +00:00
configFilename = base_dir + '/config.json'
2020-10-25 20:38:01 +00:00
if not os.path.isfile(configFilename):
return False
configJson = loadJson(configFilename, 0)
if not configJson:
return False
2021-12-25 19:19:14 +00:00
configJson['icons_as_buttons'] = useButtons
2020-10-25 20:38:01 +00:00
return saveJson(configJson, configFilename)
2021-12-25 16:17:53 +00:00
def _setRssIconAtTop(base_dir: str, atTop: bool) -> bool:
"""Whether to show RSS icon at the top of the timeline
"""
2021-12-25 16:17:53 +00:00
configFilename = base_dir + '/config.json'
if not os.path.isfile(configFilename):
return False
configJson = loadJson(configFilename, 0)
if not configJson:
return False
2021-12-25 19:09:03 +00:00
configJson['rss_icon_at_top'] = atTop
return saveJson(configJson, configFilename)
2020-10-26 21:33:40 +00:00
2021-12-25 16:17:53 +00:00
def _setPublishButtonAtTop(base_dir: str, atTop: bool) -> bool:
2020-10-26 21:33:40 +00:00
"""Whether to show the publish button above the title image
in the newswire column
2020-10-26 21:32:08 +00:00
"""
2021-12-25 16:17:53 +00:00
configFilename = base_dir + '/config.json'
2020-10-26 21:32:08 +00:00
if not os.path.isfile(configFilename):
return False
configJson = loadJson(configFilename, 0)
if not configJson:
return False
2021-12-25 19:00:00 +00:00
configJson['publish_button_at_top'] = atTop
2020-10-26 21:32:08 +00:00
return saveJson(configJson, configFilename)
2021-12-25 16:17:53 +00:00
def _setFullWidthTimelineButtonHeader(base_dir: str, fullWidth: bool) -> bool:
"""Shows the timeline button header containing inbox, outbox,
calendar, etc as full width
"""
2021-12-25 16:17:53 +00:00
configFilename = base_dir + '/config.json'
if not os.path.isfile(configFilename):
return False
configJson = loadJson(configFilename, 0)
if not configJson:
return False
2021-12-25 19:31:24 +00:00
configJson['full_width_tl_button_header'] = fullWidth
return saveJson(configJson, configFilename)
2021-12-25 16:17:53 +00:00
def getTheme(base_dir: str) -> str:
2021-02-07 15:22:06 +00:00
"""Gets the current theme name from config.json
"""
2021-12-25 16:17:53 +00:00
configFilename = base_dir + '/config.json'
2020-05-27 13:05:23 +00:00
if os.path.isfile(configFilename):
configJson = loadJson(configFilename, 0)
if configJson:
if configJson.get('theme'):
return configJson['theme']
2020-05-26 20:17:16 +00:00
return 'default'
2021-12-25 16:17:53 +00:00
def _removeTheme(base_dir: str):
2021-02-07 15:22:06 +00:00
"""Removes the current theme style sheets
"""
themeFiles = _getThemeFiles()
2019-11-23 13:04:11 +00:00
for filename in themeFiles:
2021-12-25 16:17:53 +00:00
if not os.path.isfile(base_dir + '/' + filename):
2021-10-29 18:48:15 +00:00
continue
try:
2021-12-25 16:17:53 +00:00
os.remove(base_dir + '/' + filename)
2021-11-25 18:42:38 +00:00
except OSError:
2021-10-29 18:48:15 +00:00
print('EX: _removeTheme unable to delete ' +
2021-12-25 16:17:53 +00:00
base_dir + '/' + filename)
2020-04-04 12:03:28 +00:00
2019-11-23 13:04:11 +00:00
2020-04-04 12:03:28 +00:00
def setCSSparam(css: str, param: str, value: str) -> str:
2019-11-23 13:04:11 +00:00
"""Sets a CSS parameter to a given value
"""
# is this just a simple string replacement?
if ';' in param:
2020-04-04 12:03:28 +00:00
return css.replace(param, value)
2019-11-23 13:04:11 +00:00
# color replacement
if param.startswith('rgba('):
2020-04-04 12:03:28 +00:00
return css.replace(param, value)
2019-11-23 13:04:11 +00:00
# if the parameter begins with * then don't prepend --
2020-09-14 12:58:24 +00:00
onceOnly = False
2019-11-23 13:04:11 +00:00
if param.startswith('*'):
2020-09-14 12:52:22 +00:00
if param.startswith('**'):
2020-09-14 12:58:24 +00:00
onceOnly = True
searchStr = param.replace('**', '') + ':'
2020-09-14 12:52:22 +00:00
else:
searchStr = param.replace('*', '') + ':'
2019-11-23 13:04:11 +00:00
else:
2020-04-04 12:03:28 +00:00
searchStr = '--' + param + ':'
2019-11-23 13:04:11 +00:00
if searchStr not in css:
return css
2020-09-14 13:05:12 +00:00
if onceOnly:
s = css.split(searchStr, 1)
else:
s = css.split(searchStr)
2020-04-04 12:03:28 +00:00
newcss = ''
2019-11-23 13:04:11 +00:00
for sectionStr in s:
2020-11-16 20:13:48 +00:00
# handle font-family which is a variable
nextSection = sectionStr
if ';' in nextSection:
nextSection = nextSection.split(';')[0] + ';'
if searchStr == 'font-family:' and "var(--" in nextSection:
newcss += searchStr + ' ' + sectionStr
continue
2019-11-23 13:04:11 +00:00
if not newcss:
if sectionStr:
2020-04-04 12:03:28 +00:00
newcss = sectionStr
2019-11-23 13:04:11 +00:00
else:
2020-04-04 12:03:28 +00:00
newcss = ' '
2019-11-23 13:04:11 +00:00
else:
if ';' in sectionStr:
2020-04-04 12:03:28 +00:00
newcss += \
searchStr + ' ' + value + ';' + sectionStr.split(';', 1)[1]
2019-11-23 13:04:11 +00:00
else:
2020-04-04 12:03:28 +00:00
newcss += searchStr + ' ' + sectionStr
2019-11-23 13:04:11 +00:00
return newcss.strip()
2020-03-22 21:16:02 +00:00
2020-04-04 12:03:28 +00:00
2021-12-25 16:17:53 +00:00
def _setThemeFromDict(base_dir: str, name: str,
themeParams: {}, bgParams: {},
2021-12-25 18:54:50 +00:00
allow_local_network_access: bool) -> None:
2019-11-23 13:04:11 +00:00
"""Uses a dictionary to set a theme
"""
2020-07-10 18:08:45 +00:00
if name:
2021-12-25 16:17:53 +00:00
_setThemeInConfig(base_dir, name)
themeFiles = _getThemeFiles()
2019-11-23 13:04:11 +00:00
for filename in themeFiles:
2020-11-14 13:18:11 +00:00
# check for custom css within the theme directory
2021-12-25 16:17:53 +00:00
templateFilename = base_dir + '/theme/' + name + '/epicyon-' + filename
2020-04-04 12:03:28 +00:00
if filename == 'epicyon.css':
2020-11-14 13:18:11 +00:00
templateFilename = \
2021-12-25 16:17:53 +00:00
base_dir + '/theme/' + name + '/epicyon-profile.css'
2020-11-14 13:18:11 +00:00
2020-11-15 11:07:12 +00:00
# Ensure that any custom CSS is mostly harmless.
# If not then just use the defaults
2021-12-25 18:54:50 +00:00
if dangerousCSS(templateFilename, allow_local_network_access) or \
2020-11-15 10:33:11 +00:00
not os.path.isfile(templateFilename):
2020-11-14 13:18:11 +00:00
# use default css
2021-12-25 16:17:53 +00:00
templateFilename = base_dir + '/epicyon-' + filename
2020-11-14 13:18:11 +00:00
if filename == 'epicyon.css':
2021-12-25 16:17:53 +00:00
templateFilename = base_dir + '/epicyon-profile.css'
2020-11-14 13:18:11 +00:00
2019-11-23 13:04:11 +00:00
if not os.path.isfile(templateFilename):
continue
2020-11-14 13:18:11 +00:00
with open(templateFilename, 'r') as cssfile:
css = cssfile.read()
2020-04-04 12:03:28 +00:00
for paramName, paramValue in themeParams.items():
2020-11-14 18:08:46 +00:00
if paramName == 'newswire-publish-icon':
2020-11-14 19:21:11 +00:00
if paramValue.lower() == 'true':
2021-12-25 16:17:53 +00:00
_setNewswirePublishAsIcon(base_dir, True)
2020-11-14 19:21:11 +00:00
else:
2021-12-25 16:17:53 +00:00
_setNewswirePublishAsIcon(base_dir, False)
2020-11-14 18:08:46 +00:00
continue
elif paramName == 'full-width-timeline-buttons':
2020-11-14 19:21:11 +00:00
if paramValue.lower() == 'true':
2021-12-25 16:17:53 +00:00
_setFullWidthTimelineButtonHeader(base_dir, True)
2020-11-14 19:21:11 +00:00
else:
2021-12-25 16:17:53 +00:00
_setFullWidthTimelineButtonHeader(base_dir, False)
2020-11-14 18:08:46 +00:00
continue
elif paramName == 'icons-as-buttons':
2020-11-14 19:21:11 +00:00
if paramValue.lower() == 'true':
2021-12-25 16:17:53 +00:00
_setIconsAsButtons(base_dir, True)
2020-11-14 19:21:11 +00:00
else:
2021-12-25 16:17:53 +00:00
_setIconsAsButtons(base_dir, False)
2020-11-14 18:08:46 +00:00
continue
elif paramName == 'rss-icon-at-top':
2020-11-14 19:21:11 +00:00
if paramValue.lower() == 'true':
2021-12-25 16:17:53 +00:00
_setRssIconAtTop(base_dir, True)
2020-11-14 19:21:11 +00:00
else:
2021-12-25 16:17:53 +00:00
_setRssIconAtTop(base_dir, False)
2020-11-14 18:08:46 +00:00
continue
elif paramName == 'publish-button-at-top':
2020-11-14 19:21:11 +00:00
if paramValue.lower() == 'true':
2021-12-25 16:17:53 +00:00
_setPublishButtonAtTop(base_dir, True)
2020-11-14 19:21:11 +00:00
else:
2021-12-25 16:17:53 +00:00
_setPublishButtonAtTop(base_dir, False)
2020-11-14 18:08:46 +00:00
continue
2020-04-04 12:03:28 +00:00
css = setCSSparam(css, paramName, paramValue)
2021-12-25 16:17:53 +00:00
filename = base_dir + '/' + filename
with open(filename, 'w+') as cssfile:
cssfile.write(css)
2019-11-23 13:04:11 +00:00
2021-07-05 16:46:32 +00:00
screenName = (
'login', 'follow', 'options', 'search', 'welcome'
)
for s in screenName:
if bgParams.get(s):
2021-12-25 16:17:53 +00:00
_setBackgroundFormat(base_dir, name, s, bgParams[s])
2020-04-04 12:03:28 +00:00
2021-12-25 16:17:53 +00:00
def _setBackgroundFormat(base_dir: str, name: str,
backgroundType: str, extension: str) -> None:
2020-07-25 16:25:04 +00:00
"""Sets the background file extension
"""
2020-07-25 19:07:06 +00:00
if extension == 'jpg':
2020-07-25 16:25:04 +00:00
return
2021-12-25 16:17:53 +00:00
cssFilename = base_dir + '/' + backgroundType + '.css'
if not os.path.isfile(cssFilename):
return
with open(cssFilename, 'r') as cssfile:
css = cssfile.read()
css = css.replace('background.jpg', 'background.' + extension)
with open(cssFilename, 'w+') as cssfile2:
cssfile2.write(css)
2020-07-25 16:25:04 +00:00
2021-12-25 16:17:53 +00:00
def enableGrayscale(base_dir: str) -> None:
2020-07-11 09:22:06 +00:00
"""Enables grayscale for the current theme
"""
themeFiles = _getThemeFiles()
for filename in themeFiles:
2021-12-25 16:17:53 +00:00
templateFilename = base_dir + '/' + filename
if not os.path.isfile(templateFilename):
continue
with open(templateFilename, 'r') as cssfile:
css = cssfile.read()
2020-07-11 09:22:06 +00:00
if 'grayscale' not in css:
css = \
css.replace('body, html {',
'body, html {\n filter: grayscale(100%);')
2021-12-25 16:17:53 +00:00
filename = base_dir + '/' + filename
with open(filename, 'w+') as cssfile:
cssfile.write(css)
2021-12-25 16:17:53 +00:00
grayscaleFilename = base_dir + '/accounts/.grayscale'
2020-07-11 09:22:06 +00:00
if not os.path.isfile(grayscaleFilename):
with open(grayscaleFilename, 'w+') as grayfile:
grayfile.write(' ')
2020-07-11 09:22:06 +00:00
2021-12-25 16:17:53 +00:00
def disableGrayscale(base_dir: str) -> None:
2020-07-11 09:22:06 +00:00
"""Disables grayscale for the current theme
"""
themeFiles = _getThemeFiles()
2020-07-11 09:22:06 +00:00
for filename in themeFiles:
2021-12-25 16:17:53 +00:00
templateFilename = base_dir + '/' + filename
2020-07-11 09:22:06 +00:00
if not os.path.isfile(templateFilename):
continue
with open(templateFilename, 'r') as cssfile:
css = cssfile.read()
2020-07-11 09:22:06 +00:00
if 'grayscale' in css:
css = \
css.replace('\n filter: grayscale(100%);', '')
2021-12-25 16:17:53 +00:00
filename = base_dir + '/' + filename
with open(filename, 'w+') as cssfile:
cssfile.write(css)
2021-12-25 16:17:53 +00:00
grayscaleFilename = base_dir + '/accounts/.grayscale'
2020-07-11 09:22:06 +00:00
if os.path.isfile(grayscaleFilename):
try:
os.remove(grayscaleFilename)
2021-11-25 18:42:38 +00:00
except OSError:
2021-10-29 18:48:15 +00:00
print('EX: disableGrayscale unable to delete ' +
grayscaleFilename)
2021-12-25 16:17:53 +00:00
def _setCustomFont(base_dir: str):
2020-05-26 20:17:16 +00:00
"""Uses a dictionary to set a theme
"""
customFontExt = None
customFontType = None
fontExtension = {
'woff': 'woff',
'woff2': 'woff2',
'otf': 'opentype',
'ttf': 'truetype'
}
for ext, extType in fontExtension.items():
2021-12-25 16:17:53 +00:00
filename = base_dir + '/fonts/custom.' + ext
2020-05-26 20:17:16 +00:00
if os.path.isfile(filename):
customFontExt = ext
customFontType = extType
if not customFontExt:
return
themeFiles = _getThemeFiles()
2020-05-26 20:17:16 +00:00
for filename in themeFiles:
2021-12-25 16:17:53 +00:00
templateFilename = base_dir + '/' + filename
2020-05-26 20:17:16 +00:00
if not os.path.isfile(templateFilename):
continue
with open(templateFilename, 'r') as cssfile:
css = cssfile.read()
2020-05-26 20:17:16 +00:00
css = \
setCSSparam(css, "*src",
"url('./fonts/custom." +
customFontExt +
"') format('" +
customFontType + "')")
css = setCSSparam(css, "*font-family", "'CustomFont'")
2021-12-25 16:17:53 +00:00
filename = base_dir + '/' + filename
with open(filename, 'w+') as cssfile:
cssfile.write(css)
2020-05-26 20:17:16 +00:00
2021-12-25 23:35:50 +00:00
def setThemeFromDesigner(base_dir: str, theme_name: str, domain: str,
themeParams: {},
2021-12-25 18:54:50 +00:00
allow_local_network_access: bool,
2021-12-25 23:03:28 +00:00
system_language: str):
2021-12-25 16:17:53 +00:00
customThemeFilename = base_dir + '/accounts/theme.json'
2021-12-05 11:03:25 +00:00
saveJson(themeParams, customThemeFilename)
2021-12-25 23:35:50 +00:00
setTheme(base_dir, theme_name, domain,
2021-12-25 23:03:28 +00:00
allow_local_network_access, system_language)
2021-12-25 23:35:50 +00:00
def resetThemeDesignerSettings(base_dir: str, theme_name: str, domain: str,
2021-12-25 18:54:50 +00:00
allow_local_network_access: bool,
2021-12-25 23:03:28 +00:00
system_language: str) -> None:
"""Resets the theme designer settings
"""
2021-12-25 16:17:53 +00:00
customVariablesFile = base_dir + '/accounts/theme.json'
if os.path.isfile(customVariablesFile):
try:
os.remove(customVariablesFile)
except OSError:
print('EX: unable to remove theme designer settings on reset')
2020-11-14 17:34:11 +00:00
2021-12-25 23:35:50 +00:00
def _readVariablesFile(base_dir: str, theme_name: str,
variablesFile: str,
2021-12-25 18:54:50 +00:00
allow_local_network_access: bool) -> None:
"""Reads variables from a file in the theme directory
"""
themeParams = loadJson(variablesFile, 0)
if not themeParams:
return
2021-12-05 11:03:25 +00:00
# set custom theme parameters
2021-12-25 16:17:53 +00:00
customVariablesFile = base_dir + '/accounts/theme.json'
2021-12-05 11:03:25 +00:00
if os.path.isfile(customVariablesFile):
customThemeParams = loadJson(customVariablesFile, 0)
if customThemeParams:
for variableName, value in customThemeParams.items():
themeParams[variableName] = value
bgParams = {
"login": "jpg",
"follow": "jpg",
"options": "jpg",
"search": "jpg"
}
2021-12-25 23:35:50 +00:00
_setThemeFromDict(base_dir, theme_name, themeParams, bgParams,
2021-12-25 18:54:50 +00:00
allow_local_network_access)
2021-12-25 18:54:50 +00:00
def _setThemeDefault(base_dir: str, allow_local_network_access: bool):
2020-07-26 11:45:09 +00:00
name = 'default'
2021-12-25 16:17:53 +00:00
_removeTheme(base_dir)
_setThemeInConfig(base_dir, name)
bgParams = {
"login": "jpg",
"follow": "jpg",
"options": "jpg",
"search": "jpg"
}
2020-08-31 11:51:31 +00:00
themeParams = {
2020-11-14 18:08:46 +00:00
"newswire-publish-icon": True,
"full-width-timeline-buttons": False,
"icons-as-buttons": False,
"rss-icon-at-top": True,
"publish-button-at-top": False,
2020-10-28 22:32:13 +00:00
"banner-height": "20vh",
2020-11-07 20:14:46 +00:00
"banner-height-mobile": "10vh",
"search-banner-height-mobile": "15vh"
2020-08-31 11:51:31 +00:00
}
2021-12-25 16:17:53 +00:00
_setThemeFromDict(base_dir, name, themeParams, bgParams,
2021-12-25 18:54:50 +00:00
allow_local_network_access)
2020-05-29 10:00:05 +00:00
2021-12-25 23:35:50 +00:00
def _setThemeFonts(base_dir: str, theme_name: str) -> None:
2020-11-15 09:55:49 +00:00
"""Adds custom theme fonts
"""
2021-12-25 23:35:50 +00:00
theme_name_lower = theme_name.lower()
2021-12-25 16:17:53 +00:00
fontsDir = base_dir + '/fonts'
2020-11-15 09:55:49 +00:00
themeFontsDir = \
2021-12-25 23:35:50 +00:00
base_dir + '/theme/' + theme_name_lower + '/fonts'
2020-11-15 09:55:49 +00:00
if not os.path.isdir(themeFontsDir):
return
for subdir, dirs, files in os.walk(themeFontsDir):
for filename in files:
if filename.endswith('.woff2') or \
filename.endswith('.woff') or \
filename.endswith('.ttf') or \
filename.endswith('.otf'):
destFilename = fontsDir + '/' + filename
if os.path.isfile(destFilename):
# font already exists in the destination location
continue
copyfile(themeFontsDir + '/' + filename,
destFilename)
break
2021-12-25 16:17:53 +00:00
def getTextModeBanner(base_dir: str) -> str:
2021-02-05 19:50:09 +00:00
"""Returns the banner used for shell browsers, like Lynx
"""
2021-12-25 23:09:49 +00:00
text_mode_bannerFilename = base_dir + '/accounts/banner.txt'
if os.path.isfile(text_mode_bannerFilename):
with open(text_mode_bannerFilename, 'r') as fp:
bannerStr = fp.read()
if bannerStr:
return bannerStr.replace('\n', '<br>')
2021-02-05 19:50:09 +00:00
return None
2021-12-25 16:17:53 +00:00
def getTextModeLogo(base_dir: str) -> str:
2021-02-06 14:30:18 +00:00
"""Returns the login screen logo used for shell browsers, like Lynx
"""
2021-12-25 16:17:53 +00:00
textModeLogoFilename = base_dir + '/accounts/logo.txt'
2021-02-06 14:30:18 +00:00
if not os.path.isfile(textModeLogoFilename):
2021-12-25 16:17:53 +00:00
textModeLogoFilename = base_dir + '/img/logo.txt'
2021-02-06 14:30:18 +00:00
with open(textModeLogoFilename, 'r') as fp:
logoStr = fp.read()
if logoStr:
return logoStr.replace('\n', '<br>')
2021-02-06 14:30:18 +00:00
return None
2021-12-25 16:17:53 +00:00
def _setTextModeTheme(base_dir: str, name: str) -> None:
2021-02-07 15:32:15 +00:00
# set the text mode logo which appears on the login screen
# in browsers such as Lynx
textModeLogoFilename = \
2021-12-25 16:17:53 +00:00
base_dir + '/theme/' + name + '/logo.txt'
if os.path.isfile(textModeLogoFilename):
try:
copyfile(textModeLogoFilename,
2021-12-25 16:17:53 +00:00
base_dir + '/accounts/logo.txt')
2021-11-25 22:22:54 +00:00
except OSError:
2021-10-29 18:48:15 +00:00
print('EX: _setTextModeTheme unable to copy ' +
textModeLogoFilename + ' ' +
2021-12-25 16:17:53 +00:00
base_dir + '/accounts/logo.txt')
else:
try:
2021-12-25 16:17:53 +00:00
copyfile(base_dir + '/img/logo.txt',
base_dir + '/accounts/logo.txt')
2021-11-25 22:22:54 +00:00
except OSError:
2021-10-29 18:48:15 +00:00
print('EX: _setTextModeTheme unable to copy ' +
2021-12-25 16:17:53 +00:00
base_dir + '/img/logo.txt ' +
base_dir + '/accounts/logo.txt')
2021-02-07 15:32:15 +00:00
# set the text mode banner which appears in browsers such as Lynx
2021-12-25 23:09:49 +00:00
text_mode_bannerFilename = \
2021-12-25 16:17:53 +00:00
base_dir + '/theme/' + name + '/banner.txt'
if os.path.isfile(base_dir + '/accounts/banner.txt'):
try:
2021-12-25 16:17:53 +00:00
os.remove(base_dir + '/accounts/banner.txt')
2021-11-25 18:42:38 +00:00
except OSError:
2021-10-29 18:48:15 +00:00
print('EX: _setTextModeTheme unable to delete ' +
2021-12-25 16:17:53 +00:00
base_dir + '/accounts/banner.txt')
2021-12-25 23:09:49 +00:00
if os.path.isfile(text_mode_bannerFilename):
try:
2021-12-25 23:09:49 +00:00
copyfile(text_mode_bannerFilename,
2021-12-25 16:17:53 +00:00
base_dir + '/accounts/banner.txt')
2021-11-25 18:42:38 +00:00
except OSError:
2021-10-29 18:48:15 +00:00
print('EX: _setTextModeTheme unable to copy ' +
2021-12-25 23:09:49 +00:00
text_mode_bannerFilename + ' ' +
2021-12-25 16:17:53 +00:00
base_dir + '/accounts/banner.txt')
2021-02-07 15:32:15 +00:00
2021-12-25 16:17:53 +00:00
def _setThemeImages(base_dir: str, name: str) -> None:
2021-02-07 15:32:15 +00:00
"""Changes the profile background image
and banner to the defaults
"""
2021-12-25 23:35:50 +00:00
theme_name_lower = name.lower()
2021-02-07 15:32:15 +00:00
profileImageFilename = \
2021-12-25 23:35:50 +00:00
base_dir + '/theme/' + theme_name_lower + '/image.png'
2021-02-07 15:32:15 +00:00
bannerFilename = \
2021-12-25 23:35:50 +00:00
base_dir + '/theme/' + theme_name_lower + '/banner.png'
2021-02-07 15:32:15 +00:00
searchBannerFilename = \
2021-12-25 23:35:50 +00:00
base_dir + '/theme/' + theme_name_lower + '/search_banner.png'
2021-02-07 15:32:15 +00:00
leftColImageFilename = \
2021-12-25 23:35:50 +00:00
base_dir + '/theme/' + theme_name_lower + '/left_col_image.png'
2021-02-07 15:32:15 +00:00
rightColImageFilename = \
2021-12-25 23:35:50 +00:00
base_dir + '/theme/' + theme_name_lower + '/right_col_image.png'
2021-02-07 15:32:15 +00:00
2021-12-25 23:35:50 +00:00
_setTextModeTheme(base_dir, theme_name_lower)
2021-02-07 15:32:15 +00:00
2020-07-25 14:03:09 +00:00
backgroundNames = ('login', 'shares', 'delete', 'follow',
2021-02-25 19:07:45 +00:00
'options', 'block', 'search', 'calendar',
'welcome')
2020-11-21 11:54:29 +00:00
extensions = getImageExtensions()
2020-07-25 14:03:09 +00:00
2021-12-25 16:17:53 +00:00
for subdir, dirs, files in os.walk(base_dir + '/accounts'):
2020-07-25 09:55:11 +00:00
for acct in dirs:
2021-06-25 18:02:05 +00:00
if not isAccountDir(acct):
2020-07-25 09:55:11 +00:00
continue
2021-12-25 16:17:53 +00:00
accountDir = os.path.join(base_dir + '/accounts', acct)
2020-07-25 09:55:11 +00:00
2020-07-25 10:13:24 +00:00
for backgroundType in backgroundNames:
for ext in extensions:
2021-12-25 23:35:50 +00:00
if theme_name_lower == 'default':
backgroundImageFilename = \
2021-12-25 16:17:53 +00:00
base_dir + '/theme/default/' + \
2020-11-14 12:44:24 +00:00
backgroundType + '_background.' + ext
else:
backgroundImageFilename = \
2021-12-25 23:35:50 +00:00
base_dir + '/theme/' + theme_name_lower + '/' + \
2020-11-14 12:44:24 +00:00
backgroundType + '_background' + '.' + ext
if os.path.isfile(backgroundImageFilename):
try:
copyfile(backgroundImageFilename,
2021-12-25 16:17:53 +00:00
base_dir + '/accounts/' +
2020-11-14 12:44:24 +00:00
backgroundType + '-background.' + ext)
2020-07-25 17:56:06 +00:00
continue
2021-11-25 18:42:38 +00:00
except OSError:
2021-10-29 18:48:15 +00:00
print('EX: _setThemeImages unable to copy ' +
backgroundImageFilename)
# background image was not found
# so remove any existing file
2021-12-25 16:17:53 +00:00
if os.path.isfile(base_dir + '/accounts/' +
2020-11-14 12:44:24 +00:00
backgroundType + '-background.' + ext):
try:
2021-12-25 16:17:53 +00:00
os.remove(base_dir + '/accounts/' +
2020-11-14 12:44:24 +00:00
backgroundType + '-background.' + ext)
2021-11-25 18:42:38 +00:00
except OSError:
2021-10-29 18:48:15 +00:00
print('EX: _setThemeImages unable to delete ' +
2021-12-25 16:17:53 +00:00
base_dir + '/accounts/' +
2021-10-29 18:48:15 +00:00
backgroundType + '-background.' + ext)
2020-07-25 10:03:03 +00:00
2020-07-25 09:55:11 +00:00
if os.path.isfile(profileImageFilename) and \
os.path.isfile(bannerFilename):
2020-05-28 21:39:41 +00:00
try:
copyfile(profileImageFilename,
accountDir + '/image.png')
2021-11-25 18:42:38 +00:00
except OSError:
2021-10-29 18:48:15 +00:00
print('EX: _setThemeImages unable to copy ' +
profileImageFilename)
2020-06-10 15:09:14 +00:00
try:
2020-05-28 21:39:41 +00:00
copyfile(bannerFilename,
accountDir + '/banner.png')
2021-11-25 22:22:54 +00:00
except OSError:
2021-10-29 18:48:15 +00:00
print('EX: _setThemeImages unable to copy ' +
bannerFilename)
2020-05-28 21:39:41 +00:00
2020-06-10 15:09:14 +00:00
try:
if os.path.isfile(searchBannerFilename):
copyfile(searchBannerFilename,
accountDir + '/search_banner.png')
2021-11-25 18:42:38 +00:00
except OSError:
2021-10-29 18:48:15 +00:00
print('EX: _setThemeImages unable to copy ' +
searchBannerFilename)
2020-06-10 15:09:14 +00:00
try:
if os.path.isfile(leftColImageFilename):
copyfile(leftColImageFilename,
accountDir + '/left_col_image.png')
2021-10-29 18:48:15 +00:00
elif os.path.isfile(accountDir +
'/left_col_image.png'):
try:
os.remove(accountDir + '/left_col_image.png')
2021-11-25 18:42:38 +00:00
except OSError:
2021-10-29 18:48:15 +00:00
print('EX: _setThemeImages unable to delete ' +
accountDir + '/left_col_image.png')
2021-11-25 22:22:54 +00:00
except OSError:
2021-10-29 18:48:15 +00:00
print('EX: _setThemeImages unable to copy ' +
leftColImageFilename)
try:
if os.path.isfile(rightColImageFilename):
copyfile(rightColImageFilename,
accountDir + '/right_col_image.png')
else:
if os.path.isfile(accountDir +
'/right_col_image.png'):
try:
os.remove(accountDir + '/right_col_image.png')
2021-11-25 18:42:38 +00:00
except OSError:
2021-10-29 18:48:15 +00:00
print('EX: _setThemeImages unable to delete ' +
accountDir + '/right_col_image.png')
2021-11-25 22:22:54 +00:00
except OSError:
2021-10-29 18:48:15 +00:00
print('EX: _setThemeImages unable to copy ' +
rightColImageFilename)
2020-12-13 22:13:45 +00:00
break
2020-05-28 21:39:41 +00:00
2021-12-25 16:17:53 +00:00
def setNewsAvatar(base_dir: str, name: str,
2021-12-25 17:09:22 +00:00
http_prefix: str,
2021-12-26 10:00:46 +00:00
domain: str, domain_full: str) -> None:
2020-10-13 21:38:19 +00:00
"""Sets the avatar for the news account
"""
nickname = 'news'
2021-12-25 16:17:53 +00:00
newFilename = base_dir + '/theme/' + name + '/icons/avatar_news.png'
2020-10-15 18:21:05 +00:00
if not os.path.isfile(newFilename):
2021-12-25 16:17:53 +00:00
newFilename = base_dir + '/theme/default/icons/avatar_news.png'
2020-10-13 21:38:19 +00:00
if not os.path.isfile(newFilename):
return
2020-10-13 21:45:53 +00:00
avatarFilename = \
2021-12-26 10:00:46 +00:00
localActorUrl(http_prefix, domain_full, nickname) + '.png'
2020-10-13 21:45:53 +00:00
avatarFilename = avatarFilename.replace('/', '-')
2021-12-25 16:17:53 +00:00
filename = base_dir + '/cache/avatars/' + avatarFilename
2020-10-13 21:45:53 +00:00
2020-10-13 21:38:19 +00:00
if os.path.isfile(filename):
try:
os.remove(filename)
2021-11-25 22:22:54 +00:00
except OSError:
2021-10-29 18:48:15 +00:00
print('EX: setNewsAvatar unable to delete ' + filename)
2021-12-25 16:17:53 +00:00
if os.path.isdir(base_dir + '/cache/avatars'):
2020-10-13 21:38:19 +00:00
copyfile(newFilename, filename)
2021-12-25 16:17:53 +00:00
accountDir = acctDir(base_dir, nickname, domain)
2021-07-13 21:59:53 +00:00
copyfile(newFilename, accountDir + '/avatar.png')
2020-10-13 21:38:19 +00:00
2021-12-25 16:17:53 +00:00
def _setClearCacheFlag(base_dir: str) -> None:
"""Sets a flag which can be used by an external system
(eg. a script in a cron job) to clear the browser cache
"""
2021-12-25 16:17:53 +00:00
if not os.path.isdir(base_dir + '/accounts'):
2021-03-03 13:39:39 +00:00
return
2021-12-25 16:17:53 +00:00
flagFilename = base_dir + '/accounts/.clear_cache'
with open(flagFilename, 'w+') as flagFile:
flagFile.write('\n')
2021-12-25 16:17:53 +00:00
def setTheme(base_dir: str, name: str, domain: str,
2021-12-25 23:03:28 +00:00
allow_local_network_access: bool, system_language: str) -> bool:
2021-02-07 15:22:06 +00:00
"""Sets the theme with the given name as the current theme
"""
2020-05-26 20:17:16 +00:00
result = False
2020-05-28 09:11:21 +00:00
2021-12-25 16:17:53 +00:00
prevThemeName = getTheme(base_dir)
2021-12-05 11:03:25 +00:00
# if the theme has changed then remove any custom settings
if prevThemeName != name:
2021-12-25 16:17:53 +00:00
resetThemeDesignerSettings(base_dir, name, domain,
2021-12-25 18:54:50 +00:00
allow_local_network_access,
2021-12-25 23:03:28 +00:00
system_language)
2021-12-05 11:03:25 +00:00
2021-12-25 16:17:53 +00:00
_removeTheme(base_dir)
2020-05-28 21:30:40 +00:00
2021-12-25 16:17:53 +00:00
themes = getThemesList(base_dir)
2021-12-25 23:35:50 +00:00
for theme_name in themes:
theme_name_lower = theme_name.lower()
if name == theme_name_lower:
allow_access = allow_local_network_access
try:
2021-12-25 23:35:50 +00:00
globals()['setTheme' + theme_name](base_dir, allow_access)
except BaseException:
2021-12-25 23:35:50 +00:00
print('EX: setTheme unable to set theme ' + theme_name)
2020-05-28 21:30:40 +00:00
if prevThemeName:
2021-12-25 23:35:50 +00:00
if prevThemeName.lower() != theme_name_lower:
2020-05-28 21:30:40 +00:00
# change the banner and profile image
# to the default for the theme
2021-12-25 16:17:53 +00:00
_setThemeImages(base_dir, name)
_setThemeFonts(base_dir, name)
2020-05-28 09:11:21 +00:00
result = True
if not result:
2020-05-27 11:02:51 +00:00
# default
2021-12-25 18:54:50 +00:00
_setThemeDefault(base_dir, allow_local_network_access)
2020-05-27 11:02:51 +00:00
result = True
2020-05-28 09:11:21 +00:00
2021-12-25 16:17:53 +00:00
variablesFile = base_dir + '/theme/' + name + '/theme.json'
2020-11-14 17:34:11 +00:00
if os.path.isfile(variablesFile):
2021-12-25 16:17:53 +00:00
_readVariablesFile(base_dir, name, variablesFile,
2021-12-25 18:54:50 +00:00
allow_local_network_access)
2020-11-14 17:34:11 +00:00
2021-12-25 16:17:53 +00:00
_setCustomFont(base_dir)
2020-10-09 19:28:09 +00:00
# set the news avatar
newsAvatarThemeFilename = \
2021-12-25 16:17:53 +00:00
base_dir + '/theme/' + name + '/icons/avatar_news.png'
if os.path.isdir(base_dir + '/accounts/news@' + domain):
2020-11-20 09:34:31 +00:00
if os.path.isfile(newsAvatarThemeFilename):
newsAvatarFilename = \
2021-12-25 16:17:53 +00:00
base_dir + '/accounts/news@' + domain + '/avatar.png'
2020-11-20 09:34:31 +00:00
copyfile(newsAvatarThemeFilename, newsAvatarFilename)
2020-10-09 19:28:09 +00:00
2021-12-25 16:17:53 +00:00
grayscaleFilename = base_dir + '/accounts/.grayscale'
2020-07-10 18:08:45 +00:00
if os.path.isfile(grayscaleFilename):
2021-12-25 16:17:53 +00:00
enableGrayscale(base_dir)
2020-07-10 18:08:45 +00:00
else:
2021-12-25 16:17:53 +00:00
disableGrayscale(base_dir)
2020-11-14 18:32:34 +00:00
2021-12-25 23:03:28 +00:00
_copyThemeHelpFiles(base_dir, name, system_language)
2021-12-25 16:17:53 +00:00
_setThemeInConfig(base_dir, name)
_setClearCacheFlag(base_dir)
2020-05-26 20:17:16 +00:00
return result
2021-12-25 16:17:53 +00:00
def updateDefaultThemesList(base_dir: str) -> None:
"""Recreates the list of default themes
"""
2021-12-25 23:35:50 +00:00
theme_names = getThemesList(base_dir)
2021-12-25 16:17:53 +00:00
defaultThemesFilename = base_dir + '/defaultthemes.txt'
2021-06-22 12:27:10 +00:00
with open(defaultThemesFilename, 'w+') as defaultThemesFile:
2021-12-25 23:35:50 +00:00
for name in theme_names:
2021-06-22 12:27:10 +00:00
defaultThemesFile.write(name + '\n')
2021-12-25 16:17:53 +00:00
def scanThemesForScripts(base_dir: str) -> bool:
"""Scans the theme directory for any svg files containing scripts
"""
2021-12-25 16:17:53 +00:00
for subdir, dirs, files in os.walk(base_dir + '/theme'):
for f in files:
if not f.endswith('.svg'):
continue
svgFilename = os.path.join(subdir, f)
content = ''
with open(svgFilename, 'r') as fp:
content = fp.read()
svgDangerous = dangerousSVG(content, False)
if svgDangerous:
print('svg file contains script: ' + svgFilename)
2021-09-13 18:50:02 +00:00
return True
# deliberately no break - should resursively scan
2021-09-13 18:50:02 +00:00
return False