<?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_comcode_pages
 */

/*EXTRA FUNCTIONS: shell_exec|escapeshellarg*/

/**
 * Module page class.
 */
class Module_cms_comcode_pages
{
    /**
     * 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'] = 4;
        $info['locked'] = false;
        $info['min_cms_version'] = 11.0;
        $info['addon'] = 'core_comcode_pages';
        return $info;
    }

    /**
     * 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
    {
        if ($member_id === null) {
            $member_id = get_member();
        }

        $ret = [
            'browse' => ['COMCODE_PAGE_MANAGEMENT', 'menu/cms/comcode_page_edit'],
            'generate_page_sitemap' => ['GENERATE_PAGE_SITEMAP', 'tool_buttons/sitemap'],
        ];

        if ((is_dir(get_custom_file_base() . '/.git')) && (php_function_allowed('shell_exec')) && (php_function_allowed('escapeshellarg'))) {
            if ((!$check_perms) || (has_privilege($member_id, 'mass_import'))) {
                $ret['sync_revisions_with_git'] = ['SYNC_REVISIONS_WITH_GIT', 'admin/sync'];
            }
        }

        if ($support_crosslinks) {
            require_code('fields');
            $ret += manage_custom_fields_entry_points('comcode_page');
        }

        return $ret;
    }

    /**
     * Find privileges defined as overridable by this module.
     *
     * @return array A map of privileges that are overridable; privilege to 0 or 1. 0 means "not category overridable". 1 means "category overridable".
     */
    public function get_privilege_overrides() : array
    {
        return ['submit_highrange_content' => [1, 'COMCODE_PAGE_ADD'], 'edit_highrange_content' => [1, 'COMCODE_PAGE_EDIT'], 'edit_own_highrange_content' => [1, 'COMCODE_PAGE_OWN_EDIT'], 'bypass_validation_highrange_content' => [1, 'BYPASS_COMCODE_PAGE_VALIDATION']];
    }

    public $title;
    public $page_link;
    public $zone;
    public $file;

    /**
     * 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('zones');

        require_code('content2');

        inform_non_canonical_parameter('parent_page');

        set_helper_panel_tutorial('tut_comcode_pages');

        if ($type == 'browse') {
            $identify_translations = (get_param_integer('translations', 0) == 1);
            if ($identify_translations) {
                $this->title = get_screen_title('COMCODE_PAGE_TRANSLATIONS');
            } else {
                $this->title = get_screen_title('COMCODE_PAGE_EDIT');
                breadcrumb_set_self(do_lang_tempcode('COMCODE_PAGES'));
                set_helper_panel_text(comcode_lang_string('DOC_COMCODE_PAGE_EDIT'));
            }
        }

        if ($type == '_edit') {
            require_lang('menus');
            set_helper_panel_text(comcode_lang_string('DOC_WRITING'));

            // Work out what we're editing, and where it's coming from (support for two page_link specifying parameters for destination)
            $page_link = filter_naughty(get_param_string('page_link', ''));
            if ($page_link == '') {
                $page_link = get_param_string('page_link_2', '');
            }
            if (strpos($page_link, ':') === false) {
                $zone = get_param_string('zone', '');

                $page_link = $zone . ':' . $page_link;
            }
            $page_link_parts = explode(':', $page_link);
            if (count($page_link_parts) != 2) {
                warn_exit(do_lang_tempcode('ZONE_COLON_PAGE'));
            }
            $zone = $page_link_parts[0];
            $file = $page_link_parts[1];

            if ($page_link != '') {
                breadcrumb_set_self(do_lang_tempcode('EDIT'));
            }
            breadcrumb_set_parents([['_SELF:_SELF:browse:lang=' . get_param_string('lang', ''), do_lang_tempcode('menus:_COMCODE_PAGES')]]);

            $lang = get_param_string('lang', get_site_default_lang());
            if (!does_lang_exist($lang)) {
                $lang = get_site_default_lang();
            }

            if ($file == '') {
                $this->title = get_screen_title('COMCODE_PAGE_ADD', true, [escape_html($zone), escape_html($file)]);
            } else {
                if ($lang == get_site_default_lang()) {
                    $this->title = get_screen_title('_COMCODE_PAGE_EDIT', true, [escape_html($zone), escape_html($file)]);
                } else {
                    require_code('lang2');
                    $this->title = get_screen_title('__COMCODE_PAGE_EDIT', true, [escape_html($zone), escape_html($file), escape_html(lookup_language_full_name($lang))]);
                }
            }

            $this->page_link = $page_link;
            $this->zone = $zone;
            $this->file = $file;
        }

        if ($type == '__edit') {
            breadcrumb_set_self(do_lang_tempcode('DONE'));

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

        if ($type == 'generate_page_sitemap') {
            $this->title = get_screen_title('GENERATE_PAGE_SITEMAP');

            breadcrumb_set_parents([['_SELF:_SELF:browse:lang=' . get_param_string('lang', ''), do_lang_tempcode('menus:_COMCODE_PAGES')]]);
        }

        if ($type == 'sync_revisions_with_git') {
            $this->title = get_screen_title('SYNC_REVISIONS_WITH_GIT');
        }

        return null;
    }

    /**
     * Execute the module.
     *
     * @return Tempcode The result of execution
     */
    public function run() : object
    {
        require_code('zones2');
        require_code('zones3');
        require_code('files');

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

        if ($type == 'browse') {
            return $this->edit();
        }
        if ($type == '_edit') {
            return $this->_edit();
        }
        if ($type == '__edit') {
            return $this->__edit();
        }
        if ($type == 'generate_page_sitemap') {
            return $this->generate_page_sitemap();
        }
        if ($type == 'sync_revisions_with_git') {
            return $this->sync_revisions_with_git();
        }

        return new Tempcode();
    }

    /**
     * The do-next manager for after content management.
     *
     * @param  ?Tempcode $title The title (output of get_screen_title) (null: don't use full page)
     * @param  ?ID_TEXT $page The name of the page just handled (null: none)
     * @param  ID_TEXT $zone The name of the zone just handled (blank: none/welcome-zone)
     * @param  Tempcode $completion_text The text to show (blank: default)
     * @return Tempcode The UI
     */
    public function do_next_manager(?object $title, ?string $page, string $zone, object $completion_text) : object
    {
        if (!addon_installed('page_management')) {
            return redirect_screen($title, build_url(['page' => '_SELF', 'type' => 'browse'], '_SELF'), $completion_text);
        }

        return sitemap_do_next_manager($title, $page, $zone, $completion_text);
    }

    /**
     * Find all pages using a disk search.
     *
     * @param  LANGUAGE_NAME $lang The language we are searching for pages of
     * @param  ?array $zone_filter List of zones to limit to (null: no filter)
     * @param  boolean $check_permissions Whether to check edit permissions
     * @return array The map (page-link => map [path & row])
     */
    public function get_comcode_files_list_disk_search(string $lang, ?array $zone_filter, bool $check_permissions = true) : array
    {
        $zones = find_all_zones();
        $out = [];
        foreach ($zones as $zone) {
            if (($zone_filter !== null) && (!in_array($zone, $zone_filter))) {
                continue;
            }

            $_out = [];
            foreach (array_unique([fallback_lang(), get_site_default_lang(), $lang]) as $_lang) {
                $_out += find_all_pages($zone, 'comcode_custom/' . $_lang, 'txt', false, null, FIND_ALL_PAGES__NEWEST);
                $_out += find_all_pages($zone, 'comcode/' . $_lang, 'txt', false, null, FIND_ALL_PAGES__NEWEST);
            }

            foreach ($_out as $page => $subdir) {
                if (is_integer($page)) {
                    $page = strval($page);
                }

                if ($check_permissions) {
                    $resource_owner = $GLOBALS['SITE_DB']->query_select_value_if_there('comcode_pages', 'p_submitter', ['the_zone' => $zone, 'the_page' => $page]);
                    if (!has_edit_permission('high', get_member(), $resource_owner, 'cms_comcode_pages')) {
                        continue;
                    }
                }

                $located = _request_page($page, $zone, null, $lang);
                if (($located !== false) && (strpos($located[0], 'COMCODE') !== false)) {
                    $_full_path = $located[count($located) - 1];
                    $full_path = get_custom_file_base() . '/' . $_full_path;
                    if (!is_file($full_path)) {
                        $full_path = get_file_base() . '/' . $_full_path;
                    }

                    $out[$zone . ':' . $page] = [
                        $full_path, // page path
                        null, // row
                    ];
                }
            }
        }

        return $out;
    }

