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

require_code('crud_module');

/**
 * Module page class.
 */
class Module_cms_catalogues extends Standard_crud_module
{
    protected $lang_type = 'CATALOGUE_ENTRY';
    protected $select_name = 'ENTRY';
    protected $permissions_require = 'mid';
    protected $permissions_module_require = 'catalogues_catalogue';
    protected $permissions_cat_name = 'catalogue_name';
    protected $permissions_module_require_b = 'catalogues_category';
    protected $permissions_cat_name_b = 'category_id';
    protected $user_facing = true;
    protected $seo_type = 'catalogue_entry';
    protected $catalogue = true;
    protected $content_type = 'catalogue_entry';
    protected $possibly_some_kind_of_upload = true;
    protected $do_preview = null;
    protected $menu_label = 'CATALOGUES';
    protected $orderer = 'ce_add_date';
    protected $table = 'catalogue_entries';
    protected $supports_mass_delete = true;

    protected $donext_category_id;
    protected $donext_catalogue_name;

    /**
     * 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.
     * @param  boolean $simplified Whether to simplify this down for only a specific catalogue (only applied to cms_catalogues module)
     * @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, bool $simplified = false) : ?array
    {
        if (!addon_installed('catalogues')) {
            return null;
        }

        if ($member_id === null) {
            $member_id = get_member();
        }

        $ret = [
            'browse' => ['MANAGE_CATALOGUES', 'menu/rich_content/catalogues/catalogues'],
        ];

        if (has_privilege($member_id, 'submit_cat_highrange_content', 'cms_catalogues')) {
            $ret += [
                'add_category' => ['ADD_CATALOGUE_CATEGORY', 'admin/add_one_category'],
            ];
        }

        if (has_privilege($member_id, 'edit_cat_highrange_content', 'cms_catalogues')) {
            $ret += [
                'edit_category' => ['EDIT_CATALOGUE_CATEGORY', 'admin/edit_one_category'],
            ];
        }

        if (!$simplified) {
            if (has_privilege($member_id, 'submit_cat_highrange_content', 'cms_catalogues')) {
                $ret += [
                    'add_catalogue' => ['ADD_CATALOGUE', 'menu/cms/catalogues/add_one_catalogue'],
                ];
            }

            if (has_privilege($member_id, 'edit_cat_highrange_content', 'cms_catalogues')) {
                $ret += [
                    'edit_catalogue' => ['EDIT_CATALOGUE', 'menu/cms/catalogues/edit_one_catalogue'],
                ];
            }
        }

        if (has_privilege($member_id, 'mass_import', 'cms_catalogues')) {
            $ret += [
                'import' => ['IMPORT_CATALOGUE_ENTRIES', 'admin/import_spreadsheet'],
            ];
        }

        if ($GLOBALS['FORUM_DRIVER']->is_super_admin($member_id)) {
            $ret += [
                'export' => ['EXPORT_CATALOGUE_ENTRIES', 'admin/export_spreadsheet'],
            ];
        }

        $this->cat_crud_module = class_exists('Mx_cms_catalogues_cat') ? new Mx_cms_catalogues_cat() : new Module_cms_catalogues_cat();
        $this->alt_crud_module = class_exists('Mx_cms_catalogues_alt') ? new Mx_cms_catalogues_alt() : new Module_cms_catalogues_alt();

        $ret += parent::get_entry_points();
        unset($ret['add_other']);
        unset($ret['edit_other']);

        if (!$simplified) {
            if ($support_crosslinks) {
                require_code('fields');
                $ret += manage_custom_fields_entry_points('catalogue') + manage_custom_fields_entry_points('catalogue_category');
            }
        }

        if (has_privilege($member_id, 'mass_import')) {
            $ret += [
                'predefined_content' => ['PREDEFINED_CONTENT', 'admin/import'],
            ];
        }

        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
    {
        require_lang('catalogues');
        return ['view_private_content' => 0, 'mass_import' => 0, 'high_catalogue_entry_timeout' => 1, 'submit_cat_highrange_content' => [0, 'ADD_CATALOGUE'], 'edit_cat_highrange_content' => [0, 'EDIT_CATALOGUE'], 'delete_cat_highrange_content' => [0, 'DELETE_CATALOGUE'], 'submit_cat_midrange_content' => [0, 'ADD_CATALOGUE_CATEGORY'], 'edit_cat_midrange_content' => [0, 'EDIT_CATALOGUE_CATEGORY'], 'delete_cat_midrange_content' => [0, 'DELETE_CATALOGUE_CATEGORY'], 'submit_midrange_content' => [1, 'ADD_CATALOGUE_ENTRY'], 'bypass_validation_midrange_content' => [1, 'BYPASS_VALIDATION_CATALOGUE_ENTRY'], 'edit_own_midrange_content' => [1, 'EDIT_OWN_CATALOGUE_ENTRY'], 'edit_midrange_content' => [1, 'EDIT_CATALOGUE_ENTRY'], 'delete_own_midrange_content' => [1, 'DELETE_OWN_CATALOGUE_ENTRY'], 'delete_midrange_content' => [1, 'DELETE_CATALOGUE_ENTRY']];
    }

    public $title;

    /**
     * Module pre-run function. Allows us to know metadata for <head> before we start streaming output.
     *
     * @param  boolean $top_level Whether this is running at the top level, prior to having sub-objects called
     * @param  ?ID_TEXT $type The screen type to consider for metadata purposes (null: read from environment)
     * @return ?Tempcode Tempcode indicating some kind of exceptional output (null: none)
     */
    public function pre_run(bool $top_level = true, ?string $type = null) : ?object
    {
        $error_msg = new Tempcode();
        if (!addon_installed__messaged('catalogues', $error_msg)) {
            return $error_msg;
        }

        require_code('input_filter_2');
        if (get_value('disable_modsecurity_workaround') !== '1') {
            modsecurity_workaround_enable();
        }

        $this->cat_crud_module = class_exists('Mx_cms_catalogues_cat') ? new Mx_cms_catalogues_cat() : new Module_cms_catalogues_cat();
        $this->alt_crud_module = class_exists('Mx_cms_catalogues_alt') ? new Mx_cms_catalogues_alt() : new Module_cms_catalogues_alt();
        $GLOBALS['MODULE_CMS_CATALOGUES'] = $this;

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

        require_lang('catalogues');
        require_css('catalogues');
        require_javascript('catalogues');

        inform_non_canonical_parameter('parent_id');
        inform_non_canonical_parameter('validated');
        inform_non_canonical_parameter('category_id');
        inform_non_canonical_parameter('category_id_suggest');
        inform_non_canonical_parameter('#^field_.*$#');

        set_helper_panel_tutorial('tut_catalogues');
        set_helper_panel_text(comcode_lang_string('DOC_CATALOGUES'));

        if ($type == '_import') {
            breadcrumb_set_parents([['_SELF:_SELF:import', do_lang_tempcode('MANAGE_CATALOGUES')], ['_SELF:_SELF:import', do_lang_tempcode('IMPORT_CATALOGUE_ENTRIES')]]);
            breadcrumb_set_self(do_lang_tempcode('DONE'));
        }

        if ($type == 'import' || $type == '_import') {
            $this->title = get_screen_title('CATALOGUE_IMPORT');
        }

        if ($type == 'export') {
            $this->title = get_screen_title('EXPORT_CATALOGUE_ENTRIES');
        }

        switch ($type) {
            case 'add_catalogue':
                $type = 'add_other';
                break;
            case '_add_catalogue':
                $type = '_add_other';
                break;
            case 'edit_catalogue':
                $type = 'edit_other';
                break;
            case '_edit_catalogue':
                $type = '_edit_other';
                break;
            case '__edit_catalogue':
                $type = '__edit_other';
                break;
        }

        $ret = parent::pre_run($top_level, $type);

        if ($type == 'add_other' || $type == '_add_other') {
            $content_type = $this->alt_crud_module->tied_to_content_type(get_param_string('id', null));
            if ($content_type !== null) {
                $this->alt_crud_module->title = get_screen_title('ADD_CATALOGUE_FOR_CONTENT_TYPE', true, [escape_html($content_type)]);
            }
        }

        if ($type == 'edit_other' || $type == '_edit_other') {
            $content_type = $this->alt_crud_module->tied_to_content_type(get_param_string('id', null));
            if ($content_type !== null) {
                $this->alt_crud_module->title = get_screen_title('EDIT_CATALOGUE_FOR_CONTENT_TYPE', true, [escape_html($content_type)]);
            }
        }

        if ($type == 'predefined_content') {
        }

        if ($type == '_predefined_content') {
            breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('MANAGE_CATALOGUES')], ['_SELF:_SELF:predefined_content', do_lang_tempcode('PREDEFINED_CONTENT')]]);
            breadcrumb_set_self(do_lang_tempcode('DONE'));
        }

        if ($type == 'predefined_content' || $type == '_predefined_content') {
            $this->title = get_screen_title('PREDEFINED_CONTENT');
        }

        return $ret;
    }

    /**
     * Standard crud_module run_start.
     *
     * @param  ID_TEXT $type The type of module execution
     * @return Tempcode The output of the run
     */
    public function run_start(string $type) : object
    {
        if (get_value('disable_cat_cat_perms') === '1') {
            $this->permissions_module_require_b = null;
            $this->permissions_cat_name_b = null;
            //$this->cat_crud_module->permissions_module_require = null;
            //$this->cat_crud_module->permissions_cat_name = null;
        }

        require_lang('fields');
        require_code('catalogues');

        if ($type == 'add_catalogue') {
            require_javascript('catalogues');
            $this->alt_crud_module->js_function_calls[] = 'moduleCmsCataloguesRunStartAddCatalogue';
        }

        // Decide what to do
        if ($type == 'browse') {
            return $this->browse();
        }
        if ($type == 'import') {
            return $this->import_catalogue();
        }
        if ($type == '_import') {
            return $this->_import_catalogue();
        }
        if ($type == 'export') {
            return $this->export_catalogue();
        }
        if ($type == 'predefined_content') {
            return $this->predefined_content();
        }
        if ($type == '_predefined_content') {
            return $this->_predefined_content();
        }

        return new Tempcode();
    }

    /**
     * The do-next manager for before content management.
     *
     * @return Tempcode The UI
     */
    public function browse() : object
    {
        require_code('templates_donext');
        $catalogue_name = get_param_string('catalogue_name', '');
        if ($catalogue_name == '') {
            $extra_map = [];
            $extra_map_2 = [];
        } else {
            require_lang('do_next');
            $extra_map = ['catalogue_name' => $catalogue_name];
            $extra_map_2 = ['id' => $catalogue_name];
            $cat_rows = $GLOBALS['SITE_DB']->query_select('catalogues', ['c_title', 'c_description'], ['c_name' => $catalogue_name], '', 1);
            if (!array_key_exists(0, $cat_rows)) {
                warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'catalogue'));
            }
            $cat_title = $cat_rows[0]['c_title'];
        }

        if (!cms_empty_safe($catalogue_name)) {
            $cat_count = $GLOBALS['SITE_DB']->query_select_value('catalogue_categories', 'COUNT(*)', ['c_name' => $catalogue_name]);
            $has_categories = ($cat_count != 0);
        } else {
            $has_categories = true;
        }

        require_code('fields');

        return do_next_manager(
            ($catalogue_name != '') ? get_screen_title(escape_html(get_translated_text($cat_title)), false) : get_screen_title('MANAGE_CATALOGUES'),
            ($catalogue_name != '') ? get_translated_tempcode('catalogues', $cat_rows[0], 'c_description') : comcode_lang_string('DOC_CATALOGUES'),
            array_merge([
                (has_privilege(get_member(), 'submit_cat_highrange_content', 'cms_catalogues') && ($catalogue_name == '')) ? ['menu/cms/catalogues/add_one_catalogue', ['_SELF', array_merge($extra_map, ['type' => 'add_catalogue']), '_SELF'], do_lang('ADD_CATALOGUE')] : null,
                has_privilege(get_member(), 'edit_cat_highrange_content', 'cms_catalogues') ? ['menu/cms/catalogues/edit_one_catalogue', ['_SELF', array_merge($extra_map_2, ['type' => ($catalogue_name == '') ? 'edit_catalogue' : '_edit_catalogue']), '_SELF'], do_lang(($catalogue_name == '') ? 'EDIT_CATALOGUE' : 'NEXT_ITEM_edit_this_catalogue')] : null,
                has_privilege(get_member(), 'submit_cat_midrange_content', 'cms_catalogues') ? ['admin/add_one_category', ['_SELF', array_merge($extra_map, ['type' => 'add_category']), '_SELF'], ($catalogue_name != '') ? do_lang('NEXT_ITEM_add_one_category') : do_lang('ADD_CATALOGUE_CATEGORY')] : null,
                has_privilege(get_member(), 'edit_cat_midrange_content', 'cms_catalogues') ? ['admin/edit_one_category', ['_SELF', array_merge($extra_map, ['type' => 'edit_category']), '_SELF'], ($catalogue_name != '') ? do_lang('NEXT_ITEM_edit_one_category') : do_lang('EDIT_CATALOGUE_CATEGORY')] : null,
                (!$has_categories) ? null : (has_privilege(get_member(), 'submit_midrange_content', 'cms_catalogues') ? ['admin/add', ['_SELF', array_merge($extra_map, ['type' => 'add_entry']), '_SELF'], ($catalogue_name != '') ? do_lang('NEXT_ITEM_add') : do_lang('ADD_CATALOGUE_ENTRY')] : null),
                (!$has_categories) ? null : (has_privilege(get_member(), 'edit_midrange_content', 'cms_catalogues') ? ['admin/edit', ['_SELF', array_merge($extra_map, ['type' => 'edit_entry']), '_SELF'], ($catalogue_name != '') ? do_lang('NEXT_ITEM_edit') : do_lang('EDIT_CATALOGUE_ENTRY')] : null),
                (!$has_categories) ? null : (has_privilege(get_member(), 'mass_import', 'cms_catalogues') ? ['admin/import_spreadsheet', ['_SELF', array_merge($extra_map, ['type' => 'import']), '_SELF'], do_lang('IMPORT_CATALOGUE_ENTRIES')] : null),
                (!$has_categories) ? null : ($GLOBALS['FORUM_DRIVER']->is_super_admin(get_member()) ? ['admin/export_spreadsheet', ['_SELF', array_merge($extra_map, ['type' => 'export']), '_SELF'], do_lang('EXPORT_CATALOGUE_ENTRIES')] : null),
                has_privilege(get_member(), 'mass_import') ? ['admin/install', ['_SELF', ['type' => 'predefined_content'], '_SELF'], do_lang('PREDEFINED_CONTENT')] : null,
                ($catalogue_name == '') ? null : ['admin/view_this', ['catalogues', ['type' => 'category', 'catalogue_name' => $catalogue_name], get_module_zone('catalogues')], do_lang('VIEW_CATALOGUE')], // View this
            ], manage_custom_fields_donext_link('catalogue'), manage_custom_fields_donext_link('catalogue_category')),
            ($catalogue_name != '') ? escape_html(get_translated_text($cat_title)) : do_lang('MANAGE_CATALOGUES')
        );
    }

    /**
     * Standard crud_module table function.
     *
     * @param  array $url_map Details to go to build_url for link to the next screen
     * @return array A quintet: The choose table, Whether re-ordering is supported from this screen, Search URL, Archive URL, a Filtercode box block
     */
    public function create_selection_list_choose_table(array $url_map) : array
    {
        require_code('templates_results_table');

        $current_ordering = get_param_string('sort', 'title ASC', INPUT_FILTER_GET_COMPLEX);
        list($sortable, $sort_order) = explode(' ', $current_ordering, 2);
        $sortables = [
            'cc_id' => do_lang_tempcode('CATEGORY'),
            'ce_add_date' => do_lang_tempcode('ADDED'),
            'ce_views' => do_lang_tempcode('COUNT_VIEWS'),
            'ce_submitter' => do_lang_tempcode('metadata:OWNER'),
        ];
        if (addon_installed('validation')) {
            $sortables['ce_validated'] = do_lang_tempcode('VALIDATED');
        }
        list($sql_sort, $sort_order, $sortable) = process_sorting_params('catalogue_entry', $current_ordering);

        $fh = [];
        $fh[] = do_lang_tempcode('TITLE');
        $fh[] = do_lang_tempcode('CATEGORY');
        $fh[] = do_lang_tempcode('ADDED');
        $fh[] = do_lang_tempcode('COUNT_VIEWS');
        $fh[] = do_lang_tempcode('metadata:OWNER');
        if (addon_installed('validation')) {
            $fh[] = protect_from_escaping(do_template('COMCODE_ABBR', ['_GUID' => '4341bfe5e713c94c8fd14ce1cb2e21aa', 'TITLE' => do_lang_tempcode('VALIDATED'), 'CONTENT' => do_lang_tempcode('VALIDATED_SHORT')]));
        }
        $fh[] = do_lang_tempcode('ACTIONS');
        $header_row = results_header_row($fh, $sortables, 'sort', $sortable . ' ' . $sort_order);

        $result_entries = new Tempcode();

        $only_owned = has_privilege(get_member(), 'edit_midrange_content', 'cms_catalogues') ? null : get_member();
        $catalogue_name = get_param_string('catalogue_name');
        list($rows, $max_rows) = $this->get_entry_rows(false, $sql_sort, ($only_owned === null) ? ['c_name' => $catalogue_name] : ['c_name' => $catalogue_name, 'ce_submitter' => $only_owned]);
        $_fields = [];
        $cat_titles = [];
        foreach ($rows as $row) {
            $edit_url = build_url($url_map + ['id' => $row['id']], '_SELF');

            $entry_fields = get_catalogue_entry_field_values($catalogue_name, $row['id'], [0]);
            $name = $entry_fields[0]['effective_value']; // 'Name' is value of first field

            $fr = [];
            $fr[] = protect_from_escaping(hyperlink(build_url(['page' => 'catalogues', 'type' => 'entry', 'id' => $row['id']], get_module_zone('catalogues')), $name, false, true));
            if (array_key_exists($row['cc_id'], $cat_titles)) {
                $cc_title = $cat_titles[$row['cc_id']];
            } else {
                $cc_title = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogue_categories', 'cc_title', ['id' => $row['cc_id']]);
                $cat_titles[$row['cc_id']] = $cc_title;
            }
            if ($cc_title !== null) {
                $fr[] = protect_from_escaping(hyperlink(build_url(['page' => 'catalogues', 'type' => 'category', 'id' => $row['cc_id']], get_module_zone('catalogues')), get_translated_text($cc_title), false, true));
            } else {
                $fr[] = do_lang('UNKNOWN');
            }
            $fr[] = get_timezoned_date_time($row['ce_add_date']);
            $fr[] = integer_format($row['ce_views']);
            $username = protect_from_escaping($GLOBALS['FORUM_DRIVER']->member_profile_hyperlink($row['ce_submitter']));
            $fr[] = $username;
            if (addon_installed('validation')) {
                $fr[] = $row['ce_validated'] ? do_lang_tempcode('YES') : do_lang_tempcode('NO');
            }
            $fr[] = protect_from_escaping(hyperlink($edit_url, do_lang_tempcode('EDIT'), false, true, do_lang('EDIT') . ' #' . strval($row['id'])));

            $_fields[] = ['row' => $fr, 'title' => $name];
        }
        if ($current_ordering == 'title ASC' || $current_ordering == 'title DESC') {
            sort_maps_by($_fields, 'title', false, true);
            if ($current_ordering == 'title DESC') {
                $_fields = array_reverse($_fields);
            }
        }
        foreach ($_fields as $_fr) {
            $result_entries->attach(results_entry($_fr['row'], true));
        }

        $is_tree = $GLOBALS['SITE_DB']->query_select_value('catalogues', 'c_is_tree', ['c_name' => $catalogue_name]);

        $search_url = build_url(['page' => 'search', 'id' => 'catalogue_entries', 'catalogue_name' => $catalogue_name], get_module_zone('search'));
        $archive_url = build_url(['page' => 'catalogues', 'type' => 'index', 'id' => $catalogue_name, 'tree' => $is_tree], get_module_zone('catalogues'));

        return [results_table(do_lang($this->menu_label), get_param_integer('start', 0), 'start', either_param_integer('max', 20), 'max', $max_rows, $header_row, $result_entries, $sortables, $sortable, $sort_order), false, $search_url, $archive_url];
    }

    /**
     * Standard crud_module tree-list function.
     *
     * @return ?array A triple: The tree field (Tempcode), Search URL, Archive URL (null: nothing here)
     */
    public function create_selection_list_ajax_tree() : ?array
    {
        $catalogue_name = get_param_string('catalogue_name');

        if ($GLOBALS['SITE_DB']->query_select_value('catalogue_entries', 'COUNT(*)', ['c_name' => $catalogue_name]) == 0) {
            inform_exit(do_lang_tempcode('NO_ENTRIES', 'catalogue_entry'));
        }

        $is_tree = $GLOBALS['SITE_DB']->query_select_value('catalogues', 'c_is_tree', ['c_name' => $catalogue_name]);
        if ($is_tree == 0) {
            return null;
        }

        $search_url = build_url(['page' => 'search', 'id' => 'catalogue_entries', 'catalogue_name' => $catalogue_name], get_module_zone('search'));
        $archive_url = build_url(['page' => 'catalogues', 'type' => 'index', 'id' => $catalogue_name, 'tree' => $is_tree], get_module_zone('catalogues'));

        $only_owned = has_privilege(get_member(), 'edit_midrange_content', 'cms_catalogues') ? null : get_member();
        $tree = form_input_tree_list(do_lang_tempcode('ENTRY'), '', 'id', null, 'choose_catalogue_entry', ['catalogue_name' => $catalogue_name, 'only_owned' => $only_owned, 'editable_filter' => true], true, null, false, null, $this->supports_mass_delete);
        return [$tree, $search_url, $archive_url];
    }

    /**
     * Get Tempcode for an adding form.
     *
     * @return mixed Either Tempcode; or a tuple of: (fields, hidden-fields[, delete-fields][, edit-text][, whether all delete fields are specified][, posting form text, more fields][, parsed WYSIWYG editable text])
     */
    public function get_form_fields_for_add()
    {
        return $this->get_form_fields();
    }

    /**
     * Get Tempcode for a catalogue entry adding/editing form.
     *
     * @param  ?ID_TEXT $catalogue_name The catalogue for the entry (null: detect)
     * @param  ?AUTO_LINK $category_id The category for the entry (null: first)
     * @param  BINARY $validated Whether the entry is validated
     * @param  LONG_TEXT $notes Staff notes
     * @param  ?BINARY $allow_rating Whether rating is allowed (null: decide statistically, based on existing choices)
     * @param  ?SHORT_INTEGER $allow_comments Whether comments are allowed (0=no, 1=yes, 2=review style) (null: decide statistically, based on existing choices)
     * @param  ?BINARY $allow_trackbacks Whether trackbacks are allowed (null: decide statistically, based on existing choices)
     * @param  ?AUTO_LINK $id The ID of the entry (null: not yet added)
     * @return mixed Either Tempcode; or a tuple of: (fields, hidden-fields[, delete-fields][, edit-text][, whether all delete fields are specified][, posting form text, more fields][, parsed WYSIWYG editable text])
     */
    public function get_form_fields(?string $catalogue_name = null, ?int $category_id = null, int $validated = 1, string $notes = '', ?int $allow_rating = null, ?int $allow_comments = null, ?int $allow_trackbacks = null, ?int $id = null)
    {
        list($allow_rating, $allow_comments, $allow_trackbacks) = $this->choose_feedback_fields_statistically($allow_rating, $allow_comments, $allow_trackbacks);

        if ($catalogue_name === null) {
            $catalogue_name = get_param_string('catalogue_name');
        }

        $_GET['catalogue_name'] = $catalogue_name; // Useful for referencing in templates etc

        require_code('feedback');

        $fields = new Tempcode();

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

        if (($id === null) && ($category_id === null)) {
            $category_id = get_param_integer('category_id', null);
        }

        $this->add_text = do_lang('CATALOGUE_' . $catalogue_name . '_ADD_TEXT', escape_html(get_base_url()), null, null, null, false);
        $this->edit_text = do_lang('CATALOGUE_' . $catalogue_name . '_EDIT_TEXT', escape_html(get_base_url()), null, null, null, false);

        // Standard fields
        // ===============

        // Category
        if (($id === null) && ($category_id === null) && (get_value('no_confirm_url_spec_cats') !== '1')) {
            $category_id = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogue_categories', 'MIN(id)', ['c_name' => $catalogue_name, 'cc_parent_id' => null]);
            if ($category_id === null) {
                $category_id = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogue_categories', 'MIN(id)', ['c_name' => $catalogue_name]);
            }
        }
        $num_categories = $GLOBALS['SITE_DB']->query_select_value('catalogue_categories', 'COUNT(id)', ['c_name' => $catalogue_name]);
        if (($category_id !== null) && (($id === null) && (get_value('no_confirm_url_spec_cats') === '1') || (get_value('no_spec_cat__' . $catalogue_name) === '1')) || ($num_categories == 1)) { // Adding, but defined category ID in URL, and set option saying not to ask for passed categories
            $hidden->attach(form_input_hidden('category_id', strval($category_id)));
        } else {
            if (($id === null) && ($category_id === null)) {
                $category_id = get_param_integer('category_id_suggest', null); // Less forceful than 'category_id', as may be changed even with 'no_confirm_url_spec_cats' on
            }

            $fields->attach(form_input_tree_list(do_lang_tempcode('CATEGORY'), do_lang_tempcode('DESCRIPTION_CATEGORY_TREE', 'catalogue_category'), 'category_id', null, 'choose_catalogue_category', ['catalogue_name' => $catalogue_name, 'addable_filter' => true], true, ($category_id === null) ? '' : strval($category_id)));
        }

        // Special fields
        // ==============

        if ($id !== null) {
            $special_fields = get_catalogue_entry_field_values($catalogue_name, $id);
        } else {
            $special_fields = $GLOBALS['SITE_DB']->query_select('catalogue_fields', ['*'], ['c_name' => $catalogue_name], 'ORDER BY cf_order,' . $GLOBALS['SITE_DB']->translate_field_ref('cf_name'));
        }

        $field_groups = [];

        $field_defaults = [
            // We load up these into a map to allow custom add/edit form templating
            'CATEGORY_ID' => ($category_id === null) ? '' : strval($category_id),
            'VALIDATED' => strval($validated),
            'NOTES' => $notes,
            'ALLOW_RATING' => strval($allow_rating),
            'ALLOW_COMMENTS' => strval($allow_comments),
            'ALLOW_TRACKBACKS' => strval($allow_trackbacks),
        ];

        require_code('fields');
        foreach ($special_fields as $field_num => $field) {
            $ob = get_fields_hook($field['cf_type']);
            $default = get_param_string('field_' . strval($field['id']), array_key_exists('effective_value_pure', $field) ? $field['effective_value_pure'] : $field['cf_default'], INPUT_FILTER_GET_COMPLEX);

            $_cf_name = get_translated_text($field['cf_name']);
            $field_cat = '';
            if (strpos($_cf_name, ': ') !== false) {
                $field_cat = substr($_cf_name, 0, strpos($_cf_name, ': '));
                if ($field_cat . ': ' == $_cf_name) {
                    $_cf_name = $field_cat; // Just been pulled out as heading, nothing after ": "
                } else {
                    $_cf_name = substr($_cf_name, strpos($_cf_name, ': ') + 2);
                }
            }
            if (!array_key_exists($field_cat, $field_groups)) {
                $field_groups[$field_cat] = new Tempcode();
            }

            $__cf_description = get_translated_text($field['cf_description']);
            global $LANG_FILTER_OB;
            $__cf_description = $LANG_FILTER_OB->compile_time(null, $__cf_description); // So that eCommerce catalogue fields referencing config options look better
            $_cf_description = escape_html($__cf_description);

            $GLOBALS['NO_DEV_MODE_FULLSTOP_CHECK'] = true;
            $result = $ob->get_field_inputter($_cf_name, $_cf_description, $field, $default, ($id === null), !array_key_exists($field_num + 1, $special_fields));

            $GLOBALS['NO_DEV_MODE_FULLSTOP_CHECK'] = false;

            if ($result === null) {
                continue;
            }

            if (is_array($result)) {
                $field_groups[$field_cat]->attach($result[0]);
                $hidden->attach($result[1]);
            } else {
                $field_groups[$field_cat]->attach($result);
            }

            if (strpos($field['cf_type'], '_trans') !== false) {
                $this->do_preview = true;
            }

            $field_defaults['FIELD_' . strval($field['id'])] = $default;
            $field_defaults['FIELD_' . strval($field['id']) . '_CHOICES'] = explode('|', $field['cf_default']);

            unset($result);
            unset($ob);
        }

        if (array_key_exists('', $field_groups)) { // Blank prefix must go first
            $field_groups_blank = $field_groups[''];
            unset($field_groups['']);
            $field_groups = array_merge([$field_groups_blank], $field_groups);
        }
        foreach ($field_groups as $field_group_title => $extra_fields) {
            if (is_integer($field_group_title)) {
                $field_group_title = ($field_group_title == 0) ? '' : strval($field_group_title);
            }

            if ($field_group_title != '') {
                $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', ['_GUID' => 'a03ec5b2afe5be764bd10694fc401fed', 'TITLE' => $field_group_title]));
            }
            $fields->attach($extra_fields);
        }

        $_validated = get_param_integer('validated', 0);
        if ($validated == 0) {
            if (($_validated == 1) && (addon_installed('validation'))) {
                $validated = 1;
                attach_message(do_lang_tempcode('WILL_BE_VALIDATED_WHEN_SAVING'), 'notice');
            }
        } elseif (($validated == 1) && ($_validated == 1) && ($id !== null)) {
            $action_log = build_url(['page' => 'admin_actionlog', 'type' => 'list', 'to_type' => 'VALIDATE_CATALOGUE_ENTRY', 'param_a' => strval($id)]);
            attach_message(do_lang_tempcode('ALREADY_VALIDATED', escape_html($action_log->evaluate())), 'warn');
        }
        if ((has_some_cat_privilege(get_member(), 'bypass_validation_' . $this->permissions_require . 'range_content', null, $this->permissions_module_require_b)) || (has_some_cat_privilege(get_member(), 'bypass_validation_' . $this->permissions_require . 'range_content', null, $this->permissions_module_require))) {
            if (addon_installed('validation')) {
                if (count($field_groups) != 1) {
                    $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', ['_GUID' => '17e83c2d6412bddc12ac1873f9ec6092', 'TITLE' => do_lang_tempcode('SETTINGS')]));
                }
                $fields->attach(form_input_tick(do_lang_tempcode('VALIDATED'), do_lang_tempcode($GLOBALS['FORUM_DRIVER']->is_super_admin(get_member()) ? 'DESCRIPTION_VALIDATED_SIMPLE' : 'DESCRIPTION_VALIDATED', 'catalogue_entry'), 'validated', $validated == 1));
            }
        }

        require_code('syndication');
        $fields->attach(get_syndication_option_fields('download', $id !== null));

        // Metadata
        require_code('content2');
        $seo_fields = seo_get_fields($this->seo_type, ($id === null) ? null : strval($id), false);
        require_code('feedback2');
        $feedback_fields = feedback_fields($this->content_type, $allow_rating == 1, $allow_comments == 1, $allow_trackbacks == 1, false, $notes, $allow_comments == 2, false, true, false);
        $fields->attach(metadata_get_fields('catalogue_entry', ($id === null) ? null : strval($id), false, [], ($seo_fields->is_empty() && $feedback_fields->is_empty()) ? METADATA_HEADER_YES : METADATA_HEADER_FORCE));
        $fields->attach($seo_fields);
        $fields->attach($feedback_fields);

        if (addon_installed('content_reviews')) {
            $fields->attach(content_review_get_fields('catalogue_entry', ($id === null) ? null : strval($id)));
        }

        if (addon_installed('content_privacy')) {
            require_code('content_privacy2');
            if ($id === null) {
                $fields->attach(get_privacy_form_fields('catalogue_entry'));
            } else {
                $fields->attach(get_privacy_form_fields('catalogue_entry', strval($id)));
            }
        }

        $fields2 = new Tempcode();
        if (($id !== null) && (is_ecommerce_catalogue($catalogue_name)) && (!$this->may_delete_this(strval($id)))) {
            list($submitter) = $this->get_submitter(strval($id));
            $delete_permission = has_delete_permission($this->permissions_require, get_member(), $submitter, ($this->privilege_page_name === null) ? get_page_name() : $this->privilege_page_name, [$this->permissions_module_require, ($this->permissions_cat_name === null) ? null : $this->get_cat(strval($id)), $this->permissions_module_require_b, ($this->permissions_cat_name_b === null) ? null : $this->get_cat_b(strval($id))]);
            if ($delete_permission) {
                $fields2->attach(do_template('FORM_SCREEN_FIELD_SPACER', ['_GUID' => 'f38200840366846dd7d9ed769f673657', 'TITLE' => do_lang_tempcode('ACTIONS'), 'SECTION_HIDDEN' => true]));
                $fields2->attach(form_input_tick(do_lang_tempcode('shopping:SHOPPING_FORCE_DELETE'), do_lang_tempcode('shopping:DESCRIPTION_SHOPPING_FORCE_DELETE'), 'force_delete', false));
            }
        }

        return [$fields, $hidden, null, null, false, null, $fields2, null, $field_defaults];
    }

    /**
     * Standard crud_module submitter getter.
     *
     * @param  ID_TEXT $id The entry for which the submitter is sought
     * @return array The submitter, and the time of submission (null submission time implies no known submission time)
     */
    public function get_submitter(string $id) : array
    {
        $rows = $GLOBALS['SITE_DB']->query_select('catalogue_entries', ['ce_submitter', 'ce_add_date'], ['id' => intval($id)], '', 1);
        if (!array_key_exists(0, $rows)) {
            return [null, null];
        }
        return [$rows[0]['ce_submitter'], $rows[0]['ce_add_date']];
    }

    /**
     * Standard crud_module category getter.
     *
     * @param  ID_TEXT $id The entry for which the category is sought
     * @return string The category
     */
    public function get_cat(string $id) : string
    {
        $cat = $this->get_cat_b($id);
        return $GLOBALS['SITE_DB']->query_select_value('catalogue_categories', 'c_name', ['id' => intval($cat)]);
    }

    /**
     * Standard crud_module category getter.
     *
     * @param  ID_TEXT $id The entry for which the category is sought
     * @return string The category
     */
    public function get_cat_b(string $id) : string
    {
        $temp = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogue_entries', 'cc_id', ['id' => $id]);
        if ($temp === null) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'catalogue_entry'));
        }
        return strval($temp);
    }

    /**
     * Standard crud_module edit form filler.
     *
     * @param  ID_TEXT $_id The entry being edited
     * @return mixed Either Tempcode; or a tuple of: (fields, hidden-fields[, delete-fields][, edit-text][, whether all delete fields are specified][, posting form text, more fields][, parsed WYSIWYG editable text])
     */
    public function fill_in_edit_form(string $_id)
    {
        $id = intval($_id);

        $rows = $GLOBALS['SITE_DB']->query_select('catalogue_entries', ['*'], ['id' => $id], '', 1);
        if (!array_key_exists(0, $rows)) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'catalogue_entry'));
        }
        $myrow = $rows[0];

        $catalogue_name = $myrow['c_name'];
        if ($catalogue_name === null) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'catalogue_entry'));
        }

        return $this->get_form_fields($catalogue_name, $myrow['cc_id'], $myrow['ce_validated'], $myrow['notes'], $myrow['allow_rating'], $myrow['allow_comments'], $myrow['allow_trackbacks'], $id);
    }

    /**
     * Get a entry-id=>value map of what a submitted catalogue entry form has set.
     *
     * @param  ID_TEXT $catalogue_name The name of the catalogue that was used
     * @param  MEMBER $submitter The entry submitter
     * @param  ?AUTO_LINK $editing_id ID of entry being edited (null: not being edited)
     * @return array The map
     */
    public function get_set_field_map(string $catalogue_name, int $submitter, ?int $editing_id = null) : array
    {
        // Get field values
        $fields = $GLOBALS['SITE_DB']->query_select('catalogue_fields', ['*'], ['c_name' => $catalogue_name], 'ORDER BY cf_order,' . $GLOBALS['SITE_DB']->translate_field_ref('cf_name'));
        $map = [];
        require_code('fields');
        require_code('catalogues');
        foreach ($fields as $i => $field) {
            $object = get_fields_hook($field['cf_type']);

            list(, , $storage_type) = $object->get_field_value_row_bits($field);

            if (fractional_edit()) {
                if ($i == 0) {
                    $value = post_param_string('field_0', null);
                    if ($value === null) {
                        $value = post_param_string('field_' . $field['id']);
                    }
                    $rendered = static_evaluate_tempcode($object->render_field_value($field, $value, 0, null, 'catalogue_efv_' . $storage_type, null, 'ce_id', 'cf_id', 'cv_value', $submitter));
                    $_POST['field_' . strval($field['id']) . '__altered_rendered_output'] = $rendered;
                } else {
                    continue;
                }
            } else {
                $value = $object->inputted_to_field_value($editing_id !== null, $field, 'uploads/catalogues', ($editing_id === null) ? null : _get_catalogue_entry_field($field['id'], $editing_id, $storage_type));
            }

            // Required field validation (a standard for all field hooks except tick)
            if (($field['cf_type'] != 'tick') && ($field['cf_required'] == 1) && (($value == '') || ($value === null) || (($value == STRING_MAGIC_NULL) && !fractional_edit()))) {
                warn_exit(do_lang_tempcode('_REQUIRED_NOT_FILLED_IN', get_translated_tempcode('catalogue_fields', $field, 'cf_name')));
            }

            $map[$field['id']] = $value;
        }

        return $map;
    }

    /**
     * Standard crud_module add actualiser.
     *
     * @return array A pair: The entry added, description about usage
     */
    public function add_actualisation() : array
    {
        require_code('catalogues2');

        $category_id = post_param_integer('category_id');
        $validated = post_param_integer('validated', 0);
        $notes = post_param_string('notes', '');
        $allow_rating = post_param_integer('allow_rating', 0);
        $allow_comments = post_param_integer('allow_comments', 0);
        $allow_trackbacks = post_param_integer('allow_trackbacks', 0);

        $catalogue_name = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogue_categories', 'c_name', ['id' => $category_id]);
        if ($catalogue_name === null) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'catalogue_category'));
        }

        $catalogue_title = get_translated_text($GLOBALS['SITE_DB']->query_select_value('catalogues', 'c_title', ['c_name' => $catalogue_name]));

        $map = $this->get_set_field_map($catalogue_name, get_member());

        $metadata = actual_metadata_get_fields('catalogue_entry', null);

        $id = actual_add_catalogue_entry($category_id, $validated, $notes, $allow_rating, $allow_comments, $allow_trackbacks, $map, $metadata['add_time'], $metadata['submitter'], null, $metadata['views']);

        set_url_moniker('catalogue_entry', strval($id));

        require_code('content');
        list($title, , , , , $url_safe) = content_get_details('catalogue_entry', strval($id));

        // Award points here instead of from give_submit_points (submit.php) because catalogues have their own submit point options
        if ((!is_guest()) && (addon_installed('points'))) {
            $points = $GLOBALS['SITE_DB']->query_select_value('catalogues', 'c_submit_points', ['c_name' => $catalogue_name]);
            if ($points > 0) {
                require_code('points2');
                $lang_string = 'catalogues:ACTIVITY_CATALOGUE_' . $catalogue_name . '_ADD_NO_LINK';
                if (do_lang($lang_string, null, null, null, null, false) === null) {
                    $lang_string = 'catalogues:ACTIVITY_CATALOGUE_GENERIC_ADD_NO_LINK';
                }
                points_credit_member(get_member(), do_lang($lang_string, $catalogue_title, $title), $points, 0, true, 0, 'catalogue_entry', 'add', strval($id));
            }
        }

        if (addon_installed('content_privacy')) {
            require_code('content_privacy2');
            list($privacy_level, $additional_access) = read_privacy_fields();
            save_privacy_form_fields('catalogue_entry', strval($id), $privacy_level, $additional_access);
        }

        if (($validated == 1) || (!addon_installed('validation'))) {
            require_code('users2');
            if ((has_actual_page_access(get_modal_user(), 'catalogues')) && ((get_value('disable_cat_cat_perms') === '1') || (has_category_access(get_modal_user(), 'catalogues_category', strval($category_id))) && (has_category_access(get_modal_user(), 'catalogues_catalogue', $catalogue_name)))) {
                $privacy_ok = true;
                if (addon_installed('content_privacy')) {
                    require_code('content_privacy');
                    $privacy_ok = has_privacy_access('catalogue_entry', strval($id), $GLOBALS['FORUM_DRIVER']->get_guest_id());
                }
                if ($privacy_ok) {
                    require_code('syndication');

                    $lang_string = 'catalogues:ACTIVITY_CATALOGUE_' . $catalogue_name . '_ADD';
                    if (do_lang($lang_string, null, null, null, null, false) === null) {
                        $lang_string = 'catalogues:ACTIVITY_CATALOGUE_GENERIC_ADD';
                    }

                    syndicate_content('catalogue_entry', strval($id), [[$lang_string, get_member(), $catalogue_title, $title]]);
                }
            }
        }

        if (addon_installed('content_reviews')) {
            content_review_set('catalogue_entry', strval($id));
        }

        $this->donext_category_id = $category_id;
        $this->donext_catalogue_name = $catalogue_name;

        return [strval($id), null];
    }

    /**
     * Standard crud_module edit actualiser.
     *
     * @param  ID_TEXT $_id The entry being edited
     * @return ?Tempcode Description about usage (null: none)
     */
    public function edit_actualisation(string $_id) : ?object
    {
        require_code('catalogues2');

        $id = intval($_id);

        $category_id = post_param_integer('category_id', INTEGER_MAGIC_NULL);
        $validated = post_param_integer('validated', fractional_edit() ? INTEGER_MAGIC_NULL : 0);
        $notes = post_param_string('notes', STRING_MAGIC_NULL);
        $allow_rating = post_param_integer('allow_rating', fractional_edit() ? INTEGER_MAGIC_NULL : 0);
        $allow_comments = post_param_integer('allow_comments', fractional_edit() ? INTEGER_MAGIC_NULL : 0);
        $allow_trackbacks = post_param_integer('allow_trackbacks', fractional_edit() ? INTEGER_MAGIC_NULL : 0);

        $submitter = $GLOBALS['SITE_DB']->query_select_value('catalogue_entries', 'ce_submitter', ['id' => $id]);

        if (fractional_edit()) {
            $catalogue_name = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogue_entries', 'c_name', ['id' => $id]);
        } else {
            $catalogue_name = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogue_categories', 'c_name', ['id' => $category_id]);
            if ($catalogue_name === null) {
                warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'catalogue_category'));
            }
        }
        $map = $this->get_set_field_map($catalogue_name, $submitter, $id);

        $metadata = actual_metadata_get_fields('catalogue_entry', strval($id));

        actual_edit_catalogue_entry($id, $category_id, $validated, $notes, $allow_rating, $allow_comments, $allow_trackbacks, $map, post_param_string('meta_keywords', STRING_MAGIC_NULL), post_param_string('meta_description', STRING_MAGIC_NULL), $metadata['edit_time'], $metadata['add_time'], $metadata['views'], $metadata['submitter'], true);

        if (addon_installed('content_privacy')) {
            require_code('content_privacy2');
            list($privacy_level, $additional_access) = read_privacy_fields();
            save_privacy_form_fields('catalogue_entry', strval($id), $privacy_level, $additional_access);
        }

        if (($validated == 1) || (!addon_installed('validation'))) {
            require_code('users2');
            if ((has_actual_page_access(get_modal_user(), 'catalogues')) && ((get_value('disable_cat_cat_perms') === '1') || (has_category_access(get_modal_user(), 'catalogues_category', strval($category_id))) && (has_category_access(get_modal_user(), 'catalogues_catalogue', $catalogue_name)))) {
                $privacy_ok = true;
                if (addon_installed('content_privacy')) {
                    require_code('content_privacy');
                    $privacy_ok = has_privacy_access('catalogue_entry', strval($id), $GLOBALS['FORUM_DRIVER']->get_guest_id());
                }
                if ($privacy_ok) {
                    $just_validated = (addon_installed('validation')) && ($GLOBALS['SITE_DB']->query_select_value('catalogue_entries', 'ce_validated', ['id' => $id]) == 0);

                    $map_copy = $map;
                    $title = array_shift($map_copy);
                    $catalogue_title = get_translated_text($GLOBALS['SITE_DB']->query_select_value('catalogues', 'c_title', ['c_name' => $catalogue_name]));

                    $activities = [];
                    if ($just_validated) {
                        if ($submitter != get_member()) {
                            $lang_string = 'ACTIVITY_VALIDATE_CATALOGUE_' . $catalogue_name;
                            if (do_lang($lang_string, null, null, null, null, false) === null) {
                                $lang_string = 'ACTIVITY_VALIDATE_CATALOGUE_GENERIC';
                            }
                            $activities[] = [$lang_string, get_member(), $catalogue_title, $title];
                        }

                        $lang_string = 'ACTIVITY_CATALOGUE_' . $catalogue_name;
                        if (do_lang($lang_string, null, null, null, null, false) === null) {
                            $lang_string = 'ACTIVITY_CATALOGUE_GENERIC_ADD';
                        }
                        $activities[] = [$lang_string, $submitter, $catalogue_title, $title];
                    }

                    require_code('syndication');
                    syndicate_content('catalogue_entry', strval($id), $activities);
                }
            }
        }

        if (addon_installed('content_reviews')) {
            content_review_set('catalogue_entry', strval($id));
        }

        // Purge support
        if (post_param_integer('force_delete', 0) == 1) {
            list($submitter) = $this->get_submitter(strval($id));
            $delete_permission = has_delete_permission($this->permissions_require, get_member(), $submitter, ($this->privilege_page_name === null) ? get_page_name() : $this->privilege_page_name, [$this->permissions_module_require, ($this->permissions_cat_name === null) ? null : $this->get_cat(strval($id)), $this->permissions_module_require_b, ($this->permissions_cat_name_b === null) ? null : $this->get_cat_b(strval($id))]);
            if ($delete_permission) {
                $start = 0;
                do {
                    $details = $GLOBALS['SITE_DB']->query_select('shopping_order_details', ['*'], ['p_type_code' => strval($id)], '', 1000, $start);
                    foreach ($details as $d) {
                        $GLOBALS['SITE_DB']->query_delete('shopping_orders', ['id' => $d['p_order_id'], 'total_price' => 0.0], '', 1);
                    }
                    recalculate_order_costs($d['p_order_id']);
                    $start += 1000;
                } while (!empty($details));
                $GLOBALS['SITE_DB']->query_delete('shopping_order_details', ['p_type_code' => strval($id)]);
                $GLOBALS['SITE_DB']->query_delete('shopping_cart', ['type_code' => strval($id)]);
                $this->delete_actualisation($_id);
            }

            unset($_GET['redirect']);
        }

        $this->donext_category_id = $category_id;
        $this->donext_catalogue_name = $catalogue_name;

        return null;
    }

    /**
     * Standard crud_module delete actualiser.
     *
     * @param  ID_TEXT $_id The entry being deleted
     */
    public function delete_actualisation(string $_id)
    {
        require_code('catalogues2');

        $id = intval($_id);

        $category_id = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogue_entries', 'cc_id', ['id' => $id]);
        if ($category_id === null) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'catalogue_entry'));
        }

        actual_delete_catalogue_entry($id);

        if (addon_installed('content_privacy')) {
            require_code('content_privacy2');
            delete_privacy_form_fields('catalogue_entry', strval($id));
        }

        require_code('syndication');
        unsyndicate_content('catalogue_entry', strval($id));

        $catalogue_name = $GLOBALS['SITE_DB']->query_select_value('catalogue_categories', 'c_name', ['id' => $category_id]);
        $this->donext_category_id = $category_id;
        $this->donext_catalogue_name = $catalogue_name;
    }

    /**
     * Standard crud_module delete possibility checker.
     *
     * @param  ID_TEXT $id The entry being potentially deleted
     * @return boolean Whether it may be deleted
     */
    public function may_delete_this(string $id) : bool
    {
        if (!is_ecommerce_catalogue_entry(intval($id))) {
            return true;
        }
        return
            ($GLOBALS['SITE_DB']->query_select_value_if_there('shopping_order_details', 'id', ['p_type_code' => $id]) === null)
            &&
            ($GLOBALS['SITE_DB']->query_select_value_if_there('shopping_cart', 'id', ['type_code' => $id]) === null);
    }

    /**
     * The do-next manager for after content management.
     *
     * @param  ?Tempcode $title The title (output of get_screen_title) (null: don't do full page)
     * @param  Tempcode $description Some description to show, saying what happened
     * @param  ?ID_TEXT $id The ID of whatever we are working with (null: deleted)
     * @return Tempcode The UI
     */
    public function do_next_manager(?object $title, object $description, ?string $id = null) : object
    {
        $c_name = $this->donext_catalogue_name;
        $category_id = $this->donext_category_id;

        $is_tree = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogues', 'c_is_tree', ['c_name' => $c_name]);

        require_code('templates_donext');
        return do_next_manager(
            $title,
            $description,
            [],
            null,
            /* TYPED-ORDERED LIST OF 'LINKS' */
            ['_SELF', ['type' => 'add_entry', 'catalogue_name' => $c_name, 'category_id' => $category_id], '_SELF'], // Add one
            (($id === null) || (!has_privilege(get_member(), 'edit_own_midrange_content', 'cms_catalogues', ['catalogues_category', $category_id]))) ? null : ['_SELF', ['type' => '_edit_entry', 'id' => $id, 'catalogue_name' => $c_name], '_SELF', do_lang_tempcode('EDIT_THIS_CATALOGUE_ENTRY')], // Edit this
            has_privilege(get_member(), 'edit_own_midrange_content', 'cms_catalogues') ? ['_SELF', ['type' => 'edit_entry', 'catalogue_name' => $c_name], '_SELF'] : null, // Edit one
            ($id === null) ? null : ['catalogues', ['type' => 'entry', 'id' => $id], get_module_zone('catalogues')], // View this
            null, // View archive
            has_privilege(get_member(), 'submit_cat_midrange_content', 'cms_catalogues') ? ['_SELF', ['type' => 'add_category', 'catalogue_name' => $c_name], '_SELF'] : null, // Add one category
            has_privilege(get_member(), 'edit_own_cat_midrange_content', 'cms_catalogues') ? ['_SELF', ['type' => 'edit_category', 'catalogue_name' => $c_name], '_SELF'] : null, // Edit one category
            null, // Edit this category
            null, // View this category
            /* SPECIALLY TYPED 'LINKS' */
            [],
            [],
            [
                has_privilege(get_member(), 'submit_cat_highrange_content', 'cms_catalogues') ? ['menu/cms/catalogues/add_one_catalogue', ['_SELF', ['type' => 'add_catalogue'], '_SELF']] : null,
                has_privilege(get_member(), 'edit_own_cat_highrange_content', 'cms_catalogues') ? ['menu/cms/catalogues/edit_this_catalogue', ['_SELF', ['type' => '_edit_catalogue', 'id' => $c_name], '_SELF']] : null,
                has_privilege(get_member(), 'edit_own_cat_highrange_content', 'cms_catalogues') ? ['menu/cms/catalogues/edit_one_catalogue', ['_SELF', ['type' => 'edit_catalogue'], '_SELF']] : null,
                ['admin/view_this', ['catalogues', ['type' => 'index', 'id' => $c_name, 'tree' => $is_tree], get_module_zone('catalogues')], do_lang('VIEW_CATALOGUE')],
            ],
            do_lang('MANAGE_CATALOGUES'),
            null,
            null,
            null,
            'catalogue_entry',
            'catalogue_category'
        );
    }

    /**
     * The UI to choose a catalogue to import catalogue entries.
     *
     * @return Tempcode The UI
     */
    public function import_catalogue() : object
    {
        check_privilege('mass_import');

        $catalogue_select = $this->choose_catalogue($this->title, 'admin/import_spreadsheet');

        if ($catalogue_select !== null) {
            return $catalogue_select;
        }

        $catalogue_name = get_param_string('catalogue_name');

        $post_url = build_url(['page' => '_SELF', 'type' => '_import', 'old_type' => get_param_string('type', ''), 'catalogue_name' => $catalogue_name], '_SELF');

        $submit_name = do_lang_tempcode('CATALOGUE_IMPORT');

        // Build up form
        $fields = new Tempcode();

        require_code('files_spreadsheets_read');
        $fields->attach(form_input_upload(do_lang_tempcode('UPLOAD'), do_lang_tempcode('SPREADSHEET_UPLOAD_DESC'), 'file_anytype', true, null, null, true, spreadsheet_read_file_types()));
        $hidden = new Tempcode();
        handle_max_file_size($hidden);

        $fields->attach(form_input_codename(do_lang_tempcode('CATALOGUE_SPREADSHEET_IMPORT_KEY_FIELD'), do_lang_tempcode('DESCRIPTION_CATALOGUE_SPREADSHEET_IMPORT_KEY_FIELD'), 'key_field', '', false));

        $new_handling_options = new Tempcode();
        $new_handling_options->attach(form_input_radio_entry('new_handling', 'add', true, do_lang_tempcode('NEW_HANDLING_ADD')));
        $new_handling_options->attach(form_input_radio_entry('new_handling', 'skip', false, do_lang_tempcode('NEW_HANDLING_SKIP')));
        $fields->attach(form_input_radio(do_lang_tempcode('CATALOGUE_SPREADSHEET_NEW_HANDLING'), do_lang_tempcode('DESCRIPTION_CATALOGUE_SPREADSHEET_NEW_HANDLING'), 'new_handling', $new_handling_options));

        $delete_handling_options = new Tempcode();
        $delete_handling_options->attach(form_input_radio_entry('delete_handling', 'delete', false, do_lang_tempcode('DELETE_HANDLING_DELETE')));
        $delete_handling_options->attach(form_input_radio_entry('delete_handling', 'leave', true, do_lang_tempcode('DELETE_HANDLING_LEAVE')));
        $fields->attach(form_input_radio(do_lang_tempcode('CATALOGUE_SPREADSHEET_DELETE_HANDLING'), do_lang_tempcode('DESCRIPTION_CATALOGUE_SPREADSHEET_DELETE_HANDLING'), 'delete_handling', $delete_handling_options));

        $update_handling_options = new Tempcode();
        $update_handling_options->attach(form_input_radio_entry('update_handling', 'overwrite', true, do_lang_tempcode('UPDATE_HANDLING_OVERWRITE')));
        $update_handling_options->attach(form_input_radio_entry('update_handling', 'freshen', false, do_lang_tempcode('UPDATE_HANDLING_FRESHEN')));
        $update_handling_options->attach(form_input_radio_entry('update_handling', 'skip', false, do_lang_tempcode('UPDATE_HANDLING_SKIP')));
        $update_handling_options->attach(form_input_radio_entry('update_handling', 'delete', false, do_lang_tempcode('UPDATE_HANDLING_DELETE')));
        $fields->attach(form_input_radio(do_lang_tempcode('CATALOGUE_SPREADSHEET_UPDATE_HANDLING'), do_lang_tempcode('DESCRIPTION_CATALOGUE_SPREADSHEET_UPDATE_HANDLING'), 'update_handling', $update_handling_options));


        $fields->attach(form_input_codename(do_lang_tempcode('CATALOGUE_SPREADSHEET_IMPORT_META_KEYWORDS_FIELD'), do_lang_tempcode('DESCRIPTION_CATALOGUE_SPREADSHEET_IMPORT_META_KEYWORDS_FIELD'), 'meta_keywords_field', '', false));
        $fields->attach(form_input_codename(do_lang_tempcode('CATALOGUE_SPREADSHEET_IMPORT_META_DESCRIPTION_FIELD'), do_lang_tempcode('DESCRIPTION_CATALOGUE_SPREADSHEET_IMPORT_META_DESCRIPTION_FIELD'), 'meta_description_field', '', false));
        $fields->attach(form_input_codename(do_lang_tempcode('CATALOGUE_SPREADSHEET_IMPORT_NOTES_FIELD'), do_lang_tempcode('DESCRIPTION_CATALOGUE_SPREADSHEET_IMPORT_NOTES_FIELD'), 'notes_field', '', false));

        require_code('feedback2');
        list($allow_rating, $allow_comments, $allow_trackbacks) = $this->choose_feedback_fields_statistically(1, 1, 1);
        $fields->attach(feedback_fields($this->content_type, $allow_rating == 1, $allow_comments == 1, $allow_trackbacks == 1, false, '', $allow_comments == 2, false, false));

        require_javascript('catalogues');
        return do_template('FORM_SCREEN', [
            '_GUID' => '0ad5a822bccb3de8e53fcc47594eb404',
            'TITLE' => $this->title,
            'JS_FUNCTION_CALLS' => ['cmsCataloguesImportCatalogue'],
            'TEXT' => do_lang_tempcode('CATALOGUE_IMPORT_TEXT'),
            'HIDDEN' => $hidden,
            'FIELDS' => $fields,
            'SUBMIT_ICON' => 'admin/import_spreadsheet',
            'SUBMIT_NAME' => $submit_name,
            'URL' => $post_url,
        ]);
    }

    /**
     * Standard actualiser to import catalogue entries.
     *
     * @return Tempcode The UI
     */
    public function _import_catalogue() : object
    {
        check_privilege('mass_import');

        $catalogue_name = get_param_string('catalogue_name');

        // Details about how to handle the import
        $key_field = post_param_string('key_field', '');
        if ($key_field == '') {
            $new_handling = '';
            $delete_handling = '';
            $update_handling = '';
        } else {
            $new_handling = post_param_string('new_handling');
            $delete_handling = post_param_string('delete_handling');
            $update_handling = post_param_string('update_handling');
        }

        $meta_keywords_field = post_param_string('meta_keywords_field', '');
        $meta_description_field = post_param_string('meta_description_field', '');
        $notes_field = post_param_string('notes_field', '');

        $allow_rating = post_param_integer('allow_rating', 0);
        $allow_comments = post_param_integer('allow_comments', 0);
        $allow_trackbacks = post_param_integer('allow_trackbacks', 0);

        // Grab the spreadsheet file
        require_code('uploads');
        $target_path = get_temporary_upload_path('file_anytype');

        require_code('files_spreadsheets_read');
        if (!is_spreadsheet_readable($_FILES['file_anytype']['name'])) {
            unlink($target_path);
            sync_file($target_path);
            warn_exit(do_lang_tempcode('UNKNOWN_FORMAT', escape_html(get_file_extension($_FILES['file_anytype']['name']))));
        }

        log_it('IMPORT_CATALOGUE_ENTRIES', $catalogue_name);

        require_code('tasks');
        return call_user_func_array__long_task(do_lang('CATALOGUE_IMPORT'), $this->title, 'import_catalogue', [$catalogue_name, $key_field, $new_handling, $delete_handling, $update_handling, $meta_keywords_field, $meta_description_field, $notes_field, $allow_rating, $allow_comments, $allow_trackbacks, $target_path]);
    }

    /**
     * The UI to choose a catalogue to export catalogue entries.
     *
     * @return Tempcode The UI
     */
    public function export_catalogue() : object
    {
        if (!$GLOBALS['FORUM_DRIVER']->is_super_admin(get_member())) {
            access_denied('I_ERROR');
        }

        $catalogue_select = $this->choose_catalogue($this->title, 'admin/export_spreadsheet');

        if ($catalogue_select !== null) {
            return $catalogue_select;
        }

        $catalogue_name = get_param_string('catalogue_name');
        return $this->_export_catalogue($catalogue_name);
    }

    /**
     * The actualiser to download a spreadsheet of catalogues.
     *
     * @param  ID_TEXT $catalogue_name The name of the catalogue
     * @return Tempcode The UI
     */
    public function _export_catalogue(string $catalogue_name) : object
    {
        log_it('EXPORT_CATALOGUE_ENTRIES', $catalogue_name);

        require_code('tasks');
        return call_user_func_array__long_task(do_lang('EXPORT_CATALOGUE_ENTRIES'), $this->title, 'export_catalogue', [$catalogue_name]);
    }

    /**
     * UI for install/uninstall of predefined content.
     *
     * @return Tempcode The UI
     */
    public function predefined_content() : object
    {
        require_code('content2');
        return predefined_content_changes_ui('catalogues', $this->title, build_url(['page' => '_SELF', 'type' => '_predefined_content'], '_SELF'));
    }

    /**
     * Actualise install/uninstall of predefined content.
     *
     * @return Tempcode The UI
     */
    public function _predefined_content() : object
    {
        require_code('content2');
        return predefined_content_changes_actualiser('catalogues', $this->title);
    }
}

