<?php /*

 Composr
 Copyright (c) ocProducts, 2004-2016

 See text/EN/licence.txt for full licencing 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  ocProducts Ltd
 * @package    catalogues
 */

/**
 * Standard code module initialisation function.
 *
 * @ignore
 */
function init__catalogues2()
{
    global $DISABLE_TREE_CACHE_UPDATE;
    $DISABLE_TREE_CACHE_UPDATE = false;
}

/**
 * Converts a non-tree catalogue to a tree catalogue.
 *
 * @param  ID_TEXT $catalogue_name Catalogue name
 */
function catalogue_to_tree($catalogue_name)
{
    $_c_title = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogues', 'c_title', array('c_name' => $catalogue_name));
    if ($_c_title === null) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'catalogue'));
    }
    $new_root = actual_add_catalogue_category($catalogue_name, get_translated_text($_c_title), '', '', null, '');
    $GLOBALS['SITE_DB']->query('UPDATE ' . get_table_prefix() . 'catalogue_categories SET cc_parent_id=' . strval($new_root) . ' WHERE id<>' . strval($new_root) . ' AND ' . db_string_equal_to('c_name', $catalogue_name));
    $GLOBALS['SITE_DB']->query_update('catalogues', array('c_is_tree' => 1), array('c_name' => $catalogue_name), '', 1);
}

/**
 * Converts a non-tree catalogue from a tree catalogue.
 *
 * @param  ID_TEXT $catalogue_name Catalogue name
 */
function catalogue_from_tree($catalogue_name)
{
    $GLOBALS['SITE_DB']->query_update('catalogue_categories', array('cc_parent_id' => null), array('c_name' => $catalogue_name));
    $GLOBALS['SITE_DB']->query_update('catalogues', array('c_is_tree' => 0), array('c_name' => $catalogue_name), '', 1);
}

/**
 * Add a catalogue using all the specified values.
 *
 * @param  ID_TEXT $name The codename of the catalogue
 * @param  mixed $title The title of the catalogue (either language string map or string)
 * @param  mixed $description A description (either language string map or string)
 * @param  SHORT_INTEGER $display_type The display type
 * @param  BINARY $is_tree Whether the catalogue uses a tree system (as opposed to mere categories in an index)
 * @param  LONG_TEXT $notes Hidden notes pertaining to this catalogue
 * @param  integer $submit_points How many points a member gets by submitting to this catalogue
 * @param  BINARY $ecommerce Whether the catalogue is an eCommerce catalogue
 * @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)
 * @param  ?TIME $add_time The add time (null: now)
 * @param  boolean $uniqify Whether to force the name as unique, if there's a conflict
 * @return ID_TEXT The name
 */
function actual_add_catalogue($name, $title, $description, $display_type, $is_tree, $notes, $submit_points, $ecommerce = 0, $send_view_reports = 'never', $default_review_freq = null, $add_time = null, $uniqify = false)
{
    if (is_null($add_time)) {
        $add_time = time();
    }

    require_code('type_sanitisation');
    if (!is_alphanumeric($name)) {
        warn_exit(do_lang_tempcode('BAD_CODENAME'));
    }

    // Check doesn't already exist
    $test = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogues', 'c_name', array('c_name' => $name));
    if (!is_null($test)) {
        if ($uniqify) {
            $name .= '_' . uniqid('', false);
        } else {
            warn_exit(do_lang_tempcode('ALREADY_EXISTS', escape_html($name)));
        }
    }

    // Create
    $map = array(
        'c_name' => $name,
        'c_send_view_reports' => $send_view_reports,
        'c_ecommerce' => $ecommerce,
        'c_display_type' => $display_type,
        'c_is_tree' => $is_tree,
        'c_notes' => $notes,
        'c_add_date' => $add_time,
        'c_submit_points' => $submit_points,
        'c_default_review_freq' => $default_review_freq,
    );
    if (!is_array($title)) {
        $map += insert_lang('c_title', $title, 1);
    } else {
        $map += $title;
    }
    if (!is_array($description)) {
        $map += insert_lang_comcode('c_description', $description, 2);
    } else {
        $map += $description;
    }
    $GLOBALS['SITE_DB']->query_insert('catalogues', $map);

    if ($is_tree == 1) {
        // Create root node
        $root_title = ($is_tree == 1) ? do_lang('_HOME', get_translated_text($map['c_title'])) : get_translated_text($map['c_title']);
        $map = array(
            'cc_move_days_lower' => 30,
            'cc_move_days_higher' => 60,
            'cc_move_target' => null,
            'rep_image' => '',
            'c_name' => $name,
            'cc_notes' => '',
            'cc_order' => 0,
            'cc_add_date' => time(),
            'cc_parent_id' => null,
        );
        $map += insert_lang('cc_title', $root_title, 1);
        $map += insert_lang_comcode('cc_description', '', 3);
        $category = $GLOBALS['SITE_DB']->query_insert('catalogue_categories', $map, true);
    } else {
        $category = null;
    }

    log_it('ADD_CATALOGUE', $name);

    if ((addon_installed('commandr')) && (!running_script('install')) && (!get_mass_import_mode())) {
        require_code('resource_fs');
        generate_resource_fs_moniker('catalogue', $name, null, null, true);
    }

    require_code('member_mentions');
    dispatch_member_mention_notifications('catalogue', $name);

    if (substr($name, 0, 1) == '_') {
        persistent_cache_delete('CONTENT_TYPE_HAS_CUSTOM_FIELDS_CACHE');
    }

    if (function_exists('decache')) {
        decache('_field_type_selection');
    }

    require_code('sitemap_xml');
    notify_sitemap_node_add('_SEARCH:catalogues:index:' . $name, $add_time, null, SITEMAP_IMPORTANCE_MEDIUM, 'weekly', has_category_access($GLOBALS['FORUM_DRIVER']->get_guest_id(), 'catalogues_catalogue', $name));

    return $name;
}

/**
 * Add a field to the specified catalogue, without disturbing any other data in that catalogue.
 *
 * @param  ID_TEXT $c_name The codename of the catalogue the field is for
 * @param  mixed $name The name of the field (either language string map or string)
 * @param  mixed $description A description (either language string map or string)
 * @param  ID_TEXT $type The type of the field
 * @param  ?integer $order The field order (the field order determines what order the fields are displayed within an entry) (null: next)
 * @param  BINARY $defines_order Whether this field defines the catalogue order
 * @param  BINARY $visible Whether this is a visible field
 * @param  BINARY $searchable Whether the field is usable as a search key
 * @param  LONG_TEXT $default The default value for the field
 * @param  BINARY $required Whether this field is required
 * @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
 * @param  ?AUTO_LINK $id Force this ID (null: auto-increment as normal)
 * @return AUTO_LINK Field ID
 */