    /**
     * The UI to choose a page to edit.
     *
     * @return Tempcode The UI
     */
    public function edit() : object
    {
        if (get_param_integer('clear_autosave', 0) == 1) {
            require_code('autosave');
            clear_cms_autosave();
        }

        require_css('comcode_page_manage');

        require_code('templates_results_table');

        $translations_mode = (get_param_integer('translations', 0) == 1);

        push_query_limiting(false);

        $start = get_param_integer('start', 0);
        $max = get_param_integer('max', 100);

        $_zone_filter = get_param_string('zone_filter', null);
        if ($_zone_filter !== null) {
            $zone_filter = explode(',', $_zone_filter);
        } else {
            $zone_filter = null;
        }

        $filter = get_param_string('filter', null);
        if (empty($filter)) {
            $filter = null;
        }

        // Choose language
        if (!$translations_mode) {
            $lang = choose_language($this->title, true, false, false);
            if (is_object($lang)) {
                return $lang;
            }
        } else {
            $lang = get_site_default_lang();
        }

        // Form for adding new
        $fields = new Tempcode();
        $add_new_permission = has_add_comcode_page_permission();
        if ($add_new_permission) {
            require_code('zones2');
            require_code('zones3');
            $zones = create_selection_list_zones();
            $fields->attach(form_input_list(do_lang_tempcode('ZONE'), '', 'zone', $zones, null, false, false));

            $fields->attach(form_input_line(do_lang_tempcode('PAGE'), do_lang_tempcode('DESCRIPTION_NEW_COMCODE_PAGE'), 'page_link_2', '', true, null, null, 'text', '', '([' . URL_CONTENT_REGEXP_JS . ']*:)?[' . URL_CONTENT_REGEXP_JS . ']+'));

            $template_list = create_selection_list_page_templates();
            if (!$template_list->is_empty()) {
                $fields->attach(form_input_list(do_lang_tempcode('PAGE_TEMPLATE'), do_lang_tempcode('PAGE_TEMPLATE_DESCRIPTION'), 'page_template', $template_list, null, false, false));
            }

            $submit_name = do_lang_tempcode('ADD');
        } else {
            $submit_name = null;
        }

        // URLs etc...

        if (($translations_mode) || !addon_installed('search')) {
            $text = new Tempcode();
        } else {
            $search_url = build_url(['page' => 'search', 'id' => 'comcode_pages'], get_module_zone('search'));
            $sitemap_zone = get_page_zone('sitemap', false);
            if ($sitemap_zone !== null) {
                $archive_url = build_url(['page' => 'sitemap'], $sitemap_zone);
            } else {
                $archive_url = build_url(['page' => ''], '');
            }
            $sitemap_url = build_url(['page' => '_SELF', 'type' => 'generate_page_sitemap'], '_SELF');
            $text = paragraph(do_lang_tempcode(
                'CHOOSE_EDIT_TABLE_EXTRA_COMCODE_PAGES',
                escape_html($search_url->evaluate()),
                escape_html($sitemap_url->evaluate()),
                [
                    static_evaluate_tempcode(do_template('ICON', [
                        '_GUID' => '6b23e82e1007bb4e6539db717b6327dd',
                        'NAME' => 'buttons/search',
                        'ICON_SIZE' => '18',
                        'ICON_CLASS' => 'vertical-alignment',
                    ])),
                    static_evaluate_tempcode(do_template('ICON', [
                        '_GUID' => '4ebc80701426935b4d828599f5133e9f',
                        'NAME' => 'tool_buttons/sitemap',
                        'ICON_SIZE' => '18',
                        'ICON_CLASS' => 'vertical-alignment',
                    ])),
                    static_evaluate_tempcode(do_template('ICON', [
                        '_GUID' => 'bbbdabc07f399b9b704fe47a994b1298',
                        'NAME' => 'admin/add',
                        'ICON_SIZE' => '18',
                        'ICON_CLASS' => 'vertical-alignment',
                    ])),
                ]
            ));
        }

        // Sorting
        $current_ordering = get_param_string('sort', 'page_link ASC', INPUT_FILTER_GET_COMPLEX);
        $sortables = [
            'page_title' => do_lang_tempcode('TITLE'),
            'the_page' => do_lang_tempcode('PAGE'),
            'the_zone' => do_lang_tempcode('ZONE'),
            'page_link' => do_lang_tempcode('PAGE_LINK'),
            'p_edit_date' => do_lang_tempcode('EDITED'),
            'p_submitter' => do_lang_tempcode('metadata:OWNER'),
        ];
        if (get_value('disable_comcode_page_order') !== '1') {
            $sortables['p_order'] = do_lang_tempcode('ORDER');
        }
        list($sql_sort, $sort_order, $sortable) = process_sorting_params('comcode_page', $current_ordering);
        if ($sortable == 'page_title') {
            $sql_sort = $GLOBALS['SITE_DB']->translate_field_ref('cc_page_title') . ' ' . $sort_order;
        }

        // Get file details
        $found_via_query = false;
        $total_known_pages = $GLOBALS['SITE_DB']->query_select_value('comcode_pages', 'COUNT(*)');
        $find_via_query = true;
        if (get_param_integer('force_manual_scan', null) === 0) {
            $find_via_query = true;
        } elseif ($total_known_pages < $max) {
            $find_via_query = false;
        } elseif (get_param_integer('force_manual_scan', null) === 1) {
            $find_via_query = false;
        } elseif (
            ($total_known_pages < 300) &&
            ($GLOBALS['SITE_DB']->query_select_value_if_there('comcode_pages r LEFT JOIN ' . get_table_prefix() . 'cached_comcode_pages a ON r.the_page=a.the_page AND r.the_zone=a.the_zone', 'r.the_page', ['a.the_page' => null]) !== null)
        ) {
            $find_via_query = false;
        }
        if ($find_via_query) { // Lots of pages so do it via querying
            $found_via_query = true;

            // Where map
            $where_map = '1=1';
            if (!has_some_edit_comcode_page_permission(COMCODE_EDIT_ANY)) {
                if (!has_some_edit_comcode_page_permission(COMCODE_EDIT_OWN)) { // Nothing global, so go per-zone
                    $zone_editability = get_comcode_page_editability_per_zone();
                    if (!empty($zone_editability)) {
                        $where_map .= ' AND (';
                        foreach ($zone_editability as $zi => $zone_editability_pair) {
                            if ($zi != 0) {
                                $where_map .= ' OR ';
                            }
                            if (($zone_editability_pair[1] & COMCODE_EDIT_ANY) == 0) {
                                $where_map .= '(' . db_string_equal_to('r.the_zone', $zone_editability_pair[0]) . ' AND p_submitter=' . strval(get_member()) . ')';
                            } else {
                                $where_map .= db_string_equal_to('r.the_zone', $zone_editability_pair[0]);
                            }
                        }
                        $where_map .= ')';
                    } else {
                        access_denied('ADD_OR_EDIT_COMCODE_PAGES'); // Nothing at all
                    }
                } else {
                    $where_map .= ' AND p_submitter=' . strval(get_member());
                }
            } else {
                // No additional filter; NB: this does assume no negative overrides are in place; if they are, an error will be shown when clicking through
            }
            if ($filter !== null) {
                $where_map .= ' AND (';
                $search_fields = [
                    'r.the_zone',
                    'r.the_page',
                    'p_parent_page',
                    $GLOBALS['SITE_DB']->translate_field_ref('cc_page_title'),
                    $GLOBALS['SITE_DB']->translate_field_ref('string_index'),
                ];
                foreach ($search_fields as $i => $field) {
                    if ($i != 0) {
                        $where_map .= ' OR ';
                    }
                    $where_map .= $field . ' LIKE \'' . db_encode_like('%' . $filter . '%') . '\'';
                }
                $filter_member_id = $GLOBALS['FORUM_DRIVER']->get_member_from_username($filter);
                if ($filter_member_id !== null) {
                    $where_map .= ' OR p_submitter=' . strval($filter_member_id);
                }
                $where_map .= ')';
            }
            if ($zone_filter !== null) {
                $where_map .= ' AND r.the_zone IN (';
                foreach ($zone_filter as $i => $zone) {
                    if ($i != 0) {
                        $where_map .= ',';
                    }
                    $where_map .= '\'' . db_escape_string($zone) . '\'';
                }
                $where_map .= ')';
            }

            // Do queries
            $ttable = get_table_prefix() . 'comcode_pages r LEFT JOIN ' . get_table_prefix() . 'cached_comcode_pages a ON r.the_page=a.the_page AND r.the_zone=a.the_zone';
            $sql = 'SELECT r.*,cc_page_title FROM ' . $ttable . ' WHERE ' . $where_map . ' ORDER BY ' . $sql_sort;
            $lang_fields = ['cc_page_title' => '?SHORT_TRANS', 'string_index' => 'LONG_TRANS'];
            $page_rows = $GLOBALS['SITE_DB']->query($sql, $max, $start, false, false, $lang_fields);
            $max_rows = $GLOBALS['SITE_DB']->query_value_if_there('SELECT COUNT(*) FROM (SELECT DISTINCT r.the_zone,r.the_page FROM ' . $ttable . ' WHERE ' . $where_map . ') x', false, false, $lang_fields);

            // Put together metadata
            $files_list = [];
            foreach ($page_rows as $row) {
                $located = _request_page($row['the_page'], $row['the_zone'], null, $lang);
                if (($located !== false) && ($located[0] != 'REDIRECT')) {
                    $_zone = $located[count($located) - 1];
                    $page_path = get_custom_file_base() . (($_zone == '') ? '' : '/') . $_zone;
                    if (!is_file($page_path)) {
                        $page_path = get_file_base() . (($_zone == '') ? '' : '/') . $_zone;
                    }

                    $files_list[$row['the_zone'] . ':' . $row['the_page']] = [
                        $page_path, // page_path
                        $row, // row
                    ];
                }
            }
        }
        if (!$found_via_query) {
            $files_list = $this->get_comcode_files_list_disk_search($lang, $zone_filter);
        }

        // Gather metadata, enough for sorting
        $rows = [];
        foreach ($files_list as $page_link => $path_bits) {
            list($zone, $page) = explode(':', $page_link, 2);
            if (!is_string($page)) {
                $page = strval($page);
            }

            // Check permissions
            if (!has_actual_page_access(get_member(), $page, $zone)) {
                continue;
            }

            // We need to separately read from DB to work out metadata?
            $db_row = null;
            if ($path_bits[1] === null) {
                $db_rows = $GLOBALS['SITE_DB']->query_select('comcode_pages r LEFT JOIN ' . get_table_prefix() . 'cached_comcode_pages a ON r.the_page=a.the_page AND r.the_zone=a.the_zone', ['r.*', 'cc_page_title'], ['r.the_zone' => $zone, 'r.the_page' => $page], '', 1);
                $db_row = isset($db_rows[0]) ? $db_rows[0] : null;
            } else { // Ah, no we got everything from DB in one go
                $db_row = $path_bits[1];
            }

            // Work out metadata
            if ($db_row === null) {
                $page_title = get_comcode_page_title_from_disk($path_bits[0]);
                $order = null;
                $parent_page = '';
                $submitter = null;
                $edit_date = filemtime($path_bits[0]);
                $validated = null;
            } else {
                if ($db_row['cc_page_title'] === null) {
                    require_code('zones2');
                    $page_title = get_comcode_page_title_from_disk($path_bits[0]);
                } else {
                    $page_title = get_translated_text($db_row['cc_page_title'], null, null, true);
                }
                $order = $db_row['p_order'];
                $parent_page = $db_row['p_parent_page'];
                $submitter = $db_row['p_submitter'];
                $edit_date = $db_row['p_add_date'];
                if ($db_row['p_edit_date'] !== null) {
                    $edit_date = $db_row['p_edit_date'];
                }
                $validated = $db_row['p_validated'];
            }
            $page_path = $path_bits[0];

            // Put into array
            $rows[] = [
                'page_title' => $page_title,
                'the_page' => $page,
                'the_zone' => $zone,
                'page_link' => $page_link,
                'p_order' => $order,
                'p_parent_page' => $parent_page,
                'p_submitter' => $submitter,
                'p_edit_date' => $edit_date,
                'p_validated' => $validated,
                'page_path' => $page_path,
            ];
        }

        // Search filtering
        if ($filter !== null) {
            $search_fields = [
                'the_zone',
                'the_page',
                'p_parent_page',
                'page_title',
            ];
            foreach ($rows as $i => $row) {
                foreach ($search_fields as $field) {
                    if (stripos($row[$field], $filter) !== false) {
                        continue 2;
                    }
                }

                if (($row['page_path'] !== null) && (stripos(cms_file_get_contents_safe($row['page_path']), $filter) !== false)) {
                    continue;
                }

                // Does not match
                unset($rows[$i]);
            }
        }

        // Manual sorting
        if (!$found_via_query) {
            $max_rows = count($rows);

            sort_maps_by($rows, $sortable, false, ($sortable == 'page_title'));
            if ($sort_order == 'DESC') {
                $rows = array_reverse($rows);
            }
        }

        if ($translations_mode) {
            require_code('lang2');
            $langs = find_all_langs();
            foreach (array_keys($langs) as $_lang) {
                $langs[$_lang] = lookup_language_full_name($_lang);
            }

            asort($langs, SORT_NATURAL | SORT_FLAG_CASE);

            // We want site language first
            $tmp = $langs[$lang];
            unset($langs[$lang]);
            $langs = array_merge([$lang => $tmp], $langs);
        }

        // Results table
        $has_pagination = (count($rows) > $max);
        $columns = [];
        $interactive_options = [];
        $columns[] = do_lang_tempcode('TITLE');
        $interactive_options[] = [true, false, 'alphanumeric'];
        $columns[] = do_lang_tempcode('ZONE');
        $interactive_options[] = [false, false, 'alphanumeric'];
        $columns[] = do_lang_tempcode('PAGE');
        $interactive_options[] = [true, false, 'alphanumeric'];
        if ($translations_mode) {
            foreach ($langs as $language_full_name) {
                $columns[] = $language_full_name;
                $interactive_options[] = [false,  false, 'alphanumeric'];
            }
        } else {
            $columns[] = do_lang_tempcode('metadata:OWNER');
            $interactive_options[] = [false,  false, 'alphanumeric'];
            $columns[] = do_lang_tempcode('EDITED');
            $interactive_options[] = [false,  false, 'date'];
            $columns[] = protect_from_escaping(do_template('COMCODE_ABBR', ['_GUID' => 'bd3e38aa0885f27174b4ccb4515eb727', 'TITLE' => do_lang_tempcode('VALIDATED'), 'CONTENT' => do_lang_tempcode('VALIDATED_SHORT')]));
            $interactive_options[] = [false,  false, 'alphanumeric'];
        }
        $columns[] = do_lang_tempcode('ACTIONS');
        $interactive_options[] = [false,  false, null];
        $header_row = results_header_row($columns, $sortables, 'sort', $sortable . ' ' . $sort_order, '434t6fgfh545', $has_pagination ? null : $interactive_options);
        $table_rows = new Tempcode();
        $i = 0;
        foreach ($rows as $row) {
            if (!$found_via_query) {
                if ($i < $start) {
                    continue;
                }
                if ($i > $max + $start) {
                    break;
                }
            }

            if ($translations_mode) {
                $translations_found = [];
                foreach ($langs as $_lang => $language_full_name) {
                    foreach (['comcode_custom', 'comcode'] as $page_type) {
                        foreach ((($row['the_zone'] == '') && (get_option('single_public_zone') == '1')) ? ['', 'site'] : [$row['the_zone']] as $_zone) {
                            foreach (array_unique([get_custom_file_base(), get_file_base()]) as $file_base) {
                                $test_path = $file_base . (($_zone == '') ? '' : ('/' . $_zone)) . '/pages/' . $page_type . '/' . $_lang . '/' . $row['the_page'] . '.txt';
                                if (is_file($test_path)) {
                                    $translations_found[$_lang] = filemtime($test_path);
                                    continue 4;
                                }
                            }
                        }
                    }
                }

                if ((count($translations_found) == 1) && (array_key_exists($lang, $translations_found))) {
                    // No translations
                    continue;
                }
            }

            $view_url = build_url(['page' => $row['the_page']], $row['the_zone']);
            $edit_url = build_url(['page' => '_SELF', 'type' => '_edit', 'page_link' => $row['page_link'], 'lang' => $lang], '_SELF');
            $clone_url = build_url(['page' => '_SELF', 'type' => '_edit', 'page_link' => /*new page-link only has zone specified initially*/$row['the_zone'] . ':', 'restore_from_path' => $row['page_path'], 'lang' => $lang], '_SELF');

            $page_path = $row['page_path'];

            $wrappable_page_link__tempcode = do_template('COMCODE_TELETYPE', ['_GUID' => 'bf4dbed562e189c84aa33c17d06c2791', 'CONTENT' => $row['page_link']]);
            if (get_value('disable_comcode_page_order') !== '1') {
                if (($row['p_order'] !== null) && ($row['p_order'] != ORDER_AUTOMATED_CRITERIA)) {
                    $wrappable_page_link__tempcode->attach(escape_html(' (' . do_lang('ORDER') . ' #' . integer_format($row['p_order']) . ')'));
                }
            }
            $page_hyperlink = hyperlink($edit_url, escape_html($row['page_title']), false, false, protect_from_escaping($wrappable_page_link__tempcode));

            $zone_name = $row['the_zone'];

            $page__tempcode = do_template('COMCODE_TELETYPE', [
                '_GUID' => '56e1af60e09524c20fc62dd55cda1eb9',
                'CONTENT' => escape_html($row['the_page']),
            ]);

            if ($row['p_submitter'] === null) {
                $username = do_lang('UNKNOWN');
            } else {
                $username = protect_from_escaping($GLOBALS['FORUM_DRIVER']->member_profile_hyperlink($row['p_submitter']));
            }

            $raw_edit_date = $row['p_edit_date'];
            $edit_date = make_string_tempcode(escape_html(get_timezoned_date($raw_edit_date, false)));
            $edit_date->attach(' &ndash; ');
            $edit_date->attach(do_lang_tempcode('_AGO', escape_html(display_time_period(time() - $raw_edit_date))));
            $edit_date__tempcode = do_template('COMCODE_ABBR', ['_GUID' => 'bd3e38aa0885f27174b4ccb4515eb728', 'TITLE' => protect_from_escaping($edit_date), 'CONTENT' => date('Y-m-d', $raw_edit_date)]);

            if ($row['p_validated'] === null) {
                $validated = do_lang_tempcode('YES');
            } else {
                $validated = ($row['p_validated'] == 1) ? do_lang_tempcode('YES') : do_lang_tempcode('NO');
            }

            $actions = do_template('COMCODE_PAGE_EDIT_ACTIONS', [
                '_GUID' => '6cc8c492ba9ae4035c394fbe28a56c26',
                'EDIT_URL' => $edit_url,
                'CLONE_URL' => $clone_url,
                'VIEW_URL' => $view_url,
            ]);

            $display_map = [];
            $display_map[] = protect_from_escaping($page_hyperlink);
            $display_map[] = protect_from_escaping(escape_html($zone_name));
            $display_map[] = protect_from_escaping($page__tempcode);
            if ($translations_mode) {
                foreach ($langs as $_lang => $language_full_name) {
                    if (array_key_exists($_lang, $translations_found)) {
                        $translation_time = $translations_found[$_lang];
                        $translation_label = do_lang_tempcode('_AGO', escape_html(display_time_period(time() - $translation_time)));
                        $translation_tooltip = get_timezoned_date($translation_time);
                    } else {
                        $translation_label = do_lang_tempcode('ADD');
                        $translation_tooltip = '';
                    }
                    $edit_link = build_url(['page' => '_SELF', 'type' => '_edit', 'page_link' => $row['page_link'], 'lang' => $_lang], '_SELF');
                    $display_map[] = hyperlink($edit_link, $translation_label, false, true, $translation_tooltip);
                }
            } else {
                $display_map[] = $username;
                $display_map[] = protect_from_escaping($edit_date__tempcode);
                $display_map[] = $validated;
            }
            $display_map[] = protect_from_escaping($actions);

            $table_rows->attach(static_evaluate_tempcode(results_entry($display_map, true)));

            $i++;
        }
        $table = results_table(do_lang('COMCODE_PAGES'), $start, 'start', $max, 'max', $max_rows, $header_row, $table_rows, $sortables, $sortable, $sort_order, 'sort', null, [], null, 8, 'fdgfdfdfdggfd', true, null, true);

        // Form
        if ($fields->is_empty()) {
            $extra = new Tempcode();
        } else {
            $map = ['page' => '_SELF', 'type' => '_edit', 'lang' => $lang];
            $post_url = build_url($map, '_SELF', [], false, true);

            $extra = do_template('FORM', [
                '_GUID' => '52f88211619a877e5c6f85fd4d46a90e',
                'FIELDS' => $fields,
                'TEXT' => '',
                'URL' => $post_url,
                'GET' => true,
                'HIDDEN' => '',
                'SUBMIT_NAME' => $submit_name,
                'SUBMIT_ICON' => 'admin/add',
            ]);
        }

        // Custom fields
        require_code('fields');
        $_links = manage_custom_fields_donext_link('comcode_page');
        $links = [];
        foreach ($_links as $_link) {
            $links[] = [
                'LINK_ICON' => $_link[0],
                'LINK_URL' => build_url(['page' => $_link[1][0]] + $_link[1][1], $_link[1][2]),
                'LINK_TEXT' => $_link[2],
            ];
        }

        if (multi_lang()) {
            if ($translations_mode) {
                $links[] = [
                    'LINK_ICON' => 'menu/cms/comcode_page_edit',
                    'LINK_URL' => build_url(['page' => '_SELF'], '_SELF'),
                    'LINK_TEXT' => do_lang_tempcode('COMCODE_PAGE_EDIT'),
                ];
            } else {
                $links[] = [
                    'LINK_ICON' => 'menu/adminzone/style/language/language_content',
                    'LINK_URL' => build_url(['page' => '_SELF', 'translations' => 1], '_SELF'),
                    'LINK_TEXT' => do_lang_tempcode('COMCODE_PAGE_TRANSLATIONS'),
                ];
            }
        }

        // Render...

        $url = build_url(['page' => '_SELF', 'lang' => $lang], '_SELF');

        $tpl = do_template('COMCODE_PAGE_MANAGE_SCREEN', [
            '_GUID' => 'eba3e03c65d96530e3a42d600f90ccd8',
            'TITLE' => $this->title,
            'TEXT' => $text,
            'TABLE' => $table,
            'FIELDS' => $fields,
            'URL' => $url,
            'SUBMIT_NAME' => $submit_name,
            'LINKS' => $links,
            'EXTRA' => $extra,
            'FILTER' => ($filter === null) ? '' : $filter,
            'HAS_PAGINATION' => $has_pagination,
            'TRANSLATIONS_MODE' => $translations_mode,
        ]);

        require_code('templates_internalise_screen');
        return internalise_own_screen($tpl);
    }

