<?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__catalogues()
{
    global $SEARCH_CATALOGUE_ENTRIES_CATALOGUES_CACHE;
    $SEARCH_CATALOGUE_ENTRIES_CATALOGUES_CACHE = array();

    global $PT_PAIR_CACHE;
    $PT_PAIR_CACHE = array();

    global $CAT_FIELDS_CACHE;
    $CAT_FIELDS_CACHE = array();

    // We do not actually necessarily use these constants in the code (they're based on an extensive of an old BINARY field): but they're here for reference so as to understand the codes
    if (!defined('C_DT_FIELDMAPS')) {
        define('C_DT_FIELDMAPS', 0);
        define('C_DT_TITLELIST', 1);
        define('C_DT_TABULAR', 2);
        define('C_DT_GRID', 3);
    }
}

/**
 * Get a catalogue row.
 *
 * @param  ID_TEXT $catalogue_name The catalogue name
 * @param  boolean $fail_ok Whether to return null if we can't find it (as opposed to a fatal error)
 * @return ?array Catalogue row (null: could not find it, and $fail_ok was set to true)
 */
function load_catalogue_row($catalogue_name, $fail_ok = false)
{
    static $catalogues_cache = array();
    if (!isset($catalogues_cache[$catalogue_name])) {
        $catalogue_rows = $GLOBALS['SITE_DB']->query_select('catalogues', array('*'), array('c_name' => $catalogue_name), '', 1);
        if (!array_key_exists(0, $catalogue_rows)) {
            if ($fail_ok) {
                return null;
            }
            warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'catalogue'));
        }
        $catalogues_cache[$catalogue_name] = $catalogue_rows[0];
    }

    return $catalogues_cache[$catalogue_name];
}

/**
 * Render a catalogue box.
 *
 * @param  array $row Catalogue row
 * @param  ID_TEXT $zone Zone to link through to
 * @param  boolean $give_context Whether to include context (i.e. say WHAT this is, not just show the actual content)
 * @param  boolean $include_breadcrumbs Whether to include breadcrumbs (if there are any)
 * @param  ?AUTO_LINK $root Virtual root to use (null: none)
 * @param  ID_TEXT $guid Overridden GUID to send to templates (blank: none)
 * @return Tempcode The catalogue box
 */
function render_catalogue_entry_box($row, $zone = '_SEARCH', $give_context = true, $include_breadcrumbs = true, $root = null, $guid = '')
{
    if (is_null($row)) { // Should never happen, but we need to be defensive
        return new Tempcode();
    }

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

    global $SEARCH_CATALOGUE_ENTRIES_CATALOGUES_CACHE;

    $catalogue_name = $row['c_name'];
    if (array_key_exists($catalogue_name, $SEARCH_CATALOGUE_ENTRIES_CATALOGUES_CACHE)) {
        $catalogue = $SEARCH_CATALOGUE_ENTRIES_CATALOGUES_CACHE[$catalogue_name];
    } else {
        $catalogue = load_catalogue_row($catalogue_name);
    }

    $tpl_set = $catalogue_name;
    $display = get_catalogue_entry_map($row, $catalogue, 'SEARCH', $tpl_set, $root, null, null, false, true);

    $breadcrumbs = mixed();
    if ($include_breadcrumbs) {
        $_breadcrumbs = catalogue_category_breadcrumbs($row['cc_id'], ($root === null) ? get_param_integer('keep_catalogue_' . $catalogue['c_name'] . '_root', null) : $root, false);
        $breadcrumbs = breadcrumb_segments_to_tempcode($_breadcrumbs);
    }

    $tpl_set = $catalogue_name;
    return do_template('CATALOGUE_' . $tpl_set . '_FIELDMAP_ENTRY_WRAP', array('_GUID' => ($guid != '') ? $guid : 'dfg3rergt5g433f', 'GIVE_CONTEXT' => $give_context, 'BREADCRUMBS' => $breadcrumbs) + $display, null, false, 'CATALOGUE_DEFAULT_FIELDMAP_ENTRY_WRAP');
}

/**
 * Get Tempcode for a catalogue category 'feature box' for the given row
 *
 * @param  array $row The database field row of it
 * @param  ID_TEXT $zone The zone to use
 * @param  boolean $give_context Whether to include context (i.e. say WHAT this is, not just show the actual content)
 * @param  boolean $include_breadcrumbs Whether to include breadcrumbs (if there are any)
 * @param  ?AUTO_LINK $root Virtual root to use (null: none)
 * @param  boolean $attach_to_url_filter Whether to copy through any filter parameters in the URL, under the basis that they are associated with what this box is browsing
 * @param  ID_TEXT $guid Overridden GUID to send to templates (blank: none)
 * @return Tempcode A box for it, linking to the full page
 */
function render_catalogue_category_box($row, $zone = '_SEARCH', $give_context = true, $include_breadcrumbs = true, $root = null, $attach_to_url_filter = false, $guid = '')
{
    if (is_null($row)) { // Should never happen, but we need to be defensive
        return new Tempcode();
    }

    require_lang('catalogues');

    $just_category_row = db_map_restrict($row, array('id', 'cc_description'));

    // URL
    $map = array('page' => 'catalogues', 'type' => 'category', 'id' => $row['id']);
    if ($root !== null) {
        $map['keep_catalogue_' . $row['c_name'] . '_root'] = $root;
    }
    if ($attach_to_url_filter) {
        $map += propagate_filtercode();
    }
    $url = build_url($map, $zone);

    // Title
    $_title = get_translated_text($row['cc_title']);
    $title = $_title;
    if ($give_context) {
        $catalogue_title = get_translated_text($GLOBALS['SITE_DB']->query_select_value('catalogues', 'c_title', array('c_name' => $row['c_name'])));
        $title = do_lang('CONTENT_IS_OF_TYPE', do_lang('CATALOGUE_GENERIC_CATEGORY', $catalogue_title), $_title);
    }

    // Description
    $content = get_translated_tempcode('catalogue_categories', $just_category_row, 'cc_description');

    // Breadcrumbs
    $breadcrumbs = mixed();
    if ($include_breadcrumbs) {
        $breadcrumbs = breadcrumb_segments_to_tempcode(catalogue_category_breadcrumbs($row['id'], ($root === null) ? get_param_integer('keep_catalogue_' . $row['c_name'] . '_root', null) : $root, $attach_to_url_filter));
    }

    // Image
    $rep_image = mixed();
    $_rep_image = mixed();
    if ($row['rep_image'] != '') {
        $_rep_image = $row['rep_image'];
        if (url_is_local($_rep_image)) {
            $_rep_image = get_custom_base_url() . '/' . $_rep_image;
        }
        require_code('images');
        $rep_image = do_image_thumb($row['rep_image'], $_title, false);
    }

    // Metadata
    $child_counts = count_catalogue_category_children($row['id']);
    $num_children = $child_counts['num_children_children'];
    $num_entries = $child_counts['num_entries_children'];
    $entry_details = do_lang_tempcode('CATEGORY_SUBORDINATE', escape_html(integer_format($num_entries)), escape_html(integer_format($num_children)));

    // Render
    return do_template('SIMPLE_PREVIEW_BOX', array(
        '_GUID' => ($guid != '') ? $guid : ('e3fbbe807f75c0aa24626e06082ae731_' . $row['c_name']),
        'ID' => strval($row['id']),
        'TITLE' => $title,
        'TITLE_PLAIN' => $_title,
        '_REP_IMAGE' => $_rep_image,
        'REP_IMAGE' => $rep_image,
        'BREADCRUMBS' => $breadcrumbs,
        'SUMMARY' => $content,
        'ENTRY_DETAILS' => $entry_details,
        'URL' => $url,
        'FRACTIONAL_EDIT_FIELD_NAME' => $give_context ? null : 'title',
        'FRACTIONAL_EDIT_FIELD_URL' => $give_context ? null : '_SEARCH:cms_catalogues:__edit_catalogue:' . $row['c_name'],
        'RESOURCE_TYPE' => 'catalogue_category',
    ));
}

/**
 * Render a catalogue box.
 *
 * @param  array $row Catalogue row
 * @param  ID_TEXT $zone Zone to link through to
 * @param  boolean $give_context Whether to include context (i.e. say WHAT this is, not just show the actual content)
 * @param  ID_TEXT $guid Overridden GUID to send to templates (blank: none)
 * @return Tempcode The catalogue box
 */
function render_catalogue_box($row, $zone = '_SEARCH', $give_context = true, $guid = '')
{
    if (is_null($row)) { // Should never happen, but we need to be defensive
        return new Tempcode();
    }

    require_lang('catalogues');

    $just_catalogue_row = db_map_restrict($row, array('c_name', 'c_description'));

    if ($row['c_is_tree'] == 1) {
        $url = build_url(array('page' => 'catalogues', 'type' => 'category', 'catalogue_name' => $row['c_name']), $zone);
    } else {
        $url = build_url(array('page' => 'catalogues', 'type' => 'index', 'id' => $row['c_name'], 'tree' => $row['c_is_tree']), $zone);
    }

    $_title = get_translated_text($row['c_title']);
    $title = $give_context ? do_lang('CONTENT_IS_OF_TYPE', do_lang('CATALOGUE'), $_title) : $_title;

    $summary = get_translated_tempcode('catalogues', $just_catalogue_row, 'c_description');

    $num_children = $GLOBALS['SITE_DB']->query_select_value('catalogue_categories', 'COUNT(*)', array('c_name' => $row['c_name']));
    $num_entries = $GLOBALS['SITE_DB']->query_select_value('catalogue_entries', 'COUNT(*)', array('c_name' => $row['c_name']));
    $entry_details = do_lang_tempcode(($row['c_is_tree'] == 1) ? 'CATEGORY_SUBORDINATE' : 'CATEGORY_SUBORDINATE_2', escape_html(integer_format($num_entries)), escape_html(integer_format($num_children)));

    return do_template('SIMPLE_PREVIEW_BOX', array(
        '_GUID' => ($guid != '') ? $guid : ('8d7eaf6bb3170a92fd6a4876462e6f2e_' . $row['c_name']),
        'ID' => $row['c_name'],
        'TITLE' => $title,
        'TITLE_PLAIN' => $_title,
        'SUMMARY' => $summary,
        'ENTRY_DETAILS' => $entry_details,
        'URL' => $url,
        'FRACTIONAL_EDIT_FIELD_NAME' => $give_context ? null : 'title',
        'FRACTIONAL_EDIT_FIELD_URL' => $give_context ? null : '_SEARCH:cms_catalogues:__edit_catalogue:' . $row['c_name'],
        'RESOURCE_TYPE' => 'catalogue',
    ));
}

/**
 * Count the entries and subcategories underneath the specified category, recursively.
 *
 * @param  AUTO_LINK $category_id The ID of the category for which count details are collected
 * @return array The number of entries is returned in $output['num_entries'], and the number of subcategories is returned in $output['num_children'], the (possibly recursive) number of subcategories in $output['num_children_children'], and the (possibly recursive) number of entries is returned in $output['num_entries_children'].
 */
function count_catalogue_category_children($category_id)
{
    static $total_categories = null;
    if ($total_categories === null) {
        $total_categories = $GLOBALS['SITE_DB']->query_select_value('catalogue_categories', 'COUNT(*)');
    }

    $out = array();

    $out['num_children'] = $GLOBALS['SITE_DB']->query_select_value('catalogue_categories', 'COUNT(*)', array('cc_parent_id' => $category_id));
    $out['num_entries'] = $GLOBALS['SITE_DB']->query_select_value('catalogue_entries', 'COUNT(*)', array('cc_id' => $category_id, 'ce_validated' => 1));

    $rec_record = $GLOBALS['SITE_DB']->query_select('catalogue_childcountcache', array('c_num_rec_children', 'c_num_rec_entries'), array('cc_id' => $category_id), '', 1);
    if (!array_key_exists(0, $rec_record)) {
        $rec_record[0] = array('c_num_rec_children' => $out['num_children'], 'c_num_rec_entries' => $out['num_entries']);
    }

    $out['num_children_children'] = $rec_record[0]['c_num_rec_children'];
    $out['num_entries_children'] = $rec_record[0]['c_num_rec_entries'];

    return $out;
}