function actual_add_catalogue_field($c_name, $name, $description = '', $type = 'short_text', $order = null, $defines_order = 0, $visible = 1, $searchable = 0, $default = '', $required = 0, $put_in_category = 1, $put_in_search = 1, $options = '', $id = null)
{
    if (is_null($order)) {
        $order = $GLOBALS['SITE_DB']->query_select_value('catalogue_fields', 'MAX(cf_order)', array('c_name' => $c_name));
        if (is_null($order)) {
            $order = 0;
        } else {
            $order++;
        }
    }

    $map = array(
        'c_name' => $c_name,
        'cf_type' => $type,
        'cf_order' => $order,
        'cf_defines_order' => $defines_order,
        'cf_visible' => $visible,
        'cf_searchable' => $searchable,
        'cf_default' => $default,
        'cf_required' => $required,
        'cf_put_in_category' => $put_in_category,
        'cf_put_in_search' => $put_in_search,
        'cf_options' => $options,
    );
    if (!is_array($name)) {
        $map += insert_lang('cf_name', $name, 2);
    } else {
        $map += $name;
    }
    if (!is_array($description)) {
        $map += insert_lang('cf_description', $description, 2);
    } else {
        $map += $description;
    }
    if (!is_null($id)) {
        $map['id'] = $id;
    }
    $cf_id = $GLOBALS['SITE_DB']->query_insert('catalogue_fields', $map, true);
    if (!is_null($id)) {
        $cf_id = $id;
    }

    require_code('fields');

    $ob = get_fields_hook($type);

    if (php_function_allowed('set_time_limit')) {
        @set_time_limit(0);
    }

    // Now add field values for all pre-existing entries (in the ideal world, there would be none yet)
    $start = 0;
    do {
        $entries = collapse_1d_complexity('id', $GLOBALS['SITE_DB']->query_select('catalogue_entries', array('id'), array('c_name' => $c_name), '', 300, $start));
        foreach ($entries as $entry) {
            $default = mixed();

            list($raw_type, $default, $_type) = $ob->get_field_value_row_bits($map + array('id' => $cf_id), $required == 1, $default);

            $entry_map = array('cf_id' => $cf_id, 'ce_id' => $entry);
            if (strpos($_type, '_trans') !== false) {
                $entry_map += insert_lang_comcode('cv_value', is_null($default) ? '' : $default, 3);
            } elseif ($_type == 'float') {
                $entry_map['cv_value'] = ((is_null($default)) || ($default == '')) ? null : floatval($default);
            } elseif ($_type == 'integer') {
                $entry_map['cv_value'] = ((is_null($default)) || ($default == '')) ? null : intval($default);
            } else {
                $entry_map['cv_value'] = ((is_null($default)) || ($type == 'list')) ? '' : $default;
            }
            $GLOBALS['SITE_DB']->query_insert('catalogue_efv_' . $_type, $entry_map);
        }

        $start += 300;
    } while (array_key_exists(0, $entries));

    return $cf_id;
}

/**
 * Edit a catalogue.
 *
 * @param  ID_TEXT $old_name The current name of the catalogue
 * @param  ID_TEXT $name The new 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  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  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)
 * @param  ?TIME $add_time Add time (null: do not change)
 * @param  boolean $uniqify Whether to force the name as unique, if there's a conflict
 * @return ID_TEXT The name
 */
function actual_edit_catalogue($old_name, $name, $title, $description, $display_type, $notes, $submit_points, $ecommerce, $send_view_reports, $default_review_freq, $add_time = null, $uniqify = false)
{
    if ($old_name != $name) {
        // Check doesn't already exist
        $test = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogues', 'c_name', array('c_name' => $name));
        if (!is_null($test)) {
            if ($uniqify) {
                $name .= '_' . uniqid('', false);
            } else {
                warn_exit(do_lang_tempcode('ALREADY_EXISTS', escape_html($name)));
            }
        }

        require_code('type_sanitisation');
        if (!is_alphanumeric($name)) {
            warn_exit(do_lang_tempcode('BAD_CODENAME'));
        }
    }

    $rows = $GLOBALS['SITE_DB']->query_select('catalogues', array('c_description', 'c_title'), array('c_name' => $old_name), '', 1);
    if (!array_key_exists(0, $rows)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'catalogue'));
    }
    $myrow = $rows[0];
    $_title = $myrow['c_title'];
    $_description = $myrow['c_description'];

    // Edit
    $update_map = array(
        'c_send_view_reports' => $send_view_reports,
        'c_display_type' => $display_type,
        'c_ecommerce' => $ecommerce,
        'c_name' => $name,
        'c_notes' => $notes,
        'c_submit_points' => $submit_points,
        'c_default_review_freq' => $default_review_freq,
    );
    if ($add_time !== null) {
        $update_map['c_add_date'] = $add_time;
    }
    $update_map += lang_remap('c_title', $_title, $title);
    $update_map += lang_remap_comcode('c_description', $_description, $description);
    if (!is_null($add_time)) {
        $update_map['c_add_date'] = $add_time;
    }
    $GLOBALS['SITE_DB']->query_update('catalogues', $update_map, array('c_name' => $old_name), '', 1);

    // If we're renaming, then we better change a load of references
    if ($name != $old_name) {
        $GLOBALS['SITE_DB']->query_update('catalogue_categories', array('c_name' => $name), array('c_name' => $old_name));
        $GLOBALS['SITE_DB']->query_update('catalogue_fields', array('c_name' => $name), array('c_name' => $old_name));
        $GLOBALS['SITE_DB']->query_update('catalogue_entries', array('c_name' => $name), array('c_name' => $old_name));

        if (addon_installed('awards')) {
            $types = $GLOBALS['SITE_DB']->query_select('award_types', array('id'), array('a_content_type' => 'catalogue'));
            foreach ($types as $type) {
                $GLOBALS['SITE_DB']->query_update('award_archive', array('content_id' => $name), array('content_id' => $old_name, 'a_type_id' => $type['id']));
            }
        }

        require_code('sitemap_xml');
        notify_sitemap_node_delete('_SEARCH:catalogues:index:' . $old_name);
    }

    // Update field references
    $GLOBALS['SITE_DB']->query_update('catalogue_fields', array('cf_type' => 'ck_' . $name), array('cf_type' => 'ck_' . $old_name));
    $GLOBALS['SITE_DB']->query_update('catalogue_fields', array('cf_type' => 'cx_' . $name), array('cf_type' => 'cx_' . $old_name));
    update_catalogue_content_ref('catalogue', $old_name, $name);

    decache('main_cc_embed');

    log_it('EDIT_CATALOGUE', $name);

    if ((addon_installed('commandr')) && (!running_script('install')) && (!get_mass_import_mode())) {
        if ($old_name != $name) { // We want special stability in catalogue addressing
            require_code('resource_fs');
            generate_resource_fs_moniker('catalogue', $name);
        }
    }

    if (substr($name, 0, 1) == '_') {
        persistent_cache_delete('CONTENT_TYPE_HAS_CUSTOM_FIELDS_CACHE');
    }

    decache('_field_type_selection');

    require_code('sitemap_xml');
    notify_sitemap_node_edit('_SEARCH:catalogues:index:' . $name, has_category_access($GLOBALS['FORUM_DRIVER']->get_guest_id(), 'catalogues_catalogue', $name));

    return $name;
}

/**
 * Delete a catalogue.
 *
 * @param  ID_TEXT $name The name of the catalogue
 */