    /**
     * The UI to edit a page.
     *
     * @return Tempcode The UI
     */
    public function _edit() : object
    {
        require_code('input_filter_2');
        if (get_value('disable_modsecurity_workaround') !== '1') {
            modsecurity_workaround_enable();
        }

        $page_link = $this->page_link;
        $zone = $this->zone;
        $file = $this->file;

        if (($zone != '') && (!file_exists(get_file_base() . '/' . $zone . (($zone == '') ? '' : '/') . 'pages'))) {
            set_http_status_code(404);
            warn_exit(do_lang_tempcode('NO_SUCH_ZONE'), false, false, 404);
        }

        if (strlen($file) > 80) {
            warn_exit(do_lang_tempcode('BAD_CODENAME'));
        }

        require_code('type_sanitisation');
        if ((!is_alphanumeric($file)) || ((strpos($file, '-') !== false) && (strpos($file, '_') !== false))/*can't have both*/) {
            warn_exit(do_lang_tempcode('BAD_CODENAME'));
        }

        $lang = choose_language(get_screen_title(($file == '') ? 'COMCODE_PAGE_ADD' : 'COMCODE_PAGE_EDIT'), true);
        if (is_object($lang)) {
            return $lang;
        }

        if ((get_param_string('page_template', null) === null) && (get_param_integer('may_choose_template', 0) == 1) && (get_value('page_template_always_ask', '0', true) == '1')) {
             $template_list = create_selection_list_page_templates();

             $fields = new Tempcode();
             $fields->attach(form_input_huge_list(do_lang_tempcode('PAGE_TEMPLATE'), do_lang_tempcode('PAGE_TEMPLATE_DESCRIPTION'), 'page_template', $template_list, null, true, false));

             $hidden = build_keep_post_fields();
             $url = get_self_url();

             return do_template('FORM_SCREEN', [
                 '_GUID' => '5f70abf8bfe4eb4469f8ccc13a7e6fbb',
                 'SKIP_WEBSTANDARDS' => true,
                 'GET' => true,
                 'HIDDEN' => $hidden,
                 'SUBMIT_ICON' => 'buttons/proceed',
                 'SUBMIT_NAME' => do_lang_tempcode('PROCEED'),
                 'TITLE' => $this->title,
                 'FIELDS' => $fields,
                 'URL' => $url,
                 'TEXT' => do_lang_tempcode('SELECT_PAGE_TEMPLATE'),
             ]);
        }

        $resource_owner = $GLOBALS['SITE_DB']->query_select_value_if_there('comcode_pages', 'p_submitter', ['the_zone' => $zone, 'the_page' => $file]);
        if ($resource_owner === null) { // Add
            if (!has_add_comcode_page_permission($zone)) {
                access_denied('ADD_COMCODE_PAGE');
            }
        } else { // Edit
            if (!has_edit_comcode_page_permission($zone, $file, $resource_owner)) {
                access_denied('EDIT_COMCODE_PAGE');
            }
        }

        list($file_base, $file_path) = find_comcode_page($lang, $file, $zone);

        // Check no redirects in our way
        if (addon_installed('redirects_editor')) {
            $test = $GLOBALS['SITE_DB']->query_select_value_if_there('redirects', 'r_to_zone', ['r_from_page' => $file, 'r_from_zone' => $zone]);
            if ($test !== null) {
                $redirect_url = build_url(['page' => 'admin_redirects', 'type' => 'page'], get_module_zone('admin_redirects'));
                attach_message(do_lang_tempcode('BLOCKING_REDIRECT_IN_PLACE', escape_html($redirect_url->evaluate())), 'warn');
            }
        }

        if ($file == '') {
            url_default_parameters__enable();
        }

        if ($file != '') {
            check_page_name($zone, $file);
        }

        // Default file contents
        $contents = post_param_string('new', '');
        $parsed = null;
        if ($contents == '') {
            if (is_file($file_base . '/' . $file_path)) {
                $contents = cms_file_get_contents_safe($file_base . '/' . $file_path, FILE_READ_LOCK | FILE_READ_BOM);

                if (strpos($file_path, '_custom/') === false) {
                    global $LANG_FILTER_OB;
                    $contents = $LANG_FILTER_OB->compile_time(null, $contents, $lang);
                }

                $comcode_page_rows = $GLOBALS['SITE_DB']->query_select('cached_comcode_pages', ['*'], ['the_zone' => $zone, 'the_page' => $file], '', 1);
                if (array_key_exists(0, $comcode_page_rows)) {
                    $parsed = get_translated_tempcode('cached_comcode_pages', $comcode_page_rows[0], 'string_index', null, $lang);
                }

                $new = false;
            } else {
                $template_name = get_param_string('page_template', $file);
                $contents = get_template_contents($template_name);

                $new = true;
            }

            if ($new) {
                if ((strpos($contents, '[title') === false) && (substr($file, 0, 6) != 'panel_') && (substr($file, 0, 1) != '_')) {
                    $contents = '[title]' . titleify($file) . '[/title]' . "\n\n" . $contents;

                    if ((get_option('is_on_comcode_page_children') == '1') && (has_privilege(get_member(), 'comcode_dangerous')) && (get_param_string('page_template', null) === null)) {
                        $contents .= "\n\n" . '[block]main_comcode_page_children[/block]';
                    }
                }
            }
        } else {
            $new = false;
        }

        if (addon_installed('actionlog')) {
            require_code('revisions_engine_files');
            $revision_engine = new RevisionEngineFiles();
            $directory = $zone . (($zone == '') ? '' : '/') . 'pages/comcode_custom/' . $lang;
            $revision_loaded = null;
            $revisions = $revision_engine->ui_revisions_controller($directory, $file, 'txt', 'COMCODE_PAGE_EDIT', $contents, $revision_loaded);
            if ($revision_loaded) {
                $parsed = null;
            }
            if ((get_param_integer('diffing', 0) == 1) && (!$revisions->is_empty())) {
                return $revisions;
            }
        } else {
            $revisions = new Tempcode();
        }

        // ---

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

        if ((!$new) && (strpos($file_path, '/comcode_custom/') !== false)) {
            $delete_url = build_url(['page' => '_SELF', 'type' => '__edit', 'lang' => $lang, 'delete' => 1], '_SELF');
        } else {
            $delete_url = new Tempcode();
        }

        $hidden_fields = new Tempcode();
        $fields = new Tempcode();
        $fields2 = new Tempcode();

        if (addon_installed('page_management')) {
            if (has_actual_page_access(get_member(), 'admin_sitemap')) {
                $fields->attach(form_input_codename(do_lang_tempcode('CODENAME'), do_lang_tempcode('DESCRIPTION_PAGE_NAME'), 'title', $file, true));
            }
        }

        $rows = $GLOBALS['SITE_DB']->query_select('comcode_pages', ['*'], ['the_zone' => $zone, 'the_page' => $file], '', 1);
        if (array_key_exists(0, $rows)) {
            // Existing
            $validated = $rows[0]['p_validated'] == 1;
            $parent_page = $rows[0]['p_parent_page'];
            $show_as_edit = $rows[0]['p_show_as_edit'] == 1;
            $owner = $rows[0]['p_submitter'];
            $order = $rows[0]['p_order'];
        } else {
            // New or raw .txt not processed into DB yet
            $validated = true;
            $parent_page = get_param_string('parent_page', '');
            $show_as_edit = false;
            $owner = get_member();
            $order = $GLOBALS['SITE_DB']->query_select_value_if_there('comcode_pages', 'MAX(p_order)');
            if ($order === null) {
                $order = -1;
            }
            $order++;
        }

        if (!$validated) {
            $validated = (get_param_integer('validated', 0) == 1);
        }
        if (has_bypass_validation_comcode_page_permission($zone)) {
            if (addon_installed('validation')) {
                $fields2->attach(form_input_tick(do_lang_tempcode('VALIDATED'), do_lang_tempcode($GLOBALS['FORUM_DRIVER']->is_super_admin(get_member()) ? 'DESCRIPTION_VALIDATED_SIMPLE' : 'DESCRIPTION_VALIDATED', 'comcode_page'), 'validated', $validated));
            }
        }

        if (!$new) {
            if ($delete_url->is_empty()) {
                $fields2->attach(form_input_tick(do_lang_tempcode('DELETE'), do_lang_tempcode('DESCRIPTION_DELETE'), 'delete', false));
            }
        }

        require_code('global4');
        $include_on_sitemap = comcode_page_include_on_sitemap($zone, $file);
        $fields2->attach(form_input_tick(do_lang_tempcode('INCLUDE_ON_SITEMAP'), do_lang_tempcode('DESCRIPTION_INCLUDE_ON_SITEMAP'), 'include_on_sitemap', $include_on_sitemap));

        if (get_option('is_on_comcode_page_children') == '1') {
            $_pages = find_all_pages_wrap($zone);
            cms_mb_ksort($_pages, SORT_NATURAL | SORT_FLAG_CASE);
            $pages = form_input_list_entry('', false, do_lang_tempcode('NA_EM'));
            foreach (array_keys($_pages) as $page) {
                if (!is_string($page)) {
                    $page = strval($page);
                }
                if ($page != $file) {
                    $pages->attach(form_input_list_entry($page, $parent_page == $page));
                }
            }

            $fields2->attach(form_input_list(do_lang_tempcode('PARENT_PAGE'), do_lang_tempcode('DESCRIPTION_PARENT_PAGE'), 'parent_page', $pages, null, false, false));
        }

        if ($this->has_integrated_menu_editing($zone . ':' . $file)) {
            $_existing_menu_branch = $GLOBALS['SITE_DB']->query_select('menu_items', ['i_parent_id'], ['i_link' => $zone . ':' . $file], '', 1);
            if (array_key_exists(0, $_existing_menu_branch)) {
                $existing_menu_branch = $_existing_menu_branch[0]['i_parent_id'];
            } else {
                $existing_menu_branch = -1;
            }
            $menu_branches = $GLOBALS['SITE_DB']->query_select('menu_items', ['id', 'i_caption', 'i_parent_id', 'i_link']);
            $_menu_branches = new Tempcode();
            $_menu_branches->attach(form_input_list_entry('-1', $existing_menu_branch === -1, do_lang_tempcode('NONE_EM')));
            $_menu_branches->attach(form_input_list_entry('', $existing_menu_branch === null, do_lang_tempcode('TOP_LEVEL')));
            $_menu_branches->attach($this->menu_branch_selection_under(null, $menu_branches, $existing_menu_branch, $zone . ':' . $file));
            $fields2->attach(form_input_list(do_lang_tempcode('MENU_ITEM_UNDER'), do_lang_tempcode('DESCRIPTION_MENU_ITEM_UNDER'), 'menu_item_under', $_menu_branches, null, false, false));
        }

        if (get_value('disable_comcode_page_show_as_edited') !== '1') {
            $fields2->attach(form_input_tick(do_lang_tempcode('SHOW_AS_EDITED'), do_lang_tempcode('DESCRIPTION_SHOW_AS_EDITED'), 'show_as_edit', $show_as_edit));
        }

        if (get_value('disable_comcode_page_order') !== '1') {
            $fields2->attach(get_order_field('comcode_page', 'zone', $order));
        } else {
            $hidden_fields->attach(form_input_hidden('order', strval($order)));
        }

        require_code('fields');
        append_form_custom_fields('comcode_page', $zone . ':' . $file, $fields2, $hidden_fields);

        $meta_keywords = post_param_string('meta_keywords', '');
        $meta_description = post_param_string('meta_description', '');
        if (($meta_keywords == '') && ($meta_description == '')) {
            list($meta_keywords, $meta_description) = seo_meta_get_for('comcode_page', $zone . ':' . $file);
        }
        $fields2->attach(do_template('FORM_SCREEN_FIELD_SPACER', [
            '_GUID' => 'a42341a9a2de532cecdcfbecaff00a0f',
            'TITLE' => do_lang_tempcode('SEO'),
            'SECTION_HIDDEN' => true,
            'HELP' => (get_option('show_docs') === '0') ? null : do_lang_tempcode('TUTORIAL_ON_THIS', escape_html(get_tutorial_url('tut_seo'))),
        ]));
        $fields2->attach(form_input_line_multi(do_lang_tempcode('KEYWORDS'), do_lang_tempcode('DESCRIPTION_META_KEYWORDS'), 'meta_keywords[]', array_map('trim', explode(',', preg_replace('#,+#', ',', $meta_keywords))), 0));
        $fields2->attach(form_input_line(do_lang_tempcode('META_DESCRIPTION'), do_lang_tempcode('DESCRIPTION_META_DESCRIPTION'), 'meta_description', $meta_description, false));

        if (addon_installed('awards')) {
            require_code('awards');
            $fields2->attach(get_award_fields('comcode_page', $zone . ':' . $file));
        }

        $fields2->attach(metadata_get_fields('comcode_page', ($page_link == '') ? null : $page_link));

        if (addon_installed('content_reviews')) {
            require_code('content_reviews2');
            $fields2->attach(content_review_get_fields('comcode_page', ($page_link == '') ? null : $page_link));
        }

        require_code('permissions2');
        $fields2->attach(get_page_permissions_for_environment($zone, $file));

        $hidden_fields->attach(form_input_hidden('file', $file));
        $hidden_fields->attach(form_input_hidden('zone', $zone));
        $hidden_fields->attach(form_input_hidden('redirect', static_evaluate_tempcode(protect_url_parameter(get_param_string('redirect', '', INPUT_FILTER_URL_INTERNAL)))));

        $cancel_url = build_url(['page' => '_SELF', 'clear_autosave' => 1], '_SELF');
        $posting_form = get_posting_form(do_lang(($file == '') ? 'COMCODE_PAGE_ADD' : 'SAVE'), ($file == '') ? 'admin/add' : 'admin/edit_this', $contents, $post_url, $hidden_fields, $fields, do_lang_tempcode('COMCODE_PAGE'), '', $fields2, $parsed, [], null, false, true, true, true, false, '', $cancel_url);

        if ($file == '') {
            url_default_parameters__disable();
        }

        $text = new Tempcode();
        if (addon_installed('points')) {
            $login_url = build_url(['page' => 'login', 'type' => 'browse', 'redirect' => protect_url_parameter(get_self_url(true))], get_module_zone('login'));
            $_login_url = escape_html($login_url->evaluate());
            if ((is_guest()) && ((get_forum_type() != 'cns') || (has_actual_page_access(get_member(), 'join')))) {
                $text->attach(paragraph(do_lang_tempcode('NOT_LOGGED_IN_NO_CREDIT', $_login_url)));
            }
        }

        list($warning_details, $ping_url) = handle_conflict_resolution($page_link);

        $is_translation = ($lang != get_site_default_lang());

        return do_template('COMCODE_PAGE_EDIT_SCREEN', [
            '_GUID' => 'ec1d773684757f5bf6f39cf931555bf2',
            'NEW' => $new,
            'PING_URL' => $ping_url,
            'WARNING_DETAILS' => $warning_details,
            'TEXT' => $text,
            'TITLE' => $this->title,
            'DELETE_URL' => $delete_url,
            'IS_TRANSLATION' => $is_translation,
            'ZONE' => $zone,
            'FILE' => $file,
            'POSTING_FORM' => $posting_form,
            'REVISIONS' => $revisions,
        ]);
    }