/**
 * Get an ordered array of all the entries in the specified catalogue.
 *
 * @param  ?AUTO_LINK $category_id The ID of the category for which the entries are being collected (null: no limitation)
 * @param  ID_TEXT $catalogue_name The name of the catalogue
 * @param  ?array $catalogue A database row of the catalogue we are working with (null: read it in)
 * @param  ID_TEXT $view_type The view type we're doing
 * @set    PAGE SEARCH CATEGORY
 * @param  ID_TEXT $tpl_set The template set we are rendering this category using
 * @param  ?integer $max The maximum number of entries to show on a single page of this this category (null: all)
 * @param  ?integer $start The entry number to start at (null: all)
 * @param  ?mixed $select The entries to show, may be from other categories. Can either be SQL fragment (produced from Selectcode?), or array (null: use $start and $max)
 * @param  ?AUTO_LINK $root The virtual root for display of this category (null: default)
 * @param  ?SHORT_INTEGER $display_type The display type to use (null: lookup from $catalogue)
 * @param  boolean $do_sorting Whether to perform sorting
 * @param  ?array $entries A list of entry rows (null: select them normally)
 * @param  string $filter Filtercode to apply (blank: none).
 * @param  ?ID_TEXT $order_by_high_level Orderer (null: read from environment)
 * @param  ID_TEXT $ordering_param Environment param used for ordering. You should pass in $order_by_high_level if it is set.
 * @param  ?MEMBER $viewing_member_id Viewing member ID (null: current user)
 * @return array An array containing our built up entries (renderable Tempcode), our sorting interface, and our entries (entry records from database, with an additional 'map' field), and the max rows, and the display type used
 */
function render_catalogue_category_entry_buildup($category_id, $catalogue_name, $catalogue, $view_type, $tpl_set, $max, $start, $select, $root, $display_type = null, $do_sorting = true, $entries = null, $filter = '', $order_by_high_level = null, $ordering_param = 'sort', $viewing_member_id = null)
{
    if ($filter != '') {
        require_code('filtercode');
        $filtercode = parse_filtercode($filter);
    } else {
        $filtercode = mixed();
    }

    // How to display
    if ($display_type === null) {
        $display_type = get_param_integer('keep_cat_display_type', $catalogue['c_display_type']);
    }

    // Find catalogue data
    $is_ecomm = is_ecommerce_catalogue($catalogue_name);
    if ($catalogue === null) {
        $catalogue = load_catalogue_row($catalogue_name);
    }
    require_code('fields');
    $fields = get_catalogue_fields($catalogue_name);

    // Find $order_by/$direction which are semantically quite different to $order_by_high_level
    $order_by = mixed();
    $direction = 'ASC';
    if ($do_sorting) {
        inform_non_canonical_parameter($ordering_param);

        if ((!empty($order_by_high_level)) && (strpos($order_by_high_level, ' ') !== false/*if false probably some bot probing URLs -- sorting always has a space between sorter and direction*/)) {
            // Find order by URL parameter
            list($order_by, $direction) = explode(' ', $order_by_high_level);
            if (($direction != 'ASC') && ($direction != 'DESC')) {
                log_hack_attack_and_exit('ORDERBY_HACK');
            }
            if (((!is_numeric($order_by)) || (!isset($fields[intval($order_by)]))) && (!in_array($order_by, array('fixed_random', 'average_rating', 'compound_rating', 'add_date', 'distance')))) {
                $order_by = null; // Invalid
            }
        }

        if ($order_by === null) {
            // Find default order for catalogue
            $order_by = '0';
            $direction = 'ASC';
            foreach ($fields as $i => $field) {
                if ($field['cf_defines_order'] != 0) {
                    $order_by = strval($i);
                    $direction = ($field['cf_defines_order'] == 1) ? 'ASC'/*1*/ : 'DESC'/*2*/;
                    $order_by_high_level = strval($i) . ' ' . $direction;
                    break;
                }
            }
        }
    }

    // Get entries in this category
    if ($select === '1=1') {
        $select = null;
    }
    if ($entries === null) {
        list($in_db_sorting, $num_entries, $entries) = get_catalogue_entries($catalogue_name, $category_id, $max, $start, $select, $do_sorting, $filtercode, $order_by, $direction);
    } else { // Oh, we already have $entries
        $num_entries = count($entries);
        $in_db_sorting = false;
    }

    disable_php_memory_limit();

    // Work out the actual rendering, but only for those results in our selection scope (for performance)
    foreach ($entries as $i => $entry) {
        if (($in_db_sorting /*Only select rows were grabbed so $i is not the first entry, it is the $start entry*/) || (!$in_db_sorting /*Needs data to do manual sort*/) || ((($start === null) || ($i >= $start) && (($max === null) || ($i < $start + $max))) && ((!is_array($select)) || ((is_array($select)) && (in_array($entry['id'], $select)))))) {
            $entries[$i]['map'] = get_catalogue_entry_map($entry, $catalogue, $view_type, $tpl_set, $root, $fields, (($display_type == C_DT_TITLELIST) && (!$is_ecomm) && ($order_by !== null)) ? array(0, intval($order_by)) : null, false, true, intval($order_by));
        }
    }

    if ($do_sorting) {
        // Render sort change dropdown
        $selectors = new Tempcode();
        foreach ($fields as $i => $field) {
            if ($field['cf_searchable'] == 1) {
                $potential_sorter_name = get_translated_text($field['cf_name']);
                foreach (array('ASC' => '_ASCENDING', 'DESC' => '_DESCENDING') as $dir_code => $dir_lang) {
                    $sort_sel = (($order_by == strval($i)) && ($direction == $dir_code));
                    $_potential_sorter_name = new Tempcode();
                    $_potential_sorter_name->attach(escape_html($potential_sorter_name));
                    $_potential_sorter_name->attach(do_lang_tempcode($dir_lang));
                    $selectors->attach(do_template('PAGINATION_SORTER', array('_GUID' => 'dfdsfdsusd0fsd0dsf', 'SELECTED' => $sort_sel, 'NAME' => protect_from_escaping($_potential_sorter_name), 'VALUE' => strval($i) . ' ' . $dir_code)));
                }
            }
        }
        $extra_sorts = array();
        $extra_sorts['add_date'] = 'ADDED';
        if (get_option('is_on_rating') == '0') {
            $has_ratings = false;
        } else {
            if (!is_null($entries)) {
                $has_ratings = false;
                foreach ($entries as $entry) {
                    if ($entry['allow_rating'] == 1) {
                        $has_ratings = true;
                    }
                }
                if ($has_ratings) {
                    $extra_sorts['average_rating'] = 'RATING';
                    $extra_sorts['compound_rating'] = 'POPULARITY';
                }
            } else {
                $has_ratings = true;
            }
        }
        $extra_sorts['fixed_random'] = 'RANDOM';
        foreach ($extra_sorts as $extra_sort_code => $extra_sort_lang) {
            foreach (array('ASC' => '_ASCENDING', 'DESC' => '_DESCENDING') as $dir_code => $dir_lang) {
                if (($extra_sort_code == 'fixed_random') && ($dir_code == 'DESC')) {
                    continue;
                }

                $sort_sel = (($order_by == $extra_sort_code) && ($direction == $dir_code));
                $_potential_sorter_name = new Tempcode();
                $_potential_sorter_name->attach(do_lang_tempcode($extra_sort_lang));
                if ($extra_sort_code != 'fixed_random') {
                    $_potential_sorter_name->attach(do_lang_tempcode($dir_lang));
                }
                $selectors->attach(do_template('PAGINATION_SORTER', array('_GUID' => 'xfdsfdsusd0fsd0dsf', 'SELECTED' => $sort_sel, 'NAME' => protect_from_escaping($_potential_sorter_name), 'VALUE' => $extra_sort_code . ' ' . $dir_code)));
            }
        }
        $sort_url = get_self_url(false, false, array($ordering_param => null), false, true);
        $sorting = do_template('PAGINATION_SORT', array('_GUID' => '9fgjfdklgjdfgkjlfdjgd90', 'SORT' => $ordering_param, 'URL' => $sort_url, 'SELECTORS' => $selectors));

        // Sort entries manually
        if (!$in_db_sorting) {
            catalogue_entries_manual_sort($fields, $entries, $order_by, $direction);
        }
    } else {
        $sorting = new Tempcode();
    }

    // Build up entries
    $entry_buildup = new Tempcode();

    // Possibly some extra stuff for shopping carts
    $extra_map = array();
    if ($is_ecomm) {
        require_lang('shopping');
        $i = 0;
        for ($i = 0; $i < $num_entries; $i++) {
            if (!array_key_exists($i, $entries)) {
                break;
            }
            if (!array_key_exists('map', $entries[$i])) {
                continue;
            }

            $entry = $entries[$i];
            $extra_map[$i]['ADD_TO_CART'] = build_url(array('page' => 'shopping', 'type' => 'add_item', 'product_id' => $entry['id'], 'hook' => 'catalogue_items'), get_module_zone('shopping'));
        }
    }

    // Now render the correct layout style
    switch ($display_type) {
        case C_DT_FIELDMAPS:
            for ($i = 0; $i < $num_entries; $i++) {
                if (!array_key_exists($i, $entries)) {
                    break;
                }
                if (!array_key_exists('map', $entries[$i])) {
                    continue;
                }

                $entry = $entries[$i];

                if (($max === null) || (($start === null) || ($in_db_sorting) || ($i >= $start) && (($max === null) || ($i < $start + $max))) && ((!is_array($select)) || ((is_array($select)) && (in_array($entry['id'], $select))))) {
                    $entry_buildup->attach(do_template('CATALOGUE_' . $tpl_set . '_FIELDMAP_ENTRY_WRAP', $entry['map'] + array('GIVE_CONTEXT' => false) + (array_key_exists($i, $extra_map) ? $extra_map[$i] : array()), null, false, 'CATALOGUE_DEFAULT_FIELDMAP_ENTRY_WRAP'));
                }
            }
            break;

        case C_DT_TITLELIST:
            for ($i = 0; $i < $num_entries; $i++) {
                if (!array_key_exists($i, $entries)) {
                    break;
                }
                if (!array_key_exists('map', $entries[$i])) {
                    continue;
                }

                $entry = $entries[$i];

                if ((($start === null) || ($in_db_sorting) || ($i >= $start) && (($max === null) || ($i < $start + $max))) && ((!is_array($select)) || ((is_array($select)) && (in_array($entry['id'], $select))))) {
                    $entry_buildup->attach(do_template('CATALOGUE_' . $tpl_set . '_TITLELIST_ENTRY', $entry['map'] + (array_key_exists($i, $extra_map) ? $extra_map[$i] : array()), null, false, 'CATALOGUE_DEFAULT_TITLELIST_ENTRY'));
                }
            }
            if (!$entry_buildup->is_empty()) {
                $entry_buildup = do_template('CATALOGUE_' . $tpl_set . '_TITLELIST_WRAP', $entry['map'] + array('CATALOGUE' => $catalogue_name, 'CONTENT' => $entry_buildup), null, false, 'CATALOGUE_DEFAULT_TITLELIST_WRAP');
            }
            break;

        case C_DT_TABULAR:
            // Pre-scan to know if view URLs will be used
            $has_view_screens = false;
            for ($i = 0; $i < $num_entries; $i++) {
                if (!array_key_exists($i, $entries)) {
                    break;
                }
                if (!array_key_exists('map', $entries[$i])) {
                    continue;
                }

                $entry = $entries[$i];

                if ((get_option('is_on_comments') == '1') && ($entry['allow_comments'] >= 1) || (get_option('is_on_rating') == '1') && ($entry['allow_rating'] == 1) || (get_option('is_on_trackbacks') == '1') && ($entry['allow_trackbacks'] == 1)) {
                    $has_view_screens = true;
                }
            }

            // Main scan
            for ($i = 0; $i < $num_entries; $i++) {
                if (!array_key_exists($i, $entries)) {
                    break;
                }
                if (!array_key_exists('map', $entries[$i])) {
                    continue;
                }

                $entry = $entries[$i];
                if ((($start === null) || ($in_db_sorting) || ($i >= $start) && (($max === null) || ($i < $start + $max))) && ((!is_array($select)) || (is_array($select)) && (in_array($entry['id'], $select)))) {
                    $tab_entry_map = $entry['map'] + (array_key_exists($i, $extra_map) ? $extra_map[$i] : array());
                    if ($has_view_screens) {
                        $url_map = array('page' => 'catalogues', 'type' => 'entry', 'id' => $entry['id']);
                        if ($root !== null) {
                            $url_map['keep_catalogue_' . $catalogue_name . '_root'] = $root;
                        }
                        $tab_entry_map['VIEW_URL'] = build_url($url_map, get_module_zone('catalogues'));
                    } else {
                        $tab_entry_map['VIEW_URL'] = '';
                    }

                    $entry_buildup->attach(/*Preserve memory*/static_evaluate_tempcode(do_template('CATALOGUE_' . $tpl_set . '_TABULAR_ENTRY_WRAP', $tab_entry_map, null, false, 'CATALOGUE_DEFAULT_TABULAR_ENTRY_WRAP')));
                }
                if (($start !== null) && ($i >= $start + $max)) {
                    break;
                }
            }

            // Put it together
            if (!$entry_buildup->is_empty()) {
                $head = new Tempcode();
                $field_count = 0;
                foreach ($fields as $i => $field) {
                    if (((($field['cf_put_in_category'] == 1) && ($view_type == 'CATEGORY')) || (($field['cf_put_in_search'] == 1) && ($view_type == 'SEARCH'))) && ($field['cf_visible'] == 1)) {
                        if ($field['cf_searchable'] == 1) {
                            $sort_url_asc = get_self_url(false, false, array($ordering_param => strval($i) . ' ASC'), true);
                            $sort_url_desc = get_self_url(false, false, array($ordering_param => strval($i) . ' DESC'), true);
                            $sort_asc_selected = (($order_by == strval($field['id'])) && ($direction == 'ASC'));
                            $sort_desc_selected = (($order_by == strval($field['id'])) && ($direction == 'DESC'));
                        } else {
                            $sort_url_asc = '';
                            $sort_url_desc = '';
                            $sort_asc_selected = false;
                            $sort_desc_selected = false;
                        }
                        $head->attach(do_template(
                                'CATALOGUE_' . $tpl_set . '_TABULAR_HEADCELL',
                                array(
                                    'SORT_ASC_SELECTED' => $sort_asc_selected,
                                    'SORT_DESC_SELECTED' => $sort_desc_selected,
                                    'SORT_URL_ASC' => $sort_url_asc,
                                    'SORT_URL_DESC' => $sort_url_desc,
                                    'CATALOGUE' => $catalogue_name,
                                    'FIELDID' => strval($i),
                                    '_FIELDID' => strval($field['id']),
                                    'FIELD' => get_translated_text($field['cf_name']),
                                    'FIELDTYPE' => $field['cf_type']
                                ),
                                null,
                                false,
                                'CATALOGUE_DEFAULT_TABULAR_HEADCELL'
                            )
                        );
                        $field_count++;
                    }
                }
                $entry_buildup = do_template('CATALOGUE_' . $tpl_set . '_TABULAR_WRAP', array('CATALOGUE' => $catalogue_name, 'HEAD' => $head, 'CONTENT' => $entry_buildup, 'FIELD_COUNT' => strval($field_count)), null, false, 'CATALOGUE_DEFAULT_TABULAR_WRAP');
            }
            break;

        case C_DT_GRID:
            for ($i = 0; $i < $num_entries; $i++) {
                if (!array_key_exists($i, $entries)) {
                    break;
                }

                $entry = $entries[$i];

                if (($max === null) || (($start === null) || ($in_db_sorting) || ($i >= $start) && (($max === null) || ($i < $start + $max))) && ((!is_array($select)) || ((is_array($select)) && (in_array($entry['id'], $select))))) {
                    $entry_buildup->attach(do_template('CATALOGUE_' . $tpl_set . '_GRID_ENTRY_WRAP', $entry['map'] + (array_key_exists($i, $extra_map) ? $extra_map[$i] : array()), null, false, 'CATALOGUE_DEFAULT_GRID_ENTRY_WRAP'));
                }
            }
            break;

        default:
            warn_exit(do_lang_tempcode('INTERNAL_ERROR'));
    }

    return array($entry_buildup, $sorting, $entries, $num_entries, $display_type);
}