/**
 * Module page class.
 */
class Module_cms_catalogues_cat extends Standard_crud_module
{
    protected $lang_type = 'CATALOGUE_CATEGORY';
    protected $select_name = 'NAME';
    protected $permissions_require = 'cat_mid';
    protected $permission_module = 'catalogues_category';
    protected $permissions_module_require = 'catalogues_catalogue';
    protected $permissions_cat_name = 'catalogue_name';
    protected $seo_type = 'catalogue_category';
    protected $catalogue = true;
    protected $content_type = 'catalogue_category';
    protected $upload = 'image';
    protected $menu_label = 'CATALOGUES';
    protected $table = 'catalogue_categories';
    protected $orderer = 'cc_title';
    protected $is_chained_with_parent_browse = true;

    protected $donext_catalogue_name;

    public $js_function_calls = ['moduleCmsCataloguesCat'];

    /**
     * Standard crud_module table function.
     *
     * @param  array $url_map Details to go to build_url for link to the next screen
     * @return array A quintet: The choose table, Whether re-ordering is supported from this screen, Search URL, Archive URL, a Filtercode box block
     */
    public function create_selection_list_choose_table(array $url_map) : array
    {
        require_code('templates_results_table');

        $current_ordering = get_param_string('sort', 'cc_title ASC', INPUT_FILTER_GET_COMPLEX);
        $sortables = [
            'cc_title' => do_lang_tempcode('TITLE'),
            'cc_add_date' => do_lang_tempcode('ADDED'),
        ];
        list($sql_sort, $sort_order, $sortable) = process_sorting_params('catalogue_category', $current_ordering);

        $fh = [do_lang_tempcode('TITLE'), do_lang_tempcode('ADDED')];
        $fh[] = do_lang_tempcode('ACTIONS');
        $header_row = results_header_row($fh, $sortables, 'sort', $sortable . ' ' . $sort_order);

        $fields = new Tempcode();

        $catalogue_name = get_param_string('catalogue_name');

        list($rows, $max_rows) = $this->get_entry_rows(false, $sql_sort, ['c_name' => $catalogue_name]);
        $news_cat_titles = [];
        foreach ($rows as $row) {
            $edit_url = build_url($url_map + ['id' => $row['id']], '_SELF');

            $fr = [];
            $fr[] = protect_from_escaping(hyperlink(build_url(['page' => 'catalogues', 'type' => 'category', 'id' => $row['id']], get_module_zone('catalogues')), get_translated_text($row['cc_title']), false, true));
            $fr[] = get_timezoned_date_time($row['cc_add_date']);
            $fr[] = protect_from_escaping(hyperlink($edit_url, do_lang_tempcode('EDIT'), false, true, do_lang('EDIT') . ' #' . strval($row['id'])));

            $fields->attach(results_entry($fr, true));
        }

        $is_tree = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogues', 'c_is_tree', ['c_name' => $catalogue_name]);

        $search_url = build_url(['page' => 'search', 'id' => 'catalogue_categories'], get_module_zone('search'));
        $archive_url = build_url(['page' => 'catalogues', 'type' => 'index', 'id' => $catalogue_name, 'tree' => $is_tree], get_module_zone('catalogues'));

        return [results_table(do_lang($this->menu_label), get_param_integer('start', 0), 'start', either_param_integer('max', 20), 'max', $max_rows, $header_row, $fields, $sortables, $sortable, $sort_order), false, $search_url, $archive_url];
    }

