<?php /*

 Composr
 Copyright (c) Christopher Graham, 2004-2024

 See docs/LICENSE.md for full licensing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/**
 * @license    http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright  Christopher Graham
 * @package    core_themeing
 */

/**
 * Module page class.
 */
class Module_admin_themes
{
    /**
     * Find details of the module.
     *
     * @return ?array Map of module info (null: module is disabled)
     */
    public function info() : ?array
    {
        $info = [];
        $info['author'] = 'Chris Graham';
        $info['organisation'] = 'Composr';
        $info['hacked_by'] = null;
        $info['hack_version'] = null;
        $info['version'] = 6;
        $info['locked'] = true;
        $info['update_require_upgrade'] = true;
        $info['min_cms_version'] = 11.0;
        $info['addon'] = 'core_themeing';
        return $info;
    }

    /**
     * Uninstall the module.
     */
    public function uninstall()
    {
        $tables = [
            'theme_images',
            'theme_template_relations',
            'theme_screen_tree',
        ];
        $GLOBALS['SITE_DB']->drop_table_if_exists($tables);

        require_code('files');
        $langs = find_all_langs(true);
        foreach (array_keys($langs) as $lang) {
            deldir_contents(get_custom_file_base() . '/themes/default/templates_cached/' . $lang, true);
        }
        // *_custom purposely left
    }

    /**
     * Install the module.
     *
     * @param  ?integer $upgrade_from What version we're upgrading from (null: new install)
     * @param  ?integer $upgrade_from_hack What hack version we're upgrading from (null: new-install/not-upgrading-from-a-hacked-version)
     */
    public function install(?int $upgrade_from = null, ?int $upgrade_from_hack = null)
    {
        require_code('themes2');

        if ($upgrade_from === null) {
            $GLOBALS['SITE_DB']->create_table('theme_images', [
                'id' => '*SHORT_TEXT',
                'theme' => '*MINIID_TEXT',
                'url' => 'URLPATH',
                'lang' => '*LANGUAGE_NAME',
            ], false, false, true);
            $GLOBALS['SITE_DB']->create_index('theme_images', 'theme', ['theme', 'lang']);
        }

        if (($upgrade_from === null) || ($upgrade_from < 5)) {
            $GLOBALS['SITE_DB']->create_table('theme_template_relations', [
                'rel_a' => '*ID_TEXT',
                'rel_b' => '*ID_TEXT',
            ]);

            $GLOBALS['SITE_DB']->create_table('theme_screen_tree', [
                'id' => '*AUTO',
                'page_link' => 'SHORT_TEXT',
                'json_tree' => 'LONG_TEXT',
            ]);
            $GLOBALS['SITE_DB']->create_index('theme_screen_tree', 'page_link', ['page_link']);
        }

        if (($upgrade_from !== null) && ($upgrade_from < 5)) { // LEGACY
            $GLOBALS['SITE_DB']->alter_table_field('theme_images', 'path', 'URLPATH', 'url');
        }
    }

    /**
     * Find entry-points available within this module.
     *
     * @param  boolean $check_perms Whether to check permissions
     * @param  ?MEMBER $member_id The member to check permissions as (null: current user)
     * @param  boolean $support_crosslinks Whether to allow cross links to other modules (identifiable via a full-page-link rather than a screen-name)
     * @param  boolean $be_deferential Whether to avoid any entry-point (or even return null to disable the page in the Sitemap) if we know another module, or page_group, is going to link to that entry-point. Note that "!" and "browse" entry points are automatically merged with container page nodes (likely called by page-groupings) as appropriate.
     * @return ?array A map of entry points (screen-name=>language-code/string or screen-name=>[language-code/string, icon-theme-image]) (null: disabled)
     */
    public function get_entry_points(bool $check_perms = true, ?int $member_id = null, bool $support_crosslinks = true, bool $be_deferential = false) : ?array
    {
        $ret = [
            'browse' => ['MANAGE_THEMES', 'menu/adminzone/style/themes/themes'],
            'add_theme' => ['ADD_THEME', 'admin/add'],
            'tempcode_tester' => ['TEMPCODE_TESTER', 'admin/tool'],
        ];

        if ($support_crosslinks) {
            if (addon_installed('themewizard')) {
                $ret['_SEARCH:admin_themewizard:browse'] = ['THEMEWIZARD', 'menu/adminzone/style/themes/themewizard'];
            }
        }

        return $ret;
    }

    public $title;
    public $id;
    public $theme;