/**
 * Make sure we are doing necessary join to be able to access the given field
 *
 * @param  object $db Database connection
 * @param  array $info Content type info
 * @param  ?ID_TEXT $catalogue_name Name of the catalogue (null: unknown; reduces performance)
 * @param  array $extra_join List of joins (passed as reference)
 * @param  array $extra_select List of selects (passed as reference)
 * @param  ID_TEXT $filter_key The field to get
 * @param  string $filter_val The field value for this
 * @param  array $db_fields Database field data
 * @param  string $table_join_code What the database will join the table with
 * @return ?array A triple: Proper database field name to access with, The fields API table type (blank: no special table), The new filter value (null: error)
 * @ignore
 */
function _catalogues_filtercode($db, $info, $catalogue_name, &$extra_join, &$extra_select, $filter_key, $filter_val, $db_fields, $table_join_code)
{
    if (preg_match('#^((.*)\.)?field\_(\d+)#', $filter_key) != 0) { // This is by field ID, not field sequence #
        $ret = _fields_api_filtercode($db, $info, $catalogue_name, $extra_join, $extra_select, $filter_key, $filter_val, $db_fields, $table_join_code);
        if (!is_null($ret)) {
            return $ret;
        }
    }

    // Named
    $ret = _fields_api_filtercode_named($db, $info, $catalogue_name, $extra_join, $extra_select, $filter_key, $filter_val, $db_fields, $table_join_code);
    if (!is_null($ret)) {
        return $ret;
    }

    return _default_conv_func($db, $info, $catalogue_name, $extra_join, $extra_select, $filter_key, $filter_val, $db_fields, $table_join_code);
}

/**
 * Fetch entries from database, with sorting if possible.
 *
 * @param  ID_TEXT $catalogue_name Name of the catalogue
 * @param  ?AUTO_LINK $category_id The ID of the category for which the entries are being collected (null: no limitation)
 * @param  ?integer $max The maximum number of entries to show on a single page of this this category (ignored if $select is not null) (null: all)
 * @param  ?integer $start The entry number to start at (ignored if $select is not null) (null: all)
 * @param  ?mixed $select The entries to show, may be from other categories. Can either be SQL fragment (produced from Selectcode?), or array (null: use $start and $max)
 * @param  boolean $do_sorting Whether to perform sorting
 * @param  ?array $filtercode List of filters to apply (null: none). Each filter is a triple: ORd comparison key(s) [separated by pipe symbols], comparison type (one of '<', '>', '<=', '>=', '=', '~=', or '~'), comparison value
 * @param  ID_TEXT $order_by Orderer
 * @param  ID_TEXT $direction Order direction
 * @param  string $extra_where Additional WHERE SQL to add on to query
 * @param  ?MEMBER $viewing_member_id Viewing member ID (null: current user)
 * @return array A tuple: whether sorting was done, number of entries returned, list of entries
 */
function get_catalogue_entries($catalogue_name, $category_id, $max, $start, $select, $do_sorting, $filtercode, $order_by, $direction, $extra_where = '', $viewing_member_id = null)
{
    $where_clause = '1=1' . $extra_where;
    if ($category_id !== null) {
        // WHERE clause
        $where_clause .= ' AND r.cc_id=' . strval($category_id);
    }
    if ((!has_privilege(get_member(), 'see_unvalidated')) && (addon_installed('unvalidated'))) {
        $where_clause .= ' AND r.ce_validated=1';
    }

    // Convert the filters to SQL
    require_code('filtercode');

    list($extra_select, $extra_join, $extra_where) = filtercode_to_sql($GLOBALS['SITE_DB'], $filtercode, 'catalogue_entry', $catalogue_name);
    $where_clause .= $extra_where . ' AND ' . db_string_equal_to('r.c_name', $catalogue_name);

    $privacy_join = '';
    $privacy_where = '';
    if (addon_installed('content_privacy')) {
        require_code('content_privacy');
        list($privacy_join, $privacy_where) = get_privacy_where_clause('catalogue_entry', 'r', $viewing_member_id);
    }
    $extra_join[] = $privacy_join;
    $where_clause .= $privacy_where;

    // If we're listing what IDs to look at, work out SQL for this
    if (($category_id === null) && ($select !== null)) {
        if (((is_array($select)) && (count($select) == 0)) || ((is_string($select)) && ($select == ''))) {
            $entries = array(); // This is saying we are selecting nothing, so just say that - it'll save us a query
        } else { // Put together some SQL for defining what to select
            if (!is_array($select)) {
                $or_list = $select;
            } else {
                $or_list = '';
                foreach ($select as $s) {
                    if ($or_list != '') {
                        $or_list .= ' OR ';
                    }
                    $or_list .= 'r.id=' . strval($s);
                }
            }
            $where_clause .= ' AND (' . $or_list . ')';
        }
    }
    require_code('fields');
    $fields = get_catalogue_fields($catalogue_name);

    $num_entries = mixed();

    if ($order_by == 'rating') { // LEGACY
        $order_by = 'average_rating';
    }

    $can_do_db_sorting = ($order_by != 'distance');

    require_code('hooks/systems/content_meta_aware/catalogue_entry');
    $cma_ob = object_factory('Hook_content_meta_aware_catalogue_entry');

    if (($do_sorting) && ($can_do_db_sorting)) {
        $virtual_order_by = $order_by;

        if ($order_by == 'add_date') {
            $virtual_order_by = 'r.ce_add_date';
        } elseif (($order_by == 'compound_rating') || ($order_by == 'average_rating') || ($order_by == 'fixed_random')) {
            $ob = object_factory('Hook_content_meta_aware_catalogue_entry');
            $info = $ob->info();
            $bits = _catalogues_filtercode($GLOBALS['SITE_DB'], $info, $catalogue_name, $extra_join, $extra_select, $order_by, '', array(), 'r'); // Used to get JOIN for ordering
            if ($bits !== null) {
                list($virtual_order_by,) = $bits;
            } else {
                $virtual_order_by = 'r.ce_add_date'; // Should not happen
            }
        } elseif ((is_numeric($order_by)) && (isset($fields[intval($order_by)]))) { // Ah, so it's saying the nth field of this catalogue
            $ob = object_factory('Hook_content_meta_aware_catalogue_entry');
            $info = $ob->info();
            $order_by_field_id = $fields[intval($order_by)]['id'];
            $bits = _catalogues_filtercode($GLOBALS['SITE_DB'], $info, $catalogue_name, $extra_join, $extra_select, 'field_' . $order_by_field_id, '', array(), 'r'); // Used to get JOIN for ordering
            if ($bits !== null) {
                list($new_key,) = $bits;
                if ((strpos($new_key, '.text_original') !== false) && (multi_lang_content())) {
                    $num_entries = $GLOBALS['SITE_DB']->query_value_if_there('SELECT COUNT(*) FROM ' . get_table_prefix() . 'catalogue_entries r' . implode('', $extra_join) . ' WHERE ' . $where_clause, false, true);
                    if ($num_entries > 300) { // For large data sets too slow as after two MySQL joins it can't then use index for ordering
                        $virtual_order_by = 'r.id';
                        unset($extra_join[$new_key]);
                    } else {
                        $virtual_order_by = $new_key;
                    }
                } else {
                    $virtual_order_by = $new_key;
                }
            } else {
                $virtual_order_by = 'r.id';
            }
        } else {
            $virtual_order_by = 'r.id';
        }
    } else {
        $virtual_order_by = 'r.id';
    }

    if ($num_entries === null) {
        $num_entries = $GLOBALS['SITE_DB']->query_value_if_there('SELECT COUNT(*) FROM ' . get_table_prefix() . 'catalogue_entries r' . implode('', $extra_join) . ' WHERE ' . $where_clause, false, true);
    }

    if (get_value('force_memory_sort__' . $catalogue_name) === '1') {
        $can_do_db_sorting = false;
    }

    $in_db_sorting = $do_sorting && $can_do_db_sorting; // This defines whether $virtual_order_by can actually be used in SQL (if not, we have to sort manually)
    if (($num_entries > 300) && (!$in_db_sorting)) { // Needed to stop huge slow down, so reduce to sorting by ID
        $in_db_sorting = true;
        $virtual_order_by = 'r.id';
    }

    $sql = 'SELECT r.*' . implode('', $extra_select) . ' FROM ' . get_table_prefix() . 'catalogue_entries r' . implode('', $extra_join) . ' WHERE ' . $where_clause;
    if ($in_db_sorting && $do_sorting) {
        $sql .= ' ORDER BY ' . $virtual_order_by . ' ' . $direction;
    }

    if ($max > 0) {
        $entries = $GLOBALS['SITE_DB']->query($sql, $in_db_sorting ? $max : null, $in_db_sorting ? $start : 0);
    } else {
        $entries = array();
    }

    return array($in_db_sorting, $num_entries, $entries);
}