function actual_delete_catalogue($name)
{
    // Delete lang
    $rows = $GLOBALS['SITE_DB']->query_select('catalogues', array('c_description', 'c_title'), array('c_name' => $name), '', 1);
    if (!array_key_exists(0, $rows)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'catalogue'));
    }
    $myrow = $rows[0];

    // Delete anything involved (ha ha destruction!)
    if (php_function_allowed('set_time_limit')) {
        @set_time_limit(0);
    }
    do {
        $entries = collapse_1d_complexity('id', $GLOBALS['SITE_DB']->query_select('catalogue_entries', array('id'), array('c_name' => $name), '', 500));
        foreach ($entries as $entry) {
            actual_delete_catalogue_entry($entry);
        }
    } while (array_key_exists(0, $entries));
    do {
        $categories = collapse_1d_complexity('id', $GLOBALS['SITE_DB']->query_select('catalogue_categories', array('id'), array('c_name' => $name), '', 30));
        foreach ($categories as $category) {
            actual_delete_catalogue_category($category, true);
        }
    } while (array_key_exists(0, $categories));
    $fields = collapse_1d_complexity('id', $GLOBALS['SITE_DB']->query_select('catalogue_fields', array('id'), array('c_name' => $name)));
    foreach ($fields as $field) {
        actual_delete_catalogue_field($field);
    }
    $GLOBALS['SITE_DB']->query_delete('catalogues', array('c_name' => $name), '', 1);
    delete_lang($myrow['c_title']);
    delete_lang($myrow['c_description']);
    $GLOBALS['SITE_DB']->query_delete('group_category_access', array('module_the_name' => 'catalogues_catalogue', 'category_name' => $name));
    $GLOBALS['SITE_DB']->query_delete('group_privileges', array('module_the_name' => 'catalogues_catalogue', 'category_name' => $name));

    // Update field references
    $GLOBALS['SITE_DB']->query_update('catalogue_fields', array('cf_type' => 'short_text'), array('cf_type' => 'ck_' . $name));
    $GLOBALS['SITE_DB']->query_update('catalogue_fields', array('cf_type' => 'short_text'), array('cf_type' => 'cx_' . $name));

    update_catalogue_content_ref('catalogue', $name, '');

    log_it('DELETE_CATALOGUE', $name);

    if ((addon_installed('commandr')) && (!running_script('install')) && (!get_mass_import_mode())) {
        require_code('resource_fs');
        expunge_resource_fs_moniker('catalogue', $name);
    }

    if (substr($name, 0, 1) == '_') {
        persistent_cache_delete('CONTENT_TYPE_HAS_CUSTOM_FIELDS_CACHE');
    }

    require_code('sitemap_xml');
    notify_sitemap_node_delete('_SEARCH:catalogues:index:' . $name);
}

/**
 * Edit a catalogue field.
 *
 * @param  AUTO_LINK $id The ID of the field
 * @param  ID_TEXT $c_name The name of the catalogue
 * @param  ?SHORT_TEXT $name The name of the field (null: do not change)
 * @param  ?LONG_TEXT $description Description for the field (null: do not change)
 * @param  integer $order The field order (the field order determines what order the fields are displayed within an entry)
 * @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 $searchable Whether the field is usable as a search key
 * @param  LONG_TEXT $default The default value for the field
 * @param  BINARY $required Whether the field is required
 * @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
 * @param  ?ID_TEXT $type The field type (null: do not change)
 */
function actual_edit_catalogue_field($id, $c_name, $name, $description, $order, $defines_order, $visible, $searchable, $default, $required, $put_in_category = 1, $put_in_search = 1, $options = '', $type = null) // You cannot edit a field type
{
    $rows = $GLOBALS['SITE_DB']->query_select('catalogue_fields', array('cf_description', 'cf_name'), array('id' => $id));
    if (!array_key_exists(0, $rows)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE'));
    }
    $myrow = $rows[0];
    $_name = $myrow['cf_name'];
    $_description = $myrow['cf_description'];

    $map = array(
        'c_name' => $c_name,
        'cf_order' => $order,
        'cf_defines_order' => $defines_order,
        'cf_visible' => $visible,
        'cf_searchable' => $searchable,
        'cf_default' => $default,
        'cf_required' => $required,
        'cf_put_in_category' => $put_in_category,
        'cf_put_in_search' => $put_in_search,
        'cf_options' => $options,
    );
    if (!is_null($type)) {
        $map['cf_type'] = $type;
    }
    if (!is_null($name)) {
        $map += lang_remap('cf_name', $_name, $name);
    }
    if (!is_null($description)) {
        $map += lang_remap('cf_description', $_description, $description);
    }

    $GLOBALS['SITE_DB']->query_update('catalogue_fields', $map, array('id' => $id), '', 1);
}

/**
 * Delete a catalogue field.
 *
 * @param  AUTO_LINK $id The ID of the field
 */
function actual_delete_catalogue_field($id)
{
    $rows = $GLOBALS['SITE_DB']->query_select('catalogue_fields', array('cf_name', 'cf_description', 'cf_type'), array('id' => $id));
    if (!array_key_exists(0, $rows)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE'));
    }
    $myrow = $rows[0];
    delete_lang($myrow['cf_name']);
    delete_lang($myrow['cf_description']);

    $GLOBALS['SITE_DB']->query_delete('catalogue_fields', array('id' => $id), '', 1);
}

/**
 * Add a catalogue category
 *
 * @param  ID_TEXT $catalogue_name The codename of the catalogue the category is in
 * @param  mixed $title The title of this category (either language string map or string)
 * @param  mixed $description A description (either language string map or string)
 * @param  LONG_TEXT $notes Hidden notes pertaining to this category
 * @param  ?AUTO_LINK $parent_id The ID of this categories parent (null: a root category, or not a tree catalogue)
 * @param  URLPATH $rep_image The representative image for the category (blank: none)
 * @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)
 * @param  ?integer $order The order (null: automatic)
 * @param  ?TIME $add_date The add time (null: now)
 * @param  ?AUTO_LINK $id Force an ID (null: don't force an ID)
 * @param  ?SHORT_TEXT $meta_keywords Meta keywords for this resource (null: do not edit) (blank: implicit)
 * @param  ?LONG_TEXT $meta_description Meta description for this resource (null: do not edit) (blank: implicit)
 * @return AUTO_LINK The ID of the new category
 */
function actual_add_catalogue_category($catalogue_name, $title, $description, $notes, $parent_id, $rep_image = '', $move_days_lower = 30, $move_days_higher = 60, $move_target = null, $order = null, $add_date = null, $id = null, $meta_keywords = '', $meta_description = '')
{
    if (is_null($add_date)) {
        $add_date = time();
    }

    if (is_string($title)) {
        require_code('global4');
        prevent_double_submit('ADD_CATALOGUE_CATEGORY', null, $title);
    }

    if (is_null($order)) {
        $order = $GLOBALS['SITE_DB']->query_select_value('catalogue_categories', 'MAX(cc_order)', array('c_name' => $catalogue_name));
        if ((is_null($order)) || ($order >= 2147483647)) { // TODO: #3046 in tracker
            $order = 0;
        } else {
            $order++;
        }
    }

    $map = array(
        'cc_move_days_lower' => $move_days_lower,
        'cc_move_days_higher' => $move_days_higher,
        'cc_move_target' => $move_target,
        'cc_order' => $order,
        'rep_image' => $rep_image,
        'cc_add_date' => $add_date,
        'c_name' => $catalogue_name,
        'cc_notes' => $notes,
        'cc_parent_id' => $parent_id,
    );
    if (!is_array($title)) {
        $map += insert_lang('cc_title', $title, 2);
    } else {
        $map += $title;
    }
    if (!is_array($description)) {
        $map += insert_lang_comcode('cc_description', $description, 2);
    } else {
        $map += $description;
    }
    if (!is_null($id)) {
        $map['id'] = $id;
    }
    $id = $GLOBALS['SITE_DB']->query_insert('catalogue_categories', $map, true);

    store_in_catalogue_cat_treecache($id, $parent_id); // *must* run before calculate_category_child_count_cache

    calculate_category_child_count_cache($parent_id);

    reorganise_uploads__catalogue_categories(array('id' => $id));

    log_it('ADD_CATALOGUE_CATEGORY', strval($id), get_translated_text($map['cc_title']));

    require_code('seo2');
    if (($meta_keywords == '') && ($meta_description == '')) {
        if (!is_array($title)) {
            seo_meta_set_for_implicit('catalogue_category', strval($id), array($title, $description), $title);
        }
    } else {
        seo_meta_set_for_explicit('catalogue_category', strval($id), $meta_keywords, $meta_description);
    }

    if (!is_null($parent_id)) {
        require_code('notifications2');
        copy_notifications_to_new_child('catalogue_entry', strval($parent_id), strval($id));
    }

    if ((addon_installed('commandr')) && (!running_script('install')) && (!get_mass_import_mode())) {
        require_code('resource_fs');
        generate_resource_fs_moniker('catalogue_category', strval($id), null, null, true);
    }

    if (function_exists('get_member')) {
        require_code('member_mentions');
        dispatch_member_mention_notifications('catalogue_category', strval($id), get_member());
    }

    require_code('sitemap_xml');
    notify_sitemap_node_add(
        '_SEARCH:catalogues:category:' . strval($id),
        $add_date,
        null,
        is_null($parent_id) ? SITEMAP_IMPORTANCE_MEDIUM : SITEMAP_IMPORTANCE_LOW,
        'weekly',
        has_category_access($GLOBALS['FORUM_DRIVER']->get_guest_id(), 'catalogues_catalogue', $catalogue_name) && has_category_access($GLOBALS['FORUM_DRIVER']->get_guest_id(), 'catalogues_category', strval($id))
    );

    return $id;
}