    /**
     * Whether the Comcode page editor has integrated menu editing.
     *
     * @param  string $page_link Page-link of page
     * @return boolean Whether it has
     */
    protected function has_integrated_menu_editing(string $page_link) : bool
    {
        $num_menus = $GLOBALS['SITE_DB']->query_select_value('menu_items', 'COUNT(DISTINCT i_menu)');
        if ($num_menus != 1) {
            return false;
        }

        $num_menus_in_now = $GLOBALS['SITE_DB']->query_select_value('menu_items', 'COUNT(*)', ['i_link' => $page_link]);
        if ($num_menus_in_now > 1) {
            return false;
        }

        return true;
    }

    /**
     * Find menu branches for selection, in a tree structure.
     *
     * @param  ?AUTO_LINK $parent_id Parent ID (null: root)
     * @param  array $menu_branches List of menu branches
     * @param  ?AUTO_LINK $existing_menu_branch The menu branch currently selected (null: none)
     * @param  string $page_link Page-link of page being edited
     * @param  string $prefix Prefix before labels
     * @return Tempcode The menu branch list entries
     */
    protected function menu_branch_selection_under(?int $parent_id, array $menu_branches, ?int $existing_menu_branch, string $page_link, string $prefix = '') : object
    {
        $_menu_branches = new Tempcode();
        foreach ($menu_branches as $menu_branch) {
            if (($menu_branch['i_parent_id'] === $parent_id) && ($menu_branch['i_link'] != $page_link)) {
                $caption = get_translated_text($menu_branch['i_caption']);
                $_menu_branches->attach(form_input_list_entry(strval($menu_branch['id']), $menu_branch['id'] === $existing_menu_branch, $prefix . $caption));
                $under = $this->menu_branch_selection_under($menu_branch['id'], $menu_branches, $existing_menu_branch, $page_link, $prefix . $caption . ' > ');
                $_menu_branches->attach($under);
            }
        }
        return $_menu_branches;
    }