    /**
     * Standard crud_module tree-list function.
     *
     * @return ?array A triple: The tree field (Tempcode), Search URL, Archive URL (null: nothing here)
     */
    public function create_selection_list_ajax_tree() : ?array
    {
        $catalogue_name = get_param_string('catalogue_name');

        $is_tree = $GLOBALS['SITE_DB']->query_select_value('catalogues', 'c_is_tree', ['c_name' => $catalogue_name]);
        if ($is_tree == 0) {
            return null;
        }

        $search_url = build_url(['page' => 'search', 'id' => 'catalogue_categories', 'catalogue_name' => $catalogue_name], get_module_zone('search'));
        $archive_url = build_url(['page' => 'catalogues', 'type' => 'index', 'id' => $catalogue_name, 'tree' => $is_tree], get_module_zone('catalogues'));

        $tree = form_input_tree_list(do_lang_tempcode('CODENAME'), '', 'id', null, 'choose_catalogue_category', ['catalogue_name' => $catalogue_name], true);
        return [$tree, $search_url, $archive_url];
    }

    /**
     * Get Tempcode for an adding form.
     *
     * @return mixed Either Tempcode; or a tuple of: (fields, hidden-fields[, delete-fields][, edit-text][, whether all delete fields are specified][, posting form text, more fields][, parsed WYSIWYG editable text])
     */
    public function get_form_fields_for_add()
    {
        return $this->get_form_fields();
    }