/**
 * Rebuild the efficient catalogue category tree structure ancestry cache.
 */
function rebuild_catalogue_cat_treecache()
{
    if (php_function_allowed('set_time_limit')) {
        @set_time_limit(0);
    }

    $GLOBALS['SITE_DB']->query_delete('catalogue_cat_treecache');
    $GLOBALS['SITE_DB']->query_delete('catalogue_childcountcache');

    $GLOBALS['NO_QUERY_LIMIT'] = true;

    $max = 1000;
    $start = 0;
    do {
        $rows = $GLOBALS['SITE_DB']->query_select('catalogue_categories', array('id', 'cc_parent_id'), null, '', $max, $start);

        foreach ($rows as $row) {
            store_in_catalogue_cat_treecache($row['id'], $row['cc_parent_id'], false);
        }

        $start += $max;
    } while (count($rows) != 0);
    $start = 0;
    do {
        $rows = $GLOBALS['SITE_DB']->query_select('catalogue_categories', array('id', 'cc_parent_id'), null, '', $max, $start);

        foreach ($rows as $row) {
            calculate_category_child_count_cache($row['id'], false);
        }

        $start += $max;
    } while (count($rows) != 0);
}

/**
 * Update the treecache for a catalogue category node.
 *
 * @param  AUTO_LINK $id The ID of the category
 * @param  ?AUTO_LINK $parent_id The ID of the parent category (null: no parent)
 * @param  boolean $cleanup_first Whether to delete any possible pre-existing records for the category first
 */
function store_in_catalogue_cat_treecache($id, $parent_id, $cleanup_first = true)
{
    if ($cleanup_first) {
        $GLOBALS['SITE_DB']->query_delete('catalogue_cat_treecache', array('cc_id' => $id));
    }

    // Self reference
    $GLOBALS['SITE_DB']->query_insert('catalogue_cat_treecache', array(
        'cc_id' => $id,
        'cc_ancestor_id' => $id,
    ));

    // Stored recursed referenced towards root
    while (!is_null($parent_id)) {
        $GLOBALS['SITE_DB']->query_insert('catalogue_cat_treecache', array(
            'cc_id' => $id,
            'cc_ancestor_id' => $parent_id,
        ));
        $parent_id = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogue_categories', 'cc_parent_id', array('id' => $parent_id));
    }
}

/**
 * Update cache for a categories child counts.
 *
 * @param  ?AUTO_LINK $cat_id The ID of the category (null: skip, called by some code that didn't realise it didn't impact a tree parent)
 * @param  boolean $recursive_updates Whether to recurse up the tree to force recalculations on other categories (recommended, unless you are doing a complete rebuild)
 */
function calculate_category_child_count_cache($cat_id, $recursive_updates = true)
{
    if (is_null($cat_id)) {
        return;
    }

    global $DISABLE_TREE_CACHE_UPDATE;
    if ($DISABLE_TREE_CACHE_UPDATE) {
        return;
    }

    $GLOBALS['SITE_DB']->query_delete('catalogue_childcountcache', array(
        'cc_id' => $cat_id,
    ), '', 1);

    $catalogue_name = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogue_categories', 'c_name', array('id' => $cat_id));

    $num_rec_children = max(0, $GLOBALS['SITE_DB']->query_select_value('catalogue_cat_treecache', 'COUNT(*)', array('cc_ancestor_id' => $cat_id)) - 1);
    $num_rec_entries = $GLOBALS['SITE_DB']->query_select_value('catalogue_cat_treecache t JOIN ' . get_table_prefix() . 'catalogue_entries e ON e.cc_id=t.cc_id', 'COUNT(*)', array('ce_validated' => 1, 't.cc_ancestor_id' => $cat_id, 'c_name' => $catalogue_name/*important, else custom field cats could be included*/));

    $GLOBALS['SITE_DB']->query_insert('catalogue_childcountcache', array(
        'cc_id' => $cat_id,
        'c_num_rec_children' => $num_rec_children,
        'c_num_rec_entries' => $num_rec_entries,
    ));

    if ($recursive_updates) {
        $parent_id = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogue_categories', 'cc_parent_id', array('id' => $cat_id));
        if (!is_null($parent_id)) {
            calculate_category_child_count_cache($parent_id);
        }
    }
}

/**
 * Edit a catalogue category.
 *
 * @param  AUTO_LINK $id The ID of the category
 * @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)
 * @param  SHORT_TEXT $meta_keywords Meta keywords for the category
 * @param  LONG_TEXT $meta_description Meta description for the category
 * @param  URLPATH $rep_image The representative image for the category (blank: none)
 * @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)
 * @param  integer $order The order
 * @param  ?TIME $add_time Add time (null: do not change)
 * @param  ?ID_TEXT $c_name The catalogue name (null: do not change)
 */
function actual_edit_catalogue_category($id, $title, $description, $notes, $parent_id, $meta_keywords, $meta_description, $rep_image, $move_days_lower, $move_days_higher, $move_target, $order, $add_time = null, $c_name = null)
{
    $under_category_id = $parent_id;
    while ((!is_null($under_category_id)) && ($under_category_id != INTEGER_MAGIC_NULL)) {
        if ($id == $under_category_id) {
            warn_exit(do_lang_tempcode('OWN_PARENT_ERROR', 'catalogue_category'));
        }
        $_under_category_id = $GLOBALS['SITE_DB']->query_select_value('catalogue_categories', 'cc_parent_id', array('id' => $under_category_id));
        if ($under_category_id === $_under_category_id) {
            warn_exit(do_lang_tempcode('INTERNAL_ERROR'));
        }
        $under_category_id = $_under_category_id;
    }

    $rows = $GLOBALS['SITE_DB']->query_select('catalogue_categories', array('cc_description', 'cc_title', 'c_name'), array('id' => $id), '', 1);
    if (!array_key_exists(0, $rows)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'catalogue_category'));
    }
    $myrow = $rows[0];
    $_title = $myrow['cc_title'];
    $_description = $myrow['cc_description'];

    store_in_catalogue_cat_treecache($id, $parent_id);

    $update_map = array(
        'cc_move_days_lower' => $move_days_lower,
        'cc_move_days_higher' => $move_days_higher,
        'cc_move_target' => $move_target,
        'cc_order' => $order,
        'cc_notes' => $notes,
        'cc_parent_id' => $parent_id,
    );
    $update_map += lang_remap('cc_title', $_title, $title);
    $update_map += lang_remap_comcode('cc_description', $_description, $description);

    if (!is_null($rep_image)) {
        $update_map['rep_image'] = $rep_image;
        require_code('files2');
        delete_upload('uploads/repimages', 'catalogue_categories', 'rep_image', 'id', $id, $rep_image);
    }

    if (!is_null($add_time)) {
        $update_map['cc_add_date'] = $add_time;
    }

    if (!is_null($c_name)) { // Moving to a different catalogue
        $update_map['c_name'] = $c_name;
        $GLOBALS['SITE_DB']->query_update('catalogue_entries', array('c_name' => $c_name), array('cc_id' => $id));
        $sub_ids = $GLOBALS['SITE_DB']->query_select('catalogue_cat_treecache', array('cc_id'), array('cc_ancestor_id' => $id));
        foreach ($sub_ids as $_sub_id) {
            $sub_id = $_sub_id['cc_id'];
            $GLOBALS['SITE_DB']->query_update('catalogue_categories', array('c_name' => $c_name), array('id' => $sub_id), '', 1);
        }
    }

    $old_parent_id = $GLOBALS['SITE_DB']->query_select_value('catalogue_categories', 'cc_parent_id', array('id' => $id));

    $GLOBALS['SITE_DB']->query_update('catalogue_categories', $update_map, array('id' => $id), '', 1);

    require_code('urls2');
    suggest_new_idmoniker_for('catalogues', 'category', strval($id), '', $title);

    require_code('seo2');
    seo_meta_set_for_explicit('catalogue_category', strval($id), $meta_keywords, $meta_description);

    if ($old_parent_id !== $parent_id) {
        calculate_category_child_count_cache($old_parent_id);
        calculate_category_child_count_cache($parent_id);
    }

    reorganise_uploads__catalogue_categories(array('id' => $id));

    log_it('EDIT_CATALOGUE_CATEGORY', strval($id), $title);

    decache('main_cc_embed');

    if ((addon_installed('commandr')) && (!running_script('install')) && (!get_mass_import_mode())) {
        require_code('resource_fs');
        generate_resource_fs_moniker('catalogue_category', strval($id));
    }

    require_code('sitemap_xml');
    notify_sitemap_node_edit(
        '_SEARCH:catalogues:category:' . strval($id),
        has_category_access($GLOBALS['FORUM_DRIVER']->get_guest_id(), 'catalogues_catalogue', $rows[0]['c_name']) && has_category_access($GLOBALS['FORUM_DRIVER']->get_guest_id(), 'catalogues_category', strval($id))
    );
}