    /**
     * The actualiser to edit a Comcode page.
     *
     * @return Tempcode The UI
     */
    public function __edit() : object
    {
        require_code('input_filter_2');
        if (get_value('disable_modsecurity_workaround') !== '1') {
            modsecurity_workaround_enable();
        }

        // Load up settings from the environments
        $file = filter_naughty(post_param_string('file'));
        $lang = user_lang__with__translation_override();
        $zone = filter_naughty(post_param_string('zone'));
        if (addon_installed('page_management')) {
            $new_file = filter_naughty(has_actual_page_access(get_member(), 'admin_sitemap') ? post_param_string('title', $file) : $file);
        } else {
            $new_file = filter_naughty($file);
        }

        require_code('type_sanitisation');
        if ((!is_alphanumeric($new_file)) || (strpos($new_file, '-') !== false && strpos($new_file, '_') !== false)/*can't have both*/) {
            warn_exit(do_lang_tempcode('BAD_CODENAME'));
        }

        if ($file == '') {
            $file = $new_file;
        }

        $validated = post_param_integer('validated', 0);
        if (!addon_installed('validation')) {
            $validated = 1;
        }
        $include_on_sitemap = post_param_integer('include_on_sitemap', 0);
        require_code('antispam');
        inject_action_spamcheck();
        if (!has_bypass_validation_comcode_page_permission($zone)) {
            $validated = 0;
        }
        $parent_page = post_param_string('parent_page', '');
        $order = post_param_order_field();
        $show_as_edit = post_param_integer('show_as_edit', 0);
        $text_raw = post_param_string('post');
        $metadata = actual_metadata_get_fields('comcode_page', $zone . ':' . $file, [], $new_file);
        $is_deleting = (post_param_integer('delete', 0) == 1);

        $resource_owner = $GLOBALS['SITE_DB']->query_select_value_if_there('comcode_pages', 'p_submitter', ['the_zone' => $zone, 'the_page' => $file]);

        // Privilege and permission checking
        if ($resource_owner === null) { // Add
            if (!has_add_comcode_page_permission($zone)) {
                access_denied('ADD_COMCODE_PAGE');
            }
        } elseif (!$is_deleting) { // Edit
            if (!has_edit_comcode_page_permission($zone, $file, $resource_owner)) {
                access_denied('EDIT_COMCODE_PAGE');
            }
        } else { // Delete
            if (!has_delete_comcode_page_permission($zone, $file, $resource_owner)) {
                access_denied('DELETE_COMCODE_PAGE');
            }
        }

        // Deleting?
        if ($is_deleting) {
            $is_translation = ($lang != get_site_default_lang());

            require_code('zones3');
            delete_cms_page($zone, $file, 'comcode_custom', false, $is_translation ? $lang : null);

            $completion_text = do_lang_tempcode($is_translation ? 'SUCCESS_PAGE_TRANSLATION_DELETED' : 'SUCCESS_PAGE_DELETED');
            $after_delete_url = build_url(['page' => '_SELF', 'translations' => $is_translation ? 1 : null, 'lang' => $lang], '_SELF');
            return redirect_screen($this->title, $after_delete_url, $completion_text);
        }

        // Handle attachments
        require_code('attachments2');
        $_text = do_comcode_attachments($text_raw, 'comcode_page', $zone . ':' . $file);
        $text = $_text['comcode'];

        // Save custom fields
        require_code('fields');
        save_form_custom_fields('comcode_page', $zone . ':' . $new_file, $zone . ':' . $file);

        // Main save function
        $path = save_comcode_page($zone, $new_file, $lang, $text, $validated, $include_on_sitemap, $parent_page, $order, $metadata['add_time'], $metadata['edit_time'], $show_as_edit, $metadata['submitter'], $file, post_param_string('meta_keywords', ''), post_param_string('meta_description', ''));

        // Some general CRUD maintenance that we don't do within the save_comcode_page function
        if ($resource_owner === null) { // Add
            require_code('submit');
            give_submit_points('COMCODE_PAGE_ADD', 'comcode_page', $zone . ':' . $file);

            require_code('member_mentions');
            dispatch_member_mention_notifications('comcode_page', $zone . ':' . $file, $resource_owner);
        } else { // Edit
            if (!has_edit_comcode_page_permission($zone, $file, $resource_owner)) {
                access_denied('EDIT_COMCODE_PAGE');
            }

            require_code('submit');
            $just_validated = (!content_validated('comcode_page', $zone . ':' . $file)) && ($validated == 1);
            if ($just_validated) {
                send_content_validated_notification('comcode_page', $zone . ':' . $file);
            }
        }

        require_code('permissions2');
        set_page_permissions_from_environment($zone, $file);
        if (addon_installed('awards')) {
            require_code('awards');
            handle_award_setting('comcode_page', $zone . ':' . $new_file);
        }
        if (addon_installed('content_reviews')) {
            require_code('content_reviews2');
            content_review_set('comcode_page', $zone . ':' . $new_file, $zone . ':' . $file);
        }

        // Update any current menu link(s)
        $GLOBALS['SITE_DB']->query_update('menu_items', ['i_link' => $zone . ':' . $new_file], ['i_link' => $zone . ':' . $file]);

        // Menu editing
        if ($this->has_integrated_menu_editing($zone . ':' . $file)) {
            $menu_item_under = post_param_integer('menu_item_under', null);
            require_code('menus2');
            if ($menu_item_under == -1) {
                delete_menu_item_simple($zone . ':' . $new_file);
            } else {
                $_existing_menu_branch = $GLOBALS['SITE_DB']->query_select('menu_items', ['i_parent_id'], ['i_link' => $zone . ':' . $new_file], '', 1);
                if (array_key_exists(0, $_existing_menu_branch)) {
                    $GLOBALS['SITE_DB']->query_update('menu_items', ['i_parent_id' => $menu_item_under], ['i_link' => $zone . ':' . $new_file], '', 1);
                } else {
                    $menu = $GLOBALS['SITE_DB']->query_select_value('menu_items', 'i_menu');
                    require_code('zones2');
                    add_menu_item_simple($menu, $menu_item_under, get_comcode_page_title_from_disk($path), $zone . ':' . $new_file, 0, 1);
                }
            }
        }

        // Health Check
        if (addon_installed('health_check')) {
            require_code('health_check');
            $has_fails = false;
            $categories = run_health_check($has_fails, null, false, false, false, false, null, [$zone . ':' . $new_file], null, CHECK_CONTEXT__SPECIFIC_PAGE_LINKS);
            foreach ($categories as $category_label => $sections) {
                foreach ($sections['SECTIONS'] as $section_label => $results) {
                    foreach ($results['RESULTS'] as $result) {
                        attach_message($result['MESSAGE'], 'notice');
                    }
                }
            }
        }

        // Look for conflicting page names
        $existing_page = __request_page($new_file, $zone);
        if (($existing_page !== false) && (strpos($existing_page[0], 'COMCODE') === false)) {
            if ($existing_page[0] == 'REDIRECT') {
                attach_message(do_lang_tempcode('CONFLICTING_PAGE_NAME_REDIRECT'), 'warn');
            } else {
                attach_message(do_lang_tempcode('CONFLICTING_PAGE_NAME_MODULE', escape_html($existing_page[count($existing_page) - 1])), 'warn');
            }
        }

        // Messaging to user
        if ($validated == 0) {
            require_code('submit');
            $edit_url = build_url(['page' => '_SELF', 'type' => '_edit', 'page_link' => $zone . ':' . $new_file], '_SELF', [], false, false, true);
            if (addon_installed('validation')) {
                send_validation_request('COMCODE_PAGE_EDIT', 'comcode_pages', true, $zone . ':' . $new_file, $edit_url);
            }
        }
        $completion_text = ($validated == 0) ? do_lang_tempcode('SUBMIT_NOT_VALIDATED', 'comcode_page') : do_lang_tempcode('SUCCESS');
        if ($new_file == $file) {
            $url = post_param_string('redirect', '', INPUT_FILTER_URL_INTERNAL);
        } else {
            $url = '';
        }
        if ($url != '') {
            return redirect_screen($this->title, $url, $completion_text);
        }
        return $this->do_next_manager($this->title, $new_file, $zone, $completion_text);
    }