    /**
     * Get Tempcode for a catalogue category adding/editing form.
     *
     * @param  ?ID_TEXT $catalogue_name The name of the catalogue the category is in (null: detect)
     * @param  SHORT_TEXT $title The title of the category
     * @param  LONG_TEXT $description Description for the category
     * @param  LONG_TEXT $notes Admin notes
     * @param  ?AUTO_LINK $parent_id The ID of the parent category (null: no parent) (-1: arbitrary default)
     * @param  ?AUTO_LINK $id The ID of this category (null: we're adding, not editing)
     * @param  URLPATH $rep_image The rep-image for the catalogue category
     * @param  integer $move_days_lower The number of days before expiry (lower limit)
     * @param  integer $move_days_higher The number of days before expiry (higher limit)
     * @param  ?AUTO_LINK $move_target The expiry category (null: do not expire)
     * @return array A pair: The input fields, Hidden fields
     */
    public function get_form_fields(?string $catalogue_name = null, string $title = '', string $description = '', string $notes = '', ?int $parent_id = -1, ?int $id = null, string $rep_image = '', int $move_days_lower = 30, int $move_days_higher = 60, ?int $move_target = null) : array
    {
        if ($catalogue_name === null) {
            $catalogue_name = get_param_string('catalogue_name', ($id === null) ? false : $GLOBALS['SITE_DB']->query_select_value('catalogues_categories', 'c_name', ['id' => $id]));
        }

        if ($parent_id == -1) {
            $parent_id = get_param_integer('parent_id', -1);
        }

        $fields = new Tempcode(); // Not the fields in a category (no such thing: fields are in catalogues) - the HTML form fields to input the details for a category
        $hidden = new Tempcode();

        $hidden->attach(form_input_hidden('catalogue_name', $catalogue_name));

        $fields->attach(form_input_line(do_lang_tempcode('TITLE'), do_lang_tempcode('DESCRIPTION_TITLE'), 'title', $title, true));

        $fields->attach(form_input_text_comcode(do_lang_tempcode('DESCRIPTION'), do_lang_tempcode('DESCRIPTION_DESCRIPTION'), 'description', $description, false));

        if (get_option('enable_staff_notes') == '1') {
            $fields->attach(form_input_text(do_lang_tempcode('NOTES'), do_lang_tempcode('DESCRIPTION_NOTES'), 'notes', $notes, false, false));
        }

        require_code('images');
        $fields->attach(form_input_upload_multi_source(do_lang_tempcode('REPRESENTATIVE_IMAGE'), do_lang_tempcode('DESCRIPTION_REPRESENTATIVE_IMAGE', 'catalogue_category'), $hidden, 'image', null, false, $rep_image, false, null, IMAGE_CRITERIA_WEBSAFE));

        // Is the catalogue a tree?
        $is_tree = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogues', 'c_is_tree', ['c_name' => $catalogue_name]);
        if ($is_tree === null) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'catalogue'));
        }
        if (($is_tree == 1) && ($parent_id !== null)) {
            $fields->attach(form_input_tree_list(do_lang_tempcode('PARENT'), do_lang_tempcode('DESCRIPTION_PARENT', 'catalogue_category'), 'parent_id', null, 'choose_catalogue_category', ['catalogue_name' => $catalogue_name], true, (($parent_id === null) || ($parent_id == -1)) ? '' : strval($parent_id)));
        }

        if (cron_installed()) {
            $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', ['_GUID' => '745236e628a4d3da5355f07874433600', 'SECTION_HIDDEN' => ($move_target === null), 'TITLE' => do_lang_tempcode('CLASSIFIED_ADS')]));
            $fields->attach(form_input_tree_list(do_lang_tempcode('EXPIRY_MOVE_TARGET'), do_lang_tempcode('DESCRIPTION_EXPIRY_MOVE_TARGET'), 'move_target', null, 'choose_catalogue_category', ['catalogue_name' => $catalogue_name], false, ($move_target === null) ? null : strval($move_target)));
            $fields->attach(form_input_integer(do_lang_tempcode('EXPIRY_MOVE_DAYS_LOWER'), do_lang_tempcode('DESCRIPTION_EXPIRY_MOVE_DAYS_LOWER'), 'move_days_lower', $move_days_lower, true));
            $fields->attach(form_input_integer(do_lang_tempcode('EXPIRY_MOVE_DAYS_HIGHER'), do_lang_tempcode('DESCRIPTION_EXPIRY_MOVE_DAYS_HIGHER'), 'move_days_higher', $move_days_higher, true));
        }

        $fields->attach(metadata_get_fields('catalogue_category', ($id === null) ? null : strval($id)));
        require_code('content2');
        $fields->attach(seo_get_fields($this->seo_type, ($id === null) ? null : strval($id), false));

        if (addon_installed('content_reviews')) {
            $fields->attach(content_review_get_fields('catalogue_category', ($id === null) ? null : strval($id)));
        }

        // Permissions
        if (get_value('disable_cat_cat_perms') !== '1') {
            $fields->attach($this->get_permission_fields(($id === null) ? '' : strval($id), null, ($id === null)));
            if (addon_installed('ecommerce')) {
                require_code('ecommerce_permission_products');
                $fields->attach(permission_product_form('catalogue_category', ($id === null) ? null : strval($id)));
            }
        }

        return [$fields, $hidden];
    }

    /**
     * Standard crud_module category getter.
     *
     * @param  ID_TEXT $id The entry for which the category is sought
     * @return string The category
     */
    public function get_cat(string $id) : string
    {
        $c_name = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogue_categories', 'c_name', ['id' => intval($id)]);
        if ($c_name === null) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'catalogue_category'));
        }
        return $c_name;
    }

    /**
     * Standard crud_module edit form filler.
     *
     * @param  ID_TEXT $_id The category being edited
     * @return mixed Either Tempcode; or a tuple of: (fields, hidden-fields[, delete-fields][, edit-text][, whether all delete fields are specified][, posting form text, more fields][, parsed WYSIWYG editable text])
     */
    public function fill_in_edit_form(string $_id)
    {
        $category_id = intval($_id);

        $catalogue_name = get_param_string('catalogue_name', $GLOBALS['SITE_DB']->query_select_value('catalogue_categories', 'c_name', ['id' => $category_id]));

        $rows = $GLOBALS['SITE_DB']->query_select('catalogue_categories', ['*'], ['id' => $category_id], '', 1);
        if (!array_key_exists(0, $rows)) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'catalogue_category'));
        }
        $myrow = $rows[0];

        return $this->get_form_fields($catalogue_name, get_translated_text($myrow['cc_title']), get_translated_text($myrow['cc_description']), $myrow['cc_notes'], $myrow['cc_parent_id'], $category_id, $myrow['rep_image'], $myrow['cc_move_days_lower'], $myrow['cc_move_days_higher'], $myrow['cc_move_target_id']);
    }

    /**
     * Standard crud_module delete possibility checker.
     *
     * @param  ID_TEXT $id The category being potentially deleted
     * @return boolean Whether it may be deleted
     */
    public function may_delete_this(string $id) : bool
    {
        $cat = $GLOBALS['SITE_DB']->query_select('catalogue_categories cc LEFT JOIN ' . get_table_prefix() . 'catalogues c ON c.c_name=cc.c_name', ['cc_parent_id', 'c_is_tree'], ['id' => intval($id)], '', 1);
        if (!array_key_exists(0, $cat)) {
            return true;
        }
        return ($cat[0]['c_is_tree'] == 0) || ($cat[0]['cc_parent_id'] !== null);
    }

    /**
     * Standard crud_module add actualiser.
     *
     * @return array A pair: The category added, description about usage
     */
    public function add_actualisation() : array
    {
        require_code('catalogues2');

        $catalogue_name = post_param_string('catalogue_name');

        $title = post_param_string('title');
        $description = post_param_string('description');
        $notes = post_param_string('notes', '');
        $parent_id = post_param_integer('parent_id', null);
        require_code('images2');
        $rep_image = post_param_image('image', 'uploads/repimages', null, false);

        $move_days_lower = post_param_integer('move_days_lower', 30);
        $move_days_higher = post_param_integer('move_days_higher', 60);
        $move_target = post_param_integer('move_target', null);
        if ($move_target !== null) {
            if (!has_submit_permission('mid', get_member(), get_ip_address(), 'cms_catalogues', ['catalogues_catalogue', $catalogue_name] + ((get_value('disable_cat_cat_perms') !== '1') ? ['catalogues_category', $move_target] : []))) {
                access_denied('CATEGORY_ACCESS');
            }
        }

        $metadata = actual_metadata_get_fields('catalogue_category', null);

        $category_id = actual_add_catalogue_category($catalogue_name, $title, $description, $notes, $parent_id, $rep_image, $move_days_lower, $move_days_higher, $move_target, $metadata['add_time']);

        set_url_moniker('catalogue_category', strval($category_id));

        if (get_value('disable_cat_cat_perms') !== '1') {
            $this->set_permissions(strval($category_id));
            if (addon_installed('ecommerce')) {
                require_code('ecommerce_permission_products');
                permission_product_save('catalogue_category', strval($category_id));
            }
        }

        if (addon_installed('content_reviews')) {
            content_review_set('catalogue_category', strval($category_id));
        }

        $this->donext_category_id = $category_id;
        $this->donext_catalogue_name = $catalogue_name;

        return [strval($category_id), null];
    }

    /**
     * Standard crud_module edit actualiser.
     *
     * @param  ID_TEXT $_id The category being edited
     * @return ?Tempcode Description about usage (null: none)
     */
    public function edit_actualisation(string $_id) : ?object
    {
        require_code('catalogues2');

        $category_id = intval($_id);

        $catalogue_name = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogue_categories', 'c_name', ['id' => $category_id]);
        if ($catalogue_name === null) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'catalogue_category'));
        }

        $title = post_param_string('title');
        $description = post_param_string('description', STRING_MAGIC_NULL);
        $notes = post_param_string('notes', STRING_MAGIC_NULL);
        $parent_id = post_param_integer('parent_id', fractional_edit() ? INTEGER_MAGIC_NULL : null/*may be non-tree catalogue or root node*/);

        $move_days_lower = post_param_integer('move_days_lower', fractional_edit() ? INTEGER_MAGIC_NULL : 30/*may be the system scheduler is disabled*/);
        $move_days_higher = post_param_integer('move_days_higher', fractional_edit() ? INTEGER_MAGIC_NULL : 60/*may be the system scheduler is disabled*/);
        $move_target = post_param_integer('move_target', fractional_edit() ? INTEGER_MAGIC_NULL : null);
        if (($move_target !== null) && ($move_target != INTEGER_MAGIC_NULL)) {
            if (!has_submit_permission('mid', get_member(), get_ip_address(), 'cms_catalogues', ['catalogues_catalogue', $catalogue_name] + ((get_value('disable_cat_cat_perms') !== '1') ? ['catalogues_category', $move_target] : []))) {
                access_denied('CATEGORY_ACCESS');
            }
        }

        if (!fractional_edit()) {
            require_code('images2');
            $rep_image = post_param_image('image', 'uploads/repimages', null, false, true);
        } else {
            $rep_image = STRING_MAGIC_NULL;
        }

        $metadata = actual_metadata_get_fields('catalogue_category', strval($category_id));

        actual_edit_catalogue_category($category_id, $title, $description, $notes, $parent_id, post_param_string('meta_keywords', STRING_MAGIC_NULL), post_param_string('meta_description', STRING_MAGIC_NULL), $rep_image, $move_days_lower, $move_days_higher, $move_target, $metadata['add_time']);
        if (!fractional_edit()) {
            if (get_value('disable_cat_cat_perms') !== '1') {
                $this->set_permissions(strval($category_id));
                if (addon_installed('ecommerce')) {
                    require_code('ecommerce_permission_products');
                    permission_product_save('catalogue_category', strval($category_id));
                }
            }
        }

        if (addon_installed('content_reviews')) {
            content_review_set('catalogue_category', strval($category_id));
        }

        $this->donext_category_id = $category_id;
        $this->donext_catalogue_name = $catalogue_name;

        return null;
    }

    /**
     * Standard crud_module delete actualiser.
     *
     * @param  ID_TEXT $id The category being deleted
     */
    public function delete_actualisation(string $id)
    {
        require_code('catalogues2');

        $catalogue_name = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogue_categories', 'c_name', ['id' => $id]);
        if ($catalogue_name === null) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'catalogue_category'));
        }

        actual_delete_catalogue_category(intval($id));

        $this->donext_catalogue_name = $catalogue_name;
    }

    /**
     * The do-next manager for after catalogue 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 $id The ID of whatever we are working with (null: deleted)
     * @return Tempcode The UI
     */
    public function do_next_manager(?object $title, object $description, ?string $id = null) : object
    {
        $catalogue_name = $this->donext_catalogue_name;

        $is_tree = $GLOBALS['SITE_DB']->query_select_value('catalogues', 'c_is_tree', ['c_name' => $catalogue_name]);

        require_code('templates_donext');
        return do_next_manager(
            $title,
            $description,
            [],
            null,
            /* TYPED-ORDERED LIST OF 'LINKS' */
            ['_SELF', ['type' => 'add_entry', 'category_id' => $id, 'catalogue_name' => $catalogue_name], '_SELF'], // Add one
            null, // Edit this
            has_privilege(get_member(), 'edit_own_midrange_content', 'cms_catalogues') ? ['_SELF', ['type' => 'edit_entry', 'catalogue_name' => $catalogue_name], '_SELF'] : null, // Edit one
            null, // View this
            null, // View archive
            has_privilege(get_member(), 'submit_cat_midrange_content', 'cms_catalogues') ? ['_SELF', ['type' => 'add_category', 'catalogue_name' => $catalogue_name], '_SELF'] : null, // Add one category
            has_privilege(get_member(), 'edit_own_cat_midrange_content', 'cms_catalogues') ? ['_SELF', ['type' => 'edit_category', 'catalogue_name' => $catalogue_name], '_SELF'] : null, // Edit one category
            (($id === null) || (!has_privilege(get_member(), 'edit_own_cat_midrange_content', 'cms_catalogues'))) ? null : ['_SELF', ['type' => '_edit_category', 'id' => $id, 'catalogue_name' => $catalogue_name], '_SELF', do_lang_tempcode('EDIT_THIS_CATALOGUE_CATEGORY')], // Edit this category
            ($id === null) ? null : ['catalogues', ['type' => 'category', 'id' => $id], get_module_zone('catalogues')], // View this category

            /* SPECIALLY TYPED 'LINKS' */
            [],
            [],
            [
                has_privilege(get_member(), 'submit_cat_highrange_content', 'cms_catalogues') ? ['menu/cms/catalogues/add_one_catalogue', ['_SELF', ['type' => 'add_catalogue'], '_SELF']] : null,
                has_privilege(get_member(), 'edit_own_cat_highrange_content', 'cms_catalogues') ? ['menu/cms/catalogues/edit_this_catalogue', ['_SELF', ['type' => '_edit_catalogue', 'id' => $catalogue_name], '_SELF']] : null,
                has_privilege(get_member(), 'edit_own_cat_highrange_content', 'cms_catalogues') ? ['menu/cms/catalogues/edit_one_catalogue', ['_SELF', ['type' => 'edit_catalogue'], '_SELF']] : null,
                ['admin/view_this', ['catalogues', ['type' => 'index', 'id' => $catalogue_name, 'tree' => $is_tree], get_module_zone('catalogues')], do_lang('INDEX')],
            ],
            do_lang('MANAGE_CATALOGUES'),
            null,
            null,
            null,
            'catalogue_entry',
            'catalogue_category'
        );
    }
}