/**
 * Delete a catalogue category.
 *
 * @param  AUTO_LINK $id The ID of the category
 * @param  boolean $deleting_all Whether we're deleting everything under the category; if FALSE we will actively reassign child categories and entries up a level (if tree) or deletes (if not tree)
 */
function actual_delete_catalogue_category($id, $deleting_all = false)
{
    // Info about our category
    $rows = $GLOBALS['SITE_DB']->query_select('catalogue_categories c LEFT JOIN ' . $GLOBALS['SITE_DB']->get_table_prefix() . 'catalogues x ON c.c_name=x.c_name', array('c_is_tree', 'c.c_name', 'cc_description', 'cc_title', 'cc_parent_id'), array('id' => $id), '', 1);
    if (!array_key_exists(0, $rows)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'catalogue_category'));
    }
    $myrow = $rows[0];

    // If we aren't deleting the entire catalogue, make sure we don't delete the root category
    if ((!$deleting_all) && ($myrow['c_is_tree'] == 1)) {
        $root_category = $GLOBALS['SITE_DB']->query_select_value('catalogue_categories', 'MIN(id)', array('c_name' => $myrow['c_name'], 'cc_parent_id' => null));
        if ($id == $root_category) {
            warn_exit(do_lang_tempcode('CATALOGUE_NO_DELETE_ROOT'));
        }
    }

    $GLOBALS['SITE_DB']->query_delete('catalogue_cat_treecache', array('cc_id' => $id));
    $GLOBALS['SITE_DB']->query_delete('catalogue_childcountcache', array('cc_id' => $id));

    require_code('files2');
    delete_upload('uploads/repimages', 'catalogue_categories', 'rep_image', 'id', $id);

    if (!$deleting_all) { // If not deleting the whole catalogue
        if (php_function_allowed('set_time_limit')) {
            @set_time_limit(0);
        }

        if ($myrow['c_is_tree'] == 1) {
            // If we're in a tree...

            $GLOBALS['SITE_DB']->query_update('catalogue_categories', array('cc_parent_id' => $myrow['cc_parent_id']), array('cc_parent_id' => $id));
            $GLOBALS['SITE_DB']->query_update('catalogue_entries', array('cc_id' => $myrow['cc_parent_id']), array('cc_id' => $id));
        } else { // If we're not in a tree catalogue we can't move them, we have to delete
            // If we're not in a tree...

            if (php_function_allowed('set_time_limit')) {
                @set_time_limit(0);
            }

            $GLOBALS['SITE_DB']->query_delete('catalogue_categories', array('cc_parent_id' => $id)); // Does nothing, in theory, as it's not a tree!
            $start = 0;
            do {
                $entries = $GLOBALS['SITE_DB']->query_select('catalogue_entries', array('id'), array('cc_id' => $id), '', 500, $start);
                foreach ($entries as $entry) {
                    actual_delete_catalogue_entry($entry['id']);
                }
                $start += 500;
            } while (count($entries) == 500);
        }

        $GLOBALS['SITE_DB']->query_update('catalogue_categories', array('cc_move_target' => null), array('cc_move_target' => $id));
    }

    update_catalogue_content_ref('catalogue_category', strval($id), '');

    require_code('seo2');
    seo_meta_erase_storage('catalogue_category', strval($id));

    $_title = get_translated_text($myrow['cc_title']);

    // Delete lang
    delete_lang($myrow['cc_title']);
    delete_lang($myrow['cc_description']);

    $old_parent_id = $myrow['cc_parent_id'];

    $GLOBALS['SITE_DB']->query_delete('catalogue_categories', array('id' => $id), '', 1);

    $GLOBALS['SITE_DB']->query_delete('group_category_access', array('module_the_name' => 'catalogues_category', 'category_name' => strval($id)));
    $GLOBALS['SITE_DB']->query_delete('group_privileges', array('module_the_name' => 'catalogues_category', 'category_name' => strval($id)));

    $GLOBALS['SITE_DB']->query_update('url_id_monikers', array('m_deprecated' => 1), array('m_resource_page' => 'catalogues', 'm_resource_type' => 'category', 'm_resource_id' => strval($id)));

    calculate_category_child_count_cache($old_parent_id);

    require_code('uploads2');
    clean_empty_upload_directories('uploads/repimages');

    log_it('DELETE_CATALOGUE_CATEGORY', strval($id), $_title);

    decache('main_cc_embed');

    if ((addon_installed('commandr')) && (!running_script('install')) && (!get_mass_import_mode())) {
        require_code('resource_fs');
        expunge_resource_fs_moniker('catalogue_category', strval($id));
    }

    require_code('sitemap_xml');
    notify_sitemap_node_delete('_SEARCH:catalogues:category:' . strval($id));
}

/**
 * Adds an entry to the specified catalogue.
 *
 * @param  AUTO_LINK $category_id The ID of the category that the entry is in
 * @param  BINARY $validated Whether the entry has been validated
 * @param  LONG_TEXT $notes Hidden notes pertaining to the entry
 * @param  BINARY $allow_rating Whether the entry may be rated
 * @param  SHORT_INTEGER $allow_comments Whether comments are allowed (0=no, 1=yes, 2=review style)
 * @param  BINARY $allow_trackbacks Whether the entry may be trackbacked
 * @param  array $map A map of field IDs, to values, that defines the entries settings
 * @param  ?TIME $time The time the entry was added (null: now)
 * @param  ?MEMBER $submitter The entries submitter (null: current user)
 * @param  ?TIME $edit_date The edit time (null: never)
 * @param  integer $views The number of views
 * @param  ?AUTO_LINK $id Force an ID (null: don't force an ID)
 * @param  ?SHORT_TEXT $meta_keywords Meta keywords for this resource (null: do not edit) (blank: implicit)
 * @param  ?LONG_TEXT $meta_description Meta description for this resource (null: do not edit) (blank: implicit)
 * @return AUTO_LINK The ID of the newly added entry
 */