    /**
     * Generate page sitemap.
     *
     * @return Tempcode The UI
     */
    public function generate_page_sitemap() : object
    {
        require_css('comcode_page_manage');

        require_code('type_sanitisation');
        require_code('global4');

        disable_php_memory_limit();
        cms_extend_time_limit(TIME_LIMIT_EXTEND__SLOW);

        $zone = get_param_string('filter', null);

        $total_known_pages = $GLOBALS['SITE_DB']->query_select_value('comcode_pages', 'COUNT(*)');
        if (
            ($total_known_pages < 300) &&
            ($GLOBALS['SITE_DB']->query_select_value_if_there('comcode_pages c LEFT JOIN ' . get_table_prefix() . 'cached_comcode_pages a ON c.the_page=a.the_page AND c.the_zone=a.the_zone', 'c.the_page', ['a.the_page' => null]) !== null)
        ) {
            $____pages = $GLOBALS['SITE_DB']->query_select('comcode_pages', ['the_zone', 'the_page', 'p_parent_page', 'p_validated', 'p_include_on_sitemap']);
            $___pages = [];
            foreach ($____pages as $____page) {
                $___pages[$____page['the_zone'] . ':' . $____page['the_page']] = $____page;
            }

            $files_list = $this->get_comcode_files_list_disk_search(user_lang(), null, false);
            $__pages = [];
            foreach ($files_list as $page_link => $path_bits) {
                send_http_output_ping();

                list($_zone, $_page) = explode(':', $page_link, 2);
                if (!is_string($_page)) {
                    $_page = strval($_page);
                }

                $__pages[] = [
                    'the_zone' => $_zone,
                    'the_page' => $_page,
                    'p_parent_page' => isset($___pages[$page_link]) ? $___pages[$page_link]['p_parent_page'] : '',
                    'p_validated' => isset($___pages[$page_link]) ? $___pages[$page_link]['p_validated'] : 1,
                    'cc_page_title' => get_comcode_page_title_from_disk($path_bits[0]),
                    'string_index' => cms_file_get_contents_safe($path_bits[0]),
                    'p_include_on_sitemap' => _comcode_page_include_on_sitemap_default($_zone, $_page),
                ];
            }
        } else {
            $__pages = $GLOBALS['SITE_DB']->query_select(
                'comcode_pages a LEFT JOIN ' . get_table_prefix() . 'cached_comcode_pages b ON a.the_zone=b.the_zone AND a.the_page=b.the_page',
                ['a.*', 'b.cc_page_title', 'b.string_index', 'a.p_include_on_sitemap'],
                [],
                'ORDER BY the_zone,the_page'
            );
        }
        $_pages = [];
        foreach ($__pages as $i => $__page) {
            if (!comcode_page_include_on_sitemap($__page['the_zone'], $__page['the_page'], $__page)) {
                unset($__pages[$i]);
                continue;
            }

            $_pages[] = $__page;
        }
        $pages = [];
        foreach ($_pages as $__page) {
            if (($zone !== null) && ($__page['the_zone'] != $zone)) {
                continue;
            }

            if (!isset($pages[$__page['p_parent_page']])) {
                $pages[$__page['p_parent_page']] = [];
            }
            $pages[$__page['p_parent_page']][] = $__page;
        }

        $_zones = find_all_zones(false, true, true);
        $zones = [];
        foreach ($_zones as $_zone) {
            if (!is_alphanumeric($_zone[0])) {
                continue;
            }

            // Check has something in the zone
            $ok = false;
            foreach ($__pages as $__page) {
                if ($_zone[0] == $__page['the_zone']) {
                    $ok = true;
                    break;
                }
            }
            if (!$ok) {
                continue;
            }

            $zones[$_zone[0]] = $_zone[1];
        }

        // Sort zones
        cms_mb_asort($zones, SORT_NATURAL | SORT_FLAG_CASE);
        if (isset($zones[''])) { // Move welcome zone to start of list
            $zones = array_merge(['' => $zones['']], $zones);
        }

        $page_structure = $this->organise_page_tree($pages);

        return do_template('GENERATE_PAGE_SITEMAP_SCREEN', ['_GUID' => 'b07b07bfca5d4191e99671040b70ed51', 'TITLE' => $this->title, 'ZONES' => $zones, 'PAGE_STRUCTURE' => $page_structure]);
    }