    /**
     * Module pre-run function. Allows us to know metadata for <head> before we start streaming output.
     *
     * @return ?Tempcode Tempcode indicating some kind of exceptional output (null: none)
     */
    public function pre_run() : ?object
    {
        require_code('form_templates'); // Needs to run high so that the anti-click-hacking header is sent

        $type = get_param_string('type', 'browse');

        require_lang('themes');

        $this->theme = get_param_string('theme', $GLOBALS['FORUM_DRIVER']->get_theme(''));

        if ($type == 'browse') {
            set_helper_panel_tutorial('tut_themes');

            set_helper_panel_text(comcode_lang_string('DOC_THEMES'));

            $this->title = get_screen_title('MANAGE_THEMES');
        }

        if ($type == 'add_theme') {
            breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('MANAGE_THEMES')]]);

            $this->title = get_screen_title('ADD_THEME');
        }
        if ($type == '_add_theme') {
            breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('MANAGE_THEMES')]]);
            breadcrumb_set_self(do_lang_tempcode('DONE'));

            breadcrumb_set_self(do_lang_tempcode('DONE'));

            $this->title = get_screen_title('ADD_THEME');
        }

        if ($type == 'edit_templates') {
            breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('MANAGE_THEMES')]]);

            $this->title = get_screen_title('_EDIT_TEMPLATES', true, [escape_html($this->theme)]);

            breadcrumb_set_self(do_lang_tempcode('EDIT_TEMPLATES'));
        }

        if ($type == 'manage_images') {
            breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('MANAGE_THEMES')]]);
            breadcrumb_set_self(do_lang_tempcode('CHOOSE_THEME_IMAGE'));

            $this->title = get_screen_title('EDIT_THEME_IMAGES');
        }

        if ($type == 'add_image') {
            breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('MANAGE_THEMES')]]);

            $this->title = get_screen_title('ADD_THEME_IMAGE');
        }
        if ($type == '_add_image') {
            breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('MANAGE_THEMES')], ['_SELF:_SELF:add_image:theme=' . post_param_string('theme'), do_lang_tempcode('ADD_THEME_IMAGE')]]);
            breadcrumb_set_self(do_lang_tempcode('DONE'));

            breadcrumb_set_self(do_lang_tempcode('DONE'));

            $this->title = get_screen_title('ADD_THEME_IMAGE');
        }

        if ($type == 'edit_image') {
            $theme = $this->theme;

            $url = get_param_string('url', '', INPUT_FILTER_URL_GENERAL);
            if ($url != '') {
                $url = preg_replace('#\.pagespeed\..*$#', '', $url); // Support for working around https://developers.google.com/speed/docs/mod_pagespeed/filter-cache-extend

                if (substr($url, 0, strlen(get_base_url())) == get_base_url()) {
                    $url = substr($url, strlen(get_base_url()));
                }
                $pos = strpos($url, 'themes/');
                if ($pos === false) {
                    warn_exit(do_lang_tempcode('NOT_THEME_IMAGE'));
                }
                $url = substr($url, $pos);
                $id = $GLOBALS['SITE_DB']->query_select_value_if_there('theme_images', 'id', ['url' => $url, 'theme' => $theme]);
                if ($id === null) {
                    warn_exit(do_lang_tempcode('MISSING_RESOURCE'));
                }
            } else {
                $id = get_param_string('id');
            }

            set_short_title($id);

            breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('MANAGE_THEMES')], ['_SELF:_SELF:manage_images:theme=' . $theme, do_lang_tempcode('CHOOSE_THEME_IMAGE')]]);

            $this->title = get_screen_title('EDIT_THEME_IMAGE');

            $this->id = $id;
        }
        if ($type == '_edit_image') {
            $theme = post_param_string('theme');
            $this->theme = $theme;
            $id = post_param_string('id');
            breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('MANAGE_THEMES')], ['_SELF:_SELF:manage_images:theme=' . $theme, do_lang_tempcode('CHOOSE_THEME_IMAGE')], ['_SELF:_SELF:edit_image:' . $id, do_lang_tempcode('EDIT_THEME_IMAGE')]]);
            breadcrumb_set_self(do_lang_tempcode('DONE'));

            $this->title = get_screen_title('EDIT_THEME_IMAGE');
        }
        if ($type == '_themewizard_image') {
            $theme = post_param_string('theme');
            $this->theme = $theme;
            $id = post_param_string('id');
            breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('MANAGE_THEMES')], ['_SELF:_SELF:manage_images:theme=' . $theme, do_lang_tempcode('CHOOSE_THEME_IMAGE')], ['_SELF:_SELF:edit_image:' . $id, do_lang_tempcode('EDIT_THEME_IMAGE')]]);
            breadcrumb_set_self(do_lang_tempcode('DONE'));

            $this->title = get_screen_title('THEMEWIZARD');
        }

        if ($type == 'edit_theme') {
            breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('MANAGE_THEMES')]]);

            $this->title = get_screen_title('EDIT_THEME');
        }
        if ($type == '_edit_theme') {
            breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('MANAGE_THEMES')]]);
            breadcrumb_set_self(do_lang_tempcode('DONE'));

            breadcrumb_set_self(do_lang_tempcode('DONE'));

            if (post_param_integer('delete', 0) == 1) {
                $this->title = get_screen_title('DELETE_THEME');
            } elseif (post_param_integer('copy', 0) == 1) {
                $this->title = get_screen_title('COPY_THEME');
            } else {
                $this->title = get_screen_title('EDIT_THEME');
            }
        }

        if ($type == 'validate_page') {
            $this->title = get_screen_title('WEBSTANDARDS');
        }

        if ($type == 'screen_previews') {
            breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('MANAGE_THEMES')]]);

            $this->title = get_screen_title('SCREEN_PREVIEWS');
        }
        if ($type == 'screen_preview') {
            if (get_param_string('validate', '') != '') {
                $this->title = get_screen_title('WEBSTANDARDS');
            } else {
                $function = get_param_string('function');
                $this->title = get_screen_title($function, false);
            }

            breadcrumb_set_parents([['_SELF:_SELF:screen_previews', do_lang_tempcode('SCREEN_PREVIEWS')]]);
        }

        if ($type == 'tempcode_tester') {
            $this->title = get_screen_title('TEMPCODE_TESTER');

            breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('MANAGE_THEMES')]]);
        }

        return null;
    }

    /**
     * Execute the module.
     *
     * @return Tempcode The result of execution
     */
    public function run() : object
    {
        require_lang('themes');
        require_code('view_modes');
        require_code('themes2');
        require_code('themes3');
        require_lang('zones');
        require_code('files');
        require_code('images');

        require_css('themes_editor');

        $type = get_param_string('type', 'browse');

        if ($type == 'browse') {
            return $this->manage_themes();
        }
        if ($type == 'add_theme') {
            return $this->add_theme();
        }
        if ($type == '_add_theme') {
            return $this->_add_theme();
        }
        if ($type == 'edit_theme') {
            return $this->edit_theme();
        }
        if ($type == '_edit_theme') {
            return $this->_edit_theme();
        }
        if ($type == 'edit_templates') {
            return $this->edit_templates();
        }
        if ($type == 'manage_images') {
            return $this->manage_images();
        }
        if ($type == 'add_image') {
            return $this->add_image();
        }
        if ($type == '_add_image') {
            return $this->_add_image();
        }
        if ($type == 'edit_image') {
            return $this->edit_image();
        }
        if ($type == '_edit_image') {
            return $this->_edit_image();
        }
        if ($type == '_themewizard_image') {
            return $this->_themewizard_image();
        }
        if ($type == 'validate_page') {
            return $this->validate_page();
        }
        if ($type == 'screen_previews') {
            return $this->screen_previews();
        }
        if ($type == 'screen_preview') {
            return $this->screen_preview();
        }
        if ($type == 'tempcode_tester') {
            return $this->tempcode_tester();
        }
        if ($type == 'diff') {
            return $this->diff();
        }

        return new Tempcode();
    }

    /**
     * The UI to manage themes.
     *
     * @return Tempcode The UI
     */
    public function manage_themes() : object
    {
        $_themes = find_all_themes();

        // Look through zones
        $zones = $GLOBALS['SITE_DB']->query_select('zones', ['*'], [], 'ORDER BY zone_title', 50/*reasonable limit; zone_title is sequential for default zones*/);
        $free_choices = 0;
        $zone_list_free_choices = new Tempcode();
        $no_themes_explicitly_set = true;
        foreach ($zones as $zone) {
            if (array_key_exists($zone['zone_theme'], $_themes)) {
                if (($zone['zone_name'] == '') && ($zone['zone_theme'] != '-1')) {
                    $no_themes_explicitly_set = false;
                }
            } else {
                if (!$zone_list_free_choices->is_empty()) {
                    $zone_list_free_choices->attach(do_lang_tempcode('LIST_SEP'));
                }
                $zone_list_free_choices->attach(($zone['zone_name'] == '') ? do_lang('_WELCOME') : $zone['zone_name']);

                $free_choices++;
            }
        }

        require_css('do_next');

        // Show all themes
        $site_default_theme = $GLOBALS['FORUM_DRIVER']->_get_theme(true);
        $themes = [];
        $theme_default_reason = do_lang_tempcode('DEFAULT_THEME_BY_DEFAULT', escape_html(get_default_theme_name()));
        foreach ($_themes as $theme => $theme_title) {
            if (is_integer($theme)) {
                $theme = strval($theme);
            }

            $is_main_theme = false;

            // Get URLs
            $templates_url = build_url(['page' => '_SELF', 'type' => 'edit_templates', 'theme' => $theme], '_SELF');
            $images_url = build_url(['page' => '_SELF', 'type' => 'manage_images', 'theme' => $theme], '_SELF');
            $deletable = ($theme != 'default');
            $edit_url = build_url(['page' => '_SELF', 'type' => 'edit_theme', 'theme' => $theme], '_SELF');
            $delete_url = build_url(['page' => '_SELF', 'type' => 'delete_theme', 'theme' => $theme], '_SELF');
            $screen_preview_url = build_url(['page' => '_SELF', 'type' => 'screen_previews', 'theme' => $theme], '_SELF');

            // Theme date
            $date = $this->_get_theme_date($theme);

            // Where the theme is used
            $zone_list = new Tempcode();
            if ($theme == $site_default_theme) {
                if ((count($zones) < 10) && (!is_on_multi_site_network()) && ($free_choices == 0)) {
                    $zone_list->attach($zone_list_free_choices); // Actually will do nothing, as $free_choices == 0
                } else {
                    $zone_list->attach(do_lang_tempcode('THEME_DEFAULT_FOR_SITE'));

                    if ($no_themes_explicitly_set) {
                        $is_main_theme = true;
                    }
                }

                // Why is this the site-default theme?
                if ($theme == preg_replace('#[^' . URL_CONTENT_REGEXP . ']#', '_', get_site_name())) {
                    $theme_default_reason = do_lang_tempcode('DEFAULT_THEME_BY_SITENAME');
                } elseif ($theme != 'default') {
                    $theme_default_reason = do_lang_tempcode('DEFAULT_THEME_BY_FORUM');
                }
            }
            foreach ($zones as $zone) {
                if ($zone['zone_theme'] == $theme) {
                    if ($zone['zone_name'] == '') {
                        $is_main_theme = true;
                    }

                    if ((get_option('single_public_zone') == '1') && ($zone['zone_name'] == 'site')) {
                        continue;
                    }

                    if (!$zone_list->is_empty()) {
                        $zone_list->attach(do_lang_tempcode('LIST_SEP'));
                    }
                    $zone_list->attach($zone['zone_name']);
                }
            }
            if (!$zone_list->is_empty()) {
                $theme_usage = do_lang_tempcode('THEME_USED_ON', $zone_list);
            } else {
                $theme_usage = new Tempcode();
            }

            // Render
            $seed = null;
            if (addon_installed('themewizard')) {
                require_code('themes2');
                $seed = find_theme_seed($theme);
            }
            $themes[] = [
                '_GUID' => 'c65c7f3f87d62ad425c7a104a6018840',
                'SEED' => $seed,
                'THEME_USAGE' => $theme_usage,
                'DATE' => $date,
                'NAME' => $theme,
                'DESCRIPTION' => get_theme_option('description'),
                'AUTHOR' => get_theme_option('author'),
                'TITLE' => $theme_title,
                'TEMPLATES_URL' => $templates_url,
                'IMAGES_URL' => $images_url,
                'DELETABLE' => $deletable,
                'EDIT_URL' => $edit_url,
                'DELETE_URL' => $delete_url,
                'SCREEN_PREVIEW_URL' => $screen_preview_url,
                'IS_MAIN_THEME' => $is_main_theme,
            ];
        }

        $zones = find_all_zones(false, true);

        if ((count($zones) < 7) && (!is_on_multi_site_network()) && ($free_choices == 0)) {
            $theme_default_reason = new Tempcode(); // We don't need to know the reason really; don't over-complicate simple sites
        }

        return do_template('THEME_MANAGE_SCREEN', [
            '_GUID' => '1dc277f18562976f6a23facec56a98e8',
            'TITLE' => $this->title,
            'THEMES' => $themes,
            'THEME_DEFAULT_REASON' => $theme_default_reason,
            'ZONES' => $zones,
            'HAS_FREE_CHOICES' => $free_choices != 0,
        ]);
    }

    /**
     * Get standard form input fields for inputting a theme.
     *
     * @param  string $name The name of the theme
     * @param  boolean $use_on_all_zones Whether to use this theme on all zones
     * @return Tempcode The fields
     */
    public function get_theme_fields(string $name = '', bool $use_on_all_zones = false) : object
    {
        require_code('form_templates');
        require_code('permissions2');

        $fields = new Tempcode();

        // General options
        $title = get_theme_option('title', null, $name);
        $fields->attach(form_input_line(do_lang_tempcode('TITLE'), do_lang_tempcode('DESCRIPTION_TITLE'), 'title', $title, true));
        if ($name != 'default') {
            $site_default_theme = preg_replace('#[^' . URL_CONTENT_REGEXP . ']#', '_', get_site_name());
            $has_site_default_theme = file_exists(get_custom_file_base() . '/themes/' . $site_default_theme);
            $fields->attach(form_input_codename(do_lang_tempcode('CODENAME'), do_lang_tempcode($has_site_default_theme ? 'DESCRIPTION_CODENAME_THEME' : 'DESCRIPTION_CODENAME_THEME_HELPER', escape_html($site_default_theme)), 'theme', $name, true));
        }

        // Metadata
        $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', ['_GUID' => '6fc4708641814ddf5e99d0771abaa8f8', 'SECTION_HIDDEN' => true, 'TITLE' => do_lang_tempcode('METADATA')]));
        $description = get_theme_option('description', null, $name);
        $fields->attach(form_input_line(do_lang_tempcode('DESCRIPTION'), do_lang_tempcode('DESCRIPTION_DESCRIPTION'), 'description', $description, false));
        $author = get_theme_option('author', $GLOBALS['FORUM_DRIVER']->get_username(get_member(), true), $name);
        $fields->attach(form_input_line(do_lang_tempcode('AUTHOR'), do_lang_tempcode('DESCRIPTION_AUTHOR_THEME', do_lang_tempcode('THEME')), 'author', $author, true));
        $capabilities = new Tempcode();
        foreach (['administrative', 'block_layouts', 'emails', 'printing'] as $capability) {
            $capabilities->attach(form_input_list_entry($capability, get_theme_option('capability_' . $capability, null, $name) == '1', do_lang_tempcode('THEME_CAPABILITY_' . $capability)));
        }
        $fields->attach(form_input_multi_list(do_lang_tempcode('CAPABILITIES'), '', 'capabilities', $capabilities));
        $language = get_theme_option('language', null, $name);
        $fields->attach(form_input_line(do_lang_tempcode('LANGUAGE'), do_lang_tempcode('DESCRIPTION_THEME_LANGUAGE'), 'language', $language, false));
        $cms_version = get_theme_option('cms_version', null, $name);
        $fields->attach(form_input_line(do_lang_tempcode('THEME_SOFTWARE_VERSION'), do_lang_tempcode('THEME_DESCRIPTION_SOFTWARE_VERSION'), 'cms_version', $cms_version, !in_array($name, ['default', 'admin'])));

        // Option overrides
        $show_theme_option_overrides = false;
        $hooks = find_all_hook_obs('systems', 'config', 'Hook_config_');
        foreach ($hooks as $hook => $ob) {
            $details = $ob->get_details();
            if (!empty($details['theme_override'])) {
                $current_value = get_theme_option($hook, '', $name);
                if ($current_value != '') {
                    $show_theme_option_overrides = true;
                }
            }
        }
        $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', ['_GUID' => '2b82c955e92a4bddad2f57bfe23fc7b0', 'SECTION_HIDDEN' => !$show_theme_option_overrides, 'TITLE' => do_lang_tempcode('THEME__OPTION_OVERRIDES'), 'HELP' => do_lang_tempcode('DESCRIPTION__THEME__OPTION_OVERRIDES')]));
        require_all_lang();
        require_code('config2');
        $config_groups = [];
        foreach (array_keys($hooks) as $hook) {
            require_code('hooks/systems/config/' . filter_naughty_harsh($hook));
            $ob = object_factory('Hook_config_' . filter_naughty_harsh($hook));
            $details = $ob->get_details();
            if (!empty($details['theme_override'])) {
                $group_title = do_lang($details['group']);
                if (!isset($config_groups[$group_title])) {
                    $config_groups[$group_title] = [];
                }
                $order_in_category_group = isset($details['order_in_category_group']) ? $details['order_in_category_group'] : -count($config_groups[$group_title]);
                $config_groups[$group_title][$order_in_category_group] = [$hook, $details];
            }
        }
        cms_mb_ksort($config_groups, SORT_NATURAL | SORT_FLAG_CASE);
        foreach ($config_groups as $config_group) {
            ksort($config_group);
            foreach ($config_group as $config_option) {
                list($hook, $details) = $config_option;
                $current_value = get_theme_option($hook, '', $name);
                $fields->attach(build_config_inputter($hook, $details, $current_value, true, true));
            }
        }

        // Setup Wizard
        if (addon_installed('setupwizard')) {
            $settings = [
                'setupwizard__install_profile',
                'setupwizard__lock_fixed_width_choice',
                'setupwizard__lock_addons_on',
                'setupwizard__provide_cms_advert_choice',
                'setupwizard__lock_show_content_tagging',
                'setupwizard__lock_show_content_tagging_inline',
                'setupwizard__lock_show_screen_actions',
                'setupwizard__lock_single_public_zone',
            ];
            require_lang('config');
            $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', ['_GUID' => 'df2d04a52c38700e84dfb82e7c89cd36', 'SECTION_HIDDEN' => true, 'TITLE' => do_lang_tempcode('THEME_SETTING_SECTION', do_lang_tempcode('SETUPWIZARD'))]));
            foreach ($settings as $setting) {
                $fields->attach(form_input_line(titleify(preg_replace('#^setupwizard__#', '', $setting)), '', $setting, get_theme_option($setting, null, $name), false));
            }
        }

        // Theme Wizard
        if (addon_installed('themewizard')) {
            $settings = [
                'enable_themewizard',
                'supports_themewizard_equations',
                'themewizard_images',
                'themewizard_images_no_wild',
            ];
            $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', ['_GUID' => '020059c4c22d12788115a2810f277af5', 'SECTION_HIDDEN' => true, 'TITLE' => do_lang_tempcode('THEME_SETTING_SECTION', do_lang_tempcode('THEMEWIZARD'))]));
            foreach ($settings as $setting) {
                $fields->attach(form_input_line(titleify(preg_replace('#^themewizard__#', '', $setting)), '', $setting, get_theme_option($setting, null, $name), false));
            }
        }

        // Logo Wizard
        if (addon_installed('themewizard')) {
            $settings = [
                'enable_logowizard',
                'logo_x_offset',
                'logo_y_offset',
                'site_name_split',
                'site_name_split_gap',
                'site_name_font_size_small',
                'site_name_font_size',
                'site_name_font_size_small_non_ttf',
                'site_name_font_size_nonttf',
                'site_name_x_offset',
                'site_name_y_offset',
                'site_name_y_offset_small',
            ];
            $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', ['_GUID' => '63543b1e90b67557a440a193efef4ada', 'SECTION_HIDDEN' => true, 'TITLE' => do_lang_tempcode('THEME_SETTING_SECTION', do_lang_tempcode('LOGOWIZARD'))]));
            foreach ($settings as $setting) {
                $fields->attach(form_input_line(titleify($setting), '', $setting, get_theme_option($setting, null, $name), false));
            }
        }

        // Permissions
        if ($name != 'default') {
            $fields->attach(get_category_permissions_for_environment('theme', $name, null, null, ($name == '')));
        }

        // Actions
        $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', ['_GUID' => '805d278a21d59eaa4568c2a77fbb5073', 'TITLE' => do_lang_tempcode('ACTIONS')]));
        $fields->attach(form_input_tick(do_lang_tempcode('USE_ON_ZONES'), do_lang_tempcode('DESCRIPTION_USE_ON_ZONES'), 'use_on_all', $use_on_all_zones));

        // Mapping
        if (method_exists($GLOBALS['FORUM_DRIVER'], 'get_skin_list')) {
            $map = file_exists(get_file_base() . '/themes/map.ini') ? cms_parse_ini_file_fast(get_file_base() . '/themes/map.ini') : [];
            $default_selection = [];
            $mapping = new Tempcode();
            $all_skins = $GLOBALS['FORUM_DRIVER']->get_skin_list();
            foreach ($map as $key => $val) {
                if ($val == $name) {
                    $default_selection[] = $key;
                }
            }
            foreach ($all_skins as $key) {
                $mapping->attach(form_input_list_entry($key, in_array($key, $default_selection)));
            }
            $fields->attach(form_input_multi_list(do_lang_tempcode('THEME_MAPPING'), do_lang_tempcode('DESCRIPTION_THEME_MAPPING'), 'mapping', $mapping));
        }

        return $fields;
    }

    /**
     * The UI to add a theme.
     *
     * @return Tempcode The UI
     */
    public function add_theme() : object
    {
        $fields = $this->get_theme_fields();

        $post_url = build_url(['page' => '_SELF', 'type' => '_add_theme'], '_SELF');
        $submit_name = do_lang_tempcode('ADD');

        if (addon_installed('themewizard')) {
            $themewizard_url = build_url(['page' => 'admin_themewizard', 'type' => 'browse'], get_module_zone('admin_themewizard'));
            $text = do_lang_tempcode('DESCRIPTION_ADD_THEME_MANUAL', escape_html($themewizard_url->evaluate()));
        } else {
            $text = new Tempcode();
        }

        require_javascript('core_themeing');
        return do_template('FORM_SCREEN', [
            '_GUID' => '08b45be04f4035c7595458a719260bd9',
            'HIDDEN' => '',
            'JS_FUNCTION_CALLS' => ['adminThemesAddTheme'],
            'TITLE' => $this->title,
            'URL' => $post_url,
            'FIELDS' => $fields,
            'TEXT' => $text,
            'SUBMIT_ICON' => 'admin/add',
            'SUBMIT_NAME' => $submit_name,
            'SUPPORT_AUTOSAVE' => true,
        ]);
    }

    /**
     * The actualiser to add a theme.
     *
     * @return Tempcode The UI
     */
    public function _add_theme() : object
    {
        $theme = post_param_string('theme');
        require_code('type_sanitisation');
        if (!is_alphanumeric($theme)) {
            $theme = preg_replace('#[^' . URL_CONTENT_REGEXP . ']#', '_', $theme);
            //warn_exit(do_lang_tempcode('BAD_CODENAME'));
        }
        actual_add_theme($theme);

        $this->save_theme_changes($theme);

        return $this->do_next_manager($this->title, do_lang_tempcode('SUCCESS'), $theme, '', 'theme', $theme);
    }

    /**
     * The UI to edit/rename a theme.
     *
     * @return Tempcode The UI
     */
    public function edit_theme() : object
    {
        $theme = $this->theme;

        if (($theme == 'default') || ($theme == 'admin')) {
            if ($GLOBALS['CURRENT_SHARE_USER'] !== null) {
                warn_exit(do_lang_tempcode('SHARED_INSTALL_PROHIBIT'));
            }
        }

        $fields = $this->get_theme_fields($theme, false);

        if ($theme != 'default') {
            $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', ['_GUID' => '66a48b082356b9e9bfdb1a8107f5e567', 'TITLE' => do_lang_tempcode('ACTIONS')]));
            $fields->attach(form_input_tick(do_lang_tempcode('COPY_THEME'), do_lang_tempcode('DESCRIPTION_COPY_THEME', escape_html($theme)), 'copy', false));
            $fields->attach(form_input_tick(do_lang_tempcode('DELETE'), do_lang_tempcode('DESCRIPTION_DELETE'), 'delete', false));
        }

        $post_url = build_url(['page' => '_SELF', 'type' => '_edit_theme', 'old_theme' => $theme], '_SELF');
        $submit_name = do_lang_tempcode('EDIT_THEME');

        // Theme date
        $date = $this->_get_theme_date($theme);
        if ($date === null) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE'));
        }

        require_javascript('core_themeing');
        return do_template('FORM_SCREEN', [
            '_GUID' => '2734c55cd4d7cfa785d307d932ce8af1',
            'JS_FUNCTION_CALLS' => ['adminThemesEditTheme'],
            'HIDDEN' => '',
            'TITLE' => $this->title,
            'TEXT' => do_lang_tempcode('DESCRIPTION_EDIT_THEME', $date/*Tempcode*/),
            'URL' => $post_url,
            'FIELDS' => $fields,
            'SUBMIT_ICON' => 'admin/edit_this',
            'SUBMIT_NAME' => $submit_name,
            'SUPPORT_AUTOSAVE' => true,
        ]);
    }

    /**
     * Find a theme date.
     *
     * @param  ID_TEXT $theme The theme codename
     * @return ?Tempcode The theme date (null: theme not found)
     */
    protected function _get_theme_date(string $theme) : ?object
    {
        $path = (($theme == 'default' || $theme == 'admin') ? get_file_base() : get_custom_file_base()) . '/themes/' . $theme;
        if (!is_dir($path)) {
            return null;
        }
        $date = filemtime($path);
        return ($theme == 'default' || $theme == 'admin') ? do_lang_tempcode('NA_EM') : protect_from_escaping(escape_html(get_timezoned_date($date)));
    }

    /**
     * The actualiser to edit/rename a theme.
     *
     * @return Tempcode The UI
     */
    public function _edit_theme() : object
    {
        $theme = get_param_string('old_theme', false, INPUT_FILTER_GET_COMPLEX);

        if (($theme == 'default') || ($theme == 'admin')) {
            if ($GLOBALS['CURRENT_SHARE_USER'] !== null) {
                warn_exit(do_lang_tempcode('SHARED_INSTALL_PROHIBIT'));
            }
        }

        if (post_param_integer('delete', 0) == 1) {
            actual_delete_theme($theme);

            $to = '';
        } elseif (post_param_integer('copy', 0) == 1) {
            $to = post_param_string('theme', $theme); // Can't rename the default theme, so there's no such field for it
            if ($theme == $to) {
                warn_exit(do_lang_tempcode('ALREADY_EXISTS', escape_html($to)));
            }

            actual_copy_theme($theme, $to);

            $this->save_theme_changes($to);

            log_it('EDIT_THEME', $to, post_param_string('title'));
        } else {
            $to = post_param_string('theme', $theme); // Can't rename the default theme, so there's no such field for it
            if ($theme != $to) {
                require_code('type_sanitisation');
                if (!is_alphanumeric($to)) {
                    $to = preg_replace('#[^' . URL_CONTENT_REGEXP . ']#', '_', $to);
                    //warn_exit(do_lang_tempcode('BAD_CODENAME'));
                }

                if (!file_exists(get_custom_file_base() . '/themes/' . $theme)) {
                    warn_exit(do_lang_tempcode('MISSING_RESOURCE'));
                }

                actual_rename_theme($theme, $to);
            }

            $this->save_theme_changes($to);
        }

        return $this->do_next_manager($this->title, do_lang_tempcode('SUCCESS'), $to, '', 'theme', $to);
    }

    /**
     * Common theme change saving for adding and editing themes.
     *
     * @param  ID_TEXT $theme The name of the theme
     */
    protected function save_theme_changes(string $theme)
    {
        // Make live if requested
        if (post_param_integer('use_on_all', 0) == 1) {
            require_code('themes3');
            set_live_theme($theme);

            require_code('site');
            attach_message(do_lang_tempcode('THEME_MADE_LIVE'), 'inform');
        }

        // Save theme.ini
        $ini_file = (($theme == 'default' || $theme == 'admin') ? get_file_base() : get_custom_file_base()) . '/themes/' . filter_naughty($theme) . '/theme.ini';
        $before = file_exists($ini_file) ? cms_parse_ini_file_fast($ini_file) : [];
        $contents = '';
        $themeonly_options = ['title', 'description', 'author'];
        foreach ($themeonly_options as $themeonly_option) {
            $contents .= $themeonly_option . '=' . str_replace("\n", '\n', post_param_string($themeonly_option, '')) . "\n";
            unset($before[$themeonly_option]);
        }
        require_code('config2');
        $hooks = find_all_hook_obs('systems', 'config', 'Hook_config_');
        foreach ($hooks as $hook => $ob) {
            $details = $ob->get_details();
            if (!empty($details['theme_override'])) {
                $val = get_submitted_config_value($hook, $details, true);
                if ($val != '') {
                    $contents .= $hook . '=' . $val . "\n";
                }
            }
        }
        foreach ($before as $key => $val) {
            if (strpos($contents, ($key . '=')) === false) { // Something not edited in the UI
                $contents .= $key . '=' . $val . "\n";
            }
            unset($before[$key]);
        }
        if (isset($_POST['capabilities'])) {
            foreach ($_POST['capabilities'] as $capability) {
                $contents .= 'capability_' . $capability . '=1' . "\n";
            }
        }
        cms_file_put_contents_safe($ini_file, $contents, FILE_WRITE_FIX_PERMISSIONS | FILE_WRITE_SYNC_FILE | FILE_WRITE_BOM);

        // Save permissions
        require_code('permissions2');
        set_category_permissions_from_environment('theme', $theme);

        // Update map file
        $map = file_exists(get_file_base() . '/themes/map.ini') ? cms_parse_ini_file_fast(get_file_base() . '/themes/map.ini') : [];
        $new_map = [];
        foreach ($map as $key => $val) {
            if ($val != $theme) {
                $new_map[$key] = $val;
            }
        }
        if (array_key_exists('mapping', $_POST)) {
            foreach ($_POST['mapping'] as $val) {
                $new_map[$val] = $theme;
            }
        }
        $path = get_file_base() . '/themes/map.ini';
        $contents = '';
        foreach ($new_map as $key => $val) {
            $contents .= $key . '=' . $val . "\n";
        }
        cms_file_put_contents_safe($path, $contents, FILE_WRITE_FIX_PERMISSIONS | FILE_WRITE_SYNC_FILE | FILE_WRITE_BOM);

        // Empty caching
        require_code('caches3');
        erase_persistent_cache();
        erase_cached_templates();
    }

    /**
     * The UI to edit a template file (actualisation is via AJAX).
     *
     * @return Tempcode The UI
     */
    public function edit_templates() : object
    {
        $theme = $this->theme;

        require_javascript('tree_list');
        require_javascript('editing');
        require_javascript('posting');

        require_code('lorem');

        $active_guid = get_param_string('active_guid', null);

        // Files to load
        $files_to_load = [];
        foreach ($_GET + $_POST as $key => $val) {
            if (preg_match('#^f\d+file$#', $key) != 0) {
                if (is_array($val)) { // We support passing lists by array
                    foreach ($val as $_val) {
                        $files_to_load[] = $_val;
                    }
                } else { // ... and also by string
                    $files_to_load[] = get_param_string($key);
                }
            }
        }
        $files_to_load = array_unique($files_to_load);
        // Default files
        if (empty($files_to_load)) {
            $files_to_load = [
                'templates/HTML_HEAD.tpl',
                'templates/GLOBAL_HTML_WRAP.tpl',
                'css/_base.css',
                'css/_colours.css',
                'css/global.css',
                'javascript/global.js',
                'templates/MAIL.tpl',
                ':' . DEFAULT_ZONE_PAGE_NAME,
            ];
        }

        // NB: Files are loaded via AJAX template_editor_load snippet hook, and saved via template_editor_save snippet hook

        // Conflict resolution
        list($warning_details, $ping_url) = handle_conflict_resolution(''); // Intentionally blank, because only one person should edit any of all templates at any time (because they depend on each other)

        // Screen preview feature
        $live_preview_url = get_param_string('live_preview_url', null, INPUT_FILTER_URL_INTERNAL);

        // Opening up file browser location
        $default_theme_files_location = get_param_string('default_theme_files_location', null, INPUT_FILTER_GET_COMPLEX);

        return do_template('THEME_TEMPLATE_EDITOR_SCREEN', [
            '_GUID' => 'bd4284c92fd25e0f489f6f4add25340b',
            'TITLE' => $this->title,
            'FILES_TO_LOAD' => $files_to_load,
            'THEME' => $theme,
            'LIVE_PREVIEW_URL' => $live_preview_url,
            'WARNING_DETAILS' => $warning_details,
            'PING_URL' => $ping_url,
            'ACTIVE_GUID' => $active_guid,
            'DEFAULT_THEME_FILES_LOCATION' => $default_theme_files_location,
        ]);
    }

    /**
     * Get Tempcode for a theme image adding/editing form.
     *
     * @param  ID_TEXT $theme The theme the theme image is in
     * @param  LANGUAGE_NAME $lang The language the theme image is for
     * @param  SHORT_TEXT $id The theme image ID
     * @param  URLPATH $url The URL to the theme image
     * @return array A pair: the Tempcode for the visible fields, and the Tempcode for the hidden fields
     */
    public function get_image_form_fields(string $theme, string $lang, string $id = '', string $url = '') : array
    {
        $fields = new Tempcode();
        $hidden = new Tempcode();
        $hidden->attach(form_input_hidden('theme', $theme));
        $hidden->attach(form_input_hidden('lang', $lang));
        $fields->attach(form_input_line(do_lang_tempcode('CODENAME'), do_lang_tempcode('DESCRIPTION_THEME_IMAGE_NAME'), 'id', $id, true, null, null, 'text', 'some/path/name'));

        /* Actually we don't want to allow selection from existing -- too weird, creating these cross-links
        $list = combo_get_image_paths($url, get_base_url() . '/themes/' . rawurlencode($theme) . '/images/', get_file_base() . '/themes/' . filter_naughty($theme) . '/images/');
        $list->attach(combo_get_image_paths($url, get_base_url() . '/themes/' . rawurlencode($theme) . '/images_custom/', get_file_base() . '/themes/' . filter_naughty($theme) . '/images_custom/'));
        if ($theme != 'default') {
            $list->attach(combo_get_image_paths($url, get_base_url() . '/themes/default/images/', get_file_base() . '/themes/default/images/'));
            $list->attach(combo_get_image_paths($url, get_base_url() . '/themes/default/images_custom/', get_file_base() . '/themes/default/images_custom/'));
        }
        */
        handle_max_file_size($hidden, 'image');

        $set_name = 'image';
        $required = true;
        $set_title = do_lang_tempcode('IMAGE');
        $field_set = alternate_fields_set__start($set_name);

        require_code('images');
        $field_set->attach(form_input_upload(do_lang_tempcode('UPLOAD'), '', 'file', false, null, null, true, get_allowed_image_file_types()));
        //$fields->attach(form_input_radio(do_lang_tempcode('CHOOSE'), '', $list));

        $field_set->attach(form_input_url(do_lang_tempcode('URL'), '', 'url', $url, false));

        $fields->attach(alternate_fields_set__end($set_name, $set_title, '', $field_set, $required));

        return [$fields, $hidden];
    }

    /**
     * The UI to select a theme image to edit.
     *
     * @return Tempcode The UI
     */
    public function manage_images() : object
    {
        cms_extend_time_limit(TIME_LIMIT_EXTEND__SLOW);

        $lang = choose_language($this->title, true, true);
        if (is_object($lang)) {
            return $lang;
        }

        push_query_limiting(false);

        $theme = $this->theme;

        regen_theme_images($theme, [$lang => 1]);
        if ($theme != 'default') {
            regen_theme_images('default', [$lang => 1], $theme);
        }

        $skip = [
            'icons', // Too many of these to show
            'icons_monochrome',
        ];
        $ids = get_all_image_ids_type(get_param_string('under', ''), true, $GLOBALS['SITE_DB'], $theme, false, true/*stops new theme images being detected, as we know regen_theme_images did that (and more conservatively - it won't scan images_custom dirs for NEW codes which an unbridled get_all_image_ids_type call would)*/, $skip, true);

        single_field__start();
        $fields = form_input_theme_image(do_lang_tempcode('CODENAME'), '', 'id', $ids, null, null, null, false, null, $theme, $lang, true, true);
        single_field__end();

        $hidden = form_input_hidden('theme', $theme);

        $post_url = build_url(['page' => '_SELF', 'type' => 'edit_image', 'lang' => $lang], '_SELF');

        $edit_form = do_template('FORM_SINGLE_FIELD', [
            '_GUID' => '48b3218750fcea21e0bf3be31ae58296',
            'HIDDEN' => $hidden,
            'TEXT' => do_lang_tempcode('CHOOSE_EDIT_LIST'),
            'GET' => true,
            'URL' => $post_url,
            'FIELD' => $fields,
            'SUBMIT_ICON' => 'buttons/proceed',
            'SUBMIT_NAME' => new Tempcode(), // We don't want a button because clicking on images takes you through
        ]);

        $add_url = build_url(['page' => '_SELF', 'type' => 'add_image', 'theme' => $theme, 'lang' => $lang], '_SELF');

        return do_template('THEME_IMAGE_MANAGE_SCREEN', ['_GUID' => '4e760b0aa59b1bbb6fcf289c0b93ec46', 'ADD_URL' => $add_url, 'TITLE' => $this->title, 'FORM' => $edit_form]);
    }

    /**
     * The UI to add a theme image.
     *
     * @return Tempcode The UI
     */
    public function add_image() : object
    {
        $theme = $this->theme;
        $lang = user_lang__with__translation_override();
        if ($lang == '') {
            $lang = user_lang();
        }
        list($fields, $hidden) = $this->get_image_form_fields($theme, $lang);
        $fields->attach(form_input_tick(do_lang_tempcode('USE_ALL_THEMES'), do_lang_tempcode('DESCRIPTION_USE_ALL_THEMES'), 'use_all_themes', true));
        $fields->attach(form_input_tick(do_lang_tempcode('USE_ALL_LANGS'), do_lang_tempcode('DESCRIPTION_USE_ALL_LANGS'), 'use_all_langs', true));

        $post_url = build_url(['page' => '_SELF', 'type' => '_add_image', 'uploading' => 1], '_SELF');
        $submit_name = do_lang_tempcode('ADD');

        $text = new Tempcode();
        $text->attach(paragraph(do_lang_tempcode('DESCRIPTION_ADDING_THEME_IMAGE')));

        require_code('images');
        $max = floatval(get_max_image_size()) / floatval(1024 * 1024);
        if ($max < 3.0) {
            require_code('files2');
            $text->attach(get_maximum_upload_message($max));
        }

        return do_template('FORM_SCREEN', [
            '_GUID' => '7b8066b63002cda0a7628ddadddd9962',
            'HIDDEN' => $hidden,
            'TITLE' => $this->title,
            'URL' => $post_url,
            'FIELDS' => $fields,
            'TEXT' => $text,
            'SUBMIT_ICON' => 'admin/add',
            'SUBMIT_NAME' => $submit_name,
        ]);
    }

    /**
     * The actualiser to add a theme image.
     *
     * @return Tempcode The UI
     */
    public function _add_image() : object
    {
        require_code('uploads');

        $theme = post_param_string('theme');
        $lang = user_lang__with__translation_override(true);
        $id = post_param_string('id');
        $use_all_themes = post_param_integer('use_all_themes', 0);
        $use_all_langs = post_param_integer('use_all_langs', 0);

        $target_dir = 'themes/' . (($use_all_themes == 1) ? 'default' : $theme) . '/images_custom';
        if (strpos($id, '/') !== false) {
            $target_dir .= '/' . str_replace('/' . basename($id), '', $id);
        }
        $urls = get_url('url', 'file', $target_dir, 0, CMS_UPLOAD_IMAGE, false, '', '', false, false, false, false, null, null, null, basename($id . '.XXX'));
        if ($urls[0] == '') {
            return warn_screen($this->title, do_lang_tempcode('IMPROPERLY_FILLED_IN_UPLOAD'));
        }

        $theme_list = array_keys(find_all_themes());
        $lang_list = find_all_langs(true);

        if (($use_all_themes == 1) && ($use_all_langs == 1)) {
            $GLOBALS['SITE_DB']->query_delete('theme_images', ['id' => $id]);
            foreach ($theme_list as $theme) {
                foreach (array_keys($lang_list) as $_lang) {
                    actual_add_theme_image($theme, $_lang, $id, $urls[0], true);
                }
            }
        } elseif ($use_all_themes == 1) {
            $GLOBALS['SITE_DB']->query_delete('theme_images', ['id' => $id, 'lang' => $lang]);
            foreach ($theme_list as $theme) {
                actual_add_theme_image($theme, $lang, $id, $urls[0], true);
            }
        } elseif ($use_all_langs == 1) {
            $GLOBALS['SITE_DB']->query_delete('theme_images', ['id' => $id, 'theme' => $theme]);
            foreach (array_keys($lang_list) as $_lang) {
                actual_add_theme_image($theme, $_lang, $id, $urls[0], true);
            }
        } else {
            actual_add_theme_image($theme, $lang, $id, $urls[0]);
        }

        require_code('caches3');
        Self_learning_cache::erase_smart_cache();
        erase_cached_templates(false, null, TEMPLATE_DECACHE_WITH_THEME_IMAGE);

        return $this->do_next_manager($this->title, do_lang_tempcode('SUCCESS'), $theme, $lang, 'image', $id);
    }

    /**
     * The UI to edit a theme image.
     *
     * @return Tempcode The UI
     */
    public function edit_image() : object
    {
        $lang = choose_language($this->title, true, true);
        if (is_object($lang)) {
            return $lang;
        }

        $theme = $this->theme;
        $id = $this->id;

        $where_map = ['theme' => $theme, 'id' => $id];
        if ($lang != '') {
            $where_map['lang'] = $lang;
        }
        $url = $GLOBALS['SITE_DB']->query_select_value_if_there('theme_images', 'url', $where_map);
        if ($url === null) {
            $url = $GLOBALS['SITE_DB']->query_select_value_if_there('theme_images', 'url', ['theme' => $theme, 'lang' => get_site_default_lang(), 'id' => $id]);
        }
        if ($url === null) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE'));
        }

        set_short_title($id);

        $unmodified = (strpos($url, 'themes/default/images/') !== false);

        disable_php_memory_limit();
        $full_path = ($unmodified ? get_file_base() : get_custom_file_base()) . '/' . rawurldecode($url);
        $width = do_lang_tempcode('UNKNOWN_EM');
        $height = do_lang_tempcode('UNKNOWN_EM');
        $image_size = cms_getimagesize($full_path);
        if (($image_size !== false) && ($image_size[0] !== null) && ($image_size[1] !== null)) {
            $width = make_string_tempcode(strval($image_size[0]));
            $height = make_string_tempcode(strval($image_size[1]));
        }

        $image_url = ($unmodified ? get_base_url() : get_custom_base_url()) . '/' . $url;

        list($fields_edit_file, $hidden_edit_file) = $this->get_image_form_fields($theme, $lang, $id, $url);
        $hidden_edit_file->attach(form_input_hidden('old_id', $id));
        if (strpos($url, 'images_custom') !== false) {
            $fields_edit_file->attach(do_template('FORM_SCREEN_FIELD_SPACER', ['_GUID' => '9468297854009243da7b47c9bb3992bb', 'TITLE' => do_lang_tempcode('ACTIONS')]));
            $fields_edit_file->attach(form_input_tick(do_lang_tempcode('DELETE'), do_lang_tempcode('DESCRIPTION_DELETE_THEME_IMAGE'), 'delete', false));
        }

        $url_edit_file = build_url(['page' => '_SELF', 'type' => '_edit_image', 'uploading' => 1], '_SELF', [], false, true);

        require_code('images');
        $text_edit_file = new Tempcode();
        $max = floatval(get_max_image_size()) / floatval(1024 * 1024);
        if ($max < 3.0) {
            require_code('files2');
            $text_edit_file->attach(get_maximum_upload_message($max));
        }

        if (addon_installed('themewizard')) {
            require_code('themewizard');
            require_code('themes2');

            $url_theme_wizard = build_url(['page' => '_SELF', 'type' => '_themewizard_image'], '_SELF', [], false, true);

            $hidden_themewizard = new Tempcode();
            $hidden_themewizard->attach(form_input_hidden('theme', $theme));
            $hidden_themewizard->attach(form_input_hidden('lang', $lang));
            $hidden_themewizard->attach(form_input_hidden('id', $id));

            $field_themewizard = new Tempcode();
            $field_themewizard->attach(form_input_colour(do_lang_tempcode('SEED_COLOUR'), '', 'seed', '#' . preg_replace('/^\#/', '', find_theme_seed($theme)), true));
            $field_themewizard->attach(form_input_tick(do_lang_tempcode('DARK_THEME'), do_lang_tempcode('DESCRIPTION_DARK_THEME'), 'dark', find_theme_dark($theme)));
        } else {
            $url_theme_wizard = null;
            $hidden_themewizard = null;
            $field_themewizard = null;
        }

        return do_template('THEME_IMAGE_EDIT_SCREEN', [
            '_GUID' => 'b0e178ad1f840a07c4967f3c266c750b',

            'TITLE' => $this->title,

            'WIDTH' => $width,
            'HEIGHT' => $height,
            'IMAGE_URL' => $image_url,
            'UNMODIFIED' => $unmodified,

            'URL_EDIT_FILE' => $url_edit_file,
            'HIDDEN_EDIT_FILE' => $hidden_edit_file,
            'FIELDS_EDIT_FILE' => $fields_edit_file,
            'TEXT_EDIT_FILE' => $text_edit_file,

            'URL_THEMEWIZARD' => $url_theme_wizard,
            'HIDDEN_THEMEWIZARD' => $hidden_themewizard,
            'FIELDS_THEMEWIZARD' => $field_themewizard,
        ]);
    }

    /**
     * The actualiser to edit a theme image.
     *
     * @return Tempcode The UI
     */
    public function _edit_image() : object
    {
        require_code('uploads');

        $theme = post_param_string('theme');
        $lang = user_lang__with__translation_override(true);
        $id = post_param_string('id');
        $old_id = post_param_string('old_id');

        if (post_param_integer('delete', 0) == 1) {
            actual_delete_theme_image($old_id, $theme, $lang);
        } else {
            // Remove old file first so we can re-use the filepath
            is_plupload(true);
            if (((array_key_exists('file', $_FILES)) && ((is_plupload()) || (is_uploaded_file($_FILES['file']['tmp_name'])))) || (post_param_string('url', '') != '')) {
                $old_url = find_theme_image($old_id, true, true, $theme, ($lang == '') ? null : $lang);
                if ($old_url != '' && ((array_key_exists('file', $_FILES)) && ((is_plupload()) || (is_uploaded_file($_FILES['file']['tmp_name']))) || $old_url != post_param_string('url', ''))) {
                    if (($theme == 'default' || $theme == 'admin') || (strpos($old_url, 'themes/default/') === false)) {
                        $where_map = ['theme' => $theme, 'id' => $old_id];
                        if (($lang != '') && ($lang !== null)) {
                            $where_map['lang'] = $lang;
                        }
                        $GLOBALS['SITE_DB']->query_delete('theme_images', $where_map);

                        cleanup_after_theme_image_file_removal($old_url);
                    }
                }
            }

            $target_dir = 'themes/' . $theme . '/images_custom';
            if (strpos($id, '/') !== false) {
                $target_dir .= '/' . str_replace('/' . basename($id), '', $id);
            }
            $urls = get_url('url', 'file', $target_dir, 0, CMS_UPLOAD_IMAGE, false, '', '', false, false, false, false, null, null, null, basename($id . '.XXX'));

            if ((url_is_local($urls[0])) && (!file_exists(((substr($urls[0], 0, 15) == 'themes/default/') ? get_file_base() : get_custom_file_base()) . '/' . rawurldecode($urls[0])))) {
                warn_screen($this->title, do_lang_tempcode('IMPROPERLY_FILLED_IN_UPLOAD'));
            }

            if ($urls[0] == '') {
                return warn_screen($this->title, do_lang_tempcode('IMPROPERLY_FILLED_IN_UPLOAD'));
            }
            actual_edit_theme_image($old_id, $theme, $lang, $id, $urls[0]);
        }

        require_code('caches3');
        Self_learning_cache::erase_smart_cache();
        erase_cached_templates(false, null, TEMPLATE_DECACHE_WITH_THEME_IMAGE);
        persistent_cache_delete('IMAGE_DIMS');

        return $this->do_next_manager($this->title, do_lang_tempcode('SUCCESS'), $theme, $lang, 'image', $id);
    }

    /**
     * The actualiser to Theme Wizardify a theme image.
     *
     * @return Tempcode The UI
     */
    public function _themewizard_image() : object
    {
        if (!addon_installed('themewizard')) {
            warn_exit(do_lang_tempcode('INTERNAL_ERROR'));
        }

        require_code('themewizard');
        require_code('themes3');
        require_code('urls2');

        $theme = post_param_string('theme');
        $lang = user_lang__with__translation_override(true);
        $id = post_param_string('id');

        $seed = post_param_string('seed');
        $dark = (post_param_integer('dark', 0) == 1);

        $default_url = find_theme_image($id, true, true, 'default', ($lang == '') ? null : $lang);

        // Remove old file first so we can re-use the filepath
        $old_url = find_theme_image($id, true, true, $theme, ($lang == '') ? null : $lang);
        if ($old_url != '') {
            $where_map = ['theme' => $theme, 'id' => $id];
            if (($lang != '') && ($lang !== null)) {
                $where_map['lang'] = $lang;
            }
            $GLOBALS['SITE_DB']->query_delete('theme_images', $where_map);

            cleanup_after_theme_image_file_removal($old_url);
        }

        // Find save path
        $target_dir = 'themes/' . $theme . '/images_custom';
        if (strpos($id, '/') !== false) {
            $target_dir .= '/' . str_replace('/' . basename($id), '', $id);
        }
        list($place, $url, $filename) = find_unique_path($target_dir, rawurldecode(basename($default_url)));
        if (!file_exists(dirname($place))) {
            make_missing_directory(dirname($place));
        }

        // Generate image
        $img = generate_themewizard_image($seed, $dark, 'default', 'hsv', $id, null, null, $lang);

        if ($img === null) {
            warn_exit(do_lang_tempcode('INTERNAL_ERROR'));
        }

        // Save image
        if (is_string($img)) {
            file_put_contents($place, $img);
        } else {
            cms_imagesave($img, $place, 'png') or intelligent_write_error($place);
            imagedestroy($img);
        }
        actual_edit_theme_image($id, $theme, $lang, $id, $url, true);

        return inform_screen($this->title, do_lang_tempcode('SUCCESS'));
    }

    /**
     * Shows the list of templates.
     *
     * @return Tempcode The UI
     */
    public function screen_previews() : object
    {
        cms_extend_time_limit(TIME_LIMIT_EXTEND__SLOW);

        require_code('lorem');
        require_code('files2');

        $theme = get_param_string('theme');

        $list = [];

        // LISTING SCREEN PREVIEWS...

        $all_previews__by_screen = find_all_previews__by_screen();

        $displayed_already = [];
        $list_non_admin = new Tempcode();
        $list_admin = new Tempcode();
        $done_templates = [];
        foreach ($all_previews__by_screen as $func => $details) {
            send_http_output_ping();

            list($hook, $tpls) = $details;

            $template = isset($tpls[0]) ? $tpls[0] : null;
            $url_map = ['page' => '_SELF', 'type' => 'screen_preview', 'id' => $template, 'hook' => $hook, 'function' => $func, 'keep_theme' => $theme, 'keep_wide_high' => 1];
            $screen_preview_url = build_url($url_map + ['keep_mobile' => 0, 'keep_hide_dev_mode_message' => 1], '_SELF');
            $mobile_screen_preview_url = build_url($url_map + ['keep_wide_high' => 1, 'keep_mobile' => 1, 'keep_hide_dev_mode_message' => 1], '_SELF');
            $cms_validation_url = build_url($url_map + ['validate' => 'cms'], '_SELF');
            $w3c_validation_url = build_url($url_map + ['validate' => 'w3c'], '_SELF');

            if (empty($tpls)) {
                $templates_used = '';
                $raw_code = true;
            } else {
                $templates_used = '(';
                foreach ($tpls as $tpl) {
                    if ($templates_used != '(') {
                        $templates_used .= ', ';
                    }
                    $templates_used .= preg_replace('#^templates/#', '', $tpl);
                    $done_templates[$tpl] = true;
                }
                $templates_used .= ')';

                $raw_code = is_raw_code_template($tpls[0]);
            }

            $tpl_x = do_template('THEME_SCREEN_PREVIEW', [
                '_GUID' => '1f27f619db553dfcb8d427e70a736226',

                'SCREEN_PREVIEW_URL' => $screen_preview_url,
                'MOBILE_SCREEN_PREVIEW_URL' => $mobile_screen_preview_url,
                'CMS_VALIDATION_URL' => $raw_code ? '' : $cms_validation_url,
                'W3C_VALIDATION_URL' => $raw_code ? '' : $w3c_validation_url,

                'COLOR' => 'green',
                'SCREEN' => preg_replace('#^tpl_preview__#', '', $func),
                'LIST' => $templates_used,
            ]);
            if (preg_match('#^tpl_preview__administrative__#', $func) != 0) {
                $list_admin->attach($tpl_x->evaluate());
            } else {
                $list_non_admin->attach($tpl_x->evaluate());
            }

            $displayed_already[$func] = true;
        }

        // Find all templates
        $templates = [];
        foreach ([get_custom_file_base(), get_file_base()] as $file_base) {
            foreach (['templates_custom', 'templates'] as $subdir) {
                foreach (['default', $theme] as $theme) {
                    $templates = array_merge($templates, get_directory_contents($file_base . '/themes/' . $theme . '/' . $subdir, '', 0, false, true, ['tpl']));
                }
            }
        }
        $templates = array_unique($templates);
        sort($templates);
        foreach ($templates as $t) {
            // No preview for these
            if (!array_key_exists('templates/' . $t, $done_templates)) {
                $tpl_x = do_template('THEME_SCREEN_PREVIEW', [
                    '_GUID' => '96115a3b168769744b4b69fd2e1e7f6c',

                    'SCREEN_PREVIEW_URL' => '',
                    'MOBILE_SCREEN_PREVIEW_URL' => '',
                    'CMS_VALIDATION_URL' => '',
                    'W3C_VALIDATION_URL' => '',

                    'COLOR' => 'red',
                    'SCREEN' => $t,
                    'LIST' => '',
                ]);
                $list_non_admin->attach($tpl_x->evaluate());
            }
        }

        $list[do_lang('menus:SCREENS')] = $list_non_admin;
        $list[do_lang('ADMIN_SCREENS')] = $list_admin;

        // LISTING COMCODE FILES...

        $comcode_files = find_comcodes();

        $com_li = new Tempcode();
        foreach ($comcode_files as $zone => $pages) {
            if ($zone == 'pages') {
                $zone = '';
            }
            foreach ($pages as $page => $type) {
                if (!is_string($page)) {
                    $page = strval($page);
                }

                $file = $page . '.txt (' . do_lang('ZONE') . ': ' . (($zone != '') ? $zone : 'welcome') . ')';
                $screen_preview_url = build_url(['page' => $page, 'keep_theme' => $theme, 'keep_mobile' => 0], $zone);
                $mobile_screen_preview_url = build_url(['page' => $page, 'keep_theme' => $theme, 'keep_mobile' => 1], $zone);
                $cms_validation_url = build_url(['page' => '_SELF', 'type' => 'validate_page', 'id' => $zone . ':' . $page, 'validate' => 'cms', 'keep_theme' => $theme], '_SELF');
                $w3c_validation_url = build_url(['page' => '_SELF', 'type' => 'validate_page', 'id' => $zone . ':' . $page, 'validate' => 'w3c', 'keep_theme' => $theme], '_SELF');

                $tpl_x = do_template('THEME_SCREEN_PREVIEW', [
                    '_GUID' => '9db6fa9333470137ccf9bb752fd9b19e',

                    'SCREEN_PREVIEW_URL' => $screen_preview_url,
                    'MOBILE_SCREEN_PREVIEW_URL' => $mobile_screen_preview_url,
                    'CMS_VALIDATION_URL' => $cms_validation_url,
                    'W3C_VALIDATION_URL' => $w3c_validation_url,

                    'COLOR' => '',
                    'SCREEN' => $file,
                    'LIST' => '',
                ]);
                $com_li->attach($tpl_x->evaluate());
            }
        }
        $list[do_lang('COMCODE_PAGES')] = $com_li;

        // LISTING HTML FILES...

        $html_files = find_html();

        $htm_li = new Tempcode();
        foreach ($html_files as $zone => $pages) {
            foreach ($pages as $page => $type) {
                if (!is_string($page)) {
                    $page = strval($page);
                }

                $file = $page . '.htm (' . do_lang('ZONE') . ': ' . (($zone != '') ? $zone : 'welcome') . ')';
                $screen_preview_url = build_url(['page' => $page, 'keep_theme' => $theme, 'keep_mobile' => 0], $zone);
                $mobile_screen_preview_url = build_url(['page' => $page, 'keep_theme' => $theme, 'keep_mobile' => 1], $zone);
                $cms_validation_url = build_url(['page' => '_SELF', 'type' => 'validate_page', 'id' => $zone . ':' . $page, 'validate' => 'cms', 'keep_theme' => $theme], '_SELF');
                $w3c_validation_url = build_url(['page' => '_SELF', 'type' => 'validate_page', 'id' => $zone . ':' . $page, 'validate' => 'w3c', 'keep_theme' => $theme], '_SELF');

                $tpl_x = do_template('THEME_SCREEN_PREVIEW', [
                    '_GUID' => '16d1c1c5dc5556254f7a3f28a44fdb52',

                    'SCREEN_PREVIEW_URL' => $screen_preview_url,
                    'MOBILE_SCREEN_PREVIEW_URL' => $mobile_screen_preview_url,
                    'CMS_VALIDATION_URL' => $cms_validation_url,
                    'W3C_VALIDATION_URL' => $w3c_validation_url,

                    'COLOR' => '',
                    'SCREEN' => $file,
                    'LIST' => '',
                ]);
                $htm_li->attach($tpl_x->evaluate());
            }
        }
        $list[do_lang('HTML_PAGES')] = $htm_li;

        // LISTING CSS FILES...

        $css_files = [];
        foreach ([get_custom_file_base(), get_file_base()] as $file_base) {
            foreach (['css_custom', 'css'] as $subdir) {
                foreach (['default', $theme] as $theme) {
                    $css_files = array_merge($css_files, get_directory_contents($file_base . '/themes/' . $theme . '/' . $subdir, '', 0, false, true, ['css']));
                }
            }
        }
        $css_files = array_unique($css_files);
        sort($css_files);

        $css_li = new Tempcode();
        foreach ($css_files as $css_file) {
            $_url = find_script('sheet') . '?sheet=' . urlencode(basename($css_file, '.css'));
            $_screen_preview_url = 'http://jigsaw.w3.org/css-validator/validator?uri=' . urlencode($_url);
            $tpl_x = do_template('INDEX_SCREEN_FANCIER_ENTRY', [
                '_GUID' => '26d1c1c5dc5556254f7a3f28a44fdb52',
                'URL' => $_screen_preview_url,
                'TARGET' => '_blank',
                'NAME' => $css_file,
                'TITLE' => '',
                'DESCRIPTION' => '',
            ]);
            $css_li->attach($tpl_x->evaluate());
        }
        $list['CSS (W3C validation)'] = $css_li;

        // --

        return do_template('INDEX_SCREEN_FANCIER_SCREEN', ['_GUID' => '6137f107de679580a6aafe36af427cdd', 'TITLE' => $this->title, 'ARRAY' => true, 'CONTENT' => $list, 'POST' => '', 'PRE' => '']);
    }

    /**
     * Validate a page, routing to the correct validator.
     *
     * @return Tempcode The UI
     */
    public function validate_page() : object
    {
        list($zone, $page) = explode(':', get_param_string('id'), 2);

        $preview = request_page($page, true, $zone, null, false, false);

        $validate = get_param_string('validate');
        switch ($validate) {
            case 'cms':
                $global = globalise($preview);

                require_code('view_modes');
                return make_string_tempcode(check_xhtml_webstandards($global->evaluate(), true, 2, true));

            case 'w3c':
                $refresh = do_template('JS_REFRESH', ['_GUID' => '96f419cc3658454f17ed2b47b3b13e8f', 'FORM_NAME' => 'redir-form']);

                $global = globalise($preview);

                $post = new Tempcode();
                $post->attach(form_input_hidden('fragment', $global->evaluate()));

                $url = 'https://validator.w3.org/check';

                return do_template('REDIRECT_POST_METHOD_SCREEN', [
                    '_GUID' => 'de4fa7509ea5334a806cea57cc334a26',
                    'REFRESH' => $refresh,
                    'TITLE' => $this->title,
                    'TEXT' => do_lang_tempcode('REDIRECTING'),
                    'URL' => $url,
                    'POST' => $post,
                    'ENCTYPE' => 'multipart/form-data',
                ]);
        }

        return new Tempcode(); // Should never get here
    }

    /**
     * Shows the preview of a screen.
     *
     * @return Tempcode The UI
     */
    public function screen_preview() : object
    {
        $GLOBALS['SCREEN_TEMPLATE_CALLED'] = '';

        require_code('lorem');

        $template = get_param_string('id', null);
        $hook = get_param_string('hook');
        $function = get_param_string('function');

        $preview = render_screen_preview($hook, $function, $template);

        $validate = get_param_string('validate', '');
        switch ($validate) {
            case 'cms':
                require_code('view_modes');
                return make_string_tempcode(check_xhtml_webstandards($preview->evaluate(), true, 2, true));

            case 'w3c':
                $refresh = do_template('JS_REFRESH', ['_GUID' => 'd7d2f9e7a2cc637f3cf9ac4d1cf97eca', 'FORM_NAME' => 'redir-form']);

                $post = new Tempcode();
                $post->attach(form_input_hidden('fragment', $preview->evaluate()));

                $url = 'https://validator.w3.org/check';

                return do_template('REDIRECT_POST_METHOD_SCREEN', [
                    '_GUID' => '92e056de9150bbed185120eac3571f40',
                    'REFRESH' => $refresh,
                    'TITLE' => $this->title,
                    'TEXT' => do_lang_tempcode('REDIRECTING'),
                    'URL' => $url,
                    'POST' => $post,
                    'ENCTYPE' => 'multipart/form-data',
                ]);
        }

        $GLOBALS['SCREEN_TEMPLATE_CALLED'] = '';

        exit($preview->evaluate());
    }

    /**
     * The UI to run the Tempcode tester.
     *
     * @return Tempcode The UI
     */
    public function tempcode_tester() : object
    {
        if (get_option('editarea') == '1') {
            attach_to_screen_header(make_string_tempcode(/**@lang HTML*/'
                <script ' . csp_nonce_html() . ' src="' . get_base_url() . '/data/ace/ace.js"></script>
                <script ' . csp_nonce_html() . ' src="' . get_base_url() . '/data/ace/ace_composr.js"></script>
                <script ' . csp_nonce_html() . '>
                    aceComposrLoader(\'tempcode\',\'text\');
                </script>
            ')); // XHTMLXHTML
        }

        return do_template('TEMPCODE_TESTER_SCREEN', ['_GUID' => 'b7ea146fffc0dfdcefcb8e8e0c0168a4', 'TITLE' => $this->title]);
    }

    /**
     * The do-next manager for after download content management.
     *
     * @param  ?Tempcode $title The title (output of get_screen_title) (null: don't use full page)
     * @param  Tempcode $description Some description to show, saying what happened
     * @param  ID_TEXT $theme The theme that was just handled
     * @param  ?LANGUAGE_NAME $lang The language we were working in (null: autodetect) (blank: autodetect)
     * @param  ID_TEXT $type Code to determine what kind of links to show
     * @param  ID_TEXT $file ID of file that an edit link should load (blank: N/A)
     * @return Tempcode The UI
     */
    public function do_next_manager(?object $title, object $description, string $theme, ?string $lang, string $type, string $file) : object
    {
        if ($lang === null) {
            $lang = '';
        }

        switch ($type) {
            case 'templates':
                $add_one = null;
                $edit_this = ['_SELF', ['type' => 'edit_templates', 'f0file' => file_exists(get_custom_file_base() . '/' . str_replace('/templates/', '/templates_custom/', $file)) ? str_replace('/templates/', '/templates_custom/', $file) : $file, 'theme' => $theme], '_SELF'];
                $edit_one = ['_SELF', ['type' => 'edit_templates', 'theme' => $theme], '_SELF', do_lang_tempcode('EDIT_THIS_TEMPLATE')];
                $section_title = do_lang_tempcode('TEMPLATES');
                break;

            case 'image':
                $add_one = ['_SELF', ['type' => 'add_image', 'theme' => $theme, 'lang' => $lang], '_SELF'];
                $edit_this = ['_SELF', ['type' => 'edit_image', 'id' => $file, 'theme' => $theme, 'lang' => $lang], '_SELF'];
                $edit_one = ['_SELF', ['type' => 'manage_images', 'theme' => $theme, 'lang' => $lang], '_SELF'];
                $section_title = do_lang_tempcode('THEME_IMAGES');
                break;

            default:
                $add_one = null;
                $edit_this = null;
                $edit_one = null;
                $section_title = null;
                break;
        }

        require_code('templates_donext');
        return do_next_manager(
            $title,
            $description,
            [],
            null,
            /* TYPED-ORDERED LIST OF 'LINKS' */
            $add_one, // Add one
            $edit_this, // Edit this
            $edit_one, // Edit one
            null, // View this
            null, // View archive
            null, // Add one category
            null, // Edit one category
            null, // Edit this category
            null, // View this category
            /* SPECIALLY TYPED 'LINKS' */
            [],
            [],
            [
                ['admin/add', ['_SELF', ['type' => 'add_theme'], '_SELF'], do_lang_tempcode('ADD_THEME')], // Add one
                ($theme === null) ? null : ['admin/edit_this', ['_SELF', ['type' => 'edit_theme', 'theme' => $theme], '_SELF'], do_lang_tempcode('EDIT_THEME')],
                ($theme === null) ? null : ['menu/adminzone/style/themes/templates', ['_SELF', ['type' => 'edit_templates', 'theme' => $theme], '_SELF'], do_lang('EDIT_TEMPLATES')],
                ($theme === null) ? null : ['menu/adminzone/style/themes/theme_images', ['_SELF', ['type' => 'manage_images', 'theme' => $theme, 'lang' => $lang], '_SELF'], do_lang('EDIT_THEME_IMAGES')],
                ['menu/adminzone/style/themes/themes', ['_SELF', ['type' => 'browse'], '_SELF'], do_lang('MANAGE_THEMES')],
            ],
            do_lang('MANAGE_THEMES'),
            null,
            $section_title
        );
    }

    /**
     * Show a diff.
     *
     * @return Tempcode The UI
     */
    public function diff() : object
    {
        if (!addon_installed('actionlog')) {
            fatal_exit(do_lang_tempcode('INTERNAL_ERROR'));
        }

        require_code('revisions_engine_files');
        $revision_engine = new RevisionEngineFiles();

        $directory = get_param_string('directory');
        $filename = get_param_string('filename');
        $ext = get_param_string('ext');

        if ((strpos($directory, '..') !== false) || (preg_match('#(^themes/|pages/comcode/\w+$|pages/comcode_custom/\w+$)#', $directory) == 0)) {
            log_hack_attack_and_exit('PATH_HACK');
        }
        if (!in_array($ext, ['txt', 'tpl', 'xml', 'css', 'js'])) {
            log_hack_attack_and_exit('PATH_HACK');
        }

        $diff_more_recent_revision = get_param_integer('more_recent_revision', null);
        $diff_revision = get_param_integer('revision');

        $revision_loaded = null;
        $revisions = $revision_engine->render_diff_between(
            $directory,
            $filename,
            $ext,
            $diff_more_recent_revision,
            $diff_revision
        );

        return $revisions;
    }
}