function actual_add_catalogue_entry($category_id, $validated, $notes, $allow_rating, $allow_comments, $allow_trackbacks, $map, $time = null, $submitter = null, $edit_date = null, $views = 0, $id = null, $meta_keywords = '', $meta_description = '')
{
    if (is_null($time)) {
        $time = time();
    }
    if (is_null($submitter)) {
        $submitter = get_member();
    }

    $catalogue_name = $GLOBALS['SITE_DB']->query_select_value('catalogue_categories', 'c_name', array('id' => $category_id));
    $catalogue_title = get_translated_text($GLOBALS['SITE_DB']->query_select_value('catalogues', 'c_title', array('c_name' => $catalogue_name)));
    $_fields = list_to_map('id', $GLOBALS['SITE_DB']->query_select('catalogue_fields', array('id', 'cf_type'), array('c_name' => $catalogue_name)));
    $fields = collapse_2d_complexity('id', 'cf_type', $_fields);

    require_code('comcode_check');
    require_code('fields');

    @ignore_user_abort(true);

    if (!addon_installed('unvalidated')) {
        $validated = 1;
    }
    $imap = array('c_name' => $catalogue_name, 'ce_edit_date' => $edit_date, 'cc_id' => $category_id, 'ce_last_moved' => time(), 'ce_submitter' => $submitter, 'ce_add_date' => $time, 'ce_views' => $views, 'ce_views_prior' => $views, 'ce_validated' => $validated, 'notes' => $notes, 'allow_rating' => $allow_rating, 'allow_comments' => $allow_comments, 'allow_trackbacks' => $allow_trackbacks);
    if (!is_null($id)) {
        $imap['id'] = $id;
    }
    $val = mixed();
    foreach ($map as $field_id => $val) {
        $type = $fields[$field_id];

        $ob = get_fields_hook($type);
        list($raw_type) = $ob->get_field_value_row_bits($_fields[$field_id]);

        if (strpos($raw_type, '_trans') !== false) {
            check_comcode($val);
        }
    }

    $title = null;
    foreach ($map as $field_id => $val) {
        if ($val == STRING_MAGIC_NULL) {
            $val = '';
        }

        if (is_null($title)) {
            $title = $val;
            require_code('global4');
            prevent_double_submit('ADD_CATALOGUE_ENTRY', null, $title);
        }
    }

    static $done_one_posting_field = false;

    $id = $GLOBALS['SITE_DB']->query_insert('catalogue_entries', $imap, true);
    foreach ($map as $field_id => $val) {
        if ($val == STRING_MAGIC_NULL) {
            $val = '';
        }

        $type = $fields[$field_id];

        $ob = get_fields_hook($type);
        list($raw_type, , $sup_table_name) = $ob->get_field_value_row_bits($_fields[$field_id]);

        $smap = array(
            'cf_id' => $field_id,
            'ce_id' => $id,
        );

        if (strpos($raw_type, '_trans') !== false) {
            if (($type == 'posting_field') && (!$done_one_posting_field)) {
                $done_one_posting_field = true;
                require_code('attachments2');
                $smap += insert_lang_comcode_attachments('cv_value', 3, $val, 'catalogue_entry', strval($id));
            } else {
                $smap += insert_lang_comcode('cv_value', $val, 3);
            }
        } else {
            if ($sup_table_name == 'short') {
                $val = cms_mb_substr($val, 0, 255);
            }

            if ($sup_table_name == 'float') {
                $smap['cv_value'] = ((is_null($val)) || ($val == '')) ? null : floatval($val);
            } elseif ($sup_table_name == 'integer') {
                $smap['cv_value'] = ((is_null($val)) || ($val == '')) ? null : intval($val);
            } else {
                $smap['cv_value'] = $val;
            }
        }
        $GLOBALS['SITE_DB']->query_insert('catalogue_efv_' . $sup_table_name, $smap);
    }

    require_code('seo2');
    if (($meta_keywords == '') && ($meta_description == '')) {
        $seo_source_map = $map;
        $seo_source_map__specific = get_value('catalogue_seo_source_map__' . $catalogue_name);
        if (!is_null($seo_source_map__specific)) {
            $seo_source_map = array();
            foreach (explode(',', $seo_source_map__specific) as $_map_source) {
                if (substr($seo_source_map__specific, -1) == '!') {
                    $must_use = true;
                    $seo_source_map__specific = substr($seo_source_map__specific, 0, strlen($seo_source_map__specific) - 1);
                } else {
                    $must_use = false;
                }
                if (isset($map[intval($seo_source_map__specific)])) {
                    $seo_source_map[] = array($map[intval($seo_source_map__specific)], $must_use);
                }
            }
        }
        foreach ($fields as $field_id => $cf_type) {
            if (($cf_type == 'long_trans') || ($cf_type == 'long_text') || ($cf_type == 'posting_field')) {
                $meta_description = isset($map[$field_id]) ? $map[$field_id] : '';
                break;
            }
        }

        seo_meta_set_for_implicit('catalogue_entry', strval($id), $seo_source_map, $meta_description);
    } else {
        seo_meta_set_for_explicit('catalogue_entry', strval($id), $meta_keywords, $meta_description);
    }

    calculate_category_child_count_cache($category_id);

    if ($catalogue_name[0] != '_') {
        if ($validated == 1) {
            if (addon_installed('content_privacy')) {
                require_code('content_privacy');
                $privacy_limits = privacy_limits_for('catalogue_entry', strval($id));
            } else {
                $privacy_limits = null;
            }

            require_lang('catalogues');
            require_code('notifications');
            $subject = do_lang('CATALOGUE_ENTRY_NOTIFICATION_MAIL_SUBJECT', get_site_name(), strip_comcode($title), array($catalogue_title));
            $self_url = build_url(array('page' => 'catalogues', 'type' => 'entry', 'id' => $id), get_module_zone('catalogues'), null, false, false, true);
            $mail = do_notification_lang('CATALOGUE_ENTRY_NOTIFICATION_MAIL', comcode_escape(get_site_name()), comcode_escape(strip_comcode($title)), array(comcode_escape($self_url->evaluate()), comcode_escape($catalogue_title)));
            dispatch_notification('catalogue_entry__' . $catalogue_name, strval($category_id), $subject, $mail, $privacy_limits);
        }

        log_it('ADD_CATALOGUE_ENTRY', strval($id), $title);

        if ((addon_installed('commandr')) && (!running_script('install')) && (!get_mass_import_mode())) {
            require_code('resource_fs');
            generate_resource_fs_moniker('catalogue_entry', strval($id), null, null, true);
        }
    }

    reorganise_uploads__catalogue_entries(array('ce_id' => $id));

    decache('main_cc_embed');

    require_code('member_mentions');
    dispatch_member_mention_notifications('catalogue_entry', strval($id), $submitter);

    require_code('sitemap_xml');
    notify_sitemap_node_add(
        '_SEARCH:catalogues:entry:' . strval($id),
        $time,
        $edit_date,
        SITEMAP_IMPORTANCE_MEDIUM,
        'monthly',
        has_category_access($GLOBALS['FORUM_DRIVER']->get_guest_id(), 'catalogues_catalogue', $catalogue_name) && has_category_access($GLOBALS['FORUM_DRIVER']->get_guest_id(), 'catalogues_category', strval($category_id))
    );

    @ignore_user_abort(false);

    return $id;
}