/**
 * Manually sort some catalogue entries.
 *
 * @param  array $fields Fields array for catalogue
 * @param  array $entries Entries to sort (by reference)
 * @param  ID_TEXT $order_by What to sort by
 * @param  ID_TEXT $direction Sort direction
 * @return array Entries
 */
function catalogue_entries_manual_sort($fields, &$entries, $order_by, $direction)
{
    $num_entries = count($entries);

    for ($i = 0; $i < $num_entries; $i++) { // Bubble sort
        for ($j = $i + 1; $j < $num_entries; $j++) {
            if ($order_by == 'distance') {
                $considered_field = 'DISTANCE_PLAIN'; // Not in there by default, but addons might make it
            } else {
                $considered_field = 'FIELD_' . $order_by;
            }

            $a = $entries[$j]['map'][$considered_field];
            if (array_key_exists($considered_field . '_PLAIN', $entries[$j]['map'])) {
                $a = $entries[$j]['map'][$considered_field . '_PLAIN'];
            }
            $b = $entries[$i]['map'][$considered_field];

            if (array_key_exists($considered_field . '_PLAIN', $entries[$i]['map'])) {
                $b = $entries[$i]['map'][$considered_field . '_PLAIN'];
            }
            if (is_object($a)) {
                $a = $a->evaluate();
            }
            if (is_object($b)) {
                $b = $b->evaluate();
            }

            if ((isset($fields[$order_by])) && ($fields[$order_by]['cf_type'] == 'date')) { // Special case for dates
                $bits = explode(' ', $a, 2);
                $date_bits = explode((strpos($bits[0], '-') !== false) ? '-' : '/', $bits[0], 3);
                if (!array_key_exists(1, $date_bits)) {
                    $date_bits[1] = date('m');
                }
                if (!array_key_exists(2, $date_bits)) {
                    $date_bits[2] = date('Y');
                }
                $time_bits = explode(':', $bits[1], 3);
                if (!array_key_exists(1, $time_bits)) {
                    $time_bits[1] = '00';
                }
                if (!array_key_exists(2, $time_bits)) {
                    $time_bits[2] = '00';
                }
                $time_a = mktime(intval($time_bits[0]), intval($time_bits[1]), intval($time_bits[2]), intval($date_bits[1]), intval($date_bits[2]), intval($date_bits[0]));
                $bits = explode(' ', $b, 2);
                $date_bits = explode((strpos($bits[0], '-') !== false) ? '-' : '/', $bits[0], 3);
                if (!array_key_exists(1, $date_bits)) {
                    $date_bits[1] = date('m');
                }
                if (!array_key_exists(2, $date_bits)) {
                    $date_bits[2] = date('Y');
                }
                $time_bits = explode(':', $bits[1], 3);
                if (!array_key_exists(1, $time_bits)) {
                    $time_bits[1] = '00';
                }
                if (!array_key_exists(2, $time_bits)) {
                    $time_bits[2] = '00';
                }
                $time_b = mktime(intval($time_bits[0]), intval($time_bits[1]), intval($time_bits[2]), intval($date_bits[1]), intval($date_bits[2]), intval($date_bits[0]));

                $r = ($time_a < $time_b) ? -1 : (($time_a == $time_b) ? 0 : 1);
            } elseif ($order_by == 'distance') { // By distance
                if (($a === null) || ($b === null)) {
                    $r = 0;
                } else {
                    $r = (floatval($a) < floatval($b)) ? -1 : 1;
                }
            } else { // Normal case
                $r = strnatcmp(strtolower($a), strtolower($b));
            }
            if ((($r < 0) && ($direction == 'ASC')) || (($r > 0) && ($direction == 'DESC'))) {
                $temp = $entries[$i];
                $entries[$i] = $entries[$j];
                $entries[$j] = $temp;
            }
        }
    }

    return $entries;
}

/**
 * Get a map of the fields for the given entry.
 *
 * @param  array $entry A database row of the entry we are working with
 * @param  ?array $catalogue A database row of the catalogue we are working with (null: read it in here)
 * @param  ID_TEXT $view_type The view type we're doing
 * @set    PAGE SEARCH CATEGORY
 * @param  ID_TEXT $tpl_set The template set we are rendering this category using
 * @param  ?AUTO_LINK $root The virtual root for display of this category (null: none)
 * @param  ?array $fields The database rows for the fields for this catalogue (null: find them)
 * @param  ?array $only_fields A list of fields (sequence numbers) that we are limiting ourselves to (null: get ALL fields)
 * @param  boolean $feedback_details Whether to grab the feedback details
 * @param  boolean $breadcrumbs_details Whether to grab the breadcrumbs details
 * @param  ?integer $order_by Field index to order by (null: none)
 * @param  ?array $_breadcrumbs Write breadcrumbs into here (null: don't bother)
 * @param  boolean $force_view_all Whether to render everything
 * @return array A map of information relating to the entry. The map contains 'FIELDS' (Tempcode for all accumulated fields), 'FIELD_x' (for each field x applying to the entry), STAFF_DETAILS, COMMENT_DETAILS, RATING_DETAILS, VIEW_URL, BREADCRUMBS
 */
