__filename__ = "theme.py" __author__ = "Bob Mottram" __license__ = "AGPL3+" __version__ = "1.2.0" __maintainer__ = "Bob Mottram" __email__ = "bob@libreserver.org" __status__ = "Production" __module_group__ = "Web Interface" import os from utils import is_account_dir from utils import load_json from utils import save_json from utils import get_image_extensions from utils import copytree from utils import acct_dir from utils import dangerousSVG from utils import local_actor_url from shutil import copyfile from shutil import make_archive from shutil import unpack_archive from shutil import rmtree from content import dangerousCSS def importTheme(base_dir: str, filename: str) -> bool: """Imports a theme """ if not os.path.isfile(filename): return False tempThemeDir = base_dir + '/imports/files' if os.path.isdir(tempThemeDir): rmtree(tempThemeDir, ignore_errors=False, onerror=None) os.mkdir(tempThemeDir) 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', '') 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? defaultThemesFilename = base_dir + '/defaultthemes.txt' if os.path.isfile(defaultThemesFilename): if newThemeName.title() + '\n' in open(defaultThemesFilename).read(): newThemeName = newThemeName + '2' themeDir = base_dir + '/theme/' + newThemeName if not os.path.isdir(themeDir): os.mkdir(themeDir) copytree(tempThemeDir, themeDir) if os.path.isdir(tempThemeDir): rmtree(tempThemeDir, ignore_errors=False, onerror=None) if scanThemesForScripts(themeDir): rmtree(themeDir, ignore_errors=False, onerror=None) return False return os.path.isfile(themeDir + '/theme.json') def exportTheme(base_dir: str, theme: str) -> bool: """Exports a theme as a zip file """ themeDir = base_dir + '/theme/' + theme if not os.path.isfile(themeDir + '/theme.json'): return False if not os.path.isdir(base_dir + '/exports'): os.mkdir(base_dir + '/exports') exportFilename = base_dir + '/exports/' + theme + '.zip' if os.path.isfile(exportFilename): try: os.remove(exportFilename) except OSError: print('EX: exportTheme unable to delete ' + str(exportFilename)) try: make_archive(base_dir + '/exports/' + theme, 'zip', themeDir) except BaseException: print('EX: exportTheme unable to archive ' + base_dir + '/exports/' + str(theme)) pass return os.path.isfile(exportFilename) def _getThemeFiles() -> []: """Gets the list of theme style sheets """ return ('epicyon.css', 'login.css', 'follow.css', 'suspended.css', 'calendar.css', 'blog.css', 'options.css', 'search.css', 'links.css', 'welcome.css', 'graph.css') def isNewsThemeName(base_dir: str, theme_name: str) -> bool: """Returns true if the given theme is a news instance """ themeDir = base_dir + '/theme/' + theme_name if os.path.isfile(themeDir + '/is_news_instance'): return True return False def getThemesList(base_dir: str) -> []: """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 """ themes = [] for subdir, dirs, files in os.walk(base_dir + '/theme'): for theme_name in dirs: if '~' not in theme_name and \ theme_name != 'icons' and theme_name != 'fonts': themes.append(theme_name.title()) break themes.sort() print('Themes available: ' + str(themes)) return themes def _copyThemeHelpFiles(base_dir: str, theme_name: str, system_language: str) -> None: """Copies any theme specific help files from the welcome subdirectory """ if not system_language: system_language = 'en' themeDir = base_dir + '/theme/' + theme_name + '/welcome' if not os.path.isdir(themeDir): themeDir = base_dir + '/defaultwelcome' for subdir, dirs, files in os.walk(themeDir): for helpMarkdownFile in files: if not helpMarkdownFile.endswith('_' + system_language + '.md'): continue destHelpMarkdownFile = \ helpMarkdownFile.replace('_' + system_language + '.md', '.md') if destHelpMarkdownFile == 'profile.md' or \ destHelpMarkdownFile == 'final.md': destHelpMarkdownFile = 'welcome_' + destHelpMarkdownFile if os.path.isdir(base_dir + '/accounts'): copyfile(themeDir + '/' + helpMarkdownFile, base_dir + '/accounts/' + destHelpMarkdownFile) break def _setThemeInConfig(base_dir: str, name: str) -> bool: """Sets the theme with the given name within config.json """ config_filename = base_dir + '/config.json' if not os.path.isfile(config_filename): return False configJson = load_json(config_filename, 0) if not configJson: return False configJson['theme'] = name return save_json(configJson, config_filename) def _setNewswirePublishAsIcon(base_dir: str, useIcon: bool) -> bool: """Shows the newswire publish action as an icon or a button """ config_filename = base_dir + '/config.json' if not os.path.isfile(config_filename): return False configJson = load_json(config_filename, 0) if not configJson: return False configJson['show_publish_as_icon'] = useIcon return save_json(configJson, config_filename) def _setIconsAsButtons(base_dir: str, useButtons: bool) -> bool: """Whether to show icons in the header (inbox, outbox, etc) as buttons """ config_filename = base_dir + '/config.json' if not os.path.isfile(config_filename): return False configJson = load_json(config_filename, 0) if not configJson: return False configJson['icons_as_buttons'] = useButtons return save_json(configJson, config_filename) def _setRssIconAtTop(base_dir: str, atTop: bool) -> bool: """Whether to show RSS icon at the top of the timeline """ config_filename = base_dir + '/config.json' if not os.path.isfile(config_filename): return False configJson = load_json(config_filename, 0) if not configJson: return False configJson['rss_icon_at_top'] = atTop return save_json(configJson, config_filename) def _setPublishButtonAtTop(base_dir: str, atTop: bool) -> bool: """Whether to show the publish button above the title image in the newswire column """ config_filename = base_dir + '/config.json' if not os.path.isfile(config_filename): return False configJson = load_json(config_filename, 0) if not configJson: return False configJson['publish_button_at_top'] = atTop return save_json(configJson, config_filename) def _setFullWidthTimelineButtonHeader(base_dir: str, fullWidth: bool) -> bool: """Shows the timeline button header containing inbox, outbox, calendar, etc as full width """ config_filename = base_dir + '/config.json' if not os.path.isfile(config_filename): return False configJson = load_json(config_filename, 0) if not configJson: return False configJson['full_width_tl_button_header'] = fullWidth return save_json(configJson, config_filename) def getTheme(base_dir: str) -> str: """Gets the current theme name from config.json """ config_filename = base_dir + '/config.json' if os.path.isfile(config_filename): configJson = load_json(config_filename, 0) if configJson: if configJson.get('theme'): return configJson['theme'] return 'default' def _removeTheme(base_dir: str): """Removes the current theme style sheets """ themeFiles = _getThemeFiles() for filename in themeFiles: if not os.path.isfile(base_dir + '/' + filename): continue try: os.remove(base_dir + '/' + filename) except OSError: print('EX: _removeTheme unable to delete ' + base_dir + '/' + filename) def setCSSparam(css: str, param: str, value: str) -> str: """Sets a CSS parameter to a given value """ # is this just a simple string replacement? if ';' in param: return css.replace(param, value) # color replacement if param.startswith('rgba('): return css.replace(param, value) # if the parameter begins with * then don't prepend -- onceOnly = False if param.startswith('*'): if param.startswith('**'): onceOnly = True searchStr = param.replace('**', '') + ':' else: searchStr = param.replace('*', '') + ':' else: searchStr = '--' + param + ':' if searchStr not in css: return css if onceOnly: s = css.split(searchStr, 1) else: s = css.split(searchStr) newcss = '' for sectionStr in s: # 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 if not newcss: if sectionStr: newcss = sectionStr else: newcss = ' ' else: if ';' in sectionStr: newcss += \ searchStr + ' ' + value + ';' + sectionStr.split(';', 1)[1] else: newcss += searchStr + ' ' + sectionStr return newcss.strip() def _setThemeFromDict(base_dir: str, name: str, themeParams: {}, bgParams: {}, allow_local_network_access: bool) -> None: """Uses a dictionary to set a theme """ if name: _setThemeInConfig(base_dir, name) themeFiles = _getThemeFiles() for filename in themeFiles: # check for custom css within the theme directory templateFilename = base_dir + '/theme/' + name + '/epicyon-' + filename if filename == 'epicyon.css': templateFilename = \ base_dir + '/theme/' + name + '/epicyon-profile.css' # Ensure that any custom CSS is mostly harmless. # If not then just use the defaults if dangerousCSS(templateFilename, allow_local_network_access) or \ not os.path.isfile(templateFilename): # use default css templateFilename = base_dir + '/epicyon-' + filename if filename == 'epicyon.css': templateFilename = base_dir + '/epicyon-profile.css' if not os.path.isfile(templateFilename): continue with open(templateFilename, 'r') as cssfile: css = cssfile.read() for paramName, paramValue in themeParams.items(): if paramName == 'newswire-publish-icon': if paramValue.lower() == 'true': _setNewswirePublishAsIcon(base_dir, True) else: _setNewswirePublishAsIcon(base_dir, False) continue elif paramName == 'full-width-timeline-buttons': if paramValue.lower() == 'true': _setFullWidthTimelineButtonHeader(base_dir, True) else: _setFullWidthTimelineButtonHeader(base_dir, False) continue elif paramName == 'icons-as-buttons': if paramValue.lower() == 'true': _setIconsAsButtons(base_dir, True) else: _setIconsAsButtons(base_dir, False) continue elif paramName == 'rss-icon-at-top': if paramValue.lower() == 'true': _setRssIconAtTop(base_dir, True) else: _setRssIconAtTop(base_dir, False) continue elif paramName == 'publish-button-at-top': if paramValue.lower() == 'true': _setPublishButtonAtTop(base_dir, True) else: _setPublishButtonAtTop(base_dir, False) continue css = setCSSparam(css, paramName, paramValue) filename = base_dir + '/' + filename with open(filename, 'w+') as cssfile: cssfile.write(css) screenName = ( 'login', 'follow', 'options', 'search', 'welcome' ) for s in screenName: if bgParams.get(s): _setBackgroundFormat(base_dir, name, s, bgParams[s]) def _setBackgroundFormat(base_dir: str, name: str, backgroundType: str, extension: str) -> None: """Sets the background file extension """ if extension == 'jpg': return 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) def enableGrayscale(base_dir: str) -> None: """Enables grayscale for the current theme """ themeFiles = _getThemeFiles() for filename in themeFiles: templateFilename = base_dir + '/' + filename if not os.path.isfile(templateFilename): continue with open(templateFilename, 'r') as cssfile: css = cssfile.read() if 'grayscale' not in css: css = \ css.replace('body, html {', 'body, html {\n filter: grayscale(100%);') filename = base_dir + '/' + filename with open(filename, 'w+') as cssfile: cssfile.write(css) grayscaleFilename = base_dir + '/accounts/.grayscale' if not os.path.isfile(grayscaleFilename): with open(grayscaleFilename, 'w+') as grayfile: grayfile.write(' ') def disableGrayscale(base_dir: str) -> None: """Disables grayscale for the current theme """ themeFiles = _getThemeFiles() for filename in themeFiles: templateFilename = base_dir + '/' + filename if not os.path.isfile(templateFilename): continue with open(templateFilename, 'r') as cssfile: css = cssfile.read() if 'grayscale' in css: css = \ css.replace('\n filter: grayscale(100%);', '') filename = base_dir + '/' + filename with open(filename, 'w+') as cssfile: cssfile.write(css) grayscaleFilename = base_dir + '/accounts/.grayscale' if os.path.isfile(grayscaleFilename): try: os.remove(grayscaleFilename) except OSError: print('EX: disableGrayscale unable to delete ' + grayscaleFilename) def _setCustomFont(base_dir: str): """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(): filename = base_dir + '/fonts/custom.' + ext if os.path.isfile(filename): customFontExt = ext customFontType = extType if not customFontExt: return themeFiles = _getThemeFiles() for filename in themeFiles: templateFilename = base_dir + '/' + filename if not os.path.isfile(templateFilename): continue with open(templateFilename, 'r') as cssfile: css = cssfile.read() css = \ setCSSparam(css, "*src", "url('./fonts/custom." + customFontExt + "') format('" + customFontType + "')") css = setCSSparam(css, "*font-family", "'CustomFont'") filename = base_dir + '/' + filename with open(filename, 'w+') as cssfile: cssfile.write(css) def setThemeFromDesigner(base_dir: str, theme_name: str, domain: str, themeParams: {}, allow_local_network_access: bool, system_language: str): customThemeFilename = base_dir + '/accounts/theme.json' save_json(themeParams, customThemeFilename) setTheme(base_dir, theme_name, domain, allow_local_network_access, system_language) def resetThemeDesignerSettings(base_dir: str, theme_name: str, domain: str, allow_local_network_access: bool, system_language: str) -> None: """Resets the theme designer settings """ 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') def _readVariablesFile(base_dir: str, theme_name: str, variablesFile: str, allow_local_network_access: bool) -> None: """Reads variables from a file in the theme directory """ themeParams = load_json(variablesFile, 0) if not themeParams: return # set custom theme parameters customVariablesFile = base_dir + '/accounts/theme.json' if os.path.isfile(customVariablesFile): customThemeParams = load_json(customVariablesFile, 0) if customThemeParams: for variableName, value in customThemeParams.items(): themeParams[variableName] = value bgParams = { "login": "jpg", "follow": "jpg", "options": "jpg", "search": "jpg" } _setThemeFromDict(base_dir, theme_name, themeParams, bgParams, allow_local_network_access) def _setThemeDefault(base_dir: str, allow_local_network_access: bool): name = 'default' _removeTheme(base_dir) _setThemeInConfig(base_dir, name) bgParams = { "login": "jpg", "follow": "jpg", "options": "jpg", "search": "jpg" } themeParams = { "newswire-publish-icon": True, "full-width-timeline-buttons": False, "icons-as-buttons": False, "rss-icon-at-top": True, "publish-button-at-top": False, "banner-height": "20vh", "banner-height-mobile": "10vh", "search-banner-height-mobile": "15vh" } _setThemeFromDict(base_dir, name, themeParams, bgParams, allow_local_network_access) def _setThemeFonts(base_dir: str, theme_name: str) -> None: """Adds custom theme fonts """ theme_name_lower = theme_name.lower() fontsDir = base_dir + '/fonts' themeFontsDir = \ base_dir + '/theme/' + theme_name_lower + '/fonts' 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 def getTextModeBanner(base_dir: str) -> str: """Returns the banner used for shell browsers, like Lynx """ 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', '
') return None def getTextModeLogo(base_dir: str) -> str: """Returns the login screen logo used for shell browsers, like Lynx """ textModeLogoFilename = base_dir + '/accounts/logo.txt' if not os.path.isfile(textModeLogoFilename): textModeLogoFilename = base_dir + '/img/logo.txt' with open(textModeLogoFilename, 'r') as fp: logoStr = fp.read() if logoStr: return logoStr.replace('\n', '
') return None def _setTextModeTheme(base_dir: str, name: str) -> None: # set the text mode logo which appears on the login screen # in browsers such as Lynx textModeLogoFilename = \ base_dir + '/theme/' + name + '/logo.txt' if os.path.isfile(textModeLogoFilename): try: copyfile(textModeLogoFilename, base_dir + '/accounts/logo.txt') except OSError: print('EX: _setTextModeTheme unable to copy ' + textModeLogoFilename + ' ' + base_dir + '/accounts/logo.txt') else: try: copyfile(base_dir + '/img/logo.txt', base_dir + '/accounts/logo.txt') except OSError: print('EX: _setTextModeTheme unable to copy ' + base_dir + '/img/logo.txt ' + base_dir + '/accounts/logo.txt') # set the text mode banner which appears in browsers such as Lynx text_mode_bannerFilename = \ base_dir + '/theme/' + name + '/banner.txt' if os.path.isfile(base_dir + '/accounts/banner.txt'): try: os.remove(base_dir + '/accounts/banner.txt') except OSError: print('EX: _setTextModeTheme unable to delete ' + base_dir + '/accounts/banner.txt') if os.path.isfile(text_mode_bannerFilename): try: copyfile(text_mode_bannerFilename, base_dir + '/accounts/banner.txt') except OSError: print('EX: _setTextModeTheme unable to copy ' + text_mode_bannerFilename + ' ' + base_dir + '/accounts/banner.txt') def _setThemeImages(base_dir: str, name: str) -> None: """Changes the profile background image and banner to the defaults """ theme_name_lower = name.lower() profileImageFilename = \ base_dir + '/theme/' + theme_name_lower + '/image.png' bannerFilename = \ base_dir + '/theme/' + theme_name_lower + '/banner.png' searchBannerFilename = \ base_dir + '/theme/' + theme_name_lower + '/search_banner.png' leftColImageFilename = \ base_dir + '/theme/' + theme_name_lower + '/left_col_image.png' rightColImageFilename = \ base_dir + '/theme/' + theme_name_lower + '/right_col_image.png' _setTextModeTheme(base_dir, theme_name_lower) backgroundNames = ('login', 'shares', 'delete', 'follow', 'options', 'block', 'search', 'calendar', 'welcome') extensions = get_image_extensions() for subdir, dirs, files in os.walk(base_dir + '/accounts'): for acct in dirs: if not is_account_dir(acct): continue accountDir = os.path.join(base_dir + '/accounts', acct) for backgroundType in backgroundNames: for ext in extensions: if theme_name_lower == 'default': backgroundImageFilename = \ base_dir + '/theme/default/' + \ backgroundType + '_background.' + ext else: backgroundImageFilename = \ base_dir + '/theme/' + theme_name_lower + '/' + \ backgroundType + '_background' + '.' + ext if os.path.isfile(backgroundImageFilename): try: copyfile(backgroundImageFilename, base_dir + '/accounts/' + backgroundType + '-background.' + ext) continue except OSError: print('EX: _setThemeImages unable to copy ' + backgroundImageFilename) # background image was not found # so remove any existing file if os.path.isfile(base_dir + '/accounts/' + backgroundType + '-background.' + ext): try: os.remove(base_dir + '/accounts/' + backgroundType + '-background.' + ext) except OSError: print('EX: _setThemeImages unable to delete ' + base_dir + '/accounts/' + backgroundType + '-background.' + ext) if os.path.isfile(profileImageFilename) and \ os.path.isfile(bannerFilename): try: copyfile(profileImageFilename, accountDir + '/image.png') except OSError: print('EX: _setThemeImages unable to copy ' + profileImageFilename) try: copyfile(bannerFilename, accountDir + '/banner.png') except OSError: print('EX: _setThemeImages unable to copy ' + bannerFilename) try: if os.path.isfile(searchBannerFilename): copyfile(searchBannerFilename, accountDir + '/search_banner.png') except OSError: print('EX: _setThemeImages unable to copy ' + searchBannerFilename) try: if os.path.isfile(leftColImageFilename): copyfile(leftColImageFilename, accountDir + '/left_col_image.png') elif os.path.isfile(accountDir + '/left_col_image.png'): try: os.remove(accountDir + '/left_col_image.png') except OSError: print('EX: _setThemeImages unable to delete ' + accountDir + '/left_col_image.png') except OSError: 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') except OSError: print('EX: _setThemeImages unable to delete ' + accountDir + '/right_col_image.png') except OSError: print('EX: _setThemeImages unable to copy ' + rightColImageFilename) break def setNewsAvatar(base_dir: str, name: str, http_prefix: str, domain: str, domain_full: str) -> None: """Sets the avatar for the news account """ nickname = 'news' newFilename = base_dir + '/theme/' + name + '/icons/avatar_news.png' if not os.path.isfile(newFilename): newFilename = base_dir + '/theme/default/icons/avatar_news.png' if not os.path.isfile(newFilename): return avatarFilename = \ local_actor_url(http_prefix, domain_full, nickname) + '.png' avatarFilename = avatarFilename.replace('/', '-') filename = base_dir + '/cache/avatars/' + avatarFilename if os.path.isfile(filename): try: os.remove(filename) except OSError: print('EX: setNewsAvatar unable to delete ' + filename) if os.path.isdir(base_dir + '/cache/avatars'): copyfile(newFilename, filename) accountDir = acct_dir(base_dir, nickname, domain) copyfile(newFilename, accountDir + '/avatar.png') 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 """ if not os.path.isdir(base_dir + '/accounts'): return flagFilename = base_dir + '/accounts/.clear_cache' with open(flagFilename, 'w+') as flagFile: flagFile.write('\n') def setTheme(base_dir: str, name: str, domain: str, allow_local_network_access: bool, system_language: str) -> bool: """Sets the theme with the given name as the current theme """ result = False prevThemeName = getTheme(base_dir) # if the theme has changed then remove any custom settings if prevThemeName != name: resetThemeDesignerSettings(base_dir, name, domain, allow_local_network_access, system_language) _removeTheme(base_dir) themes = getThemesList(base_dir) for theme_name in themes: theme_name_lower = theme_name.lower() if name == theme_name_lower: allow_access = allow_local_network_access try: globals()['setTheme' + theme_name](base_dir, allow_access) except BaseException: print('EX: setTheme unable to set theme ' + theme_name) if prevThemeName: if prevThemeName.lower() != theme_name_lower: # change the banner and profile image # to the default for the theme _setThemeImages(base_dir, name) _setThemeFonts(base_dir, name) result = True if not result: # default _setThemeDefault(base_dir, allow_local_network_access) result = True variablesFile = base_dir + '/theme/' + name + '/theme.json' if os.path.isfile(variablesFile): _readVariablesFile(base_dir, name, variablesFile, allow_local_network_access) _setCustomFont(base_dir) # set the news avatar newsAvatarThemeFilename = \ base_dir + '/theme/' + name + '/icons/avatar_news.png' if os.path.isdir(base_dir + '/accounts/news@' + domain): if os.path.isfile(newsAvatarThemeFilename): newsAvatarFilename = \ base_dir + '/accounts/news@' + domain + '/avatar.png' copyfile(newsAvatarThemeFilename, newsAvatarFilename) grayscaleFilename = base_dir + '/accounts/.grayscale' if os.path.isfile(grayscaleFilename): enableGrayscale(base_dir) else: disableGrayscale(base_dir) _copyThemeHelpFiles(base_dir, name, system_language) _setThemeInConfig(base_dir, name) _setClearCacheFlag(base_dir) return result def updateDefaultThemesList(base_dir: str) -> None: """Recreates the list of default themes """ theme_names = getThemesList(base_dir) defaultThemesFilename = base_dir + '/defaultthemes.txt' with open(defaultThemesFilename, 'w+') as defaultThemesFile: for name in theme_names: defaultThemesFile.write(name + '\n') def scanThemesForScripts(base_dir: str) -> bool: """Scans the theme directory for any svg files containing scripts """ 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) return True # deliberately no break - should resursively scan return False