/**
 * Module page class.
 */
class Module_cms_catalogues_alt extends Standard_crud_module
{
    protected $lang_type = 'CATALOGUE';
    protected $select_name = 'CATALOGUE';
    protected $select_name_description = 'DESCRIPTION_CATALOGUE';
    protected $permissions_require = 'cat_high';
    protected $permission_module = 'catalogues_catalogue';
    protected $non_integer_id = true;
    protected $content_type = 'catalogue';
    protected $is_tree_catalogue = false; // Set for usage by do-next-manager
    protected $menu_label = 'CATALOGUES';
    protected $table = 'catalogues';
    protected $is_chained_with_parent_browse = true;

    public $js_function_calls = ['moduleCmsCataloguesAlt'];

    /**
     * Standard crud_module list function.
     *
     * @return Tempcode The selection list
     */
    public function create_selection_list_entries() : object
    {
        return create_selection_list_catalogues();
    }

    /**
     * Standard aed_module delete possibility checker.
     *
     * @param  ID_TEXT $id The catalogue being potentially deleted
     * @return boolean Whether it may be deleted
     */
    public function may_delete_this(string $id) : bool
    {
        if (substr($id, 0, 1) == '_') {
            return false;
        }
        return true;
    }

    /**
     * Find what content type this is for.
     *
     * @param  ?ID_TEXT $name The catalogue name (null: n/a)
     * @return ?ID_TEXT The content type (null: none)
     */
    public function tied_to_content_type(?string $name) : ?string
    {
        if ($name === null) {
            return null;
        }

        $rem_name = substr($name, 1);
        $tied_to_content_type = (substr($name, 0, 1) == '_') && ((file_exists(get_file_base() . '/sources_custom/hooks/systems/content_meta_aware/' . $rem_name . '.php')) || (file_exists(get_file_base() . '/sources/hooks/systems/content_meta_aware/' . $rem_name . '.php')));
        if ($tied_to_content_type) {
            return $rem_name;
        }
        return null;
    }