    /**
     * Get the page sitemap tree structure.
     *
     * @param  array $pages An array of pages
     * @param  ID_TEXT $under The page we are looking under
     * @return Tempcode The structure
     */
    public function organise_page_tree(array &$pages, string $under = '') : object
    {
        send_http_output_ping();

        static $todo_checks = null;
        static $no_validation_support = null;
        static $menu_branches_by_url = null;
        static $menu_branches_by_id = null;
        static $user_lang = null;
        if ($todo_checks === null) {
            $_todo_checks = cms_mb_strtolower(do_lang('UNDER_CONSTRUCTION_MARKERS'));
            $todo_checks = ($_todo_checks == '') ? [] : explode('|', $_todo_checks);

            $no_validation_support = !addon_installed('validation');

            $zone_start_pages = collapse_2d_complexity('zone_name', 'zone_default_page', $GLOBALS['SITE_DB']->query_select('zones', ['zone_name', 'zone_default_page']));

            $menu_branches = $GLOBALS['SITE_DB']->query_select('menu_items', ['id', 'i_menu', 'i_parent_id', 'i_caption', 'i_link'], [], 'ORDER BY i_menu');
            $menu_branches_by_url = [];
            foreach ($menu_branches as $menu_branch) {
                $matches = [];
                if (preg_match('#^(\w*):$#', $menu_branch['i_link'], $matches) != 0) {
                    if (isset($zone_start_pages[$matches[1]])) {
                        $menu_branch['i_link'] .= $zone_start_pages[$matches[1]];
                    }
                }

                if (!isset($menu_branches_by_url[$menu_branch['i_link']])) {
                    $menu_branches_by_url[$menu_branch['i_link']] = [];
                }
                $menu_branches_by_url[$menu_branch['i_link']][] = $menu_branch;
            }
            $menu_branches_by_id = list_to_map('id', $menu_branches);

            $user_lang = user_lang();
        }

        $page_structure = [];
        foreach (isset($pages[$under]) ? $pages[$under] : [] as $page) {
            $todo = false;
            if ($page['string_index'] !== null) {
                $page_contents = is_string($page['string_index']) ? $page['string_index'] : get_translated_text($page['string_index']);
            } else {
                $located = _request_page($page['the_page'], $page['the_zone']);
                if (($located !== false) && ($located[0] != 'REDIRECT') && (isset($located[4]))) {
                    $_zone = $located[count($located) - 1];
                    $page_path = get_custom_file_base() . (($_zone == '') ? '' : '/') . $_zone;
                    if (!is_file($page_path)) {
                        $page_path = get_file_base() . (($_zone == '') ? '' : '/') . $_zone;
                    }
                    $page_contents = cms_file_get_contents_safe($page_path, FILE_READ_LOCK | FILE_READ_BOM);
                } else {
                    $page_contents = '';
                }
            }
            if (!cms_empty_safe($page_contents)) {
                if ($user_lang == 'EN') {
                    $page_contents_lower = $page_contents; // FUDGE: For performance we can assume English only requires ASCII characters for this check
                } else {
                    $page_contents_lower = cms_mb_strtolower($page_contents);
                }

                foreach ($todo_checks as $todo_check) {
                    if (strpos($page_contents_lower, $todo_check) !== false) {
                        $todo = true;
                    }
                }
                if (strpos($page_contents, 'TODO') !== false) {
                    $todo = true;
                }
            }

            $page_link = $page['the_zone'] . ':' . $page['the_page'];

            $menu_paths = [];
            foreach (isset($menu_branches_by_url[$page_link]) ? $menu_branches_by_url[$page_link] : [] as $menu_branch) {
                $menu_path_components = [];
                $menu_branch_temp = $menu_branch;
                do {
                    $menu_path_components[] = get_translated_text($menu_branch_temp['i_caption']);
                    $parent = $menu_branch_temp['i_parent_id'];
                    if ($parent !== null) {
                        $menu_branch_temp = $menu_branches_by_id[$parent];
                    }
                } while ($parent !== null);

                $menu_url = build_url(['page' => 'admin_menus', 'type' => 'edit', 'id' => $menu_branch['i_menu']], get_module_zone('admin_menus'));
                $menu_paths[] = [
                    'MENU' => $menu_branch['i_menu'],
                    'MENU_URL' => $menu_url,
                    'MENU_PATH_COMPONENTS' => array_reverse($menu_path_components),
                ];
            }

            $edit_url = build_url(['page' => '_SELF', 'type' => '_edit', 'page_link' => $page_link], '_SELF');

            $page_structure[] = [
                'EDIT_URL' => $edit_url,
                'ZONE_NAME' => $page['the_zone'],
                'PAGE_NAME' => $page['the_page'],
                'PAGE_TITLE' => ($page['cc_page_title'] === null) ? '' : (is_string($page['cc_page_title']) ? $page['cc_page_title'] : get_translated_text($page['cc_page_title'])),
                'VALIDATED' => $no_validation_support || ($page['p_validated'] == 1),
                'TODO' => $todo,
                'MENU_PATHS' => $menu_paths,
                'CHILDREN' => $this->organise_page_tree($pages, $page['the_page']),
            ];
        }

        return do_template('GENERATE_PAGE_SITEMAP', ['_GUID' => 'f92876ca45010873c71a9fea574d17c1', 'PAGE_STRUCTURE' => $page_structure]);
    }