/**
 * Edit the specified catalogue entry
 *
 * @param  AUTO_LINK $id The ID of the entry being edited
 * @param  AUTO_LINK $category_id The ID of the category that the entry is in
 * @param  BINARY $validated Whether the entry has been validated
 * @param  LONG_TEXT $notes Hidden notes pertaining to the entry
 * @param  BINARY $allow_rating Whether the entry may be rated
 * @param  SHORT_INTEGER $allow_comments Whether comments are allowed (0=no, 1=yes, 2=review style)
 * @param  BINARY $allow_trackbacks Whether the entry may be trackbacked
 * @param  array $map A map of field IDs, to values, that defines the entries settings
 * @param  ?SHORT_TEXT $meta_keywords Meta keywords for this resource (null: do not edit)
 * @param  ?LONG_TEXT $meta_description Meta description for this resource (null: do not edit)
 * @param  ?TIME $edit_time Edit time (null: either means current time, or if $null_is_literal, means reset to to null)
 * @param  ?TIME $add_time Add time (null: do not change)
 * @param  ?integer $views Number of views (null: do not change)
 * @param  ?MEMBER $submitter Submitter (null: do not change)
 * @param  boolean $null_is_literal Determines whether some nulls passed mean 'use a default' or literally mean 'set to null
 */
function actual_edit_catalogue_entry($id, $category_id, $validated, $notes, $allow_rating, $allow_comments, $allow_trackbacks, $map, $meta_keywords = '', $meta_description = '', $edit_time = null, $add_time = null, $views = null, $submitter = null, $null_is_literal = false)
{
    if (is_null($edit_time)) {
        $edit_time = $null_is_literal ? null : time();
    }

    $catalogue_name = $GLOBALS['SITE_DB']->query_select_value('catalogue_categories', 'c_name', array('id' => $category_id));
    $catalogue_title = get_translated_text($GLOBALS['SITE_DB']->query_select_value('catalogues', 'c_title', array('c_name' => $catalogue_name)));
    $_fields = list_to_map('id', $GLOBALS['SITE_DB']->query_select('catalogue_fields', array('id', 'cf_type'), array('c_name' => $catalogue_name)));
    $fields = collapse_2d_complexity('id', 'cf_type', $_fields);

    $original_submitter = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogue_entries', 'ce_submitter', array('id' => $id));
    if (is_null($original_submitter)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'catalogue_entry'));
    }

    $old_category_id = $GLOBALS['SITE_DB']->query_select_value('catalogue_entries', 'cc_id', array('id' => $id));

    if (!addon_installed('unvalidated')) {
        $validated = 1;
    }

    require_code('submit');
    $was_validated = content_validated('catalogue_entry', strval($id));
    $just_validated = (!$was_validated) && ($validated == 1);
    if ($just_validated) {
        send_content_validated_notification('catalogue_entry', strval($id));
    }

    $update_map = array(
        'cc_id' => $category_id,
        'ce_validated' => $validated,
        'notes' => $notes,
        'allow_rating' => $allow_rating,
        'allow_comments' => $allow_comments,
        'allow_trackbacks' => $allow_trackbacks,
    );

    $update_map['ce_edit_date'] = $edit_time;
    if (!is_null($add_time)) {
        $update_map['ce_add_date'] = $add_time;
    }
    if (!is_null($views)) {
        $update_map['ce_views'] = $views;
    }
    if (!is_null($submitter)) {
        $update_map['ce_submitter'] = $submitter;
    }

    @ignore_user_abort(true);

    static $done_one_posting_field = false;

    $GLOBALS['SITE_DB']->query_update('catalogue_entries', $update_map, array('id' => $id), '', 1);

    require_code('fields');
    $title = null;
    foreach ($map as $field_id => $val) {
        if (is_null($title)) {
            $title = $val;
        }

        $type = $fields[$field_id];

        $ob = get_fields_hook($type);
        list(, , $sup_table_name) = $ob->get_field_value_row_bits($_fields[$field_id]);

        $smap = array();

        if (substr($sup_table_name, -6) == '_trans') {
            $_val = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogue_efv_' . $sup_table_name, 'cv_value', array('cf_id' => $field_id, 'ce_id' => $id));
            if (is_null($_val)) {
                if (($type == 'posting_field') && (!$done_one_posting_field)) {
                    $done_one_posting_field = true;
                    require_code('attachments2');
                    require_code('attachments3');
                    $smap += insert_lang_comcode_attachments('cv_value', 3, $val, 'catalogue_entry', strval($id), null, false, $original_submitter);
                } else {
                    $smap += insert_lang_comcode('cv_value', $val, 3);
                }
            } else {
                if (($type == 'posting_field') && (!$done_one_posting_field)) {
                    $done_one_posting_field = true;
                    require_code('attachments2');
                    require_code('attachments3');
                    $smap += update_lang_comcode_attachments('cv_value', $_val, $val, 'catalogue_entry', strval($id), null, $original_submitter);
                } else {
                    $smap += lang_remap_comcode('cv_value', $_val, $val);
                }
            }
        } else {
            if ($sup_table_name == 'short') {
                $val = cms_mb_substr($val, 0, 255);
            }

            if ($sup_table_name == 'float') {
                $smap['cv_value'] = ((is_null($val)) || ($val == '')) ? null : floatval($val);
            } elseif ($sup_table_name == 'integer') {
                $smap['cv_value'] = ((is_null($val)) || ($val == '')) ? null : intval($val);
            } else {
                $smap['cv_value'] = $val;
            }
        }

        $cnt_field_rows_already = $GLOBALS['SITE_DB']->query_select_value('catalogue_efv_' . $sup_table_name, 'COUNT(*)', array('cf_id' => $field_id, 'ce_id' => $id));
        if ($cnt_field_rows_already == 0) {
            $GLOBALS['SITE_DB']->query_insert('catalogue_efv_' . $sup_table_name, $smap + array('cf_id' => $field_id, 'ce_id' => $id)); // Corruption, doing a repair
        } else {
            $GLOBALS['SITE_DB']->query_update('catalogue_efv_' . $sup_table_name, $smap, array('cf_id' => $field_id, 'ce_id' => $id), '', 1);
        }
    }

    require_code('urls2');
    suggest_new_idmoniker_for('catalogues', 'entry', strval($id), '', strip_comcode($title));

    require_code('seo2');
    seo_meta_set_for_explicit('catalogue_entry', strval($id), $meta_keywords, $meta_description);

    $self_url = build_url(array('page' => 'catalogues', 'type' => 'entry', 'id' => $id), get_module_zone('catalogues'), null, false, false, true);

    if (($category_id != $old_category_id) || ($was_validated != ($validated == 1))) {
        calculate_category_child_count_cache($category_id);
        if ($category_id != $old_category_id) {
            calculate_category_child_count_cache($old_category_id);
        }
    }

    decache('main_cc_embed');

    if ($catalogue_name[0] != '_') {
        log_it('EDIT_CATALOGUE_ENTRY', strval($id), $title);

        if ((addon_installed('commandr')) && (!running_script('install')) && (!get_mass_import_mode())) {
            require_code('resource_fs');
            generate_resource_fs_moniker('catalogue_entry', strval($id));
        }

        if ($just_validated) {
            if (addon_installed('content_privacy')) {
                require_code('content_privacy');
                $privacy_limits = privacy_limits_for('catalogue_entry', strval($id));
            } else {
                $privacy_limits = null;
            }

            require_lang('catalogues');
            require_code('notifications');
            $subject = do_lang('CATALOGUE_ENTRY_NOTIFICATION_MAIL_SUBJECT', get_site_name(), strip_comcode($title), array($catalogue_title));
            $mail = do_notification_lang('CATALOGUE_ENTRY_NOTIFICATION_MAIL', comcode_escape(get_site_name()), comcode_escape(strip_comcode($title)), array(comcode_escape($self_url->evaluate()), comcode_escape($catalogue_title)));
            dispatch_notification('catalogue_entry__' . $catalogue_name, strval($category_id), $subject, $mail, $privacy_limits);
        }
    }

    reorganise_uploads__catalogue_entries(array('ce_id' => $id));

    require_code('feedback');
    update_spacer_post(
        $allow_comments != 0,
        'catalogues',
        strval($id),
        $self_url,
        $title,
        process_overridden_comment_forum('catalogues__' . $catalogue_name, strval($id), strval($category_id), strval($old_category_id))
    );

    require_code('sitemap_xml');
    notify_sitemap_node_edit(
        '_SEARCH:catalogues:entry:' . strval($id),
        has_category_access($GLOBALS['FORUM_DRIVER']->get_guest_id(), 'catalogues_catalogue', $catalogue_name) && has_category_access($GLOBALS['FORUM_DRIVER']->get_guest_id(), 'catalogues_category', strval($category_id))
    );

    @ignore_user_abort(false);
}