    /**
     * Get Tempcode for an adding form.
     *
     * @return mixed Either Tempcode; or a tuple of: (fields, hidden-fields[, delete-fields][, edit-text][, whether all delete fields are specified][, posting form text, more fields][, parsed WYSIWYG editable text])
     */
    public function get_form_fields_for_add()
    {
        return $this->get_form_fields();
    }

    /**
     * Get Tempcode for a catalogue adding/editing form.
     *
     * @param  ID_TEXT $name The name of the catalogue
     * @param  SHORT_TEXT $title The human readable name/title of the catalogue
     * @param  LONG_TEXT $description The description
     * @param  SHORT_INTEGER $display_type The display type
     * @param  BINARY $is_tree Whether the catalogue uses a hierarchy
     * @param  LONG_TEXT $notes Admin notes
     * @param  integer $submit_points How many points are given to a member that submits to the catalogue
     * @param  BINARY $ecommerce Whether the catalogue is an eCommerce catalogue
     * @param  SHORT_TEXT $categories_sort_order Category sort order
     * @set "title ASC" "recent ASC" "recent DESC"
     * @param  ID_TEXT $send_view_reports How to send view reports
     * @set never daily weekly monthly quarterly
     * @param  ?integer $default_review_freq Default review frequency for catalogue entries (null: none)
     * @return array A tuple: The input fields, Hidden fields, ...
     */
    public function get_form_fields(string $name = '', string $title = '', string $description = '', int $display_type = 0, int $is_tree = 1, string $notes = '', int $submit_points = 0, int $ecommerce = 0, string $categories_sort_order = 'title ASC', string $send_view_reports = 'never', ?int $default_review_freq = null) : array
    {
        $fields = new Tempcode();
        $hidden = new Tempcode();

        if ($name == '') {
            $name = get_param_string('id', '');
        }
        $content_type = $this->tied_to_content_type($name);
        if ($content_type !== null) {
            require_code('content');
            $ob = get_content_object($content_type);
            $info = $ob->info();
            if ($info === null) {
                fatal_exit(do_lang_tempcode('INTERNAL_ERROR', escape_html('519fd1ccae3a567ca45003de1ab5c32a')));
            }

            $title = do_lang('CUSTOM_FIELDS_FOR', do_lang($info['content_type_label']));

            $hidden->attach(form_input_hidden('title', $title));
            $hidden->attach(form_input_hidden('catalogue_name', $name));
            $hidden->attach(form_input_hidden('description', ''));
            $hidden->attach(form_input_hidden('notes', ''));
            $hidden->attach(form_input_hidden('auto_fill', ''));
            $hidden->attach(form_input_hidden('display_type', '0'));
            $hidden->attach(form_input_hidden('submit_points', '0'));
            $hidden->attach(form_input_hidden('send_view_reports', 'never'));

            attach_message(do_lang_tempcode('EDITING_CUSTOM_FIELDS_HELP', do_lang_tempcode($info['content_type_label'])), 'inform', true);

            $actions = new Tempcode();
        } else {
            $fields->attach(form_input_line(do_lang_tempcode('TITLE'), do_lang_tempcode('DESCRIPTION_TITLE'), 'title', $title, true));
            $complex_rename = ($name == 'products');
            $catalogue_name_field = form_input_codename(do_lang_tempcode('CODENAME'), do_lang_tempcode($complex_rename ? 'DESCRIPTION_CODENAME_SHOULDNT' : 'DESCRIPTION_CODENAME'), 'catalogue_name', $name, true);
            if (!$complex_rename) {
                $fields->attach($catalogue_name_field);
            }
            $fields->attach(form_input_text_comcode(do_lang_tempcode('DESCRIPTION'), do_lang_tempcode('DESCRIPTION_CATALOGUE_DESCRIPTION'), 'description', $description, false));

            $display_types = new Tempcode();
            foreach ([C_DT_FIELDMAPS => 'DT_FIELDMAPS', C_DT_TITLELIST => 'DT_TITLELIST', C_DT_TABULAR => 'DT_TABULAR', C_DT_GRID => 'DT_GRID'] as $_display_type => $display_type_str) {
                $display_types->attach(form_input_list_entry(strval($_display_type), $display_type == $_display_type, do_lang_tempcode($display_type_str)));
            }
            $fields->attach(form_input_list(do_lang_tempcode('DISPLAY_TYPE'), do_lang_tempcode('DESCRIPTION_DISPLAY_TYPE'), 'display_type', $display_types));

            $fields->attach(form_input_tick(do_lang_tempcode('IS_TREE'), do_lang_tempcode('DESCRIPTION_IS_TREE'), 'is_tree', $is_tree == 1));
            if ($name == '') {
                $fields->attach(form_input_line(do_lang_tempcode('AUTO_FILL'), do_lang_tempcode('DESCRIPTION_AUTO_FILL'), 'auto_fill', '', false, null, pow(2, 31) - 1));
            }

            $fields->attach(do_template('FORM_SCREEN_FIELD_SPACER', ['_GUID' => 'd7f7e0da078bdfaab0b3387d200d57a4', 'SECTION_HIDDEN' => $notes == '' && $submit_points == 0 && $send_view_reports == 'never', 'TITLE' => do_lang_tempcode('ADVANCED')]));

            if ($complex_rename) {
                $fields->attach($catalogue_name_field);
            }

            if (addon_installed('shopping')) {
                if ($ecommerce == 1) {
                    if (get_forum_type() != 'cns') {
                        warn_exit(do_lang_tempcode('NO_CNS'));
                    }
                }

                $fields->attach(form_input_tick(do_lang_tempcode('CAT_ECOMMERCE'), do_lang_tempcode('DESCRIPTION_CAT_ECOMMERCE'), 'ecommerce', $ecommerce == 1));
            }

            $sort_orders = new Tempcode();
            $sort_orders->attach(form_input_list_entry('title ASC', $categories_sort_order == 'title ASC', do_lang_tempcode('TITLE')));
            $sort_orders->attach(form_input_list_entry('recent DESC', $categories_sort_order == 'recent DESC', do_lang_tempcode('NEWEST_FIRST')));
            $sort_orders->attach(form_input_list_entry('recent ASC', $categories_sort_order == 'recent ASC', do_lang_tempcode('OLDEST_FIRST')));
            $fields->attach(form_input_list(do_lang_tempcode('CATEGORIES_SORT_ORDER'), do_lang_tempcode('DESCRIPTION_CATEGORIES_SORT_ORDER'), 'categories_sort_order', $sort_orders));

            if (get_option('enable_staff_notes') == '1') {
                $fields->attach(form_input_text(do_lang_tempcode('NOTES'), do_lang_tempcode('DESCRIPTION_NOTES'), 'notes', $notes, false, false));
            }
            if (addon_installed('points')) {
                $fields->attach(form_input_integer(do_lang_tempcode('SUBMIT_POINTS'), do_lang_tempcode('DESCRIPTION_SUBMIT_POINTS'), 'submit_points', $submit_points, false));
            }

            $view_report_types = new Tempcode();
            $view_report_types->attach(form_input_list_entry('never', $send_view_reports == 'never', do_lang_tempcode('VR_NEVER')));
            $view_report_types->attach(form_input_list_entry('daily', $send_view_reports == 'daily', do_lang_tempcode('VR_DAILY')));
            $view_report_types->attach(form_input_list_entry('weekly', $send_view_reports == 'weekly', do_lang_tempcode('VR_WEEKLY')));
            $view_report_types->attach(form_input_list_entry('monthly', $send_view_reports == 'monthly', do_lang_tempcode('VR_MONTHLY')));
            $view_report_types->attach(form_input_list_entry('quarterly', $send_view_reports == 'quarterly', do_lang_tempcode('VR_QUARTERLY')));
            $fields->attach(form_input_list(do_lang_tempcode('VIEW_REPORTS'), do_lang_tempcode('DESCRIPTION_VIEW_REPORTS'), 'send_view_reports', $view_report_types));

            $fields->attach(metadata_get_fields('catalogue', ($name == '') ? null : $name));

            if (addon_installed('content_reviews')) {
                $fields->attach(content_review_get_fields('catalogue', ($name == '') ? null : $name));
            }

            // Permissions
            $fields->attach($this->get_permission_fields($name, null, ($name == '')));
            if (addon_installed('ecommerce')) {
                require_code('ecommerce_permission_products');
                $fields->attach(permission_product_form('catalogue', ($name === '') ? null : $name));
            }

            $actions = new Tempcode();
            if (($name != '') && (get_value('disable_cat_cat_perms') !== '1')) {
                $actions->attach(form_input_tick(do_lang_tempcode('RESET_CATEGORY_PERMISSIONS'), do_lang_tempcode('DESCRIPTION_RESET_CATEGORY_PERMISSIONS'), 'reset_category_permissions', false));
            }
        }

        if (addon_installed('content_reviews')) {
            require_lang('content_reviews');
            $fields->attach(form_input_integer(do_lang_tempcode('REVIEW_FREQ'), do_lang_tempcode('DESCRIPTION_CATALOGUE_REVIEW_FREQ'), 'default_review_freq', $default_review_freq, false));
        }

        return [$fields, $hidden, null, null, false, null, $actions];
    }

