2020-04-04 12:03:28 +00:00
|
|
|
__filename__ = "theme.py"
|
|
|
|
__author__ = "Bob Mottram"
|
|
|
|
__license__ = "AGPL3+"
|
2022-02-03 13:58:20 +00:00
|
|
|
__version__ = "1.3.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-12-26 18:46:43 +00:00
|
|
|
from utils import is_account_dir
|
2021-12-26 15:13:34 +00:00
|
|
|
from utils import load_json
|
2021-12-26 14:47:21 +00:00
|
|
|
from utils import save_json
|
2021-12-26 14:26:16 +00:00
|
|
|
from utils import get_image_extensions
|
2021-05-29 11:04:03 +00:00
|
|
|
from utils import copytree
|
2021-12-26 12:02:29 +00:00
|
|
|
from utils import acct_dir
|
2021-12-27 21:44:48 +00:00
|
|
|
from utils import dangerous_svg
|
2021-12-26 10:19:59 +00:00
|
|
|
from utils import local_actor_url
|
2022-02-27 11:13:11 +00:00
|
|
|
from utils import remove_html
|
2022-06-10 13:29:51 +00:00
|
|
|
from utils import text_in_file
|
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
|
2021-12-29 21:55:09 +00:00
|
|
|
from content import dangerous_css
|
2020-11-15 10:33:11 +00:00
|
|
|
|
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
def import_theme(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
|
2022-01-03 18:37:14 +00:00
|
|
|
temp_theme_dir = base_dir + '/imports/files'
|
|
|
|
if os.path.isdir(temp_theme_dir):
|
|
|
|
rmtree(temp_theme_dir, ignore_errors=False, onerror=None)
|
|
|
|
os.mkdir(temp_theme_dir)
|
|
|
|
unpack_archive(filename, temp_theme_dir, 'zip')
|
|
|
|
essential_theme_files = ('name.txt', 'theme.json')
|
|
|
|
for theme_file in essential_theme_files:
|
|
|
|
if not os.path.isfile(temp_theme_dir + '/' + theme_file):
|
|
|
|
print('WARN: ' + theme_file +
|
2021-05-29 11:04:03 +00:00
|
|
|
' missing from imported theme')
|
|
|
|
return False
|
2022-01-03 18:37:14 +00:00
|
|
|
new_theme_name = None
|
2022-06-09 14:46:30 +00:00
|
|
|
with open(temp_theme_dir + '/name.txt', 'r',
|
|
|
|
encoding='utf-8') as fp_theme:
|
2022-01-03 18:37:14 +00:00
|
|
|
new_theme_name = fp_theme.read().replace('\n', '').replace('\r', '')
|
|
|
|
if len(new_theme_name) > 20:
|
2021-05-29 11:04:03 +00:00
|
|
|
print('WARN: Imported theme name is too long')
|
|
|
|
return False
|
2022-01-03 18:37:14 +00:00
|
|
|
if len(new_theme_name) < 2:
|
2021-05-29 11:04:03 +00:00
|
|
|
print('WARN: Imported theme name is too short')
|
|
|
|
return False
|
2022-01-03 18:37:14 +00:00
|
|
|
new_theme_name = new_theme_name.lower()
|
|
|
|
forbidden_chars = (
|
2021-05-29 11:04:03 +00:00
|
|
|
' ', ';', '/', '\\', '?', '!', '#', '@',
|
|
|
|
':', '%', '&', '"', '+', '<', '>', '$'
|
|
|
|
)
|
2022-01-03 18:37:14 +00:00
|
|
|
for char in forbidden_chars:
|
|
|
|
if char in new_theme_name:
|
2021-05-29 11:04:03 +00:00
|
|
|
print('WARN: theme name contains forbidden character')
|
|
|
|
return False
|
2022-01-03 18:37:14 +00:00
|
|
|
if not new_theme_name:
|
2021-05-29 11:04:03 +00:00
|
|
|
return False
|
2021-05-30 11:06:15 +00:00
|
|
|
|
|
|
|
# if the theme name in the default themes list?
|
2022-01-03 18:37:14 +00:00
|
|
|
default_themes_filename = base_dir + '/defaultthemes.txt'
|
|
|
|
if os.path.isfile(default_themes_filename):
|
2022-06-10 13:29:51 +00:00
|
|
|
test_str = new_theme_name.title() + '\n'
|
|
|
|
if text_in_file(test_str, default_themes_filename):
|
2022-01-03 18:37:14 +00:00
|
|
|
new_theme_name = new_theme_name + '2'
|
|
|
|
|
|
|
|
theme_dir = base_dir + '/theme/' + new_theme_name
|
|
|
|
if not os.path.isdir(theme_dir):
|
|
|
|
os.mkdir(theme_dir)
|
|
|
|
copytree(temp_theme_dir, theme_dir)
|
|
|
|
if os.path.isdir(temp_theme_dir):
|
|
|
|
rmtree(temp_theme_dir, ignore_errors=False, onerror=None)
|
|
|
|
if scan_themes_for_scripts(theme_dir):
|
|
|
|
rmtree(theme_dir, ignore_errors=False, onerror=None)
|
2021-09-13 18:50:02 +00:00
|
|
|
return False
|
2022-01-03 18:37:14 +00:00
|
|
|
return os.path.isfile(theme_dir + '/theme.json')
|
2021-05-29 11:04:03 +00:00
|
|
|
|
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
def export_theme(base_dir: str, theme: str) -> bool:
|
2021-05-28 21:39:34 +00:00
|
|
|
"""Exports a theme as a zip file
|
|
|
|
"""
|
2022-01-03 18:37:14 +00:00
|
|
|
theme_dir = base_dir + '/theme/' + theme
|
|
|
|
if not os.path.isfile(theme_dir + '/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')
|
2022-01-03 18:37:14 +00:00
|
|
|
export_filename = base_dir + '/exports/' + theme + '.zip'
|
|
|
|
if os.path.isfile(export_filename):
|
2021-09-05 10:17:43 +00:00
|
|
|
try:
|
2022-01-03 18:37:14 +00:00
|
|
|
os.remove(export_filename)
|
2021-11-25 18:42:38 +00:00
|
|
|
except OSError:
|
2022-01-03 18:37:14 +00:00
|
|
|
print('EX: export_theme unable to delete ' + str(export_filename))
|
2021-05-28 21:39:34 +00:00
|
|
|
try:
|
2022-01-03 18:37:14 +00:00
|
|
|
make_archive(base_dir + '/exports/' + theme, 'zip', theme_dir)
|
2021-05-28 21:39:34 +00:00
|
|
|
except BaseException:
|
2021-12-29 21:55:09 +00:00
|
|
|
print('EX: export_theme unable to archive ' +
|
2021-12-25 16:17:53 +00:00
|
|
|
base_dir + '/exports/' + str(theme))
|
2022-01-03 18:37:14 +00:00
|
|
|
return os.path.isfile(export_filename)
|
2021-05-28 21:39:34 +00:00
|
|
|
|
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
def _get_theme_files() -> []:
|
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',
|
2022-01-12 20:58:27 +00:00
|
|
|
'welcome.css', 'graph.css', 'podcast.css')
|
2020-07-11 09:30:07 +00:00
|
|
|
|
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
def is_news_theme_name(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
|
|
|
|
"""
|
2022-01-03 18:37:14 +00:00
|
|
|
theme_dir = base_dir + '/theme/' + theme_name
|
|
|
|
if os.path.isfile(theme_dir + '/is_news_instance'):
|
2021-02-27 11:44:50 +00:00
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
def get_themes_list(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 = []
|
2022-01-03 18:37:14 +00:00
|
|
|
for _, dirs, _ 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-29 21:55:09 +00:00
|
|
|
def _copy_theme_help_files(base_dir: str, theme_name: str,
|
|
|
|
system_language: str) -> None:
|
2021-02-27 12:36:14 +00:00
|
|
|
"""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'
|
2022-01-03 18:37:14 +00:00
|
|
|
theme_dir = base_dir + '/theme/' + theme_name + '/welcome'
|
|
|
|
if not os.path.isdir(theme_dir):
|
|
|
|
theme_dir = base_dir + '/defaultwelcome'
|
|
|
|
for _, _, files in os.walk(theme_dir):
|
|
|
|
for help_markdown_file in files:
|
|
|
|
if not help_markdown_file.endswith('_' + system_language + '.md'):
|
2021-02-27 14:39:35 +00:00
|
|
|
continue
|
2022-01-03 18:37:14 +00:00
|
|
|
dest_help_markdown_file = \
|
|
|
|
help_markdown_file.replace('_' + system_language + '.md',
|
|
|
|
'.md')
|
|
|
|
if dest_help_markdown_file in ('profile.md', 'final.md'):
|
|
|
|
dest_help_markdown_file = 'welcome_' + dest_help_markdown_file
|
2021-12-25 16:17:53 +00:00
|
|
|
if os.path.isdir(base_dir + '/accounts'):
|
2022-01-03 18:37:14 +00:00
|
|
|
copyfile(theme_dir + '/' + help_markdown_file,
|
|
|
|
base_dir + '/accounts/' + dest_help_markdown_file)
|
2021-02-27 12:36:14 +00:00
|
|
|
break
|
|
|
|
|
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
def _set_theme_in_config(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-26 14:37:28 +00:00
|
|
|
config_filename = base_dir + '/config.json'
|
|
|
|
if not os.path.isfile(config_filename):
|
2019-11-23 13:04:11 +00:00
|
|
|
return False
|
2022-01-01 21:43:34 +00:00
|
|
|
config_json = load_json(config_filename, 0)
|
|
|
|
if not config_json:
|
2019-11-23 13:04:11 +00:00
|
|
|
return False
|
2022-01-01 21:43:34 +00:00
|
|
|
config_json['theme'] = name
|
|
|
|
return save_json(config_json, config_filename)
|
2020-04-04 12:03:28 +00:00
|
|
|
|
2019-11-23 13:04:11 +00:00
|
|
|
|
2022-01-03 18:37:14 +00:00
|
|
|
def _set_newswire_publish_as_icon(base_dir: str, use_icon: bool) -> bool:
|
2020-10-24 16:37:37 +00:00
|
|
|
"""Shows the newswire publish action as an icon or a button
|
|
|
|
"""
|
2021-12-26 14:37:28 +00:00
|
|
|
config_filename = base_dir + '/config.json'
|
|
|
|
if not os.path.isfile(config_filename):
|
2020-10-24 16:37:37 +00:00
|
|
|
return False
|
2022-01-01 21:43:34 +00:00
|
|
|
config_json = load_json(config_filename, 0)
|
|
|
|
if not config_json:
|
2020-10-24 16:37:37 +00:00
|
|
|
return False
|
2022-01-03 18:37:14 +00:00
|
|
|
config_json['showPublishAsIcon'] = use_icon
|
2022-01-01 21:43:34 +00:00
|
|
|
return save_json(config_json, config_filename)
|
2020-10-24 16:37:37 +00:00
|
|
|
|
|
|
|
|
2022-01-03 18:37:14 +00:00
|
|
|
def _set_icons_as_buttons(base_dir: str, use_buttons: bool) -> bool:
|
2020-10-25 20:38:01 +00:00
|
|
|
"""Whether to show icons in the header (inbox, outbox, etc)
|
|
|
|
as buttons
|
|
|
|
"""
|
2021-12-26 14:37:28 +00:00
|
|
|
config_filename = base_dir + '/config.json'
|
|
|
|
if not os.path.isfile(config_filename):
|
2020-10-25 20:38:01 +00:00
|
|
|
return False
|
2022-01-01 21:43:34 +00:00
|
|
|
config_json = load_json(config_filename, 0)
|
|
|
|
if not config_json:
|
2020-10-25 20:38:01 +00:00
|
|
|
return False
|
2022-01-03 18:37:14 +00:00
|
|
|
config_json['iconsAsButtons'] = use_buttons
|
2022-01-01 21:43:34 +00:00
|
|
|
return save_json(config_json, config_filename)
|
2020-10-25 20:38:01 +00:00
|
|
|
|
|
|
|
|
2022-01-03 18:37:14 +00:00
|
|
|
def _set_rss_icon_at_top(base_dir: str, at_top: bool) -> bool:
|
2020-10-26 20:32:01 +00:00
|
|
|
"""Whether to show RSS icon at the top of the timeline
|
|
|
|
"""
|
2021-12-26 14:37:28 +00:00
|
|
|
config_filename = base_dir + '/config.json'
|
|
|
|
if not os.path.isfile(config_filename):
|
2020-10-26 20:32:01 +00:00
|
|
|
return False
|
2022-01-01 21:43:34 +00:00
|
|
|
config_json = load_json(config_filename, 0)
|
|
|
|
if not config_json:
|
2020-10-26 20:32:01 +00:00
|
|
|
return False
|
2022-01-03 18:37:14 +00:00
|
|
|
config_json['rssIconAtTop'] = at_top
|
2022-01-01 21:43:34 +00:00
|
|
|
return save_json(config_json, config_filename)
|
2020-10-26 20:32:01 +00:00
|
|
|
|
2020-10-26 21:33:40 +00:00
|
|
|
|
2022-01-03 18:37:14 +00:00
|
|
|
def _set_publish_button_at_top(base_dir: str, at_top: 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-26 14:37:28 +00:00
|
|
|
config_filename = base_dir + '/config.json'
|
|
|
|
if not os.path.isfile(config_filename):
|
2020-10-26 21:32:08 +00:00
|
|
|
return False
|
2022-01-01 21:43:34 +00:00
|
|
|
config_json = load_json(config_filename, 0)
|
|
|
|
if not config_json:
|
2020-10-26 21:32:08 +00:00
|
|
|
return False
|
2022-01-03 18:37:14 +00:00
|
|
|
config_json['publishButtonAtTop'] = at_top
|
2022-01-01 21:43:34 +00:00
|
|
|
return save_json(config_json, config_filename)
|
2020-10-26 21:32:08 +00:00
|
|
|
|
2020-10-26 20:32:01 +00:00
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
def _set_full_width_timeline_button_header(base_dir: str,
|
2022-01-03 18:37:14 +00:00
|
|
|
full_width: bool) -> bool:
|
2020-10-24 17:51:00 +00:00
|
|
|
"""Shows the timeline button header containing inbox, outbox,
|
|
|
|
calendar, etc as full width
|
|
|
|
"""
|
2021-12-26 14:37:28 +00:00
|
|
|
config_filename = base_dir + '/config.json'
|
|
|
|
if not os.path.isfile(config_filename):
|
2020-10-24 17:51:00 +00:00
|
|
|
return False
|
2022-01-01 21:43:34 +00:00
|
|
|
config_json = load_json(config_filename, 0)
|
|
|
|
if not config_json:
|
2020-10-24 17:51:00 +00:00
|
|
|
return False
|
2022-01-03 18:37:14 +00:00
|
|
|
config_json['fullWidthTlButtonHeader'] = full_width
|
2022-01-01 21:43:34 +00:00
|
|
|
return save_json(config_json, config_filename)
|
2020-10-24 17:51:00 +00:00
|
|
|
|
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
def get_theme(base_dir: str) -> str:
|
2021-02-07 15:22:06 +00:00
|
|
|
"""Gets the current theme name from config.json
|
|
|
|
"""
|
2021-12-26 14:37:28 +00:00
|
|
|
config_filename = base_dir + '/config.json'
|
|
|
|
if os.path.isfile(config_filename):
|
2022-01-01 21:43:34 +00:00
|
|
|
config_json = load_json(config_filename, 0)
|
|
|
|
if config_json:
|
|
|
|
if config_json.get('theme'):
|
|
|
|
return config_json['theme']
|
2020-05-26 20:17:16 +00:00
|
|
|
return 'default'
|
|
|
|
|
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
def _remove_theme(base_dir: str):
|
2021-02-07 15:22:06 +00:00
|
|
|
"""Removes the current theme style sheets
|
|
|
|
"""
|
2022-01-03 18:37:14 +00:00
|
|
|
theme_files = _get_theme_files()
|
|
|
|
for filename in theme_files:
|
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-12-29 21:55:09 +00:00
|
|
|
print('EX: _remove_theme 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
|
|
|
|
2022-01-26 23:17:53 +00:00
|
|
|
def set_css_param(css: str, param: str, value: str) -> str:
|
2019-11-23 13:04:11 +00:00
|
|
|
"""Sets a CSS parameter to a given value
|
|
|
|
"""
|
2022-02-27 11:13:11 +00:00
|
|
|
value = remove_html(value)
|
2019-11-23 13:04:11 +00:00
|
|
|
# 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 --
|
2022-01-03 18:37:14 +00:00
|
|
|
once_only = False
|
2019-11-23 13:04:11 +00:00
|
|
|
if param.startswith('*'):
|
2020-09-14 12:52:22 +00:00
|
|
|
if param.startswith('**'):
|
2022-01-03 18:37:14 +00:00
|
|
|
once_only = True
|
|
|
|
search_str = param.replace('**', '') + ':'
|
2020-09-14 12:52:22 +00:00
|
|
|
else:
|
2022-01-03 18:37:14 +00:00
|
|
|
search_str = param.replace('*', '') + ':'
|
2019-11-23 13:04:11 +00:00
|
|
|
else:
|
2022-01-03 18:37:14 +00:00
|
|
|
search_str = '--' + param + ':'
|
|
|
|
if search_str not in css:
|
2019-11-23 13:04:11 +00:00
|
|
|
return css
|
2022-01-03 18:37:14 +00:00
|
|
|
if once_only:
|
|
|
|
sstr = css.split(search_str, 1)
|
2020-09-14 13:05:12 +00:00
|
|
|
else:
|
2022-01-03 18:37:14 +00:00
|
|
|
sstr = css.split(search_str)
|
2020-04-04 12:03:28 +00:00
|
|
|
newcss = ''
|
2022-01-03 18:37:14 +00:00
|
|
|
for section_str in sstr:
|
2020-11-16 20:13:48 +00:00
|
|
|
# handle font-family which is a variable
|
2022-01-03 18:37:14 +00:00
|
|
|
next_section = section_str
|
|
|
|
if ';' in next_section:
|
|
|
|
next_section = next_section.split(';')[0] + ';'
|
|
|
|
if search_str == 'font-family:' and "var(--" in next_section:
|
|
|
|
newcss += search_str + ' ' + section_str
|
2020-11-16 20:13:48 +00:00
|
|
|
continue
|
|
|
|
|
2019-11-23 13:04:11 +00:00
|
|
|
if not newcss:
|
2022-01-03 18:37:14 +00:00
|
|
|
if section_str:
|
|
|
|
newcss = section_str
|
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:
|
2022-01-03 18:37:14 +00:00
|
|
|
if ';' in section_str:
|
2020-04-04 12:03:28 +00:00
|
|
|
newcss += \
|
2022-01-03 18:37:14 +00:00
|
|
|
search_str + ' ' + value + ';' + \
|
|
|
|
section_str.split(';', 1)[1]
|
2019-11-23 13:04:11 +00:00
|
|
|
else:
|
2022-01-03 18:37:14 +00:00
|
|
|
newcss += search_str + ' ' + section_str
|
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-29 21:55:09 +00:00
|
|
|
def _set_theme_from_dict(base_dir: str, name: str,
|
2022-01-03 18:37:14 +00:00
|
|
|
theme_params: {}, bg_params: {},
|
2021-12-29 21:55:09 +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-29 21:55:09 +00:00
|
|
|
_set_theme_in_config(base_dir, name)
|
2022-01-03 18:37:14 +00:00
|
|
|
theme_files = _get_theme_files()
|
|
|
|
for filename in theme_files:
|
2020-11-14 13:18:11 +00:00
|
|
|
# check for custom css within the theme directory
|
2022-01-03 18:37:14 +00:00
|
|
|
template_filename = \
|
|
|
|
base_dir + '/theme/' + name + '/epicyon-' + filename
|
2020-04-04 12:03:28 +00:00
|
|
|
if filename == 'epicyon.css':
|
2022-01-03 18:37:14 +00:00
|
|
|
template_filename = \
|
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
|
2022-01-03 18:37:14 +00:00
|
|
|
if dangerous_css(template_filename, allow_local_network_access) or \
|
|
|
|
not os.path.isfile(template_filename):
|
2020-11-14 13:18:11 +00:00
|
|
|
# use default css
|
2022-01-03 18:37:14 +00:00
|
|
|
template_filename = base_dir + '/epicyon-' + filename
|
2020-11-14 13:18:11 +00:00
|
|
|
if filename == 'epicyon.css':
|
2022-01-03 18:37:14 +00:00
|
|
|
template_filename = base_dir + '/epicyon-profile.css'
|
2020-11-14 13:18:11 +00:00
|
|
|
|
2022-01-03 18:37:14 +00:00
|
|
|
if not os.path.isfile(template_filename):
|
2019-11-23 13:04:11 +00:00
|
|
|
continue
|
2020-11-14 13:18:11 +00:00
|
|
|
|
2022-06-09 14:46:30 +00:00
|
|
|
with open(template_filename, 'r', encoding='utf-8') as cssfile:
|
2021-06-21 22:52:04 +00:00
|
|
|
css = cssfile.read()
|
2022-01-03 18:37:14 +00:00
|
|
|
for param_name, param_value in theme_params.items():
|
|
|
|
if param_name == 'newswire-publish-icon':
|
|
|
|
if param_value.lower() == 'true':
|
2021-12-29 21:55:09 +00:00
|
|
|
_set_newswire_publish_as_icon(base_dir, True)
|
2020-11-14 19:21:11 +00:00
|
|
|
else:
|
2021-12-29 21:55:09 +00:00
|
|
|
_set_newswire_publish_as_icon(base_dir, False)
|
2020-11-14 18:08:46 +00:00
|
|
|
continue
|
2022-01-03 18:37:14 +00:00
|
|
|
if param_name == 'full-width-timeline-buttons':
|
|
|
|
if param_value.lower() == 'true':
|
2021-12-29 21:55:09 +00:00
|
|
|
_set_full_width_timeline_button_header(base_dir, True)
|
2020-11-14 19:21:11 +00:00
|
|
|
else:
|
2021-12-29 21:55:09 +00:00
|
|
|
_set_full_width_timeline_button_header(base_dir, False)
|
2020-11-14 18:08:46 +00:00
|
|
|
continue
|
2022-01-03 18:37:14 +00:00
|
|
|
if param_name == 'icons-as-buttons':
|
|
|
|
if param_value.lower() == 'true':
|
2021-12-29 21:55:09 +00:00
|
|
|
_set_icons_as_buttons(base_dir, True)
|
2020-11-14 19:21:11 +00:00
|
|
|
else:
|
2021-12-29 21:55:09 +00:00
|
|
|
_set_icons_as_buttons(base_dir, False)
|
2020-11-14 18:08:46 +00:00
|
|
|
continue
|
2022-01-03 18:37:14 +00:00
|
|
|
if param_name == 'rss-icon-at-top':
|
|
|
|
if param_value.lower() == 'true':
|
2021-12-29 21:55:09 +00:00
|
|
|
_set_rss_icon_at_top(base_dir, True)
|
2020-11-14 19:21:11 +00:00
|
|
|
else:
|
2021-12-29 21:55:09 +00:00
|
|
|
_set_rss_icon_at_top(base_dir, False)
|
2020-11-14 18:08:46 +00:00
|
|
|
continue
|
2022-01-03 18:37:14 +00:00
|
|
|
if param_name == 'publish-button-at-top':
|
|
|
|
if param_value.lower() == 'true':
|
2021-12-29 21:55:09 +00:00
|
|
|
_set_publish_button_at_top(base_dir, True)
|
2020-11-14 19:21:11 +00:00
|
|
|
else:
|
2021-12-29 21:55:09 +00:00
|
|
|
_set_publish_button_at_top(base_dir, False)
|
2020-11-14 18:08:46 +00:00
|
|
|
continue
|
2022-01-26 23:17:53 +00:00
|
|
|
css = set_css_param(css, param_name, param_value)
|
2021-12-25 16:17:53 +00:00
|
|
|
filename = base_dir + '/' + filename
|
2022-06-09 14:46:30 +00:00
|
|
|
with open(filename, 'w+', encoding='utf-8') as cssfile:
|
2021-06-21 22:53:04 +00:00
|
|
|
cssfile.write(css)
|
2019-11-23 13:04:11 +00:00
|
|
|
|
2022-01-03 18:37:14 +00:00
|
|
|
screen_name = (
|
2021-07-05 16:46:32 +00:00
|
|
|
'login', 'follow', 'options', 'search', 'welcome'
|
|
|
|
)
|
2022-01-03 18:37:14 +00:00
|
|
|
for scr in screen_name:
|
|
|
|
if bg_params.get(scr):
|
2022-06-12 12:55:04 +00:00
|
|
|
_set_background_format(base_dir, scr, bg_params[scr])
|
2020-04-04 12:03:28 +00:00
|
|
|
|
2020-07-26 11:59:48 +00:00
|
|
|
|
2022-06-12 12:55:04 +00:00
|
|
|
def _set_background_format(base_dir: str,
|
2022-01-03 18:37:14 +00:00
|
|
|
background_type: 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
|
2022-01-03 18:37:14 +00:00
|
|
|
css_filename = base_dir + '/' + background_type + '.css'
|
2021-12-31 21:18:12 +00:00
|
|
|
if not os.path.isfile(css_filename):
|
2020-07-26 11:59:48 +00:00
|
|
|
return
|
2022-06-09 14:46:30 +00:00
|
|
|
with open(css_filename, 'r', encoding='utf-8') as cssfile:
|
2021-06-21 22:52:04 +00:00
|
|
|
css = cssfile.read()
|
2020-07-26 11:59:48 +00:00
|
|
|
css = css.replace('background.jpg', 'background.' + extension)
|
2022-06-09 14:46:30 +00:00
|
|
|
with open(css_filename, 'w+', encoding='utf-8') as cssfile2:
|
2021-06-21 22:53:04 +00:00
|
|
|
cssfile2.write(css)
|
2020-07-25 16:25:04 +00:00
|
|
|
|
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
def enable_grayscale(base_dir: str) -> None:
|
2020-07-11 09:22:06 +00:00
|
|
|
"""Enables grayscale for the current theme
|
2020-07-10 18:16:33 +00:00
|
|
|
"""
|
2022-01-03 18:37:14 +00:00
|
|
|
theme_files = _get_theme_files()
|
|
|
|
for filename in theme_files:
|
|
|
|
template_filename = base_dir + '/' + filename
|
|
|
|
if not os.path.isfile(template_filename):
|
2020-07-10 18:16:33 +00:00
|
|
|
continue
|
2022-06-09 14:46:30 +00:00
|
|
|
with open(template_filename, 'r', encoding='utf-8') as cssfile:
|
2021-06-21 22:52:04 +00:00
|
|
|
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
|
2022-06-09 14:46:30 +00:00
|
|
|
with open(filename, 'w+', encoding='utf-8') as cssfile:
|
2021-06-21 22:53:04 +00:00
|
|
|
cssfile.write(css)
|
2022-01-03 18:37:14 +00:00
|
|
|
grayscale_filename = base_dir + '/accounts/.grayscale'
|
|
|
|
if not os.path.isfile(grayscale_filename):
|
2022-06-09 14:46:30 +00:00
|
|
|
with open(grayscale_filename, 'w+', encoding='utf-8') as grayfile:
|
2021-06-21 22:53:04 +00:00
|
|
|
grayfile.write(' ')
|
2020-07-11 09:22:06 +00:00
|
|
|
|
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
def disable_grayscale(base_dir: str) -> None:
|
2020-07-11 09:22:06 +00:00
|
|
|
"""Disables grayscale for the current theme
|
|
|
|
"""
|
2022-01-03 18:37:14 +00:00
|
|
|
theme_files = _get_theme_files()
|
|
|
|
for filename in theme_files:
|
|
|
|
template_filename = base_dir + '/' + filename
|
|
|
|
if not os.path.isfile(template_filename):
|
2020-07-11 09:22:06 +00:00
|
|
|
continue
|
2022-06-09 14:46:30 +00:00
|
|
|
with open(template_filename, 'r', encoding='utf-8') as cssfile:
|
2021-06-21 22:52:04 +00:00
|
|
|
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
|
2022-06-09 14:46:30 +00:00
|
|
|
with open(filename, 'w+', encoding='utf-8') as cssfile:
|
2021-06-21 22:53:04 +00:00
|
|
|
cssfile.write(css)
|
2022-01-03 18:37:14 +00:00
|
|
|
grayscale_filename = base_dir + '/accounts/.grayscale'
|
|
|
|
if os.path.isfile(grayscale_filename):
|
2021-09-05 10:17:43 +00:00
|
|
|
try:
|
2022-01-03 18:37:14 +00:00
|
|
|
os.remove(grayscale_filename)
|
2021-11-25 18:42:38 +00:00
|
|
|
except OSError:
|
2021-12-29 21:55:09 +00:00
|
|
|
print('EX: disable_grayscale unable to delete ' +
|
2022-01-03 18:37:14 +00:00
|
|
|
grayscale_filename)
|
2020-07-10 18:16:33 +00:00
|
|
|
|
|
|
|
|
2022-01-26 23:17:53 +00:00
|
|
|
def _set_dyslexic_font(base_dir: str) -> bool:
|
|
|
|
"""sets the dyslexic font if needed
|
|
|
|
"""
|
|
|
|
theme_files = _get_theme_files()
|
|
|
|
for filename in theme_files:
|
|
|
|
template_filename = base_dir + '/' + filename
|
|
|
|
if not os.path.isfile(template_filename):
|
|
|
|
continue
|
2022-06-09 14:46:30 +00:00
|
|
|
with open(template_filename, 'r', encoding='utf-8') as cssfile:
|
2022-01-26 23:17:53 +00:00
|
|
|
css = cssfile.read()
|
|
|
|
css = \
|
|
|
|
set_css_param(css, "*src",
|
|
|
|
"url('./fonts/OpenDyslexic-Regular.woff2" +
|
|
|
|
"') format('woff2')")
|
|
|
|
css = set_css_param(css, "*font-family", "'OpenDyslexic'")
|
|
|
|
filename = base_dir + '/' + filename
|
2022-06-09 14:46:30 +00:00
|
|
|
with open(filename, 'w+', encoding='utf-8') as cssfile:
|
2022-01-26 23:17:53 +00:00
|
|
|
cssfile.write(css)
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
def _set_custom_font(base_dir: str):
|
2020-05-26 20:17:16 +00:00
|
|
|
"""Uses a dictionary to set a theme
|
|
|
|
"""
|
2022-01-03 18:37:14 +00:00
|
|
|
custom_font_ext = None
|
|
|
|
custom_font_type = None
|
|
|
|
font_extension = {
|
2020-05-26 20:17:16 +00:00
|
|
|
'woff': 'woff',
|
|
|
|
'woff2': 'woff2',
|
|
|
|
'otf': 'opentype',
|
|
|
|
'ttf': 'truetype'
|
|
|
|
}
|
2022-01-03 18:37:14 +00:00
|
|
|
for ext, ext_type in font_extension.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):
|
2022-01-03 18:37:14 +00:00
|
|
|
custom_font_ext = ext
|
|
|
|
custom_font_type = ext_type
|
|
|
|
if not custom_font_ext:
|
2020-05-26 20:17:16 +00:00
|
|
|
return
|
|
|
|
|
2022-01-03 18:37:14 +00:00
|
|
|
theme_files = _get_theme_files()
|
|
|
|
for filename in theme_files:
|
|
|
|
template_filename = base_dir + '/' + filename
|
|
|
|
if not os.path.isfile(template_filename):
|
2020-05-26 20:17:16 +00:00
|
|
|
continue
|
2022-06-09 14:46:30 +00:00
|
|
|
with open(template_filename, 'r', encoding='utf-8') as cssfile:
|
2021-06-21 22:52:04 +00:00
|
|
|
css = cssfile.read()
|
2020-05-26 20:17:16 +00:00
|
|
|
css = \
|
2022-01-26 23:17:53 +00:00
|
|
|
set_css_param(css, "*src",
|
2021-12-29 21:55:09 +00:00
|
|
|
"url('./fonts/custom." +
|
2022-01-03 18:37:14 +00:00
|
|
|
custom_font_ext +
|
2021-12-29 21:55:09 +00:00
|
|
|
"') format('" +
|
2022-01-03 18:37:14 +00:00
|
|
|
custom_font_type + "')")
|
2022-01-26 23:17:53 +00:00
|
|
|
css = set_css_param(css, "*font-family", "'CustomFont'")
|
2022-02-27 14:28:03 +00:00
|
|
|
css = set_css_param(css, "header-font", "'CustomFont'")
|
2021-12-25 16:17:53 +00:00
|
|
|
filename = base_dir + '/' + filename
|
2022-06-09 14:46:30 +00:00
|
|
|
with open(filename, 'w+', encoding='utf-8') as cssfile:
|
2021-06-21 22:53:04 +00:00
|
|
|
cssfile.write(css)
|
2020-05-26 20:17:16 +00:00
|
|
|
|
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
def set_theme_from_designer(base_dir: str, theme_name: str, domain: str,
|
2022-01-03 18:37:14 +00:00
|
|
|
theme_params: {},
|
2021-12-29 21:55:09 +00:00
|
|
|
allow_local_network_access: bool,
|
2022-01-26 23:17:53 +00:00
|
|
|
system_language: str,
|
|
|
|
dyslexic_font: bool):
|
2022-01-03 18:37:14 +00:00
|
|
|
custom_theme_filename = base_dir + '/accounts/theme.json'
|
|
|
|
save_json(theme_params, custom_theme_filename)
|
2021-12-29 21:55:09 +00:00
|
|
|
set_theme(base_dir, theme_name, domain,
|
2022-01-26 23:17:53 +00:00
|
|
|
allow_local_network_access, system_language,
|
2022-04-05 11:10:08 +00:00
|
|
|
dyslexic_font, False)
|
2021-12-05 12:52:59 +00:00
|
|
|
|
|
|
|
|
2022-06-12 12:55:04 +00:00
|
|
|
def reset_theme_designer_settings(base_dir: str) -> None:
|
2021-12-05 12:52:59 +00:00
|
|
|
"""Resets the theme designer settings
|
|
|
|
"""
|
2022-01-03 18:37:14 +00:00
|
|
|
custom_variables_file = base_dir + '/accounts/theme.json'
|
|
|
|
if os.path.isfile(custom_variables_file):
|
2021-12-05 12:52:59 +00:00
|
|
|
try:
|
2022-01-03 18:37:14 +00:00
|
|
|
os.remove(custom_variables_file)
|
2021-12-05 12:52:59 +00:00
|
|
|
except OSError:
|
|
|
|
print('EX: unable to remove theme designer settings on reset')
|
2020-11-14 17:34:11 +00:00
|
|
|
|
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
def _read_variables_file(base_dir: str, theme_name: str,
|
2022-01-03 18:37:14 +00:00
|
|
|
variables_file: str,
|
2021-12-29 21:55:09 +00:00
|
|
|
allow_local_network_access: bool) -> None:
|
2021-12-04 19:39:00 +00:00
|
|
|
"""Reads variables from a file in the theme directory
|
|
|
|
"""
|
2022-01-03 18:37:14 +00:00
|
|
|
theme_params = load_json(variables_file, 0)
|
|
|
|
if not theme_params:
|
2021-12-04 19:39:00 +00:00
|
|
|
return
|
2021-12-05 11:03:25 +00:00
|
|
|
|
|
|
|
# set custom theme parameters
|
2022-01-03 18:37:14 +00:00
|
|
|
custom_variables_file = base_dir + '/accounts/theme.json'
|
|
|
|
if os.path.isfile(custom_variables_file):
|
|
|
|
custom_theme_params = load_json(custom_variables_file, 0)
|
|
|
|
if custom_theme_params:
|
|
|
|
for variable_name, value in custom_theme_params.items():
|
|
|
|
theme_params[variable_name] = value
|
|
|
|
|
|
|
|
bg_params = {
|
2021-12-05 11:03:25 +00:00
|
|
|
"login": "jpg",
|
|
|
|
"follow": "jpg",
|
|
|
|
"options": "jpg",
|
|
|
|
"search": "jpg"
|
|
|
|
}
|
2022-01-03 18:37:14 +00:00
|
|
|
_set_theme_from_dict(base_dir, theme_name, theme_params, bg_params,
|
2021-12-29 21:55:09 +00:00
|
|
|
allow_local_network_access)
|
2021-12-04 19:39:00 +00:00
|
|
|
|
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
def _set_theme_default(base_dir: str, allow_local_network_access: bool):
|
2020-07-26 11:45:09 +00:00
|
|
|
name = 'default'
|
2021-12-29 21:55:09 +00:00
|
|
|
_remove_theme(base_dir)
|
|
|
|
_set_theme_in_config(base_dir, name)
|
2022-04-05 11:29:06 +00:00
|
|
|
|
|
|
|
variables_file = base_dir + '/theme/' + name + '/theme.json'
|
|
|
|
if os.path.isfile(variables_file):
|
|
|
|
_read_variables_file(base_dir, name, variables_file,
|
|
|
|
allow_local_network_access)
|
|
|
|
else:
|
|
|
|
bg_params = {
|
|
|
|
"login": "jpg",
|
|
|
|
"follow": "jpg",
|
|
|
|
"options": "jpg",
|
|
|
|
"search": "jpg"
|
|
|
|
}
|
|
|
|
theme_params = {
|
|
|
|
"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"
|
|
|
|
}
|
|
|
|
_set_theme_from_dict(base_dir, name, theme_params, bg_params,
|
|
|
|
allow_local_network_access)
|
2020-05-29 10:00:05 +00:00
|
|
|
|
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
def _set_theme_fonts(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()
|
2022-01-03 18:37:14 +00:00
|
|
|
fonts_dir = base_dir + '/fonts'
|
|
|
|
theme_fonts_dir = \
|
2021-12-25 23:35:50 +00:00
|
|
|
base_dir + '/theme/' + theme_name_lower + '/fonts'
|
2022-01-03 18:37:14 +00:00
|
|
|
if not os.path.isdir(theme_fonts_dir):
|
2020-11-15 09:55:49 +00:00
|
|
|
return
|
2022-01-03 18:37:14 +00:00
|
|
|
for _, _, files in os.walk(theme_fonts_dir):
|
2020-11-15 09:55:49 +00:00
|
|
|
for filename in files:
|
|
|
|
if filename.endswith('.woff2') or \
|
|
|
|
filename.endswith('.woff') or \
|
|
|
|
filename.endswith('.ttf') or \
|
|
|
|
filename.endswith('.otf'):
|
2022-01-03 18:37:14 +00:00
|
|
|
dest_filename = fonts_dir + '/' + filename
|
|
|
|
if os.path.isfile(dest_filename):
|
2020-11-15 09:55:49 +00:00
|
|
|
# font already exists in the destination location
|
|
|
|
continue
|
2022-01-03 18:37:14 +00:00
|
|
|
copyfile(theme_fonts_dir + '/' + filename,
|
|
|
|
dest_filename)
|
2020-11-15 09:55:49 +00:00
|
|
|
break
|
|
|
|
|
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
def get_text_mode_banner(base_dir: str) -> str:
|
2021-02-05 19:50:09 +00:00
|
|
|
"""Returns the banner used for shell browsers, like Lynx
|
|
|
|
"""
|
2021-12-31 21:18:12 +00:00
|
|
|
text_mode_banner_filename = base_dir + '/accounts/banner.txt'
|
|
|
|
if os.path.isfile(text_mode_banner_filename):
|
2022-06-09 14:46:30 +00:00
|
|
|
with open(text_mode_banner_filename, 'r',
|
|
|
|
encoding='utf-8') as fp_text:
|
2022-01-03 18:37:14 +00:00
|
|
|
banner_str = fp_text.read()
|
|
|
|
if banner_str:
|
|
|
|
return banner_str.replace('\n', '<br>')
|
2021-02-05 19:50:09 +00:00
|
|
|
return None
|
|
|
|
|
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
def get_text_mode_logo(base_dir: str) -> str:
|
2021-02-06 14:30:18 +00:00
|
|
|
"""Returns the login screen logo used for shell browsers, like Lynx
|
|
|
|
"""
|
2022-01-03 18:37:14 +00:00
|
|
|
text_mode_logo_filename = base_dir + '/accounts/logo.txt'
|
|
|
|
if not os.path.isfile(text_mode_logo_filename):
|
|
|
|
text_mode_logo_filename = base_dir + '/img/logo.txt'
|
|
|
|
|
2022-06-09 14:46:30 +00:00
|
|
|
with open(text_mode_logo_filename, 'r', encoding='utf-8') as fp_text:
|
2022-01-03 18:37:14 +00:00
|
|
|
logo_str = fp_text.read()
|
|
|
|
if logo_str:
|
|
|
|
return logo_str.replace('\n', '<br>')
|
2021-02-06 14:30:18 +00:00
|
|
|
return None
|
|
|
|
|
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
def _set_text_mode_theme(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
|
2022-01-03 18:37:14 +00:00
|
|
|
text_mode_logo_filename = \
|
2021-12-25 16:17:53 +00:00
|
|
|
base_dir + '/theme/' + name + '/logo.txt'
|
2022-01-03 18:37:14 +00:00
|
|
|
if os.path.isfile(text_mode_logo_filename):
|
2021-02-06 18:51:36 +00:00
|
|
|
try:
|
2022-01-03 18:37:14 +00:00
|
|
|
copyfile(text_mode_logo_filename,
|
2021-12-25 16:17:53 +00:00
|
|
|
base_dir + '/accounts/logo.txt')
|
2021-11-25 22:22:54 +00:00
|
|
|
except OSError:
|
2021-12-29 21:55:09 +00:00
|
|
|
print('EX: _set_text_mode_theme unable to copy ' +
|
2022-01-03 18:37:14 +00:00
|
|
|
text_mode_logo_filename + ' ' +
|
2021-12-25 16:17:53 +00:00
|
|
|
base_dir + '/accounts/logo.txt')
|
2021-02-06 18:51:36 +00:00
|
|
|
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-12-29 21:55:09 +00:00
|
|
|
print('EX: _set_text_mode_theme unable to copy ' +
|
2021-12-25 16:17:53 +00:00
|
|
|
base_dir + '/img/logo.txt ' +
|
|
|
|
base_dir + '/accounts/logo.txt')
|
2021-02-06 18:51:36 +00:00
|
|
|
|
2021-02-07 15:32:15 +00:00
|
|
|
# set the text mode banner which appears in browsers such as Lynx
|
2021-12-31 21:18:12 +00:00
|
|
|
text_mode_banner_filename = \
|
2021-12-25 16:17:53 +00:00
|
|
|
base_dir + '/theme/' + name + '/banner.txt'
|
|
|
|
if os.path.isfile(base_dir + '/accounts/banner.txt'):
|
2021-09-05 10:17:43 +00:00
|
|
|
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-12-29 21:55:09 +00:00
|
|
|
print('EX: _set_text_mode_theme unable to delete ' +
|
2021-12-25 16:17:53 +00:00
|
|
|
base_dir + '/accounts/banner.txt')
|
2021-12-31 21:18:12 +00:00
|
|
|
if os.path.isfile(text_mode_banner_filename):
|
2021-02-05 19:39:21 +00:00
|
|
|
try:
|
2021-12-31 21:18:12 +00:00
|
|
|
copyfile(text_mode_banner_filename,
|
2021-12-25 16:17:53 +00:00
|
|
|
base_dir + '/accounts/banner.txt')
|
2021-11-25 18:42:38 +00:00
|
|
|
except OSError:
|
2021-12-29 21:55:09 +00:00
|
|
|
print('EX: _set_text_mode_theme unable to copy ' +
|
2021-12-31 21:18:12 +00:00
|
|
|
text_mode_banner_filename + ' ' +
|
2021-12-25 16:17:53 +00:00
|
|
|
base_dir + '/accounts/banner.txt')
|
2021-02-05 19:39:21 +00:00
|
|
|
|
2021-02-07 15:32:15 +00:00
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
def _set_theme_images(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
|
|
|
|
2022-01-03 18:37:14 +00:00
|
|
|
profile_image_filename = \
|
2021-12-25 23:35:50 +00:00
|
|
|
base_dir + '/theme/' + theme_name_lower + '/image.png'
|
2021-12-31 21:18:12 +00:00
|
|
|
banner_filename = \
|
2021-12-25 23:35:50 +00:00
|
|
|
base_dir + '/theme/' + theme_name_lower + '/banner.png'
|
2022-01-03 18:37:14 +00:00
|
|
|
search_banner_filename = \
|
2021-12-25 23:35:50 +00:00
|
|
|
base_dir + '/theme/' + theme_name_lower + '/search_banner.png'
|
2022-01-03 18:37:14 +00:00
|
|
|
left_col_image_filename = \
|
2021-12-25 23:35:50 +00:00
|
|
|
base_dir + '/theme/' + theme_name_lower + '/left_col_image.png'
|
2022-01-03 18:37:14 +00:00
|
|
|
right_col_image_filename = \
|
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-29 21:55:09 +00:00
|
|
|
_set_text_mode_theme(base_dir, theme_name_lower)
|
2021-02-07 15:32:15 +00:00
|
|
|
|
2022-01-03 18:37:14 +00:00
|
|
|
background_names = ('login', 'shares', 'delete', 'follow',
|
|
|
|
'options', 'block', 'search', 'calendar',
|
|
|
|
'welcome')
|
2021-12-26 14:26:16 +00:00
|
|
|
extensions = get_image_extensions()
|
2020-07-25 14:03:09 +00:00
|
|
|
|
2022-01-03 18:37:14 +00:00
|
|
|
for _, dirs, _ in os.walk(base_dir + '/accounts'):
|
2020-07-25 09:55:11 +00:00
|
|
|
for acct in dirs:
|
2021-12-26 18:46:43 +00:00
|
|
|
if not is_account_dir(acct):
|
2020-07-25 09:55:11 +00:00
|
|
|
continue
|
2022-01-03 18:37:14 +00:00
|
|
|
account_dir = os.path.join(base_dir + '/accounts', acct)
|
2020-07-25 09:55:11 +00:00
|
|
|
|
2022-01-03 18:37:14 +00:00
|
|
|
for background_type in background_names:
|
2020-07-25 16:12:14 +00:00
|
|
|
for ext in extensions:
|
2021-12-25 23:35:50 +00:00
|
|
|
if theme_name_lower == 'default':
|
2022-01-03 18:37:14 +00:00
|
|
|
background_image_filename = \
|
2021-12-25 16:17:53 +00:00
|
|
|
base_dir + '/theme/default/' + \
|
2022-01-03 18:37:14 +00:00
|
|
|
background_type + '_background.' + ext
|
2020-07-25 16:12:14 +00:00
|
|
|
else:
|
2022-01-03 18:37:14 +00:00
|
|
|
background_image_filename = \
|
2021-12-25 23:35:50 +00:00
|
|
|
base_dir + '/theme/' + theme_name_lower + '/' + \
|
2022-01-03 18:37:14 +00:00
|
|
|
background_type + '_background' + '.' + ext
|
2020-07-25 16:12:14 +00:00
|
|
|
|
2022-01-03 18:37:14 +00:00
|
|
|
if os.path.isfile(background_image_filename):
|
2020-07-25 16:12:14 +00:00
|
|
|
try:
|
2022-01-03 18:37:14 +00:00
|
|
|
copyfile(background_image_filename,
|
2021-12-25 16:17:53 +00:00
|
|
|
base_dir + '/accounts/' +
|
2022-01-03 18:37:14 +00:00
|
|
|
background_type + '-background.' + ext)
|
2020-07-25 17:56:06 +00:00
|
|
|
continue
|
2021-11-25 18:42:38 +00:00
|
|
|
except OSError:
|
2021-12-29 21:55:09 +00:00
|
|
|
print('EX: _set_theme_images unable to copy ' +
|
2022-01-03 18:37:14 +00:00
|
|
|
background_image_filename)
|
2020-07-25 16:12:14 +00:00
|
|
|
# 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/' +
|
2022-01-03 18:37:14 +00:00
|
|
|
background_type + '-background.' + ext):
|
2020-07-25 16:12:14 +00:00
|
|
|
try:
|
2021-12-25 16:17:53 +00:00
|
|
|
os.remove(base_dir + '/accounts/' +
|
2022-01-03 18:37:14 +00:00
|
|
|
background_type + '-background.' + ext)
|
2021-11-25 18:42:38 +00:00
|
|
|
except OSError:
|
2021-12-29 21:55:09 +00:00
|
|
|
print('EX: _set_theme_images unable to delete ' +
|
2021-12-25 16:17:53 +00:00
|
|
|
base_dir + '/accounts/' +
|
2022-01-03 18:37:14 +00:00
|
|
|
background_type + '-background.' + ext)
|
2020-07-25 10:03:03 +00:00
|
|
|
|
2022-01-03 18:37:14 +00:00
|
|
|
if os.path.isfile(profile_image_filename) and \
|
2021-12-31 21:18:12 +00:00
|
|
|
os.path.isfile(banner_filename):
|
2020-05-28 21:39:41 +00:00
|
|
|
try:
|
2022-01-03 18:37:14 +00:00
|
|
|
copyfile(profile_image_filename,
|
|
|
|
account_dir + '/image.png')
|
2021-11-25 18:42:38 +00:00
|
|
|
except OSError:
|
2021-12-29 21:55:09 +00:00
|
|
|
print('EX: _set_theme_images unable to copy ' +
|
2022-01-03 18:37:14 +00:00
|
|
|
profile_image_filename)
|
2020-06-10 15:09:14 +00:00
|
|
|
|
|
|
|
try:
|
2021-12-31 21:18:12 +00:00
|
|
|
copyfile(banner_filename,
|
2022-01-03 18:37:14 +00:00
|
|
|
account_dir + '/banner.png')
|
2021-11-25 22:22:54 +00:00
|
|
|
except OSError:
|
2021-12-29 21:55:09 +00:00
|
|
|
print('EX: _set_theme_images unable to copy ' +
|
2021-12-31 21:18:12 +00:00
|
|
|
banner_filename)
|
2020-05-28 21:39:41 +00:00
|
|
|
|
2020-06-10 15:09:14 +00:00
|
|
|
try:
|
2022-01-03 18:37:14 +00:00
|
|
|
if os.path.isfile(search_banner_filename):
|
|
|
|
copyfile(search_banner_filename,
|
|
|
|
account_dir + '/search_banner.png')
|
2021-11-25 18:42:38 +00:00
|
|
|
except OSError:
|
2021-12-29 21:55:09 +00:00
|
|
|
print('EX: _set_theme_images unable to copy ' +
|
2022-01-03 18:37:14 +00:00
|
|
|
search_banner_filename)
|
2020-06-10 15:09:14 +00:00
|
|
|
|
2020-10-03 10:09:21 +00:00
|
|
|
try:
|
2022-01-03 18:37:14 +00:00
|
|
|
if os.path.isfile(left_col_image_filename):
|
|
|
|
copyfile(left_col_image_filename,
|
|
|
|
account_dir + '/left_col_image.png')
|
|
|
|
elif os.path.isfile(account_dir +
|
2021-10-29 18:48:15 +00:00
|
|
|
'/left_col_image.png'):
|
|
|
|
try:
|
2022-01-03 18:37:14 +00:00
|
|
|
os.remove(account_dir + '/left_col_image.png')
|
2021-11-25 18:42:38 +00:00
|
|
|
except OSError:
|
2021-12-29 21:55:09 +00:00
|
|
|
print('EX: _set_theme_images unable to delete ' +
|
2022-01-03 18:37:14 +00:00
|
|
|
account_dir + '/left_col_image.png')
|
2021-11-25 22:22:54 +00:00
|
|
|
except OSError:
|
2021-12-29 21:55:09 +00:00
|
|
|
print('EX: _set_theme_images unable to copy ' +
|
2022-01-03 18:37:14 +00:00
|
|
|
left_col_image_filename)
|
2020-10-03 10:09:21 +00:00
|
|
|
|
|
|
|
try:
|
2022-01-03 18:37:14 +00:00
|
|
|
if os.path.isfile(right_col_image_filename):
|
|
|
|
copyfile(right_col_image_filename,
|
|
|
|
account_dir + '/right_col_image.png')
|
2020-10-03 10:11:26 +00:00
|
|
|
else:
|
2022-01-03 18:37:14 +00:00
|
|
|
if os.path.isfile(account_dir +
|
2020-10-03 10:11:26 +00:00
|
|
|
'/right_col_image.png'):
|
2021-09-05 10:17:43 +00:00
|
|
|
try:
|
2022-01-03 18:37:14 +00:00
|
|
|
os.remove(account_dir + '/right_col_image.png')
|
2021-11-25 18:42:38 +00:00
|
|
|
except OSError:
|
2021-12-29 21:55:09 +00:00
|
|
|
print('EX: _set_theme_images ' +
|
|
|
|
'unable to delete ' +
|
2022-01-03 18:37:14 +00:00
|
|
|
account_dir + '/right_col_image.png')
|
2021-11-25 22:22:54 +00:00
|
|
|
except OSError:
|
2021-12-29 21:55:09 +00:00
|
|
|
print('EX: _set_theme_images unable to copy ' +
|
2022-01-03 18:37:14 +00:00
|
|
|
right_col_image_filename)
|
2020-12-13 22:13:45 +00:00
|
|
|
break
|
2020-10-03 10:09:21 +00:00
|
|
|
|
2020-05-28 21:39:41 +00:00
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
def set_news_avatar(base_dir: str, name: str,
|
|
|
|
http_prefix: str,
|
|
|
|
domain: str, domain_full: str) -> None:
|
2020-10-13 21:38:19 +00:00
|
|
|
"""Sets the avatar for the news account
|
|
|
|
"""
|
|
|
|
nickname = 'news'
|
2022-01-03 18:37:14 +00:00
|
|
|
new_filename = base_dir + '/theme/' + name + '/icons/avatar_news.png'
|
|
|
|
if not os.path.isfile(new_filename):
|
|
|
|
new_filename = base_dir + '/theme/default/icons/avatar_news.png'
|
|
|
|
if not os.path.isfile(new_filename):
|
2020-10-13 21:38:19 +00:00
|
|
|
return
|
2022-01-03 18:37:14 +00:00
|
|
|
avatar_filename = \
|
2021-12-26 10:19:59 +00:00
|
|
|
local_actor_url(http_prefix, domain_full, nickname) + '.png'
|
2022-01-03 18:37:14 +00:00
|
|
|
avatar_filename = avatar_filename.replace('/', '-')
|
|
|
|
filename = base_dir + '/cache/avatars/' + avatar_filename
|
2020-10-13 21:45:53 +00:00
|
|
|
|
2020-10-13 21:38:19 +00:00
|
|
|
if os.path.isfile(filename):
|
2021-09-05 10:17:43 +00:00
|
|
|
try:
|
|
|
|
os.remove(filename)
|
2021-11-25 22:22:54 +00:00
|
|
|
except OSError:
|
2021-12-29 21:55:09 +00:00
|
|
|
print('EX: set_news_avatar unable to delete ' + filename)
|
2021-12-25 16:17:53 +00:00
|
|
|
if os.path.isdir(base_dir + '/cache/avatars'):
|
2022-01-03 18:37:14 +00:00
|
|
|
copyfile(new_filename, filename)
|
|
|
|
account_dir = acct_dir(base_dir, nickname, domain)
|
|
|
|
copyfile(new_filename, account_dir + '/avatar.png')
|
2020-10-13 21:38:19 +00:00
|
|
|
|
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
def _set_clear_cache_flag(base_dir: str) -> None:
|
2021-02-23 14:01:22 +00:00
|
|
|
"""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
|
2022-01-03 18:37:14 +00:00
|
|
|
flag_filename = base_dir + '/accounts/.clear_cache'
|
2022-06-09 14:46:30 +00:00
|
|
|
with open(flag_filename, 'w+', encoding='utf-8') as fp_flag:
|
2022-01-03 18:37:14 +00:00
|
|
|
fp_flag.write('\n')
|
2021-02-23 14:01:22 +00:00
|
|
|
|
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
def set_theme(base_dir: str, name: str, domain: str,
|
2022-01-26 23:17:53 +00:00
|
|
|
allow_local_network_access: bool, system_language: str,
|
2022-04-05 11:10:08 +00:00
|
|
|
dyslexic_font: bool, designer_reset: bool) -> 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
|
|
|
|
2022-01-03 18:37:14 +00:00
|
|
|
prev_theme_name = get_theme(base_dir)
|
2021-12-05 11:03:25 +00:00
|
|
|
|
|
|
|
# if the theme has changed then remove any custom settings
|
2022-04-05 11:10:08 +00:00
|
|
|
if prev_theme_name != name or designer_reset:
|
2022-06-12 12:55:04 +00:00
|
|
|
reset_theme_designer_settings(base_dir)
|
2021-12-05 11:03:25 +00:00
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
_remove_theme(base_dir)
|
2020-05-28 21:30:40 +00:00
|
|
|
|
2022-03-13 10:29:27 +00:00
|
|
|
# has the theme changed?
|
2021-12-29 21:55:09 +00:00
|
|
|
themes = get_themes_list(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:
|
2022-01-03 18:37:14 +00:00
|
|
|
if prev_theme_name:
|
2022-04-05 11:14:37 +00:00
|
|
|
if prev_theme_name.lower() != theme_name_lower or \
|
|
|
|
designer_reset:
|
2020-05-28 21:30:40 +00:00
|
|
|
# change the banner and profile image
|
|
|
|
# to the default for the theme
|
2021-12-29 21:55:09 +00:00
|
|
|
_set_theme_images(base_dir, name)
|
|
|
|
_set_theme_fonts(base_dir, name)
|
2020-05-28 09:11:21 +00:00
|
|
|
result = True
|
2022-03-13 10:29:27 +00:00
|
|
|
break
|
2020-05-28 09:11:21 +00:00
|
|
|
|
|
|
|
if not result:
|
2020-05-27 11:02:51 +00:00
|
|
|
# default
|
2021-12-29 21:55:09 +00:00
|
|
|
_set_theme_default(base_dir, allow_local_network_access)
|
2020-05-27 11:02:51 +00:00
|
|
|
result = True
|
2020-05-28 09:11:21 +00:00
|
|
|
|
2022-03-13 10:29:27 +00:00
|
|
|
# read theme settings from a json file in the theme directory
|
2022-01-03 18:37:14 +00:00
|
|
|
variables_file = base_dir + '/theme/' + name + '/theme.json'
|
|
|
|
if os.path.isfile(variables_file):
|
|
|
|
_read_variables_file(base_dir, name, variables_file,
|
2021-12-29 21:55:09 +00:00
|
|
|
allow_local_network_access)
|
2020-11-14 17:34:11 +00:00
|
|
|
|
2022-01-26 23:17:53 +00:00
|
|
|
if dyslexic_font:
|
|
|
|
_set_dyslexic_font(base_dir)
|
|
|
|
else:
|
|
|
|
_set_custom_font(base_dir)
|
2020-10-09 19:28:09 +00:00
|
|
|
|
|
|
|
# set the news avatar
|
2022-01-03 18:37:14 +00:00
|
|
|
news_avatar_theme_filename = \
|
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):
|
2022-01-03 18:37:14 +00:00
|
|
|
if os.path.isfile(news_avatar_theme_filename):
|
|
|
|
news_avatar_filename = \
|
2021-12-25 16:17:53 +00:00
|
|
|
base_dir + '/accounts/news@' + domain + '/avatar.png'
|
2022-01-03 18:37:14 +00:00
|
|
|
copyfile(news_avatar_theme_filename, news_avatar_filename)
|
2020-10-09 19:28:09 +00:00
|
|
|
|
2022-01-03 18:37:14 +00:00
|
|
|
grayscale_filename = base_dir + '/accounts/.grayscale'
|
|
|
|
if os.path.isfile(grayscale_filename):
|
2021-12-29 21:55:09 +00:00
|
|
|
enable_grayscale(base_dir)
|
2020-07-10 18:08:45 +00:00
|
|
|
else:
|
2021-12-29 21:55:09 +00:00
|
|
|
disable_grayscale(base_dir)
|
2020-11-14 18:32:34 +00:00
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
_copy_theme_help_files(base_dir, name, system_language)
|
|
|
|
_set_theme_in_config(base_dir, name)
|
|
|
|
_set_clear_cache_flag(base_dir)
|
2020-05-26 20:17:16 +00:00
|
|
|
return result
|
2021-05-30 11:06:15 +00:00
|
|
|
|
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
def update_default_themes_list(base_dir: str) -> None:
|
2021-05-30 11:06:15 +00:00
|
|
|
"""Recreates the list of default themes
|
|
|
|
"""
|
2021-12-29 21:55:09 +00:00
|
|
|
theme_names = get_themes_list(base_dir)
|
2022-01-03 18:37:14 +00:00
|
|
|
default_themes_filename = base_dir + '/defaultthemes.txt'
|
2022-06-09 14:46:30 +00:00
|
|
|
with open(default_themes_filename, 'w+', encoding='utf-8') as fp_def:
|
2021-12-25 23:35:50 +00:00
|
|
|
for name in theme_names:
|
2022-01-03 18:37:14 +00:00
|
|
|
fp_def.write(name + '\n')
|
2021-09-13 18:37:51 +00:00
|
|
|
|
|
|
|
|
2021-12-29 21:55:09 +00:00
|
|
|
def scan_themes_for_scripts(base_dir: str) -> bool:
|
2021-09-13 18:37:51 +00:00
|
|
|
"""Scans the theme directory for any svg files containing scripts
|
|
|
|
"""
|
2022-01-03 18:37:14 +00:00
|
|
|
for subdir, _, files in os.walk(base_dir + '/theme'):
|
|
|
|
for fname in files:
|
|
|
|
if not fname.endswith('.svg'):
|
2021-09-13 18:37:51 +00:00
|
|
|
continue
|
2022-01-03 18:37:14 +00:00
|
|
|
svg_filename = os.path.join(subdir, fname)
|
2021-09-13 18:37:51 +00:00
|
|
|
content = ''
|
2022-06-09 14:46:30 +00:00
|
|
|
with open(svg_filename, 'r', encoding='utf-8') as fp_svg:
|
2022-01-03 18:37:14 +00:00
|
|
|
content = fp_svg.read()
|
|
|
|
svg_dangerous = dangerous_svg(content, False)
|
|
|
|
if svg_dangerous:
|
|
|
|
print('svg file contains script: ' + svg_filename)
|
2021-09-13 18:50:02 +00:00
|
|
|
return True
|
2021-09-13 18:37:51 +00:00
|
|
|
# deliberately no break - should resursively scan
|
2021-09-13 18:50:02 +00:00
|
|
|
return False
|