/**
 * Delete a catalogue entry.
 *
 * @param  AUTO_LINK $id The ID of the entry to delete
 */
function actual_delete_catalogue_entry($id)
{
    $old_category_id = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogue_entries', 'cc_id', array('id' => $id));
    if ($old_category_id === null) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'catalogue_entry'));
    }

    $catalogue_name = $GLOBALS['SITE_DB']->query_select_value('catalogue_entries', 'c_name', array('id' => $id));

    @ignore_user_abort(true);

    require_code('fields');
    require_code('catalogues');
    $fields = $GLOBALS['SITE_DB']->query_select('catalogue_fields', array('*'), array('c_name' => $catalogue_name));
    $title = null;
    foreach ($fields as $field) {
        $object = get_fields_hook($field['cf_type']);
        list(, , $storage_type) = $object->get_field_value_row_bits($field);
        $value = _get_catalogue_entry_field($field['id'], $id, $storage_type);
        if (method_exists($object, 'cleanup')) {
            $object->cleanup($value);
        }
        if (is_null($title)) {
            $target = array();
            _resolve_catalogue_entry_field($field, $id, null, $target);
            $title = $target['effective_value_pure'];
        }
    }

    $lang1 = $GLOBALS['SITE_DB']->query_select('catalogue_efv_long_trans', array('cv_value'), array('ce_id' => $id));
    $lang2 = $GLOBALS['SITE_DB']->query_select('catalogue_efv_short_trans', array('cv_value'), array('ce_id' => $id));
    $lang = array_merge($lang1, $lang2);
    foreach ($lang as $lang_to_delete) {
        if (true) { // Always do this just in case it is for attachments
            require_code('attachments2');
            require_code('attachments3');
            delete_lang_comcode_attachments($lang_to_delete['cv_value'], 'catalogue_entry', strval($id));
        } else {
            delete_lang($lang_to_delete['cv_value']);
        }
    }

    $GLOBALS['SITE_DB']->query_delete('catalogue_efv_long_trans', array('ce_id' => $id));
    $GLOBALS['SITE_DB']->query_delete('catalogue_efv_short_trans', array('ce_id' => $id));
    $GLOBALS['SITE_DB']->query_delete('catalogue_efv_long', array('ce_id' => $id));
    $GLOBALS['SITE_DB']->query_delete('catalogue_efv_short', array('ce_id' => $id));
    $GLOBALS['SITE_DB']->query_delete('catalogue_efv_float', array('ce_id' => $id));
    $GLOBALS['SITE_DB']->query_delete('catalogue_efv_integer', array('ce_id' => $id));

    $GLOBALS['SITE_DB']->query_delete('catalogue_entries', array('id' => $id), '', 1);
    $GLOBALS['SITE_DB']->query_delete('trackbacks', array('trackback_for_type' => 'catalogues', 'trackback_for_id' => strval($id)));
    $GLOBALS['SITE_DB']->query_delete('rating', array('rating_for_type' => 'catalogues', 'rating_for_id' => strval($id)));
    require_code('notifications');
    delete_all_notifications_on('comment_posted', 'catalogues_' . strval($id));

    update_catalogue_content_ref('ck_' . $catalogue_name, strval($id), '');
    update_catalogue_content_ref('catalogue_entry', strval($id), '');

    require_code('seo2');
    seo_meta_erase_storage('catalogue_entry', strval($id));

    calculate_category_child_count_cache($old_category_id);

    $GLOBALS['SITE_DB']->query_update('url_id_monikers', array('m_deprecated' => 1), array('m_resource_page' => 'catalogues', 'm_resource_type' => 'entry', 'm_resource_id' => strval($id)));

    decache('main_cc_embed');

    require_code('uploads2');
    clean_empty_upload_directories('uploads/catalogues');

    if ($catalogue_name[0] != '_') {
        log_it('DELETE_CATALOGUE_ENTRY', strval($id), $title);
    }

    if ((addon_installed('commandr')) && (!running_script('install')) && (!get_mass_import_mode())) {
        require_code('resource_fs');
        expunge_resource_fs_moniker('catalogue_entry', strval($id));
    }

    require_code('sitemap_xml');
    notify_sitemap_node_delete('_SEARCH:catalogues:entry:' . strval($id));

    @ignore_user_abort(false);
}

/**
 * Reorganise the catalogue uploads.
 *
 * @param  ?array $where Limit reorganisation to rows matching this WHERE map (null: none)
 * @param  boolean $tolerate_errors Whether to tolerate missing files (false = give an error)
 */
function reorganise_uploads__catalogue_categories($where = null, $tolerate_errors = false) // TODO: Change to array() in v11
{
    require_code('uploads2');
    reorganise_uploads('catalogue_category', 'uploads/repimages', 'rep_image', $where, null, true, $tolerate_errors);
}

/**
 * Reorganise the catalogue uploads.
 *
 * @param  ?array $where Limit reorganisation to rows matching this WHERE map (null: none)
 * @param  boolean $tolerate_errors Whether to tolerate missing files (false = give an error)
 */
function reorganise_uploads__catalogue_entries($where = null, $tolerate_errors = false) // TODO: Change to array() in v11
{
    if ($where === null) {
        $where = array(); // TODO: Remove in v11
    }

    require_code('uploads2');
    $fake_cma_info = array(
        'table' => 'catalogue_efv_short d',
        'table_extended' => 'catalogue_efv_short d JOIN ' . get_table_prefix() . 'catalogue_entries e ON e.id=d.ce_id JOIN ' . get_table_prefix() . 'catalogue_fields f ON f.id=d.cf_id',
        'connection' => $GLOBALS['SITE_DB'], // TODO: Change in v11
        'id_field' => 'd.id',
        'parent_category_field' => 'cc_id',
        'title_field' => null,
        'title_field_dereference' => null,
        'parent_spec__table_name' => 'catalogue_categories',
        'parent_spec__field_name' => 'id',
        'parent_spec__parent_name' => 'cc_parent_id',
    );
    reorganise_uploads('catalogue_entry', 'uploads/catalogues', 'cv_value', $where + array('cf_type' => 'upload'), $fake_cma_info, false, $tolerate_errors);
    reorganise_uploads('catalogue_entry', 'uploads/catalogues', 'cv_value', $where + array('cf_type' => 'picture'), $fake_cma_info, false, $tolerate_errors);
    reorganise_uploads('catalogue_entry', 'uploads/catalogues', 'cv_value', $where + array('cf_type' => 'video'), $fake_cma_info, false, $tolerate_errors);
    $fake_cma_info['table'] = 'catalogue_efv_long';
    reorganise_uploads('catalogue_entry', 'uploads/catalogues', 'cv_value', $where + array('cf_type' => 'upload_multi'), $fake_cma_info, false, $tolerate_errors);
    reorganise_uploads('catalogue_entry', 'uploads/catalogues', 'cv_value', $where + array('cf_type' => 'picture_multi'), $fake_cma_info, false, $tolerate_errors);
    reorganise_uploads('catalogue_entry', 'uploads/catalogues', 'cv_value', $where + array('cf_type' => 'video_multi'), $fake_cma_info, false, $tolerate_errors);
}