function get_catalogue_entry_map($entry, $catalogue, $view_type, $tpl_set, $root = null, $fields = null, $only_fields = null, $feedback_details = false, $breadcrumbs_details = false, $order_by = null, &$_breadcrumbs = null, $force_view_all = false)
{
    $id = $entry['id'];
    $all_visible = true;
    require_code('fields');

    // Load catalogue if needed
    if ($catalogue === null) {
        $catalogue = load_catalogue_row($entry['c_name']);
    }

    // Get values
    $catalogue_name = $catalogue['c_name'];
    $fields = get_catalogue_entry_field_values($catalogue_name, $entry, $only_fields, $fields, false, $view_type);

    // Prepare output map
    $map = array();
    $map['FIELDS'] = new Tempcode();
    $map['FIELDS_GRID'] = new Tempcode();
    $map['FIELDS_TABULAR'] = new Tempcode();
    $map['fields'] = $fields;

    $no_catalogue_field_assembly = (get_value('no_catalogue_field_assembly') === '1');
    $no_catalogue_field_assembly_fieldmaps__this = (get_value('no_catalogue_field_assembly_fieldmaps__' . $catalogue['c_name']) === '1');
    $no_catalogue_field_assembly_fieldmaps = (get_value('no_catalogue_field_assembly_fieldmaps') === '1');
    $no_catalogue_field_assembly_grid__this = (get_value('no_catalogue_field_assembly_grid__' . $catalogue['c_name']) === '1');
    $no_catalogue_field_assembly_grid = (get_value('no_catalogue_field_assembly_grid') === '1');
    $no_catalogue_field_assembly_tabular__this = (get_value('no_catalogue_field_assembly_tabular__' . $catalogue['c_name']) === '1');
    $no_catalogue_field_assembly_tabular = (get_value('no_catalogue_field_assembly_tabular') === '1');

    // Loop over all fields
    foreach ($fields as $i => $field) {
        if (!isset($field['effective_value'])) { // No field value. Should actually never happen, as e.g. {!INTERNAL_ERROR} is put in if field values are missing
            $all_visible = false;
            continue;
        }

        $str_i = strval($i);
        $str_id = strval($field['id']);

        // Value to show
        $ev = $field['effective_value'];
        $ev_pure = $field['effective_value_pure'];
        $ob = get_fields_hook($field['cf_type']);
        list(, , $storage_type) = $ob->get_field_value_row_bits($field);
        if (($i == 0) && ($catalogue['c_display_type'] == C_DT_TITLELIST)) {
            $use_ev = $ev;
        } else {
            $use_ev = $ob->render_field_value($field, $ev, $i, $only_fields, 'catalogue_efv_' . $storage_type, $id, 'ce_id', 'cf_id', 'cv_value', $entry['ce_submitter'], $ev_pure);
        }

        // Special case for access to raw thumbnail
        if ($field['cf_type'] == 'picture') {
            if (($ev !== null) && ($ev_pure != '')) {
                require_code('images');
                $map['FIELD_' . $str_i . '_THUMB'] = do_image_thumb($ev_pure, ($i == 0) ? new Tempcode() : (is_object($map['FIELD_0']) ? $map['FIELD_0'] : protect_from_escaping(escape_html($map['FIELD_0']))), false, false);
            } else {
                $map['FIELD_' . $str_i . '_THUMB'] = new Tempcode();
            }
            $map['_FIELD_' . $str_id . '_THUMB'] = $map['FIELD_' . $str_i . '_THUMB'];
        }

        // Different ways of accessing the main field value, and pure version of it
        $field_name = get_translated_text($field['cf_name']);
        $field_type = $field['cf_type'];
        $map['FIELD_' . $str_i] = $use_ev;
        $map['_FIELD_' . $str_id] = &$map['FIELD_' . $str_i];
        if ($use_ev === $ev) {
            $map['FIELD_' . $str_i . '_PLAIN'] = &$map['FIELD_' . $str_i];
        } else {
            $map['FIELD_' . $str_i . '_PLAIN'] = $ev;
        }
        $map['_FIELD_' . $str_id . '_PLAIN'] = &$map['FIELD_' . $str_i . '_PLAIN'];
        if ($ev === $field['effective_value_pure']) {
            $map['FIELD_' . $str_i . '_PURE'] = &$map['FIELD_' . $str_i . '_PLAIN'];
        } else {
            $map['FIELD_' . $str_i . '_PURE'] = $field['effective_value_pure'];
        }
        $map['_FIELD_' . $str_id . '_PURE'] = &$map['FIELD_' . $str_i . '_PURE'];

        // If the field should be shown, show it
        $is_visible_in_view_type =
            ($view_type == 'PAGE') ||
            (($field['cf_put_in_category'] == 1) && ($view_type == 'CATEGORY')) ||
            (($field['cf_put_in_search'] == 1) && ($view_type == 'SEARCH')) ||
            ($force_view_all);
        if (($is_visible_in_view_type) && ($field['cf_visible'] == 1)) {
            if ((!$no_catalogue_field_assembly) || (!$feedback_details/*no feedback details implies wants all field data*/) || ($force_view_all)) {
                $f = array('ENTRYID' => strval($id), 'CATALOGUE' => $catalogue_name, 'TYPE' => $field['cf_type'], 'FIELD' => $field_name, 'FIELDID' => $str_i, '_FIELDID' => $str_id, 'FIELDTYPE' => $field_type, 'VALUE_PLAIN' => $ev, 'VALUE' => $use_ev);
                if (!$no_catalogue_field_assembly_fieldmaps__this) {
                    if ((!$no_catalogue_field_assembly_fieldmaps) || (!$feedback_details/*no feedback details implies wants all field data [as is a category view]*/)) {
                        $_field = do_template('CATALOGUE_' . $tpl_set . '_FIELDMAP_ENTRY_FIELD', $f, null, false, 'CATALOGUE_DEFAULT_FIELDMAP_ENTRY_FIELD');
                        $map['FIELDS']->attach($_field);
                    }
                }
                if (!$no_catalogue_field_assembly_grid__this) {
                    if (!$no_catalogue_field_assembly_grid) {
                        $_field = do_template('CATALOGUE_' . $tpl_set . '_GRID_ENTRY_FIELD', $f, null, false, 'CATALOGUE_DEFAULT_GRID_ENTRY_FIELD');
                        $map['FIELDS_GRID']->attach($_field);
                    }
                }
                if (!$no_catalogue_field_assembly_tabular__this) {
                    if (!$no_catalogue_field_assembly_tabular) {
                        $_field = do_template('CATALOGUE_' . $tpl_set . '_TABULAR_ENTRY_FIELD', $f, null, false, 'CATALOGUE_DEFAULT_TABULAR_ENTRY_FIELD');
                        $map['FIELDS_TABULAR']->attach($_field);
                    }
                }
            }
        } else {
            $all_visible = false;
        }
    }

    // Admin functions
    if ((has_actual_page_access(null, 'cms_catalogues', null, null)) && (has_edit_permission('mid', get_member(), $entry['ce_submitter'], 'cms_catalogues', array('catalogues_catalogue', $catalogue_name) + ((get_value('disable_cat_cat_perms') !== '1') ? array('catalogues_category', $entry['cc_id']) : array())))) {
        $map['EDIT_URL'] = build_url(array('page' => 'cms_catalogues', 'type' => '_edit_entry', 'catalogue_name' => $catalogue_name, 'id' => $id), get_module_zone('cms_catalogues'));
    } else {
        $map['EDIT_URL'] = '';
    }

    // Various bits of metadata
    $map['SUBMITTER'] = strval($entry['ce_submitter']);
    $map['VIEWS'] = strval($entry['ce_views']);
    $map['ADD_DATE_RAW'] = strval($entry['ce_add_date']);
    $map['EDIT_DATE_RAW'] = ($entry['ce_edit_date'] === null) ? '' : strval($entry['ce_edit_date']);
    $map['ADD_DATE'] = get_timezoned_date_tempcode($entry['ce_add_date']);
    $map['EDIT_DATE'] = is_null($entry['ce_edit_date']) ? new Tempcode() : get_timezoned_date_tempcode($entry['ce_edit_date']);
    $map['ID'] = strval($id);
    $map['CATALOGUE'] = $catalogue_name;
    $map['CATALOGUE_TITLE'] = array_key_exists('c_title', $catalogue) ? get_translated_text($catalogue['c_title']) : '';
    $map['CATEGORY_ID'] = strval($entry['cc_id']);
    $map['CAT'] = strval($entry['cc_id']);
    if ((get_option('is_on_comments') == '1') && (!has_no_forum()) && ($entry['allow_comments'] >= 1)) {
        $map['COMMENT_COUNT'] = '1';
    }

    $zone = get_module_zone('catalogues');

    $separate_view_screen =
        (get_option('is_on_comments') == '1') && ($entry['allow_comments'] >= 1) ||
        /*(get_option('is_on_rating') == '1') && ($entry['allow_rating'] == 1) || We'll just allow inline rating */
        (get_option('is_on_trackbacks') == '1') && ($entry['allow_trackbacks'] == 1) ||
        (!$all_visible);

    // Feedback
    $c_value = isset($map['FIELD_0_PLAIN_PURE']) ? $map['FIELD_0_PLAIN_PURE'] : (isset($map['FIELD_0_PLAIN']) ? $map['FIELD_0_PLAIN'] : do_lang('UNKNOWN'));
    if (is_object($c_value)) {
        $c_value = $c_value->evaluate();
    }
    $url_map = array('page' => 'catalogues', 'type' => 'entry', 'id' => $id);
    $self_url = build_url($url_map, $zone, null, false, false, true);
    if (($feedback_details) || ($only_fields !== array(0))) {
        require_code('feedback');
        $map['RATING'] = ($entry['allow_rating'] == 1) ? display_rating($self_url, $c_value, 'catalogues__' . $catalogue_name, strval($id), $separate_view_screen ? 'RATING_INLINE_STATIC' : 'RATING_INLINE_DYNAMIC', $entry['ce_submitter']) : new Tempcode();
    }
    $map['ALLOW_RATING'] = ($entry['allow_rating'] == 1);
    if ($feedback_details) {
        require_code('feedback');
        list($map['RATING_DETAILS'], $map['COMMENT_DETAILS'], $map['TRACKBACK_DETAILS']) = embed_feedback_systems(
            'catalogues__' . $catalogue_name,
            strval($id),
            $entry['allow_rating'],
            $entry['allow_comments'],
            $entry['allow_trackbacks'],
            $entry['ce_validated'],
            $entry['ce_submitter'],
            $self_url,
            $c_value,
            find_overridden_comment_forum('catalogues__' . $catalogue_name, strval($entry['cc_id'])),
            $entry['ce_add_date']
        );
    }

    // Link to view entry
    if ($separate_view_screen) {
        $url_map = array('page' => 'catalogues', 'type' => 'entry', 'id' => $id);
        if ($root !== null) {
            $url_map['keep_catalogue_' . $catalogue_name . '_root'] = $root;
        }
        $map['VIEW_URL'] = build_url($url_map, $zone);
    } else {
        $map['VIEW_URL'] = '';
    }

    // Breadcrumbs
    if ($breadcrumbs_details) {
        $map['BREADCRUMBS'] = '';
        if ($only_fields === null) {
            $_breadcrumbs = catalogue_category_breadcrumbs($entry['cc_id'], $root, false);
            $breadcrumbs = breadcrumb_segments_to_tempcode($_breadcrumbs);
            $map['BREADCRUMBS'] = $breadcrumbs;
        }
    }

    return $map;
}

/**
 * Get the values for the specified fields, for the stated catalogue entry.
 *
 * @param  ?ID_TEXT $catalogue_name The catalogue name we are getting an entry in (null: lookup)
 * @param  mixed $entry_id The ID of the entry we are getting OR the row
 * @param  ?array $only_fields A list of fields that we are limiting ourselves to (null: get ALL fields)
 * @param  ?array $fields The database rows for the fields for this catalogue (null: find them)
 * @param  boolean $natural_order Whether to order the fields in their natural database order. This is only used for shopping catalogues as a defence against webmaster field reordering and not a strong guarantee
 * @param  ID_TEXT $view_type The view type we're doing
 * @set    PAGE SEARCH CATEGORY
 * @return array A list of maps (each field for the entry gets a map), where each map contains 'effective_value' (the value for the field). Some maps get additional fields (effective_value_pure), depending on the field type
 */
function get_catalogue_entry_field_values($catalogue_name, $entry_id, $only_fields = null, $fields = null, $natural_order = false, $view_type = 'PAGE')
{
    global $CAT_FIELDS_CACHE;

    if ($fields === null) {
        if ($catalogue_name === null) {
            $catalogue_name = $GLOBALS['SITE_DB']->query_select_value('catalogue_entries', 'c_name', array('id' => $entry_id));
        }
        if ((isset($CAT_FIELDS_CACHE[$catalogue_name])) && (!$natural_order)) {
            $fields = $CAT_FIELDS_CACHE[$catalogue_name];
        } else {
            $order_by = (($natural_order && (strpos(get_db_type(), 'mysql')/*assumption about sequential order*/ !== false)) ? 'id' : ('cf_order,' . $GLOBALS['SITE_DB']->translate_field_ref('cf_name')));
            $fields = $GLOBALS['SITE_DB']->query_select('catalogue_fields', array('*'), array('c_name' => $catalogue_name), 'ORDER BY ' . $order_by);
        }
    }
    if (!$natural_order) {
        $CAT_FIELDS_CACHE[$catalogue_name] = $fields;
    }

    require_code('fields');

    if ($only_fields !== null) {
        $only_fields = array_flip($only_fields);
    }

    // Work out an ID filter for what fields to show
    $only_field_ids = mixed();
    if (get_value('catalogue_limit_cat_field_load__' . $catalogue_name) === '1') {
        $only_field_ids = array();
        foreach ($fields as $i => $field) {
            $field_id = $field['id'];

            if (($only_fields !== null) && (!isset($only_fields[$i]))) {
                continue;
            }
            if ($field['cf_defines_order'] == 0) {
                if (($view_type == 'CATEGORY') && ($field['cf_put_in_category'] == 0)) {
                    continue;
                }
                if (($view_type == 'SEARCH') && ($field['cf_put_in_search'] == 0)) {
                    continue;
                }
            }

            $only_field_ids[] = $field_id;
        }
    }

    foreach ($fields as $i => $field) {
        $field_id = $field['id'];

        if (($only_fields !== null) && (!isset($only_fields[$i]))) {
            continue;
        }

        _resolve_catalogue_entry_field($field, $entry_id, $only_field_ids, $fields[$i]);
    }

    return $fields;
}

/**
 * Get the standardised details for a catalogue entry field.
 *
 * @param  array $field The field row
 * @param  mixed $entry_id The ID of the entry we are getting OR the row
 * @param  ?array $only_field_ids A list of field IDs that we are limiting ourselves to (null: get ALL fields)
 * @param  array $target Save the result into here
 *
 * @ignore
 */
function _resolve_catalogue_entry_field($field, $entry_id, $only_field_ids, &$target)
{
    $ob = get_fields_hook($field['cf_type']);
    list($raw_type, , $type) = $ob->get_field_value_row_bits($field);
    if (is_null($raw_type)) {
        $raw_type = $field['cf_type'];
    }

    switch ($raw_type) {
        case 'long_trans':
        case 'short_trans':
            $temp = _get_catalogue_entry_field($field['id'], $entry_id, 'short_trans', $only_field_ids);
            if ($temp['cv_value'] === null) {
                $target['effective_value'] = '';
                $target['effective_value_pure'] = '';
            } else {
                $just_row = db_map_restrict($temp, array('cv_value')) + array('ce_id' => is_array($entry_id) ? $entry_id['id'] : $entry_id, 'cf_id' => $field['id']);
                if (multi_lang_content()) {
                    $just_row['cv_value'] = intval($just_row['cv_value']);
                }
                $target['effective_value'] = get_translated_tempcode('catalogue_efv_' . $raw_type, $just_row, 'cv_value');
                $target['effective_value_pure'] = get_translated_text($just_row['cv_value']);
            }
            break;
        case 'long_text':
        case 'short_text':
        case 'long_unescaped':
        case 'short_unescaped':
            $temp = _get_catalogue_entry_field($field['id'], $entry_id, $type, $only_field_ids);
            if ($temp['cv_value'] === null) {
                $target['effective_value'] = '';
                $target['effective_value_pure'] = '';
                break;
            } else {
                $target['effective_value'] = $temp['cv_value'];
                $target['effective_value_pure'] = $temp['cv_value'];
            }
            break;
        case 'float_unescaped':
            $temp = _get_catalogue_entry_field($field['id'], $entry_id, $type, $only_field_ids);
            if ($temp['cv_value'] === null) {
                $target['effective_value'] = do_lang_tempcode('NA_EM');
                $target['effective_value_pure'] = do_lang('NA');
                break;
            } else {
                $target['effective_value'] = float_to_raw_string($temp['cv_value'], 30, true);
                $target['effective_value_pure'] = $target['effective_value'];
            }
            break;
        case 'integer_unescaped':
            $temp = _get_catalogue_entry_field($field['id'], $entry_id, $type, $only_field_ids);
            if ($temp['cv_value'] === null) {
                $target['effective_value'] = do_lang_tempcode('NA_EM');
                $target['effective_value_pure'] = do_lang('NA');
                break;
            } else {
                $target['effective_value'] = strval($temp['cv_value']);
                $target['effective_value_pure'] = $target['effective_value'];
            }
            break;
        default:
            warn_exit(do_lang_tempcode('INTERNAL_ERROR'));
    }
}