    /**
     * Scour Git for revisions to pages and create revisions locally as needed.
     *
     * @return Tempcode The UI
     */
    public function sync_revisions_with_git() : object
    {
        check_privilege('mass_import');

        if ((!is_dir(get_custom_file_base() . '/.git')) || (!php_function_allowed('shell_exec')) || (!php_function_allowed('escapeshellarg'))) {
            warn_exit(do_lang_tempcode('INTERNAL_ERROR', escape_html('d27bece2bd9157f1957ce5ae8af15e5e')));
        }

        if (post_param_integer('confirm', 0) == 0) {
            $preview = do_lang_tempcode('SYNC_REVISIONS_WITH_GIT_CONFIRM');
            $confirm_url = get_self_url();
            require_code('templates_confirm_screen');
            return confirm_screen($this->title, $preview, $confirm_url, null, ['confirm' => 1]);
        }

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

        cms_extend_time_limit(TIME_LIMIT_EXTEND__SLOW);
        push_query_limiting(false);

        $num_revisions = 0;

        $langs = find_all_langs();
        foreach (array_keys($langs) as $lang) {
            $pages = $this->get_comcode_files_list_disk_search($lang, null, false);
            foreach ($pages as $page_link => $parts) {
                list($path, $row) = $parts;

                $path = preg_replace('#^' . preg_quote(get_custom_file_base()) . '/#', '', $path);

                if (($only !== null) && ($only != $path)) {
                    continue;
                }

                $cmd = 'git log ' . escapeshellarg($path);
                $result = shell_exec($cmd);
                if (empty($result)) {
                    continue; // File not in Git (probably)
                }
                $matches = [];
                $num_matches = preg_match_all('#(^|\n)commit (\w+)(.|\n)*?Date: \s*(.*)#', $result, $matches);
                for ($i = 0; $i < $num_matches; $i++) {
                    $commit = $matches[2][$i];
                    $timestamp = strtotime($matches[4][$i]);
                    if ($timestamp !== false) {
                        $revision_path = $path . '.' . strval($timestamp);
                        $cmd = 'git show ' . escapeshellarg($commit . '~:' . $path);
                        $revision = shell_exec($cmd);
                        if (empty($revision)) {
                            continue; // Revision not in tree for some reason (probably)
                        }

                        if (($i > 0) || ($revision != cms_file_get_contents_safe(get_custom_file_base() . '/' . $path))) {
                            cms_file_put_contents_safe(get_custom_file_base() . '/' . $revision_path, $revision);
                        }

                        $num_revisions++;

                        if ($i == 0) {
                            list($zone, $page) = explode(':', $page_link, 2);

                            $rows = $GLOBALS['SITE_DB']->query_select('comcode_pages', ['p_add_date', 'p_edit_date'], ['the_zone' => $zone, 'the_page' => $page], '', 1);
                            if (array_key_exists(0, $rows)) {
                                if ($rows[0]['p_add_date'] > $timestamp) {
                                    $timestamp = $rows[0]['p_add_date'];
                                }
                                if (($rows[0]['p_edit_date'] !== null) && ($rows[0]['p_edit_date'] > $timestamp)) {
                                    $timestamp = $rows[0]['p_edit_date'];
                                }
                            }

                            $GLOBALS['SITE_DB']->query_update('comcode_pages', ['p_edit_date' => $timestamp], ['the_zone' => $zone, 'the_page' => $page], '', 1);
                        }
                    }
                }
            }
        }

        pop_query_limiting();

        return inform_screen($this->title, do_lang_tempcode('SYNC_REVISIONS_WITH_GIT_MESSAGE', escape_html(integer_format($num_revisions))));
    }
}