    /**
     * Get Tempcode for a catalogue field adding/editing form (many of these are put together to add/edit a single catalogue!).
     *
     * @param  ID_TEXT $catalogue_name The catalogue being edited (blank: new and no name pre-specified)
     * @param  boolean $first_field Whether this is the first field of the entry fields
     * @param  integer $num_fields_to_show The number of fields that will be on the screen
     * @param  string $prefix The prefix the field input fields are given (e.g. new1_)
     * @param  integer $order The order of the field relative to the other fields
     * @param  SHORT_TEXT $name The name of the field
     * @param  LONG_TEXT $description Description for the field
     * @param  ID_TEXT $type The field type
     * @param  BINARY $defines_order Whether the field defines entry ordering
     * @param  BINARY $visible Whether the field is visible when an entry is viewed
     * @param  BINARY $sensitive Whether the field contains sensitive or personal data
     * @param  SHORT_TEXT $default Default value for the field
     * @param  BINARY $required Whether the field is required
     * @param  BINARY $is_sortable Whether the field is sortable
     * @param  BINARY $include_in_main_search Whether the field is included in main search
     * @param  BINARY $allow_template_search Whether to allow template search
     * @param  BINARY $put_in_category Whether the field is to be shown in category views (not applicable for the list display type)
     * @param  BINARY $put_in_search Whether the field is to be shown in search views (not applicable for the list display type)
     * @param  SHORT_TEXT $options Field options
     * @return array A pair: the Tempcode for the visible fields, and the Tempcode for the hidden fields
     */
    public function get_field_fields(string $catalogue_name, bool $first_field, int $num_fields_to_show, string $prefix, int $order, string $name = '', string $description = '', string $type = 'short_text', int $defines_order = 0, int $visible = 1, int $sensitive = 0, string $default = '', int $required = 0, int $is_sortable = 0, int $include_in_main_search = 0, int $allow_template_search = 0, int $put_in_category = 1, int $put_in_search = 1, string $options = '') : array
    {
        $fields = new Tempcode();
        $hidden = new Tempcode();

        $fields->attach(form_input_line(do_lang_tempcode('NAME'), do_lang_tempcode('DESCRIPTION_FIELD_NAME'), $prefix . 'name', $name, ($name != '') || $first_field)); // If this is gonna be a new field that might not be filled in, don't make them fill it in
        $fields->attach(form_input_line(do_lang_tempcode('DESCRIPTION'), do_lang_tempcode('DESCRIPTION_FIELD_DESCRIPTION'), $prefix . 'description', $description, false));

        require_code('fields');
        require_lang('fields');

        $type_list = create_selection_list_field_type($type, $name != '');

        $fields->attach(form_input_list(do_lang_tempcode('TYPE'), do_lang_tempcode(($name == '') ? 'DESCRIPTION_FIELD_TYPE_FIRST_TIME' : 'DESCRIPTION_FIELD_TYPE'), $prefix . 'type', $type_list));

        $fields->attach(form_input_line(do_lang_tempcode('FIELD_OPTIONS'), do_lang_tempcode('DESCRIPTION_FIELD_OPTIONS', escape_html(get_tutorial_url('tut_fields'))), $prefix . 'options', $options, false));

        $fields->attach(form_input_line(do_lang_tempcode('DEFAULT_VALUE'), do_lang_tempcode('DESCRIPTION_FIELD_DEFAULT'), $prefix . 'default', $default, false, null, pow(2, 31) - 1));

        $order_list = '';
        for ($i = 0; $i < $num_fields_to_show; $i++) {
            $order_title = integer_format($i + 1);
            $order_selected = ($i == $order);
            if (($i == 0) && (substr(get_param_string('id', ''), 0, 1) != '_')) {
                $order_title .= do_lang('NEW_FIELD_TITLE');
            }
            $order_list .= '<option value="' . strval($i) . '"' . ($order_selected ? ' selected="selected"' : '') . '>' . escape_html($order_title) . '</option>'; // XHTMLXHTML
        }
        $fields->attach(form_input_list(do_lang_tempcode('ORDER'), do_lang_tempcode('DESCRIPTION_FIELD_ORDER_CLEVER', do_lang_tempcode('CATALOGUE_FIELD')), $prefix . 'order', make_string_tempcode($order_list)));

        // Defines order?
        if (substr($catalogue_name, 0, 1) != '_') {
            $radios = new Tempcode();
            $radios->attach(form_input_radio_entry($prefix . 'defines_order', '0', $defines_order == 0, do_lang_tempcode('NO')));
            $radios->attach(form_input_radio_entry($prefix . 'defines_order', '1', $defines_order == 1, do_lang_tempcode('ASCENDING')));
            $radios->attach(form_input_radio_entry($prefix . 'defines_order', '2', $defines_order == 2, do_lang_tempcode('DESCENDING')));
            $fields->attach(form_input_radio(do_lang_tempcode('DEFINES_ORDER'), do_lang_tempcode('DESCRIPTION_DEFINES_ORDER'), $prefix . 'defines_order', $radios));
        } else {
            $hidden->attach(form_input_hidden($prefix . 'defines_order', strval($defines_order)));
        }

        if (substr($catalogue_name, 0, 1) != '_') {
            $fields->attach(form_input_tick(do_lang_tempcode('SORTABLE'), do_lang_tempcode('DESCRIPTION_SORTABLE'), $prefix . 'is_sortable', $is_sortable == 1));
        } else {
            $hidden->attach(form_input_hidden($prefix . 'is_sortable', strval($is_sortable)));
        }

        if ($first_field) {
            $hidden->attach(form_input_hidden($prefix . 'visible', '1'));
            $hidden->attach(form_input_hidden($prefix . 'required', '1'));
        } else {
            $fields->attach(form_input_tick(do_lang_tempcode('VISIBLE'), do_lang_tempcode('DESCRIPTION_VISIBLE'), $prefix . 'visible', $visible == 1));
            $fields->attach(form_input_tick(do_lang_tempcode('REQUIRED'), do_lang_tempcode('DESCRIPTION_REQUIRED'), $prefix . 'required', $required == 1));
        }

        $fields->attach(form_input_tick(do_lang_tempcode('SENSITIVE_FIELD'), do_lang_tempcode('DESCRIPTION_SENSITIVE_FIELD'), $prefix . 'sensitive', $sensitive == 1));
        $fields->attach(form_input_tick(do_lang_tempcode('INCLUDE_IN_MAIN_SEARCH'), do_lang_tempcode('DESCRIPTION_INCLUDE_IN_MAIN_SEARCH'), $prefix . 'include_in_main_search', $include_in_main_search == 1));
        $fields->attach(form_input_tick(do_lang_tempcode('ALLOW_TEMPLATE_SEARCH'), do_lang_tempcode('DESCRIPTION_ALLOW_TEMPLATE_SEARCH'), $prefix . 'allow_template_search', $allow_template_search == 1));
        $fields->attach(form_input_tick(do_lang_tempcode('PUT_IN_CATEGORY'), do_lang_tempcode('DESCRIPTION_PUT_IN_CATEGORY'), $prefix . 'put_in_category', $put_in_category == 1));
        $fields->attach(form_input_tick(do_lang_tempcode('PUT_IN_SEARCH'), do_lang_tempcode('DESCRIPTION_PUT_IN_SEARCH'), $prefix . 'put_in_search', $put_in_search == 1));

        return [$fields, $hidden];
    }