/**
 * Get the value for the specified field, for the stated catalogue entry.
 *
 * @param  AUTO_LINK $field_id The ID of the field we are getting
 * @param  mixed $entry_id The ID of the entry we are getting for OR the row
 * @param  ID_TEXT $type The type of field
 * @set    short long
 * @param  ?array $only_field_ids A list of field IDs that we are limiting ourselves to (null: get ALL fields)
 * @return ?array The row (null: not found)
 * @ignore
 */
function _get_catalogue_entry_field($field_id, $entry_id, $type = 'short', $only_field_ids = null)
{
    if (is_array($entry_id)) {
        $entry_id = $entry_id['id'];
    }

    // Pre-caching of whole entry
    static $catalogue_entry_cache = array();
    if ((!isset($catalogue_entry_cache[$entry_id])) || (class_exists('Resource_fs_base')/*Implies resource-fs import*/)) {
        $catalogue_entry_cache[$entry_id] = array();

        $only_fields_sql = '';
        if ($only_field_ids !== null) {
            $only_fields_sql .= ' AND (';
            if ($only_field_ids != array()) {
                foreach ($only_field_ids as $i => $_field_id) {
                    if ($i != 0) {
                        $only_fields_sql .= ' OR ';
                    }
                    $only_fields_sql .= 'f.id=' . strval($_field_id);
                }
            } else {
                $only_fields_sql .= '1=0';
            }
            $only_fields_sql .= ')';
        }

        $tables = array('catalogue_efv_float', 'catalogue_efv_integer', 'catalogue_efv_long', 'catalogue_efv_long_trans', 'catalogue_efv_short', 'catalogue_efv_short_trans',);
        if (strpos(get_db_type(), 'mysql') !== false) { // Optimised for MySQL
            $query = '';
            foreach ($tables as $table) {
                if ($query != '') {
                    $query .= ' UNION ';
                }
                $query .= 'SELECT f.id AS f_id,' . db_cast('v.cv_value', 'CHAR') . ' AS cv_value';
                if (!multi_lang_content()) {
                    if (strpos($table, '_trans') !== false) {
                        $query .= ',v.cv_value__text_parsed,v.cv_value__source_user';
                    } else {
                        $query .= ',NULL AS cv_value__text_parsed,NULL AS cv_value__source_user';
                    }
                }
                $query .= ' FROM ' . get_table_prefix() . 'catalogue_fields f JOIN ' . get_table_prefix() . $table . ' v ON v.cf_id=f.id';
                $query .= ' WHERE v.ce_id=' . strval($entry_id);
                $query .= $only_fields_sql;
            }
            $rows = $GLOBALS['SITE_DB']->query($query, null, null, false, true);
            foreach ($rows as $row) {
                $catalogue_entry_cache[$entry_id][$row['f_id']] = $row;
            }
        } else { // Other databases may not support unions with different data types, even if we do casting (PostgreSQL definitely doesn't)
            foreach ($tables as $table) {
                $query = 'SELECT f.id AS f_id,v.cv_value';
                if (!multi_lang_content()) {
                    if (strpos($table, '_trans') !== false) {
                        $query .= ',v.cv_value__text_parsed,v.cv_value__source_user';
                    } else {
                        $query .= ',NULL AS cv_value__text_parsed,NULL AS cv_value__source_user';
                    }
                }
                $query .= ' FROM ' . get_table_prefix() . 'catalogue_fields f JOIN ' . get_table_prefix() . $table . ' v ON v.cf_id=f.id';
                $query .= ' WHERE v.ce_id=' . strval($entry_id);
                $query .= $only_fields_sql;

                $rows = $GLOBALS['SITE_DB']->query($query, null, null, false, true);
                foreach ($rows as $row) {
                    $catalogue_entry_cache[$entry_id][$row['f_id']] = $row;
                }
            }
        }

        $value = isset($catalogue_entry_cache[$entry_id][$field_id]) ? $catalogue_entry_cache[$entry_id][$field_id] : null;

        if (class_exists('Resource_fs_base')) {
            $catalogue_entry_cache = array();
        }
    } else {
        if (!isset($catalogue_entry_cache[$entry_id][$field_id])) {
            switch ($type) {
                case 'float':
                case 'integer':
                    return array('cv_value' => null);

                default:
                    return array('cv_value' => '', 'cv_value__text_parsed' => '', 'cv_value__source_user' => null);
            }
        }
        $value = $catalogue_entry_cache[$entry_id][$field_id];
    }

    if (is_string($value['cv_value'])) { // UNION will coerce types to strings, undo that
        switch ($type) {
            case 'float':
                $value['cv_value'] = ($value['cv_value'] == '') ? null : floatval($value['cv_value']);
                break;

            case 'integer':
                $value['cv_value'] = ($value['cv_value'] == '') ? null : intval($value['cv_value']);
                break;
        }
    }

    return $value;
}

/**
 * Get a nice, formatted, XHTML list of all the catalogues.
 *
 * @param  ?ID_TEXT $it The name of the currently selected catalogue (null: none selected)
 * @param  boolean $prefer_ones_with_entries If there are too many to list prefer to get ones with entries rather than just the newest
 * @param  boolean $only_submittable Whether to only show catalogues that can be submitted to
 * @param  ?TIME $updated_since Time from which content must be updated (null: no limit).
 * @return Tempcode Catalogue selection list
 */
function create_selection_list_catalogues($it = null, $prefer_ones_with_entries = false, $only_submittable = false, $updated_since = null)
{
    $query = 'SELECT c.* FROM ' . get_table_prefix() . 'catalogues c';
    if (!is_null($updated_since)) {
        $privacy_join = '';
        $privacy_where = '';
        if (addon_installed('content_privacy')) {
            require_code('content_privacy');
            list($privacy_join, $privacy_where) = get_privacy_where_clause('catalogue_entry', 'e', $GLOBALS['FORUM_DRIVER']->get_guest_id());
        }
        $query .= ' WHERE EXISTS(SELECT * FROM ' . get_table_prefix() . 'catalogue_entries e' . $privacy_join . ' WHERE ce_validated=1 AND ce_add_date>' . strval($updated_since) . $privacy_where . ')';
    } else {
        if ($prefer_ones_with_entries) {
            if (can_arbitrary_groupby()) {
                $query .= ' JOIN ' . get_table_prefix() . 'catalogue_entries e ON e.c_name=c.c_name GROUP BY c.c_name';
            }
        }
    }
    $query .= ' ORDER BY ' . $GLOBALS['SITE_DB']->translate_field_ref('c_title') . ' ASC';
    $rows = $GLOBALS['SITE_DB']->query($query, intval(get_option('general_safety_listing_limit'))/*reasonable limit*/, null, false, false, array('c_title' => 'SHORT_TRANS'));
    if (count($rows) == intval(get_option('general_safety_listing_limit'))) {
        attach_message(do_lang_tempcode('TOO_MUCH_CHOOSE__ALPHABETICAL', escape_html(integer_format(intval(get_option('general_safety_listing_limit'))))), 'warn');
    }

    $catalogues = array();
    foreach ($rows as $row) {
        if (substr($row['c_name'], 0, 1) == '_') {
            continue;
        }

        if (!has_category_access(get_member(), 'catalogues_catalogue', $row['c_name'])) {
            continue;
        }

        if (($only_submittable) && (!has_privilege(get_member(), 'submit_midrange_content', 'cms_catalogues', array('catalogues_catalogue', $row['c_name'])))) {
            continue;
        }

        if (($row['c_ecommerce'] == 0) || (addon_installed('shopping'))) {
            $catalogues[$row['c_name']] = get_translated_text($row['c_title']);
        }
    }

    asort($catalogues);

    $out = new Tempcode();
    foreach ($catalogues as $name => $title) {
        $selected = ($name == $it);
        $out->attach(form_input_list_entry($name, $selected, $title));
    }

    return $out;
}

/**
 * Get a nice, formatted XHTML list extending from the root, and showing all subcategories, and their subcategories (ad infinitum).
 *
 * @param  ID_TEXT $catalogue_name The catalogue name
 * @param  ?AUTO_LINK $it The currently selected entry (null: none)
 * @param  boolean $addable_filter Whether to only show for what may be added to by the current member
 * @param  boolean $use_compound_list Whether to make the list elements store comma-separated child lists instead of IDs
 * @return Tempcode The list of categories
 */
function create_selection_list_catalogue_category_tree($catalogue_name, $it = null, $addable_filter = false, $use_compound_list = false)
{
    if ($GLOBALS['SITE_DB']->query_select_value('catalogue_categories', 'COUNT(*)', array('c_name' => $catalogue_name)) > intval(get_option('general_safety_listing_limit'))) {
        return new Tempcode(); // Too many!
    }

    $tree = array();
    $temp_rows = $GLOBALS['SITE_DB']->query_select('catalogue_categories', array('id', 'cc_title'), array('c_name' => $catalogue_name, 'cc_parent_id' => null), 'ORDER BY cc_order,' . $GLOBALS['SITE_DB']->translate_field_ref('cc_title'), intval(get_option('general_safety_listing_limit'))/*reasonable limit to stop it dying*/);
    if (count($temp_rows) == intval(get_option('general_safety_listing_limit'))) {
        attach_message(do_lang_tempcode('TOO_MUCH_CHOOSE__ALPHABETICAL', escape_html(integer_format(intval(get_option('general_safety_listing_limit'))))), 'warn');
    }
    foreach ($temp_rows as $row) {
        $category_id = $row['id'];
        $subtree = get_catalogue_category_tree($catalogue_name, $category_id, '', $row, null, $addable_filter, $use_compound_list);
        if (($use_compound_list) && (array_key_exists(0, $subtree))) {
            $subtree = $subtree[0];
        }
        $tree = array_merge($tree, $subtree);
    }

    $out = new Tempcode();
    foreach ($tree as $category) {
        if (($addable_filter) && (!$category['addable'])) {
            continue;
        }

        $selected = ($category['id'] == $it);
        $line = do_template('CATALOGUE_CATEGORIES_LIST_LINE', array('_GUID' => '9f6bfc4f28c154c8f5d8887ce0d47c1c', 'BREADCRUMBS' => $category['breadcrumbs'], 'COUNT' => integer_format($category['entries_count'])));
        $out->attach(form_input_list_entry(!$use_compound_list ? strval($category['id']) : $category['compound_list'], $selected, protect_from_escaping($line->evaluate())));
    }

    return $out;
}

/**
 * Get a list of maps containing all the subcategories, and path information, of the specified category - and those beneath it, recursively.
 *
 * @param  ID_TEXT $catalogue_name The catalogue name
 * @param  ?AUTO_LINK $category_id The category being at the root of our recursion (null: true root category)
 * @param  string $breadcrumbs The breadcrumbs up to this point in the recursion
 * @param  ?array $category_details The category details of the $category_id we are currently going through (null: look it up). This is here for efficiency reasons, as finding children IDs to recurse to also reveals the childs details
 * @param  ?integer $levels The number of recursive levels to search (null: all)
 * @param  boolean $addable_filter Whether to only show for what may be added to by the current member
 * @param  boolean $use_compound_list Whether to make the list elements store comma-separated child lists instead of IDs
 * @param  boolean $do_stats Whether to collect entry counts with our tree information
 * @return array A list of maps for all subcategories. Each map entry containins the fields 'id' (category ID) and 'breadcrumbs' (path to the category, including the categories own title), and 'entries_count' (the number of entries in the category).
 */