    /**
     * Standard crud_module edit form filler.
     *
     * @param  ID_TEXT $catalogue_name The catalogue being edited
     * @return mixed Either Tempcode; or a tuple of: (fields, hidden-fields[, delete-fields][, edit-text][, whether all delete fields are specified][, posting form text, more fields][, parsed WYSIWYG editable text])
     */
    public function fill_in_edit_form(string $catalogue_name)
    {
        $rows = $GLOBALS['SITE_DB']->query_select('catalogues', ['*'], ['c_name' => $catalogue_name], '', 1);
        if (!array_key_exists(0, $rows)) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'catalogue'));
        }
        $myrow = $rows[0];

        $title = get_translated_text($myrow['c_title']);
        $description = get_translated_text($myrow['c_description']);

        return $this->get_form_fields($catalogue_name, $title, $description, $myrow['c_display_type'], $myrow['c_is_tree'], $myrow['c_notes'], $myrow['c_submit_points'], $myrow['c_ecommerce'], $myrow['c_categories_sort_order'], $myrow['c_send_view_reports'], $myrow['c_default_review_freq']);
    }

    /**
     * Standard crud_module add actualiser.
     *
     * @return array A pair: The catalogue added, description about usage
     */
    public function add_actualisation() : array
    {
        require_code('catalogues2');
        $name = post_param_string('catalogue_name');
        $title = post_param_string('title');
        $description = post_param_string('description');
        $display_type = post_param_integer('display_type');
        $is_tree = post_param_integer('is_tree', 0);
        $this->is_tree_catalogue = ($is_tree == 1);
        $notes = post_param_string('notes', '');
        $submit_points = post_param_integer('submit_points', 0);
        $cat_tab = post_param_integer('cat_tab', 0);
        $ecommerce = post_param_integer('ecommerce', 0);
        $categories_sort_order = post_param_string('categories_sort_order', 'title ASC');
        $send_view_reports = post_param_string('send_view_reports');
        $default_review_freq = post_param_integer('default_review_freq', null);

        // What fields do we have?
        $new = [];
        foreach ($_POST as $key => $val) {
            if ((!is_string($val)) && (!is_integer($val))) {
                continue;
            }

            $matches = [];
            if (preg_match('#new_field_(\d*)_(.*)#A', $key, $matches) != 0) {
                $new[$matches[1]][$matches[2]] = $val;
            }
        }

        $num_fields = 0;
        foreach ($new as $field) {
            if ($field['name'] != '') {
                $num_fields++;
            }
        }
        if (($num_fields == 0) && (substr($name, 0, 1) != '_')) {
            warn_exit(do_lang_tempcode('NO_FIELDS'));
        }

        $metadata = actual_metadata_get_fields('catalogue', null);

        actual_add_catalogue($name, $title, $description, $display_type, $is_tree, $notes, $submit_points, $ecommerce, $categories_sort_order, $send_view_reports, $default_review_freq, $metadata['add_time']);

        set_url_moniker('catalogue', $name);

        $category_id = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogue_categories', 'id', ['c_name' => $name]);

        $this->set_permissions($name);
        if ($category_id !== null) {
            $GLOBALS['MODULE_CMS_CATALOGUES']->cat_crud_module->set_permissions(strval($category_id));
        }
        if (addon_installed('ecommerce')) {
            require_code('ecommerce_permission_products');
            permission_product_save('catalogue', $name);
        }

        if (addon_installed('content_reviews')) {
            content_review_set('catalogue', $name);
        }

        // Now onto the fields
        foreach ($new as $field) {
            if (!array_key_exists('default', $field)) {
                warn_exit(do_lang_tempcode('IMPROPERLY_FILLED_IN'));
            }
            if (!array_key_exists('description', $field)) {
                warn_exit(do_lang_tempcode('IMPROPERLY_FILLED_IN'));
            }
            if (!array_key_exists('name', $field)) {
                warn_exit(do_lang_tempcode('IMPROPERLY_FILLED_IN'));
            }
            if (!array_key_exists('order', $field)) {
                warn_exit(do_lang_tempcode('IMPROPERLY_FILLED_IN'));
            }
            if ((!array_key_exists('type', $field)) || ($field['type'] == '')) {
                warn_exit(do_lang_tempcode('IMPROPERLY_FILLED_IN'));
            }

            if ($field['order'] == '') {
                $field['order'] = 0;
            } else {
                $field['order'] = intval($field['order']);
            }
            $defines_order = array_key_exists('defines_order', $field) ? intval($field['defines_order']) : 0;
            $visible = array_key_exists('visible', $field) ? intval($field['visible']) : 0;
            $sensitive = array_key_exists('sensitive', $field) ? intval($field['sensitive']) : 0;
            $required = array_key_exists('required', $field) ? intval($field['required']) : 0;
            $is_sortable = array_key_exists('is_sortable', $field) ? intval($field['is_sortable']) : 0;
            $include_in_main_search = array_key_exists('include_in_main_search', $field) ? intval($field['include_in_main_search']) : 0;
            $allow_template_search = array_key_exists('allow_template_search', $field) ? intval($field['allow_template_search']) : 0;
            $put_in_category = array_key_exists('put_in_category', $field) ? intval($field['put_in_category']) : 0;
            $put_in_search = array_key_exists('put_in_search', $field) ? intval($field['put_in_search']) : 0;
            $options = array_key_exists('options', $field) ? $field['options'] : '';
            if ($field['name'] != '') {
                actual_add_catalogue_field($name, $field['name'], $field['description'], $field['type'], $field['order'], $defines_order, $visible, $sensitive, $field['default'], $required, $is_sortable, $include_in_main_search, $allow_template_search, $put_in_category, $put_in_search, $options);
            }
        }

        // Auto-fill feature
        $auto_fill = post_param_string('auto_fill');
        if ($auto_fill != '') {
            $categories = [];
            if (strpos($auto_fill, '|') === false) {
                $to_do = explode(',', $auto_fill);
            } else {
                $to_do = explode('|', $auto_fill);
            }
            foreach ($to_do as $doing) {
                if (trim($doing) == '') {
                    continue;
                }

                $bits = explode('\\', $doing);
                $parent_id = $category_id;
                foreach ($bits as $bit) {
                    $bit = trim($bit);

                    if (array_key_exists($bit, $categories)) {
                        if ($parent_id !== null) {
                            $parent_id = $categories[$bit];
                        }
                    } else {
                        $_parent_id = actual_add_catalogue_category($name, $bit, '', '', $parent_id, '');
                        if ($parent_id !== null) {
                            $parent_id = $_parent_id;
                        }
                        require_code('permissions2');
                        if (get_value('disable_cat_cat_perms') !== '1') {
                            set_category_permissions_from_environment('catalogues_category', strval($parent_id), $this->privilege_page);
                        }
                        $categories[$bit] = $parent_id;
                    }
                }
            }
        }

        if (($is_tree == 0) && (substr($name, 0, 1) != '_')) {
            $this->do_next_description = paragraph(do_lang_tempcode('SUGGEST_ADD_CATEGORY_NEXT'));
        }

        return [$name, null];
    }

    /**
     * Standard crud_module edit actualiser.
     *
     * @param  ID_TEXT $old_name The catalogue being edited
     * @return ?Tempcode Description about usage (null: none)
     */
    public function edit_actualisation(string $old_name) : ?object
    {
        require_code('catalogues2');

        $name = post_param_string('catalogue_name', $old_name);
        $title = post_param_string('title', STRING_MAGIC_NULL);
        $description = post_param_string('description', STRING_MAGIC_NULL);
        $display_type = post_param_integer('display_type', fractional_edit() ? INTEGER_MAGIC_NULL : 0);
        $notes = post_param_string('notes', STRING_MAGIC_NULL);
        $submit_points = post_param_integer('submit_points', fractional_edit() ? INTEGER_MAGIC_NULL : 0);
        $categories_sort_order = post_param_string('categories_sort_order', STRING_MAGIC_NULL);
        $ecommerce = post_param_integer('ecommerce', fractional_edit() ? INTEGER_MAGIC_NULL : 0);
        $send_view_reports = post_param_string('send_view_reports', STRING_MAGIC_NULL);
        if (!fractional_edit()) {
            if (post_param_integer('reset_category_permissions', 0) == 1) {
                $start = 0;
                do {
                    $old_limit = cms_set_time_limit(TIME_LIMIT_EXTEND__MODEST);

                    send_http_output_ping();

                    $rows = $GLOBALS['SITE_DB']->query_select('catalogue_categories', ['id'], ['c_name' => $name], '', 300, $start);
                    foreach ($rows as $row) {
                        $this->set_permissions(strval($row['id']));
                    }

                    $start += 300;

                    cms_set_time_limit($old_limit);
                } while (array_key_exists(0, $rows));
            }
        }
        $default_review_freq = post_param_integer('default_review_freq', fractional_edit() ? INTEGER_MAGIC_NULL : null);

        $was_tree = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogues', 'c_is_tree', ['c_name' => $old_name]);
        if ($was_tree === null) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'catalogue'));
        }
        $is_tree = post_param_integer('is_tree', fractional_edit() ? INTEGER_MAGIC_NULL : 0);

        if (!fractional_edit()) {
            // What fields do we have?
            $old = [];
            $new = [];
            foreach ($_POST as $key => $val) {
                if (!is_string($val)) {
                    continue;
                }

                $matches = [];
                if (preg_match('#new_field_(\d+)_(.*)#A', $key, $matches) != 0) {
                    $new[$matches[1]][$matches[2]] = $val;
                } elseif (preg_match('#existing_field_(\d+)_(.*)#A', $key, $matches) != 0) {
                    $old[$matches[1]][$matches[2]] = $val;
                }
            }
            $num_fields = 0;
            foreach ($new as $field) {
                if ($field['name'] != '') {
                    $num_fields++;
                }
            }
            foreach ($old as $field) {
                if (!((array_key_exists('delete', $field)) && ($field['delete'] == '1'))) {
                    $num_fields++;
                }
            }

            if ($num_fields == 0) {
                if (substr($old_name, 0, 1) == '_') {
                    actual_delete_catalogue($old_name);
                    return null;
                }
                warn_exit(do_lang_tempcode('NO_FIELDS'));
            }
        }

        if (($is_tree == 1) && ($was_tree == 0)) {
            catalogue_to_tree($name);
        }
        if (($is_tree == 0) && ($was_tree == 1)) {
            catalogue_from_tree($name);
        }
        $this->is_tree_catalogue = ($is_tree == 1);

        $metadata = actual_metadata_get_fields('catalogue', $old_name, [], $name);

        actual_edit_catalogue($old_name, $name, $title, $description, $display_type, $notes, $submit_points, $ecommerce, $categories_sort_order, $send_view_reports, $default_review_freq, $metadata['add_time']);

        if ($old_name != $name) {
            unset($_GET['redirect']);
        }

        if (addon_installed('content_reviews')) {
            content_review_set('catalogue', $name, $old_name);
        }

        $this->new_id = $name;

        if (!fractional_edit()) {
            // Now onto the fields
            //  First we must rationalise the ordering
            $o = 0;
            $orderings = [];
            foreach ($new as $current) {
                if (!array_key_exists('default', $current)) {
                    warn_exit(do_lang_tempcode('IMPROPERLY_FILLED_IN'));
                }
                if (!array_key_exists('description', $current)) {
                    warn_exit(do_lang_tempcode('IMPROPERLY_FILLED_IN'));
                }
                if (!array_key_exists('name', $current)) {
                    warn_exit(do_lang_tempcode('IMPROPERLY_FILLED_IN'));
                }
                if (!array_key_exists('order', $current)) {
                    warn_exit(do_lang_tempcode('IMPROPERLY_FILLED_IN'));
                }
                if ((!array_key_exists('type', $current)) || ($current['type'] == '')) {
                    warn_exit(do_lang_tempcode('IMPROPERLY_FILLED_IN'));
                }

                if ($current['name'] != '') {
                    if ((!array_key_exists('order', $current)) || ($current['order'] == '')) {
                        $current['order'] = strval(count($new) + count($old));
                    }
                    $orderings['new_' . strval($o)] = $current['order'];
                }
                $o++;
            }
            $o = 0;
            foreach ($old as $current) {
                if (!((array_key_exists('delete', $current)) && ($current['delete'] == '1'))) { // If not deleting
                    if ((!array_key_exists('order', $current)) || ($current['order'] == '')) {
                        $current['order'] = strval(count($new) + count($old));
                    }
                    $orderings['old_' . strval($o)] = $current['order'];
                }
                $o++;
            }
            asort($orderings);
            //  Now add/edit them
            $o = 0;
            foreach ($new as $field) {
                $p = 0;
                foreach (array_keys($orderings) as $key) {
                    if ($key == 'new_' . strval($o)) {
                        $order = $p;
                    }
                    $p++;
                }
                $defines_order = array_key_exists('defines_order', $field) ? intval($field['defines_order']) : 0;
                $visible = array_key_exists('visible', $field) ? intval($field['visible']) : 0;
                $sensitive = array_key_exists('sensitive', $field) ? intval($field['sensitive']) : 0;
                $required = array_key_exists('required', $field) ? intval($field['required']) : 0;
                $is_sortable = array_key_exists('is_sortable', $field) ? intval($field['is_sortable']) : 0;
                $include_in_main_search = array_key_exists('include_in_main_search', $field) ? intval($field['include_in_main_search']) : 0;
                $allow_template_search = array_key_exists('allow_template_search', $field) ? intval($field['allow_template_search']) : 0;
                $put_in_category = array_key_exists('put_in_category', $field) ? intval($field['put_in_category']) : 0;
                $put_in_search = array_key_exists('put_in_search', $field) ? intval($field['put_in_search']) : 0;
                $options = $field['options'];
                if ($field['name'] != '') {
                    actual_add_catalogue_field($name, $field['name'], $field['description'], $field['type'], $order, $defines_order, $visible, $sensitive, $field['default'], $required, $is_sortable, $include_in_main_search, $allow_template_search, $put_in_category, $put_in_search, $options);
                }
                $o++;
            }
            $o = 0;
            foreach ($old as $id => $field) {
                if ((array_key_exists('delete', $field)) && ($field['delete'] == '1')) {
                    actual_delete_catalogue_field($id);
                } else {
                    $p = 0;
                    foreach (array_keys($orderings) as $key) {
                        if ($key == 'old_' . strval($o)) {
                            $order = $p;
                        }
                        $p++;
                    }
                    $defines_order = array_key_exists('defines_order', $field) ? intval($field['defines_order']) : 0;
                    $visible = array_key_exists('visible', $field) ? intval($field['visible']) : 0;
                    $sensitive = array_key_exists('sensitive', $field) ? intval($field['sensitive']) : 0;
                    $is_sortable = array_key_exists('is_sortable', $field) ? intval($field['is_sortable']) : 0;
                    $include_in_main_search = array_key_exists('include_in_main_search', $field) ? intval($field['include_in_main_search']) : 0;
                    $allow_template_search = array_key_exists('allow_template_search', $field) ? intval($field['allow_template_search']) : 0;
                    $required = array_key_exists('required', $field) ? intval($field['required']) : 0;
                    $put_in_category = array_key_exists('put_in_category', $field) ? intval($field['put_in_category']) : 0;
                    $put_in_search = array_key_exists('put_in_search', $field) ? intval($field['put_in_search']) : 0;
                    $options = $field['options'];
                    $field_type = array_key_exists('type', $field) ? $field['type'] : null;
                    actual_edit_catalogue_field($id, $name, $field['name'], $field['description'], $order, $defines_order, $visible, $sensitive, $field['default'], $required, $is_sortable, $include_in_main_search, $allow_template_search, $put_in_category, $put_in_search, $options, $field_type);
                }
                $o++;
            }
        }

        // Do this last as it causes a menu decache which can cause memory errors if we do a warn_exit (i.e. we want the warn_exit's before this)
        if (!fractional_edit()) {
            $this->set_permissions($name);
            if (addon_installed('ecommerce')) {
                require_code('ecommerce_permission_products');
                permission_product_save('catalogue', $name);
            }
        }

        return null;
    }

    /**
     * Standard crud_module delete actualiser.
     *
     * @param  ID_TEXT $id The catalogue being deleted
     */
    public function delete_actualisation(string $id)
    {
        require_code('catalogues2');

        $this->is_tree_catalogue = ($GLOBALS['SITE_DB']->query_select_value_if_there('catalogues', 'c_is_tree', ['c_name' => $id]) === 1);

        require_code('menus2');
        delete_menu_item_simple('_SEARCH:catalogues:category:catalogue_name=' . $id);
        delete_menu_item_simple('_SEARCH:catalogues:index:' . $id);

        actual_delete_catalogue($id);
    }

    /**
     * The do-next manager for after catalogue 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 $name The catalogue name we were working with (null: deleted)
     * @return Tempcode The UI
     */
    public function do_next_manager(?object $title, object $description, ?string $name = null) : object
    {
        if ($name !== null) {
            $cat_count = $GLOBALS['SITE_DB']->query_select_value('catalogue_categories', 'COUNT(*)', ['c_name' => $name]);
            $has_categories = ($cat_count != 0);
        } else {
            $has_categories = false;
        }

        $is_custom_fields = ($name !== null) && (substr($name, 0, 1) == '_');

        require_code('templates_donext');
        return do_next_manager(
            $title,
            $description,
            [],
            null,
            /* TYPED-ORDERED LIST OF 'LINKS' */
            (($name === null) || (!$has_categories)) ? null : ['_SELF', ['type' => 'add_entry', 'catalogue_name' => $name], '_SELF'], // Add one
            null, // Edit this
            (($name === null) || (!$has_categories)) ? null : (has_privilege(get_member(), 'edit_own_midrange_content', 'cms_catalogues') ? ['_SELF', ['type' => 'edit_entry', 'catalogue_name' => $name], '_SELF'] : null), // Edit one
            null, // View this
            null, // View archive
            (($name === null) || $is_custom_fields) ? null : ['_SELF', ['type' => 'add_category', 'catalogue_name' => $name], '_SELF'], // Add one category
            (($name === null) || (!$has_categories)) ? null : ['_SELF', ['type' => 'edit_category', 'catalogue_name' => $name], '_SELF'], // Edit one category
            null, // Edit this category
            null, // View this category
            /* SPECIALLY TYPED 'LINKS' */
            [],
            [],
            [
                $is_custom_fields ? null : ['menu/cms/catalogues/add_one_catalogue', ['_SELF', ['type' => 'add_catalogue'], '_SELF']],
                ($name === null) ? null : ['menu/cms/catalogues/edit_this_catalogue', ['_SELF', ['type' => '_edit_catalogue', 'id' => $name], '_SELF', do_lang_tempcode('EDIT_THIS_CATALOGUE')]],
                $is_custom_fields ? null : ['menu/cms/catalogues/edit_one_catalogue', ['_SELF', ['type' => 'edit_catalogue'], '_SELF']],
                (($name === null) || $is_custom_fields) ? null : ['menu/rich_content/catalogues/catalogues', ['catalogues', $this->is_tree_catalogue ? ['type' => 'category', 'catalogue_name' => $name] : ['type' => 'index', 'id' => $name, 'tree' => $this->is_tree_catalogue ? 1 : 0], get_module_zone('catalogues')], do_lang('VIEW_CATALOGUE')],
            ],
            do_lang('MANAGE_CATALOGUES'),
            null,
            null,
            null,
            'catalogue_entry',
            'catalogue_category'
        );
    }
}