function get_catalogue_category_tree($catalogue_name, $category_id, $breadcrumbs = '', $category_details = null, $levels = null, $addable_filter = false, $use_compound_list = false, $do_stats = false)
{
    if (!$use_compound_list) {
        if ($levels == -1) {
            return $use_compound_list ? array(array(), '') : array();
        }
    }

    if (!has_category_access(get_member(), 'catalogues_catalogue', $catalogue_name)) {
        return $use_compound_list ? array(array(), '') : array();
    }
    if (($category_id !== null) && (get_value('disable_cat_cat_perms') !== '1') && (!has_category_access(get_member(), 'catalogues_category', strval($category_id)))) {
        return $use_compound_list ? array(array(), '') : array();
    }

    if ($category_details === null && $category_id !== null) {
        $_category_details = $GLOBALS['SITE_DB']->query_select('catalogue_categories', array('cc_title'), array('id' => $category_id), '', 1);
        if (!array_key_exists(0, $_category_details)) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE', 'catalogue_category'));
        }
        $category_details = $_category_details[0];
    }

    $title = ($category_details === null) ? do_lang('HOME') : get_translated_text($category_details['cc_title']);
    $breadcrumbs .= $title;

    // We'll be putting all children in this entire tree into a single list
    $children = array();
    $is_tree = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogues', 'c_is_tree', array('c_name' => $catalogue_name));
    if ($is_tree === null) {
        warn_exit(do_lang_tempcode('_MISSING_RESOURCE', escape_html($catalogue_name), 'catalogue'));
    }
    if ($category_id !== null) {
        $children[0]['id'] = $category_id;
        $children[0]['title'] = $title;
        $children[0]['breadcrumbs'] = $breadcrumbs;
        $children[0]['compound_list'] = strval($category_id) . ',';
        $children[0]['entries_count'] = $GLOBALS['SITE_DB']->query_select_value('catalogue_entries', 'COUNT(*)', array('cc_id' => $category_id));
        if ($addable_filter) {
            $children[0]['addable'] = has_submit_permission('mid', get_member(), get_ip_address(), 'cms_catalogues', array('catalogues_catalogue', $catalogue_name) + ((get_value('disable_cat_cat_perms') !== '1') ? array('catalogues_category', $category_id) : array()));
        }
    }

    // Children of this category
    $rows = $GLOBALS['SITE_DB']->query_select('catalogue_categories', array('id', 'cc_title'), array('c_name' => $catalogue_name, 'cc_parent_id' => $category_id), 'ORDER BY cc_order,' . $GLOBALS['SITE_DB']->translate_field_ref('cc_title'), intval(get_option('general_safety_listing_limit'))/*reasonable limit to stop it dying*/);
    if (get_page_name() == 'cms_catalogues') {
        if (count($rows) == intval(get_option('general_safety_listing_limit'))) {
            attach_message(do_lang_tempcode('TOO_MUCH_CHOOSE__ALPHABETICAL', escape_html(integer_format(intval(get_option('general_safety_listing_limit'))))), 'warn');
        }
    }
    $no_root = !array_key_exists(0, $children);
    if (!$no_root) {
        $children[0]['child_count'] = count($rows);
    }
    $child_breadcrumbs = $breadcrumbs . ' > ';
    if (($levels !== 0) || ($use_compound_list)) {
        foreach ($rows as $child) {
            $child_id = $child['id'];

            $child_children = get_catalogue_category_tree($catalogue_name, $child_id, $child_breadcrumbs, $child, ($levels === null) ? null : ($levels - 1), $addable_filter, $use_compound_list, $do_stats);
            if ($child_children != array()) {
                if ($use_compound_list) {
                    list($child_children, $_compound_list) = $child_children;
                    if (!$no_root) {
                        $children[0]['compound_list'] .= $_compound_list;
                    }
                }

                if ($levels !== 0) {
                    $children = array_merge($children, $child_children);
                }
            }
        }
    }

    return $use_compound_list ? array($children, $no_root ? '' : $children[0]['compound_list']) : $children;
}

/**
 * Get a nice, formatted XHTML list of entries, in catalogue category tree structure
 *
 * @param  ID_TEXT $catalogue_name The catalogue name
 * @param  ?AUTO_LINK $it The currently selected entry (null: none selected)
 * @param  ?AUTO_LINK $submitter Only show entries submitted by this member (null: no filter)
 * @param  boolean $editable_filter Whether to only show for what may be edited by the current member
 * @return Tempcode The list of entries
 */
function create_selection_list_catalogue_entries_tree($catalogue_name, $it = null, $submitter = null, $editable_filter = false)
{
    $tree = get_catalogue_entries_tree($catalogue_name, $submitter, null, null, null, null, $editable_filter);

    $out = ''; // XHTMLXHTML
    foreach ($tree as $category) {
        foreach ($category['entries'] as $eid => $etitle) {
            $selected = ($eid == $it);
            $line = do_template('CATALOGUE_ENTRIES_LIST_LINE', array('_GUID' => '0ccffeff5b80b1840188b83aaee8d9f2', 'BREADCRUMBS' => $category['breadcrumbs'], 'NAME' => $etitle));
            $out .= '<option value="' . strval($eid) . '"' . ($selected ? 'selected="selected"' : '') . '>' . $line->evaluate() . '</option>' . "\n";
        }
    }

    if ($GLOBALS['XSS_DETECT']) {
        ocp_mark_as_escaped($out);
    }

    return make_string_tempcode($out);
}

/**
 * Get a list of maps containing all the catalogue entries, and path information, under the specified category - and those beneath it, recursively.
 *
 * @param  ID_TEXT $catalogue_name The catalogue name
 * @param  ?AUTO_LINK $submitter Only show entries submitted by this member (null: no filter)
 * @param  ?AUTO_LINK $category_id The category being at the root of our recursion (null: true root)
 * @param  ?string $breadcrumbs The breadcrumbs up to this point in the recursion (null: blank, as we are starting the recursion)
 * @param  ?ID_TEXT $title The name of the $category_id we are currently going through (null: look it up). This is here for efficiency reasons, as finding children IDs to recurse to also reveals the childs title
 * @param  ?integer $levels The number of recursive levels to search (null: all)
 * @param  boolean $editable_filter Whether to only show for what may be edited by the current member
 * @return array A list of maps for all categories. Each map entry containins the fields 'id' (category ID) and 'breadcrumbs' (path to the category, including the categories own title), and more.
 */
function get_catalogue_entries_tree($catalogue_name, $submitter = null, $category_id = null, $breadcrumbs = null, $title = null, $levels = null, $editable_filter = false)
{
    if (($category_id === null) && ($levels === null)) {
        if ($GLOBALS['SITE_DB']->query_select_value('catalogue_categories', 'COUNT(*)', array('c_name' => $catalogue_name)) > intval(get_option('general_safety_listing_limit'))) {
            return array(); // Too many!
        }
    }

    if ($category_id === null) {
        $is_tree = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogues', 'c_is_tree', array('c_name' => $catalogue_name));
        if ($is_tree === null) {
            return array();
        }
        if ($is_tree == 0) {
            $temp_rows = $GLOBALS['SITE_DB']->query_select('catalogue_categories', array('id', 'cc_title'), array('c_name' => $catalogue_name, 'cc_parent_id' => null), 'ORDER BY cc_order,' . $GLOBALS['SITE_DB']->translate_field_ref('cc_title'), intval(get_option('general_safety_listing_limit'))/*reasonable limit to stop it dying*/);
            if (get_page_name() == 'cms_catalogues') {
                if (count($temp_rows) == intval(get_option('general_safety_listing_limit'))) {
                    attach_message(do_lang_tempcode('TOO_MUCH_CHOOSE__ALPHABETICAL', escape_html(integer_format(intval(get_option('general_safety_listing_limit'))))), 'warn');
                }
            }
            $children = array();
            foreach ($temp_rows as $row) {
                $children = array_merge(get_catalogue_entries_tree($catalogue_name, $submitter, $row['id'], null, get_translated_text($row['cc_title']), 1, $editable_filter), $children);
            }
            return $children;
        }

        $temp_rows = $GLOBALS['SITE_DB']->query_select('catalogue_categories', array('id', 'cc_title'), array('c_name' => $catalogue_name, 'cc_parent_id' => null), 'ORDER BY cc_order,' . $GLOBALS['SITE_DB']->translate_field_ref('cc_title'), 1);
        if (!array_key_exists(0, $temp_rows)) {
            return array();
        }
        $category_id = $temp_rows[0]['id'];
        $title = get_translated_text($temp_rows[0]['cc_title']);
    }
    if ($breadcrumbs === null) {
        $breadcrumbs = '';
    }

    if (!has_category_access(get_member(), 'catalogues_catalogue', $catalogue_name)) {
        return array();
    }
    if ((get_value('disable_cat_cat_perms') !== '1') && (!has_category_access(get_member(), 'catalogues_category', strval($category_id)))) {
        return array();
    }

    // Put our title onto our breadcrumbs
    if ($title === null) {
        $_title = $GLOBALS['SITE_DB']->query_select_value_if_there('catalogue_categories', 'cc_title', array('id' => $category_id));
        if ($_title === null) {
            return array();
        }
        $title = get_translated_text($_title);
    }
    $breadcrumbs .= $title;

    // We'll be putting all children in this entire tree into a single list
    $children = array();
    $children[0] = array();
    $children[0]['id'] = $category_id;
    $children[0]['title'] = $title;
    $children[0]['breadcrumbs'] = $breadcrumbs;

    // Children of this category
    $rows = $GLOBALS['SITE_DB']->query_select('catalogue_categories', array('id', 'cc_title'), array('cc_parent_id' => $category_id), '', intval(get_option('general_safety_listing_limit'))/*reasonable limit to stop it dying*/);
    if (get_page_name() == 'cms_catalogues') {
        if (count($rows) == intval(get_option('general_safety_listing_limit'))) {
            attach_message(do_lang_tempcode('TOO_MUCH_CHOOSE__RECENT_ONLY', escape_html(integer_format(intval(get_option('general_safety_listing_limit'))))), 'warn');
        }
    }
    $where = array('cc_id' => $category_id);
    if ($submitter !== null) {
        $where['ce_submitter'] = $submitter;
    }
    $erows = $GLOBALS['SITE_DB']->query_select('catalogue_entries', array('id', 'ce_submitter'), $where, 'ORDER BY ce_add_date DESC', intval(get_option('general_safety_listing_limit'))/*reasonable limit*/);
    if (get_page_name() == 'cms_catalogues') {
        if (count($erows) == intval(get_option('general_safety_listing_limit'))) {
            attach_message(do_lang_tempcode('TOO_MUCH_CHOOSE__RECENT_ONLY', escape_html(integer_format(intval(get_option('general_safety_listing_limit'))))), 'warn');
        }
    }
    $children[0]['entries'] = array();
    foreach ($erows as $row) {
        if (($editable_filter) && (!has_edit_permission('mid', get_member(), $row['ce_submitter'], 'cms_catalogues', array('catalogues_catalogue', $catalogue_name) + ((get_value('disable_cat_cat_perms') !== '1') ? array('catalogues_category', $category_id) : array())))) {
            continue;
        }

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

        $children[0]['entries'][$row['id']] = $name;
    }
    asort($children[0]['entries']);
    $children[0]['child_entry_count'] = count($children[0]['entries']);
    if ($levels === 0) { // We throw them away now because they're not on the desired level
        $children[0]['entries'] = array();
    }
    $children[0]['child_count'] = count($rows);
    $breadcrumbs .= ' > ';
    if ($levels !== 0) {
        foreach ($rows as $child) {
            $child_id = $child['id'];
            $child_title = get_translated_text($child['cc_title']);
            $child_breadcrumbs = $breadcrumbs;

            $child_children = get_catalogue_entries_tree($catalogue_name, $submitter, $child_id, $child_breadcrumbs, $child_title, ($levels === null) ? null : ($levels - 1), $editable_filter);

            $children = array_merge($children, $child_children);
        }
    }

    return $children;
}

/**
 * Get a formatted XHTML string of the route back to the specified root, from the specified category.
 *
 * @param  AUTO_LINK $category_id The category we are finding for
 * @param  ?AUTO_LINK $root The root of the tree (null: the true root)
 * @param  boolean $no_link_for_me_sir Whether to include category links at this level (the recursed levels will always contain links - the top level is optional, hence this parameter)
 * @param  boolean $attach_to_url_filter Whether to copy through any filter parameters in the URL, under the basis that they are associated with what this box is browsing
 * @return array The breadcrumbs
 */
function catalogue_category_breadcrumbs($category_id, $root = null, $no_link_for_me_sir = true, $attach_to_url_filter = false)
{
    if ($category_id === null) {
        return array();
    }

    $map = array('page' => 'catalogues', 'type' => 'category', 'id' => $category_id);
    if (get_page_name() == 'catalogues') {
        $map += propagate_filtercode();
    }
    $page_link = build_page_link($map, get_module_zone('catalogues'));

    if (($category_id != $root) || (!$no_link_for_me_sir)) {
        global $PT_PAIR_CACHE;
        if (!array_key_exists($category_id, $PT_PAIR_CACHE)) {
            $category_rows = $GLOBALS['SITE_DB']->query_select('catalogue_categories', array('cc_parent_id', 'cc_title'), array('id' => $category_id), '', 1);
            if (!array_key_exists(0, $category_rows)) {
                attach_message(do_lang_tempcode('CAT_NOT_FOUND', escape_html(strval($category_id)), 'catalogue_category'), 'warn');
                return array();
            }
            $PT_PAIR_CACHE[$category_id] = $category_rows[0];
        }

        if ($PT_PAIR_CACHE[$category_id]['cc_parent_id'] == $category_id) {
            fatal_exit(do_lang_tempcode('RECURSIVE_TREE_CHAIN', escape_html(strval($category_id)), 'catalogue_category'));
        }
    }

    if ($category_id == $root) {
        $below = array();
    } else {
        $below = catalogue_category_breadcrumbs($PT_PAIR_CACHE[$category_id]['cc_parent_id'], $root, false, $attach_to_url_filter);
    }

    $segments = array();

    if (!$no_link_for_me_sir) {
        $title = get_translated_text($PT_PAIR_CACHE[$category_id]['cc_title']);
        $segments[] = array($page_link, $title);
    }

    return array_merge($below, $segments);
}

/**
 * Check the current catalogue is an ecommerce catalogue
 *
 * @param  SHORT_TEXT $catalogue_name Catalogue name
 * @param  ?array $catalogue Catalogue row (null: look up)
 * @return boolean Status of ecommerce catalogue check
 */
function is_ecommerce_catalogue($catalogue_name, $catalogue = null)
{
    if (($catalogue !== null) && ($catalogue['c_ecommerce'] == 0)) {
        return false;
    }
    if (!addon_installed('ecommerce')) {
        return false;
    }
    if (!addon_installed('shopping')) {
        return false;
    }

    if ($GLOBALS['SITE_DB']->query_select_value_if_there('catalogues', 'c_name', array('c_name' => $catalogue_name, 'c_ecommerce' => 1)) === null) {
        return false;
    } else {
        return true;
    }
}

/**
 * Check selected entry is an ecommerce catalogue entry
 *
 * @param  AUTO_LINK $entry_id Entry ID
 * @return boolean Status of entry type check
 */
function is_ecommerce_catalogue_entry($entry_id)
{
    $catalogue_name = $GLOBALS['SITE_DB']->query_select_value('catalogue_entries', 'c_name', array('id' => $entry_id));

    return is_ecommerce_catalogue($catalogue_name);
}

/**
 * Display a catalogue entry
 *
 * @param  AUTO_LINK $id Entry ID
 * @param  boolean $no_title Whether to skip rendering a title
 * @param  boolean $attach_to_url_filter Whether to copy through any filter parameters in the URL, under the basis that they are associated with what this box is browsing
 * @return Tempcode Tempcode interface to display an entry
 */
function render_catalogue_entry_screen($id, $no_title = false, $attach_to_url_filter = true)
{
    if (addon_installed('content_privacy')) {
        require_code('content_privacy');
        check_privacy('catalogue_entry', strval($id));
    }

    require_code('feedback');

    if (addon_installed('ecommerce')) {
        require_code('ecommerce');
    }

    require_code('images');
    require_css('catalogues');
    require_lang('catalogues');

    $entries = $GLOBALS['SITE_DB']->query_select('catalogue_entries', array('*'), array('id' => $id), '', 1);
    if (!array_key_exists(0, $entries)) {
        return warn_screen(get_screen_title('CATALOGUES'), do_lang_tempcode('MISSING_RESOURCE', 'catalogue_entry'));
    }
    $entry = $entries[0];

    $categories = $GLOBALS['SITE_DB']->query_select('catalogue_categories', array('*'), array('id' => $entry['cc_id']), '', 1);
    if (!array_key_exists(0, $categories)) {
        warn_exit(do_lang_tempcode('CAT_NOT_FOUND', escape_html(strval($entry['cc_id'])), 'catalogue_category'));
    }
    $category = $categories[0];
    require_code('site');
    set_feed_url('?mode=catalogues&select=' . strval($entry['cc_id']));

    $catalogue_name = $category['c_name'];
    $catalogue = load_catalogue_row($catalogue_name);

    // Permission for here?
    if (!has_category_access(get_member(), 'catalogues_catalogue', $catalogue_name)) {
        access_denied('CATALOGUE_ACCESS');
    }
    if ((get_value('disable_cat_cat_perms') !== '1') && (!has_category_access(get_member(), 'catalogues_category', strval($entry['cc_id'])))) {
        access_denied('CATEGORY_ACCESS');
    }

    $ecommerce = is_ecommerce_catalogue($catalogue_name);

    if ($ecommerce) {
        $tpl_set = 'products';
    } else {
        $tpl_set = $catalogue_name;
    }

    $count_views = (get_db_type() != 'xml') && (get_value('no_view_counts') !== '1') && (is_null(get_bot_type()));
    if ($count_views) {
        $entry['ce_views']++;
    }

    $root = get_param_integer('keep_catalogue_' . $catalogue_name . '_root', null);
    $breadcrumbs = array();
    $map = get_catalogue_entry_map($entry, $catalogue, 'PAGE', $tpl_set, $root, null, null, true, true, null, $breadcrumbs);

    if ($count_views) {
        if (!$GLOBALS['SITE_DB']->table_is_locked('catalogue_entries')) {
            $GLOBALS['SITE_DB']->query_update('catalogue_entries', array('ce_views' => $entry['ce_views']), array('id' => $id), '', 1, null, false, true);
        }
    }

    // Validation
    if (($entry['ce_validated'] == 0) && (addon_installed('unvalidated'))) {
        if ((!has_privilege(get_member(), 'jump_to_unvalidated')) && ((is_guest()) || ($entry['ce_submitter'] != get_member()))) {
            access_denied('PRIVILEGE', 'jump_to_unvalidated');
        }

        $map['WARNINGS'] = do_template('WARNING_BOX', array(
            '_GUID' => 'bf604859a572ca53e969bec3d91f9cfb',
            'WARNING' => do_lang_tempcode((get_param_integer('redirected', 0) == 1) ? 'UNVALIDATED_TEXT_NON_DIRECT' : 'UNVALIDATED_TEXT', 'catalogue_entry'),
        ));
    } else {
        $map['WARNINGS'] = '';
    }

    // Finding any hook exists for this product
    if (addon_installed('ecommerce')) {
        $object = find_product(strval($id));
        if (is_object($object) && method_exists($object, 'get_custom_product_map_fields')) {
            $object->get_custom_product_map_fields($id, $map);
        }
    }

    // Main rendering...

    $map['ENTRY'] = do_template('CATALOGUE_' . $tpl_set . '_FIELDMAP_ENTRY_WRAP', $map + array('ENTRY_SCREEN' => true, 'GIVE_CONTEXT' => false), null, false, 'CATALOGUE_DEFAULT_FIELDMAP_ENTRY_WRAP');
    $map['ADD_DATE'] = get_timezoned_date_tempcode($entry['ce_add_date']);
    $map['ADD_DATE_RAW'] = strval($entry['ce_add_date']);
    $map['EDIT_DATE'] = ($entry['ce_edit_date'] === null) ? '' : get_timezoned_date_tempcode($entry['ce_edit_date']);
    $map['EDIT_DATE_RAW'] = ($entry['ce_edit_date'] === null) ? '' : strval($entry['ce_edit_date']);
    $map['VIEWS'] = integer_format($entry['ce_views']);
    $title_to_use = do_lang_tempcode($catalogue_name . '__CATALOGUE_ENTRY', $map['FIELD_0']);
    $title_to_use_2 = do_lang($catalogue_name . '__CATALOGUE_ENTRY', $map['FIELD_0_PLAIN'], null, null, null, false);
    if ($title_to_use_2 === null) {
        $title_to_use = do_lang_tempcode('DEFAULT__CATALOGUE_ENTRY', make_fractionable_editable('catalogue_entry', $id, is_object($map['FIELD_0']) ? $map['FIELD_0'] : make_string_tempcode($map['FIELD_0'])));
        $title_to_use_2 = do_lang('DEFAULT__CATALOGUE_ENTRY', is_object($map['FIELD_0']) ? $map['FIELD_0']->evaluate() : $map['FIELD_0']);
        $len = strlen(trim(strip_html($title_to_use_2)));
        if (($len > 20) || ($len < 3)) {// We revert to raw ID if it appeared the rendered one was not strippable back from HTML to text; raw ID is possibly cryptic unfortunately
            $title_to_use_2 = do_lang('DEFAULT__CATALOGUE_ENTRY', $map['FIELD_0_PLAIN']);
        }
    }
    if ($no_title) {
        $map['TITLE'] = new Tempcode();
    } else {
        if ((get_value('no_awards_in_titles') !== '1') && (addon_installed('awards'))) {
            require_code('awards');
            $awards = find_awards_for('catalogue_entry', strval($id));
        } else {
            $awards = array();
        }
        $map['TITLE'] = get_screen_title($title_to_use, false, null, null, $awards);
    }
    $map['SUBMITTER'] = strval($entry['ce_submitter']);

    require_code('seo2');
    if (is_object($title_to_use_2)) {
        $title_to_use_2 = $title_to_use_2->evaluate();
    }
    seo_meta_load_for('catalogue_entry', strval($id), strip_html($title_to_use_2));

    $map['CATEGORY_TITLE'] = get_translated_text($category['cc_title']);
    $map['CAT'] = strval($entry['cc_id']);

    $map['TAGS'] = get_loaded_tags('catalogue_entries');

    $_breadcrumbs = array();
    if ($root === null) {
        $_breadcrumbs[] = array('_SELF:_SELF:browse' . ($ecommerce ? ':ecommerce=1' : ''), do_lang_tempcode('CATALOGUES'));
    }
    if ($catalogue['c_is_tree'] == 1) {
        $breadcrumbs = array_merge($_breadcrumbs, $breadcrumbs);
    } else {
        if ($root === null) {
            $page_link = build_page_link(array('page' => '_SELF', 'type' => 'index', 'id' => $catalogue_name, 'tree' => $catalogue['c_is_tree']), '_SELF');
            $_breadcrumbs[] = array($page_link, get_translated_text($catalogue['c_title']));
        }
        $breadcrumbs = array_merge($_breadcrumbs, $breadcrumbs);
    }
    $breadcrumbs[] = array('', $title_to_use);
    breadcrumb_set_parents($breadcrumbs);

    set_extra_request_metadata(array(
        'type' => get_translated_text($catalogue['c_title']) . ' entry',
        'title' => comcode_escape($title_to_use_2),
        'identifier' => '_SEARCH:catalogues:entry:' . strval($id),
    ), $entry, 'catalogue_entry', strval($id));

    return do_template('CATALOGUE_' . $tpl_set . '_ENTRY_SCREEN', $map, null, false, 'CATALOGUE_DEFAULT_ENTRY_SCREEN');
}
