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

/*
global3.php contains further support functions, which are shared between the installer and the main installation (i.e. global.php and global2.php are not used by the installer, and the installer emulates these functions functionality via minikernel.php).
*/

/**
 * Standard code module initialisation function.
 *
 * @ignore
 */
function init__global3()
{
    global $PAGE_NAME_CACHE, $GETTING_PAGE_NAME;
    $PAGE_NAME_CACHE = null;
    $GETTING_PAGE_NAME = false;

    global $IS_MOBILE_CACHE, $IS_MOBILE_TRUTH_CACHE;
    $IS_MOBILE_CACHE = null;
    $IS_MOBILE_TRUTH_CACHE = null;

    // Heavily optimised string escaping data
    global $PHP_REP_FROM, $PHP_REP_TO, $PHP_REP_TO_TWICE;
    $PHP_REP_FROM = array('\\', "\n", '$', '"', "\0");
    $PHP_REP_TO = array('\\\\', '\n', '\$', '\\"', '\0');
    $PHP_REP_TO_TWICE = array('\\\\\\\\', '\\\\n', '\\\\\\$', '\\\\\"', '\\\\0');

    global $BOT_MAP_CACHE, $BOT_TYPE_CACHE;
    $BOT_MAP_CACHE = null;
    $BOT_TYPE_CACHE = false;

    global $LOCALE_FILTER_CACHE;
    $LOCALE_FILTER_CACHE = null;

    global $HAS_COOKIES_CACHE;
    $HAS_COOKIES_CACHE = null;

    global $BROWSER_MATCHES_CACHE;
    $BROWSER_MATCHES_CACHE = array();

    global $MSN_DB;
    $MSN_DB = null;

    // This is like null, but is a higher-precedence null that can also survive string layers (such as HTML forms). It should only be used when:
    //  - 'null' or '' or '-1' aren't appropriate (although '-1' is only appropriate when dealing with numbers held in strings, really).
    //  - OR, as the standard "ignore this field" indicator for query_update (so that "fractional edits" can happen without requiring a secondary API set or a messed up primary API)
    if (!defined('STRING_MAGIC_NULL')) {
        define('STRING_MAGIC_NULL', '!--:)abcUNLIKELY');
    }
    // This is similar, but for integers. As before, it should only be used when null and -1 aren't appropriate OR as the "ignore this field" indicator.
    if (!defined('INTEGER_MAGIC_NULL')) {
        define('INTEGER_MAGIC_NULL', 1634817353); // VERY unlikely to occur, but is both a 32bit unsigned and a 32 bit signed number
    }

    global $ZONE_DEFAULT_PAGES_CACHE;
    $ZONE_DEFAULT_PAGES_CACHE = array();

    global $IS_WIDE_CACHE, $IS_WIDE_HIGH_CACHE;
    $IS_WIDE_CACHE = null;
    $IS_WIDE_HIGH_CACHE = null;

    global $ADDON_INSTALLED_CACHE;
    $ADDON_INSTALLED_CACHE = array();

    global $OUTPUT_STATE_STACK;
    $OUTPUT_STATE_STACK = array();

    // Registry of output state globals
    global $OUTPUT_STATE_VARS;
    $OUTPUT_STATE_VARS = array(
        'HTTP_STATUS_CODE',
        'METADATA',
        'ATTACHED_MESSAGES',
        'ATTACHED_MESSAGES_RAW',
        'LATE_ATTACHED_MESSAGES',
        'SEO_KEYWORDS',
        'SEO_DESCRIPTION',
        'BREADCRUMBS',
        'BREADCRUMB_SET_PARENTS',
        'DISPLAYED_TITLE',
        'SHORT_TITLE',
        'FORCE_SET_TITLE',
        'BREADCRUMB_SET_SELF',
        'FEED_URL',
        'FEED_URL_2',
        'OUTPUT_STATE_STACK',
        'REFRESH_URL',
        'FORCE_META_REFRESH',
        'QUICK_REDIRECT',
        'EXTRA_HEAD',
        'EXTRA_FOOT',
        'HELPER_PANEL_TEXT',
        'HELPER_PANEL_TUTORIAL',
        'JAVASCRIPT',
        'JAVASCRIPTS',
        'JS_OUTPUT_STARTED',
        'JAVASCRIPT_BOTTOM',
        'CSSS',
        'CSS_OUTPUT_STARTED',
        'CYCLES',
        'TEMPCODE_SETGET',
        'COMCODE_PARSE_TITLE',
    );
    _load_blank_output_state();

    global $MASS_IMPORT_HAPPENING;
    $MASS_IMPORT_HAPPENING = false;

    if (!defined('A_NA')) {
        // Notifications (defined here, as notification_poller may need them - yet we don't want to include all the notification dispatch code)
        define('A_NA', 0x0); // Not applicable          (0 in decimal)
        //
        define('A_INSTANT_EMAIL', 0x2);         // (2 in decimal)
        define('A_DAILY_EMAIL_DIGEST', 0x4);    // (4 in decimal)
        define('A_WEEKLY_EMAIL_DIGEST', 0x8);   // (8 in decimal)
        define('A_MONTHLY_EMAIL_DIGEST', 0x10); // (16 in decimal)
        define('A_INSTANT_SMS', 0x20);          // (32 in decimal)
        define('A_INSTANT_PT', 0x40);           // (64 in decimal)  Private topic
        define('A_WEB_NOTIFICATION', 0x80);     // (128 in decimal) Desktop notification if site is open, and always shows on notification dropdown
        // And...
        define('A__ALL', 0xFFFFFF);
        // And...
        define('A__STATISTICAL', -0x1); // This is magic, it will choose whatever the user probably wants, based on their existing settings
        define('A__CHOICE', -0x2); // Never stored in DB, used as a flag inside admin_notifications module
    }

    global $ESCAPE_HTML_OUTPUT, $KNOWN_TRUE_HTML; // Used to track what is already escaped in kid-gloves modes
    $ESCAPE_HTML_OUTPUT = array();
    $KNOWN_TRUE_HTML = array();

    if (!defined('WYSIWYG_COMCODE__BUTTON')) {
        // Would normally put these in sources/comcode.php, but some of our templating references these constants
        define('WYSIWYG_COMCODE__BUTTON', 1);
        define('WYSIWYG_COMCODE__XML_BLOCK', 2);
        define('WYSIWYG_COMCODE__XML_BLOCK_ESCAPED', WYSIWYG_COMCODE__XML_BLOCK + 4);
        define('WYSIWYG_COMCODE__XML_BLOCK_ANTIESCAPED', WYSIWYG_COMCODE__XML_BLOCK + 8);
        define('WYSIWYG_COMCODE__XML_INLINE', 16);
        define('WYSIWYG_COMCODE__STANDOUT_BLOCK', WYSIWYG_COMCODE__XML_BLOCK + 32);
        define('WYSIWYG_COMCODE__STANDOUT_INLINE', WYSIWYG_COMCODE__XML_INLINE + 64);
        define('WYSIWYG_COMCODE__HTML', 128);
    }

    global $DOING_OUTPUT_PINGS;
    $DOING_OUTPUT_PINGS = false;

    global $DISABLE_SMART_DECACHING_TEMPORARILY;
    $DISABLE_SMART_DECACHING_TEMPORARILY = false;
}

/**
 * Get the file extension of the specified file. It returns without a dot.
 *
 * @param  string $name The filename
 * @return string The filename extension (no dot)
 */
function get_file_extension($name)
{
    $dot_pos = strrpos($name, '.');
    if ($dot_pos === false) {
        return '';
    }
    return strtolower(substr($name, $dot_pos + 1));
}

/**
 * Find whether we can get away with natural file access, not messing with AFMs, world-writability, etc.
 *
 * @return boolean Whether we have this
 */
function is_suexec_like()
{
    if (running_script('webdav')) {
        return true; // Has to assume so, as cannot intercede
    }

    if (GOOGLE_APPENGINE) {
        return false;
    }

    static $answer = null;
    if ($answer === null) {
        $answer = (((php_function_allowed('posix_getuid')) && (!isset($_SERVER['HTTP_X_MOSSO_DT'])) && (is_integer(@posix_getuid())) && (@posix_getuid() == @fileowner(get_file_base() . '/' . (running_script('install') ? 'install.php' : 'sources/global.php'))))
                   || (is_writable_wrap(get_file_base() . '/' . (running_script('install') ? 'install.php' : 'sources/global.php'))));
    }
    return $answer;
}

/**
 * Ensure that the specified file/folder is writeable for the FTP user (so that it can be deleted by the system), and should be called whenever a file is uploaded/created, or a folder is made. We call this function assuming we are giving world permissions.
 *
 * @param  PATH $path The full pathname to the file/directory
 * @param  ?integer $perms The permissions to make (not the permissions are reduced if the function finds that the file is owned by the web user [doesn't need world permissions then]) (null: default for file/dir)
 */
function fix_permissions($path, $perms = null)
{
    if (is_null($perms)) {
        $perms = is_dir($path) ? 0777 : 0666;
    }

    // If the file user is different to the FTP user, we need to make it world writeable
    if ((!is_suexec_like()) || (cms_srv('REQUEST_METHOD') == '')) {
        if ($perms == 0600) {
            @chmod($path, 0666);
        } else {
            @chmod($path, $perms);
        }
    } else { // Otherwise we do not
        if ($perms == 0666) {
            @chmod($path, 0644);
        } elseif ($perms == 0777) {
            @chmod($path, 0755);
        } else {
            @chmod($path, $perms);
        }
    }

    global $_CREATED_FILES; // From ocProducts PHP version, for development testing
    if (isset($_CREATED_FILES)) {
        foreach ($_CREATED_FILES as $i => $x) {
            if ($x == $path) {
                unset($_CREATED_FILES[$i]);
            }
        }
    }
}

/**
 * Get the contents of a file, with locking support.
 *
 * @param  PATH $path File path.
 * @return ~string File contents (false: error)
 */
function cms_file_get_contents_safe($path)
{
    $tmp = fopen($path, 'rb');
    if ($tmp === false) {
        return false;
    }
    flock($tmp, LOCK_SH);
    $contents = stream_get_contents($tmp);
    flock($tmp, LOCK_UN);
    fclose($tmp);
    return $contents;
}

/**
 * Return the file in the URL by downloading it over HTTP. If a byte limit is given, it will only download that many bytes. It outputs warnings, returning null, on error.
 *
 * @param  URLPATH $url The URL to download
 * @param  ?integer $byte_limit The number of bytes to download. This is not a guarantee, it is a minimum (null: all bytes)
 * @range  1 max
 * @param  boolean $trigger_error Whether to throw a Composr error, on error
 * @param  boolean $no_redirect Whether to block redirects (returns null when found)
 * @param  string $ua The user-agent to identify as
 * @param  ?array $post_params An optional array of POST parameters to send; if this is null, a GET request is used (null: none)
 * @param  ?array $cookies An optional array of cookies to send (null: none)
 * @param  ?string $accept 'accept' header value (null: don't pass one)
 * @param  ?string $accept_charset 'accept-charset' header value (null: don't pass one)
 * @param  ?string $accept_language 'accept-language' header value (null: don't pass one)
 * @param  ?resource $write_to_file File handle to write to (null: do not do that)
 * @param  ?string $referer The HTTP referer (null: none)
 * @param  ?array $auth A pair: authentication username and password (null: none)
 * @param  float $timeout The timeout (for connecting/stalling, not for overall download time); usually it is rounded up to the nearest second, depending on the downloader implementation
 * @param  boolean $raw_post Whether to treat the POST parameters as a raw POST (rather than using MIME)
 * @param  ?array $files Files to send. Map between field to file path (null: none)
 * @param  ?array $extra_headers Extra headers to send (null: none)
 * @param  ?string $http_verb HTTP verb (null: auto-decide based on other parameters)
 * @param  string $raw_content_type The content type to use if a raw HTTP post
 * @return ?string The data downloaded (null: error)
 */
function http_download_file($url, $byte_limit = null, $trigger_error = true, $no_redirect = false, $ua = 'Composr', $post_params = null, $cookies = null, $accept = null, $accept_charset = null, $accept_language = null, $write_to_file = null, $referer = null, $auth = null, $timeout = 6.0, $raw_post = false, $files = null, $extra_headers = null, $http_verb = null, $raw_content_type = 'application/xml')
{
    require_code('files2');
    cms_profile_start_for('http_download_file');
    $ret = _http_download_file($url, $byte_limit, $trigger_error, $no_redirect, $ua, $post_params, $cookies, $accept, $accept_charset, $accept_language, $write_to_file, $referer, $auth, $timeout, $raw_post, $files, $extra_headers, $http_verb, $raw_content_type);
    cms_profile_end_for('http_download_file', $url);
    return $ret;
}

/**
 * Load a fresh output state.
 *
 * @sets_output_state
 *
 * @param  boolean $just_tempcode Whether to only restore the Tempcode execution part of the state.
 * @param  boolean $true_blank Whether to go for a completely blank state (no defaults!), not just a default fresh state.
 *
 * @ignore
 */
function _load_blank_output_state($just_tempcode = false, $true_blank = false)
{
    /*
        Now lots of stuff all relating to output state (unless commented, these GLOBALs should not be written to directly, we have API calls for it)
    */

    if (!$just_tempcode) {
        global $HTTP_STATUS_CODE;
        /** Record of the HTTP status code being set.
         *
         * @sets_output_state
         *
         * @global string $HTTP_STATUS_CODE
         */
        $HTTP_STATUS_CODE = '200';

        global $METADATA;
        $METADATA = array();

        global $ATTACHED_MESSAGES, $ATTACHED_MESSAGES_RAW, $LATE_ATTACHED_MESSAGES;
        $ATTACHED_MESSAGES = null;
        /** Raw data of attached messages.
         *
         * @sets_output_state
         *
         * @global ?array $ATTACHED_MESSAGES_RAW
         */
        $ATTACHED_MESSAGES_RAW = array();
        $LATE_ATTACHED_MESSAGES = null;

        global $SEO_KEYWORDS, $SEO_DESCRIPTION, $SHORT_TITLE;
        $SEO_KEYWORDS = null;
        $SEO_DESCRIPTION = null;
        /** Shortened title to use only within header text and thus <title> tag (if not set, $DISPLAYED_TITLE will be used).
         *
         * @sets_output_state
         *
         * @global ?string $SHORT_TITLE
         */
        $SHORT_TITLE = null;

        global $BREADCRUMBS, $BREADCRUMB_SET_PARENTS, $DISPLAYED_TITLE, $FORCE_SET_TITLE, $BREADCRUMB_SET_SELF;
        $BREADCRUMBS = null;
        $BREADCRUMB_SET_PARENTS = array();
        /** The screen title that was set (i.e. <h1>).
         *
         * @sets_output_state
         *
         * @global ?string $DISPLAYED_TITLE
         */
        $DISPLAYED_TITLE = null;
        $FORCE_SET_TITLE = false;
        $BREADCRUMB_SET_SELF = null;

        global $FEED_URL, $FEED_URL_2;
        $FEED_URL = null;
        $FEED_URL_2 = null;

        global $REFRESH_URL, $FORCE_META_REFRESH, $QUICK_REDIRECT;
        $REFRESH_URL[0] = '';
        $REFRESH_URL[1] = 0;
        $FORCE_META_REFRESH = false;
        $QUICK_REDIRECT = false;

        global $EXTRA_HEAD, $EXTRA_FOOT;
        $EXTRA_HEAD = null;
        $EXTRA_FOOT = null;

        global $HELPER_PANEL_TEXT, $HELPER_PANEL_TUTORIAL;
        $HELPER_PANEL_TEXT = '';
        $HELPER_PANEL_TUTORIAL = '';

        // Register basic CSS and JavaScript requirements
        global $JAVASCRIPT, $JAVASCRIPTS, $CSSS, $JAVASCRIPTS_DEFAULT;
        $JAVASCRIPT = null;
        /** List of required JavaScript files.
         *
         * @sets_output_state
         *
         * @global ?array $JAVASCRIPTS
         */
        $JAVASCRIPTS = $true_blank ? array() : $JAVASCRIPTS_DEFAULT;
        /** List of required CSS files.
         *
         * @sets_output_state
         *
         * @global ?array $CSSS
         */
        $CSSS = $true_blank ? array() : array('no_cache' => true, 'global' => true);
    }

    global $CYCLES, $TEMPCODE_SETGET;
    /** Stores Tempcode CYCLE values during execution.
     *
     * @sets_output_state
     *
     * @global array $CYCLE
     */
    $CYCLES = array();
    /** Stores Tempcode variable values during execution.
     *
     * @sets_output_state
     *
     * @global array $TEMPCODE_SETGET
     */
    $TEMPCODE_SETGET = array();
}

/**
 * Push the output state on the stack and create a fresh one.
 *
 * @sets_output_state
 *
 * @param  boolean $just_tempcode Whether to only restore the Tempcode execution part of the state.
 * @param  boolean $true_blank Whether to go for a completely blank state (no defaults!), not just a default fresh state.
 */
function push_output_state($just_tempcode = false, $true_blank = false)
{
    global $OUTPUT_STATE_STACK, $OUTPUT_STATE_VARS;
    $current_state = array();
    foreach ($OUTPUT_STATE_VARS as $var) {
        $current_state[$var] = isset($GLOBALS[$var]) ? $GLOBALS[$var] : null;
    }
    array_push($OUTPUT_STATE_STACK, $current_state);
    _load_blank_output_state($just_tempcode, $true_blank);
}

/**
 * Restore the last output state on the stack, or a fresh one if none was pushed.
 *
 * @sets_output_state
 *
 * @param  boolean $just_tempcode Whether to only restore the Tempcode execution part of the state.
 * @param  boolean $merge_current Whether to merge the current output state in.
 * @param  ?array $keep Settings to keep / merge if possible (null: none).
 */
function restore_output_state($just_tempcode = false, $merge_current = false, $keep = null)
{
    global $OUTPUT_STATE_STACK;

    if ($keep === null) {
        $keep = array();
    }

    $mergeable_arrays = array('METADATA' => true, 'JAVASCRIPTS' => true, 'CSSS' => true, 'TEMPCODE_SETGET' => true, 'CYCLES' => true, 'ATTACHED_MESSAGES_RAW' => true);
    $mergeable_tempcode = array('EXTRA_HEAD' => true, 'EXTRA_FOOT' => true, 'JAVASCRIPT' => true, 'ATTACHED_MESSAGES' => true, 'LATE_ATTACHED_MESSAGES' => true);

    $old_state = array_pop($OUTPUT_STATE_STACK);
    if ($old_state === null) {
        _load_blank_output_state($just_tempcode);
    } else {
        foreach ($old_state as $var => $val) {
            if ((!$just_tempcode) || ($var == 'CYCLES') || ($var == 'TEMPCODE_SETGET')) {
                $merge_array = (($merge_current) && (is_array($val)) && (isset($mergeable_arrays[$var])));
                $merge_tempcode = (($merge_current) && (isset($val->codename/*faster than is_object*/)) && (isset($mergeable_tempcode[$var])));
                $mergeable = $merge_array || $merge_tempcode;
                if (($keep === array()) || (!in_array($var, $keep)) || ($mergeable)) {
                    if ($merge_array) {
                        if ($GLOBALS[$var] === null) {
                            $GLOBALS[$var] = array();
                        }
                        $GLOBALS[$var] = array_merge($val, $GLOBALS[$var]);
                    } elseif ($merge_tempcode) {
                        if ($GLOBALS[$var] === null) {
                            $GLOBALS[$var] = new Tempcode();
                        }
                        if ($val !== null) {
                            $GLOBALS[$var]->attach($val);
                        }
                    } elseif (!$merge_current || !isset($GLOBALS[$var]) || $GLOBALS[$var] === array() || $GLOBALS[$var] === false || $GLOBALS[$var] === '' || $var == 'REFRESH_URL') {
                        $GLOBALS[$var] = $val;
                    }
                }
            }
        }
    }
}

/**
 * Turn the Tempcode lump into a standalone page.
 *
 * @param  ?Tempcode $middle The Tempcode to put into a nice frame (null: support output streaming mode)
 * @param  ?mixed $message 'Additional' message (null: none)
 * @param  string $type The type of special message
 * @set    inform warn ""
 * @param  boolean $include_header_and_footer Whether to include the header/footer/panels
 * @param  boolean $show_border Whether to include a full screen rendering layout (will be overridable by 'show_border' GET parameter if present or if main page view)
 * @return Tempcode Standalone page
 */
function globalise($middle, $message = null, $type = '', $include_header_and_footer = false, $show_border = false)
{
    if (!$include_header_and_footer) { // FUDGE
        $old = mixed();
        if (isset($_GET['wide_high'])) {
            $old = $_GET['wide_high'];
        }
        $_GET['wide_high'] = '1';
    }

    require_code('site');
    if ($message !== null) {
        attach_message($message, $type);
    }

    restore_output_state(true); // Here we reset some Tempcode environmental stuff, because template compilation or preprocessing may have dirtied things

    $show_border = (get_param_integer('show_border', $show_border ? 1 : 0) == 1);
    if (!$show_border && !running_script('index')) {
        $global = do_template('STANDALONE_HTML_WRAP', array(
            '_GUID' => 'fe818a6fb0870f0b211e8e52adb23f26',
            'TITLE' => ($GLOBALS['DISPLAYED_TITLE'] === null) ? do_lang_tempcode('NA') : $GLOBALS['DISPLAYED_TITLE'],
            'FRAME' => running_script('iframe'),
            'TARGET' => '_self',
            'CONTENT' => $middle,
        ));
        if ($GLOBALS['OUTPUT_STREAMING'] || $middle !== null) {
            $global->handle_symbol_preprocessing();
        }
        return $global;
    }

    global $TEMPCODE_CURRENT_PAGE_OUTPUTTING;

    if (($middle !== null) && (isset($TEMPCODE_CURRENT_PAGE_OUTPUTTING))) { // Error happened after output and during MIDDLE processing, so bind MIDDLE as an error
        $middle->handle_symbol_preprocessing();
        $global = $TEMPCODE_CURRENT_PAGE_OUTPUTTING;
        $global->singular_bind('MIDDLE', $middle);
        // NB: We also considered the idea of using document.write() as a way to reset the output stream, but JavaScript execution will not happen before the parser (even if you force a flush and delay)
    } else {
        global $DOING_OUTPUT_PINGS;
        if (headers_sent() && !$DOING_OUTPUT_PINGS) {
            $global = do_template('STANDALONE_HTML_WRAP', array(
                '_GUID' => 'd579b62182a0f815e0ead1daa5904793',
                'TITLE' => ($GLOBALS['DISPLAYED_TITLE'] === null) ? do_lang_tempcode('NA') : $GLOBALS['DISPLAYED_TITLE'],
                'FRAME' => false,
                'TARGET' => '_self',
                'CONTENT' => $middle,
            ));
        } else {
            $global = do_template('GLOBAL_HTML_WRAP', array(
                '_GUID' => '592faa2c0e8bf2dc3492de2c11ca7131',
                'MIDDLE' => $middle,
            ));
        }
        if ($GLOBALS['OUTPUT_STREAMING'] || $middle !== null) {
            $global->handle_symbol_preprocessing();
        }
    }

    if (get_value('xhtml_strict') === '1') {
        require_code('global4');
        $global = make_xhtml_strict($global);
    }

    if ((!$include_header_and_footer) && ($old !== null)) {
        $_GET['wide_high'] = $old;
    }

    return $global;
}

/**
 * Attach some XHTML to the screen footer.
 *
 * @sets_output_state
 *
 * @param  mixed $data XHTML to attach (Tempcode or string)
 */
function attach_to_screen_footer($data)
{
    global $EXTRA_FOOT;
    if ($EXTRA_FOOT === null) {
        $EXTRA_FOOT = new Tempcode();
    }
    $EXTRA_FOOT->attach($data);
}

/**
 * Add some metadata for the request.
 *
 * @sets_output_state
 *
 * @param  array $metadata Extra metadata
 * @param  ?array $row Content row to automatically grab data from, if we also have $content_type (null: unknown)
 * @param  ?ID_TEXT $content_type Content type (null: unknown)
 * @param  ?ID_TEXT $content_id Content ID (null: unknown)
 */
function set_extra_request_metadata($metadata, $row = null, $content_type = null, $content_id = null)
{
    global $METADATA;
    $METADATA += $metadata;

    if ($content_type !== null) {
        require_code('content');
        $cma_ob = get_content_object($content_type);
        if ($cma_ob !== null) {
            $cma_info = $cma_ob->info();
            if ($cma_info === null) {
                $content_type = null;
            }
        } else {
            $content_type = null;
        }
    }

    if ($row !== null && $content_type !== null) {
        // Add in generic data...

        $cma_mappings = array(
            'created' => 'add_time_field',
            'creator' => isset($cma_info['author_field']) ? 'author_field' : 'submitter_field',
            'publisher' => 'submitter_field',
            'modified' => 'edit_time_field',
            'title' => 'title_field',
            'description' => 'description_field',
            'views' => 'views_field',
            'validated' => 'validated_field',
            'type' => 'content_type_universal_label',
        );

        foreach ($cma_mappings as $meta_type => $cma_field) {
            if (!isset($METADATA[$meta_type])) {
                if ($cma_field == 'content_type_universal_label' || isset($row[$cma_info[$cma_field]])) {
                    switch ($meta_type) {
                        case 'type':
                            $val_raw = $cma_info[$cma_field];
                            $val = $val_raw;
                            break;

                        case 'created':
                        case 'modified':
                            $val_raw = strval($row[$cma_info[$cma_field]]);
                            $val = date('Y-m-d', $row[$cma_info[$cma_field]]);
                            break;

                        case 'publisher':
                        case 'creator':
                            if ($cma_field == 'author_field') {
                                $val_raw = $row[$cma_info[$cma_field]];
                                $val = $val_raw;
                            } else {
                                $val_raw = strval($row[$cma_info[$cma_field]]);
                                $val = $GLOBALS['FORUM_DRIVER']->get_username($row[$cma_info[$cma_field]]);
                            }
                            break;

                        case 'title':
                            if ($cma_info['title_field_dereference']) {
                                $val_raw = get_translated_text($row[$cma_info[$cma_field]], $cma_info['connection']);
                            } else {
                                $val_raw = $row[$cma_info[$cma_field]];
                            }

                            if ($content_type === 'comcode_page') {
                                // FUDGE
                                if ($content_id === ':start') {
                                    $val_raw = get_site_name();
                                } else {
                                    $val_raw = titleify($val_raw);
                                }
                            }

                            if ((!isset($cma_info['title_field_supports_comcode'])) || (!$cma_info['title_field_supports_comcode'])) {
                                $val = comcode_escape($val_raw);
                            } else {
                                $val = $val_raw;
                            }
                            break;

                        case 'description':
                            if (is_integer($row[$cma_info[$cma_field]])) {
                                $val_raw = get_translated_text($row[$cma_info[$cma_field]], $cma_info['connection']);
                            } else {
                                $val_raw = $row[$cma_info[$cma_field]];
                            }
                            $val = $val_raw;
                            break;

                        case 'views':
                            $val_raw = strval($row[$cma_info[$cma_field]]);
                            $val = $val_raw;
                            break;

                        case 'validated':
                            $val_raw = strval($row[$cma_info[$cma_field]]);
                            $val = $val_raw;
                            break;

                        default:
                            $val_raw = $row[$cma_info[$cma_field]];
                            $val = $val_raw;
                            break;
                    }

                    if ($val !== null) {
                        $METADATA[$meta_type] = $val;
                        $METADATA[$meta_type . '_RAW'] = $val_raw;
                    }
                }
            }
        }

        // Add in image...

        $image_url = '';
        if ($cma_info['thumb_field'] !== null) {
            if ((strpos($cma_info['thumb_field'], 'CALL:') !== false) && ($content_id !== null)) {
                $image_url = call_user_func(trim(substr($cma_info['thumb_field'], 5)), array('id' => $content_id), $row);
            } else {
                if ($content_type === 'image') { // FUDGE (we don't actually want thumb in this case, we want full image)
                    $image_url = $row['url'];
                } else {
                    $image_url = $row[$cma_info['thumb_field']];
                }
            }
            if ($image_url != '') {
                if ($cma_info['thumb_field_is_theme_image']) {
                    $image_url = find_theme_image($image_url, true);
                } else {
                    if (url_is_local($image_url)) {
                        $image_url = get_custom_base_url() . '/' . $image_url;
                    }
                }
            }
        }
        if ((empty($image_url)) && ($cma_info['alternate_icon_theme_image'] != '') && ($content_id !== ':start'/*TODO: Fix in v11*/)) {
            $METADATA['image'] = find_theme_image($cma_info['alternate_icon_theme_image'], true);
        }
        if (!empty($image_url)) {
            $METADATA['image'] = $image_url;
        }

        // Add all $cma_info
        $METADATA += $cma_info;
        unset($METADATA['connection']);
        $METADATA['content_type_label_trans'] = do_lang($cma_info['content_type_label']);
    }

    if ($content_type !== null) {
        $METADATA['content_type'] = $content_type;
    }

    if ($content_id !== null) {
        $METADATA['content_id'] = $content_id;
    }
}

/**
 * Set the HTTP status code for the request.
 *
 * @sets_output_state
 *
 * @param  string $code The HTTP status code (should be numeric)
 */
function set_http_status_code($code)
{
    global $HTTP_STATUS_CODE;
    $HTTP_STATUS_CODE = $code; // So we can keep track

    if ((!headers_sent()) && (function_exists('browser_matches')) && (!browser_matches('ie')) && (strpos(cms_srv('SERVER_SOFTWARE'), 'IIS') === false)) {
        switch ($code) {
            case '301':
                header('HTTP/1.0 301 Moved Permanently');
                break;
            case '400':
                header('HTTP/1.0 400 Bad Request');
                break;
            case '401':
                header('HTTP/1.0 401 Unauthorized');
                break;
            case '403':
                header('HTTP/1.0 403 Forbidden');
                break;
            case '404':
                header('HTTP/1.0 404 Not Found');
                break;
            case '429':
                header('HTTP/1.0 429 Too Many Requests');
                break;
            case '500':
                header('HTTP/1.0 500 Internal server error');
                break;
        }
    }
}

/**
 * Search for a template.
 *
 * @param  ID_TEXT $codename The codename of the template being loaded
 * @param  ?LANGUAGE_NAME $lang The language to load the template in (templates can embed language references) (null: users own language)
 * @param  ID_TEXT $theme The theme to use
 * @param  string $suffix File type suffix of template file (e.g. .tpl)
 * @param  string $directory Subdirectory type to look in
 * @set    templates css
 * @param  boolean $non_custom_only Whether to only search in the default templates
 * @param  boolean $fallback_other_themes Allow fallback to other themes, in case it is defined only in a specific theme we would not noprmally look in
 * @return ?array List of parameters needed for the _do_template function to be able to load the template (null: could not find the template)
 */
function find_template_place($codename, $lang, $theme, $suffix, $directory, $non_custom_only = false, $fallback_other_themes = true)
{
    global $FILE_ARRAY, $CURRENT_SHARE_USER;

    static $tp_cache = array();
    $sz = serialize(array($codename, $lang, $theme, $suffix, $directory, $non_custom_only));
    if (isset($tp_cache[$sz])) {
        return $tp_cache[$sz];
    }

    if (addon_installed('less') && $suffix == '.css') {
        $test = find_template_place($codename, $lang, $theme, '.less', $directory, $non_custom_only, false);
        if (!is_null($test)) {
            $tp_cache[$sz] = $test;
            return $test;
        }
    }

    $prefix_default = get_file_base() . '/themes/';
    $prefix = ($theme == 'default' || $theme == 'admin') ? $prefix_default : (get_custom_file_base() . '/themes/');

    if (!isset($FILE_ARRAY)) {
        if ((is_file($prefix . $theme . '/' . $directory . '_custom/' . $codename . $suffix)) && (!in_safe_mode()) && (!$non_custom_only)) {
            $place = array($theme, '/' . $directory . '_custom/', $suffix);
        } elseif (is_file($prefix . $theme . '/' . $directory . '/' . $codename . $suffix)) {
            $place = array($theme, '/' . $directory . '/', $suffix);
        } elseif (($CURRENT_SHARE_USER !== null) && ($theme !== 'default') && (is_file(get_file_base() . '/themes/' . $theme . '/' . $directory . '_custom/' . $codename . $suffix)) && (!$non_custom_only)) {
            $place = array($theme, '/' . $directory . '_custom/', $suffix);
        } elseif (($CURRENT_SHARE_USER !== null) && ($theme !== 'default') && (is_file(get_file_base() . '/themes/' . $theme . '/' . $directory . '/' . $codename . $suffix))) {
            $place = array($theme, '/' . $directory . '/', $suffix);
        } elseif (($CURRENT_SHARE_USER !== null) && (is_file(get_custom_file_base() . '/themes/default/' . $directory . '_custom/' . $codename . $suffix)) && (!$non_custom_only)) {
            $place = array('default', '/' . $directory . '_custom/', $suffix);
        } elseif (($CURRENT_SHARE_USER !== null) && (is_file(get_custom_file_base() . '/themes/default/' . $directory . '/' . $codename . $suffix))) {
            $place = array('default', '/' . $directory . '/', $suffix);
        } elseif ((is_file($prefix_default . 'default' . '/' . $directory . '_custom/' . $codename . $suffix)) && (!in_safe_mode()) && (!$non_custom_only)) {
            $place = array('default', '/' . $directory . '_custom/', $suffix);
        } elseif (is_file($prefix_default . 'default' . '/' . $directory . '/' . $codename . $suffix)) {
            $place = array('default', '/' . $directory . '/', $suffix);
        } else {
            $place = null;
        }

        if (($place === null) && (!$non_custom_only) && ($fallback_other_themes)) { // Get desperate, search in themes other than current and default
            $dh = opendir(get_file_base() . '/themes');
            while (($possible_theme = readdir($dh))) {
                if (($possible_theme[0] !== '.') && ($possible_theme !== 'default') && ($possible_theme !== $theme) && ($possible_theme !== 'map.ini') && ($possible_theme !== 'index.html')) {
                    $full_path = get_custom_file_base() . '/themes/' . $possible_theme . '/' . $directory . '_custom/' . $codename . $suffix;
                    if (is_file($full_path)) {
                        $place = array($possible_theme, '/' . $directory . '_custom/', $suffix);
                        break;
                    }
                }
            }
            closedir($dh);
        }
    } else {
        $place = array('default', '/' . $directory . '/', $suffix);
    }

    $tp_cache[$sz] = $place;
    return $place;
}

/**
 * Find whether panels and the header/footer areas won't be shown.
 *
 * @return BINARY Result.
 */
function is_wide_high()
{
    global $IS_WIDE_HIGH_CACHE;
    if ($IS_WIDE_HIGH_CACHE !== null) {
        return $IS_WIDE_HIGH_CACHE;
    }

    $IS_WIDE_HIGH_CACHE = get_param_integer('wide_high', get_param_integer('keep_wide_high', get_param_integer('wide_print', 0)));
    return $IS_WIDE_HIGH_CACHE;
}

/**
 * Find whether panels will be shown.
 *
 * @return BINARY Result.
 */
function is_wide()
{
    global $IS_WIDE_CACHE;
    if ($IS_WIDE_CACHE !== null) {
        return $IS_WIDE_CACHE;
    }

    global $ZONE;
    $IS_WIDE_CACHE = get_param_integer('wide', get_param_integer('keep_wide', (is_wide_high() == 1) ? 1 : 0));
    if ($IS_WIDE_CACHE == 0) {
        return 0;
    }

    // Need to check it is allowed
    $theme = $GLOBALS['FORUM_DRIVER']->get_theme();
    $ini_path = (($theme == 'default' || $theme == 'admin') ? get_file_base() : get_custom_file_base()) . '/themes/' . $theme . '/theme.ini';
    if (is_file($ini_path)) {
        require_code('files');
        $details = better_parse_ini_file($ini_path);
        if ((isset($details['supports_wide'])) && ($details['supports_wide'] == '0')) {
            $IS_WIDE_CACHE = 0;
            return $IS_WIDE_CACHE;
        }
    }

    return $IS_WIDE_CACHE;
}

/**
 * Fixes bad unicode (utf-8) in the input. Useful when input may be dirty, e.g. from a txt file, or from a potential hacker.
 * The fix is imperfect, it will actually treat the input as ISO-8859-1 if not valid utf-8, then reconvert. Some limited scrambling is considered better than a stack trace.
 * This function does nothing if we are not using utf-8.
 *
 * @param  string $input Input string
 * @param  boolean $definitely_unicode If we know the input is meant to be unicode
 * @return string Guaranteed valid utf-8, if we're using it, otherwise the same as the input string
 */
function fix_bad_unicode($input, $definitely_unicode = false)
{
    // Fix bad unicode
    if (get_charset() == 'utf-8' || $definitely_unicode) {
        if (is_numeric($input) || preg_match('#[^\x00-\x7f]#', $input) == 0) {
            return $input; // No non-ASCII characters
        }

        $test_string = $input; // avoid being destructive
        $test_string = preg_replace('#[\x09\x0A\x0D\x20-\x7E]#', '', $test_string); // ASCII
        $test_string = preg_replace('#[\xC2-\xDF][\x80-\xBF]#', '', $test_string); // non-overlong 2-byte
        $test_string = preg_replace('#\xE0[\xA0-\xBF][\x80-\xBF]#', '', $test_string); // excluding overlongs
        $test_string = preg_replace('#[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}#', '', $test_string); // straight 3-byte
        $test_string = preg_replace('#\xED[\x80-\x9F][\x80-\xBF]#', '', $test_string); // excluding surrogates
        $test_string = preg_replace('#\xF0[\x90-\xBF][\x80-\xBF]{2}#', '', $test_string); // planes 1-3
        $test_string = preg_replace('#[\xF1-\xF3][\x80-\xBF]{3}#', '', $test_string); //  planes 4-15
        $test_string = preg_replace('#\xF4[\x80-\x8F][\x80-\xBF]{2}#', '', $test_string); // plane 16
        if ($test_string !== '') { // All ASCII/unicode characters stripped, so if anything is remaining it must be some kind of corruption
            $input = utf8_encode($input);
        }
    }
    return $input;
}

/**
 * Get string length, with utf-8 awareness where possible/required.
 *
 * @param  string $in The string to get the length of.
 * @param  boolean $force Whether to force unicode as on.
 * @return integer The string length.
 */
function cms_mb_strlen($in, $force = false)
{
    if (!$force && get_charset() != 'utf-8') {
        return strlen($in);
    }
    if ((function_exists('iconv_strlen')) && (function_exists('disable_iconv')) && (get_value('disable_iconv') !== '1')) {
        return @iconv_strlen($in, $force ? 'utf-8' : get_charset());
    }
    if (function_exists('mb_strlen')) {
        return @mb_strlen($in, $force ? 'utf-8' : get_charset()); // @ is because there could be invalid unicode involved
    }
    return strlen($in);
}

/**
 * Return part of a string, with utf-8 awareness where possible/required.
 *
 * @param  string $in The subject.
 * @param  integer $from The start position.
 * @param  ?integer $amount The length to extract (null: all remaining).
 * @param  boolean $force Whether to force unicode as on.
 * @return ~string String part (false: $start was over the end of the string).
 */
function cms_mb_substr($in, $from, $amount = null, $force = false)
{
    if ($amount === null) {
        $amount = cms_mb_strlen($in, $force) - $from;
    }

    if ($in == '' || strlen($in) == $from) {
        return ''; // Workaround PHP bug/inconsistency (https://bugs.php.net/bug.php?id=72320)
    }

    if ((!$force) && (get_charset() != 'utf-8')) {
        return substr($in, $from, $amount);
    }

    if ((function_exists('iconv_substr')) && (function_exists('disable_iconv')) && (get_value('disable_iconv') !== '1')) {
        return @iconv_substr($in, $from, $amount, $force ? 'utf-8' : get_charset());
    }
    if (function_exists('mb_substr')) {
        return @mb_substr($in, $from, $amount, $force ? 'utf-8' : get_charset());
    }

    $ret = substr($in, $from, $amount);
    $end = ord(substr($ret, -1));
    if (($end >= 192) && ($end <= 223)) {
        $ret .= substr($in, $from + $amount, 1);
    }
    if ($from != 0) {
        $start = ord(substr($ret, 0, 1));
        if (($start >= 192) && ($start <= 223)) {
            $ret = substr($in, $from - 1, 1) . $ret;
        }
    }
    return $ret;
}

/**
 * Workaround for when we can't enable LC_CTYPE on the locale - temporarily enable it when we really need it.
 *
 * @param  boolean $start Whether to start the workaround (as opposed to ending it).
 */
function _local_ctype_hack($start)
{
    $ctype_hack = (do_lang('locale_ctype_hack') == '1');
    if ($ctype_hack) {
        if ($start) {
            static $proper_locale = null;
            if ($proper_locale === null) {
                $proper_locale = explode(',', do_lang('locale'));
            }
            setlocale(LC_CTYPE, $proper_locale);
        } else {
            static $fallback_locale = null;
            if ($fallback_locale === null) {
                $fallback_locale = explode(',', do_lang('locale'));
            }
            setlocale(LC_CTYPE, $fallback_locale);
        }
    }
}

/**
 * Make a string title-case, with utf-8 awareness where possible/required.
 *
 * @param  string $in Subject.
 * @return string Result.
 */
function cms_mb_ucwords($in)
{
    _local_ctype_hack(true);

    if (get_charset() != 'utf-8') {
        $ret = ucwords($in);
    } elseif (function_exists('mb_convert_case')) {
        $ret = @mb_convert_case($in, MB_CASE_TITLE, get_charset());
    } else {
        $ret = ucwords($in);
    }

    _local_ctype_hack(false);

    return $ret;
}

/**
 * Make a string lowercase, with utf-8 awareness where possible/required.
 *
 * @param  string $in Subject.
 * @return string Result.
 */
function cms_mb_strtolower($in)
{
    _local_ctype_hack(true);

    if (get_charset() != 'utf-8') {
        $ret = strtolower($in);
    } elseif (function_exists('mb_strtolower')) {
        $ret = @mb_strtolower($in, get_charset());
    } else {
        $ret = strtolower($in);
    }

    _local_ctype_hack(false);

    return $ret;
}

/**
 * Make a string uppercase, with utf-8 awareness where possible/required.
 *
 * @param  string $in Subject.
 * @return string Result.
 */
function cms_mb_strtoupper($in)
{
    _local_ctype_hack(true);

    if (get_charset() != 'utf-8') {
        $ret = strtoupper($in);
    } elseif (function_exists('mb_strtoupper')) {
        $ret = @mb_strtoupper($in, get_charset());
    } else {
        $ret = strtoupper($in);
    }

    _local_ctype_hack(false);

    return $ret;
}

/**
 * Find if we a string is ASCII, and hence we can use non-UTF-safe functions on it.
 *
 * @param  string $x String to test
 * @return boolean Whether it is ASCII
 */
function is_ascii_string($x)
{
    $l = strlen($x);
    for ($i = 0; $i < $l; $i++) {
        if (ord($x[$i]) >= 128) {
            return false;
        }
    }
    return true;
}

/**
 * Find whether a file/directory is writeable. This function is designed to get past that the PHP is_writable function does not work properly on Windows.
 *
 * @param  PATH $path The file path
 * @return boolean Whether the file is writeable
 */
function is_writable_wrap($path)
{
    if (strtoupper(substr(PHP_OS, 0, 3)) != 'WIN') {
        return is_writable($path);
    }

    if (!file_exists($path)) {
        return false;
    }

    if (is_dir($path)) {
        /*if (false) { // ideal, but too dangerous as sometimes you can write files but not delete again
            $test = @fopen($path . '/cms.delete.me', GOOGLE_APPENGINE ? 'wb' : 'wt');
            if ($test !== false) {
                fclose($test);
                unlink($path . '/cms.delete.me');
                return true;
            }
            return false;
        }*/

        return is_writable($path); // imperfect unfortunately; but unlikely to cause a problem
    } else {
        $test = @fopen($path, 'ab');
        if ($test !== false) {
            fclose($test);
            return true;
        }
        return false;
    }
}

/**
 * Discern the cause of a file-write error, and show an appropriate error message.
 *
 * @param PATH $path File path that could not be written (full path, not relative)
 */
function intelligent_write_error($path)
{
    require_code('files2');
    _intelligent_write_error($path);
}

/**
 * Discern the cause of a file-write error, and return an appropriate error message.
 *
 * @param  PATH $path File path that could not be written
 * @return mixed Message (Tempcode or string)
 */
function intelligent_write_error_inline($path)
{
    require_code('files2');
    return _intelligent_write_error_inline($path);
}

/**
 * Find whether we have no forum on this website.
 *
 * @return boolean Whether we have no forum on this website
 */
function has_no_forum()
{
    if (get_forum_type() == 'none') {
        return true;
    }
    if ((get_forum_type() == 'cns') && (!addon_installed('cns_forum'))) {
        return true;
    }
    return false;
}

/**
 * Check to see if an addon is installed.
 *
 * @param  ID_TEXT $addon The addon name
 * @param  boolean $non_bundled_too Whether to check non-bundled addons (ones without an addon_registry hook)
 * @return boolean Whether it is
 */
function addon_installed($addon, $non_bundled_too = false)
{
    global $ADDON_INSTALLED_CACHE, $SITE_INFO;
    if ($ADDON_INSTALLED_CACHE == array()) {
        if (function_exists('persistent_cache_get')) {
            $ADDON_INSTALLED_CACHE = persistent_cache_get('ADDONS_INSTALLED');
        }
    }
    if (isset($ADDON_INSTALLED_CACHE[$addon])) {
        return $ADDON_INSTALLED_CACHE[$addon];
    }

    $addon = filter_naughty($addon, true);
    $answer = is_file(get_file_base() . '/sources/hooks/systems/addon_registry/' . $addon . '.php') || is_file(get_file_base() . '/sources_custom/hooks/systems/addon_registry/' . $addon . '.php');
    if ((!$answer) && ($non_bundled_too) && (!running_script('install'))) {
        $test = $GLOBALS['SITE_DB']->query_select_value_if_there('addons', 'addon_name', array('addon_name' => $addon));
        if ($test !== null) {
            $answer = true;
        }
    }

    $ADDON_INSTALLED_CACHE[$addon] = $answer;
    if (function_exists('persistent_cache_set')) {
        persistent_cache_set('ADDONS_INSTALLED', $ADDON_INSTALLED_CACHE);
    }

    return $answer;
}

/**
 * Convert a float to a "technical string representation of a float". Inverted with floatval.
 *
 * @param  float $num The number
 * @param  integer $decs_wanted The number of decimals to keep
 * @param  boolean $only_needed_decs Whether to trim trailing zeros
 * @return string The string converted
 */
function float_to_raw_string($num, $decs_wanted = 2, $only_needed_decs = false)
{
    $str = number_format($num, $decs_wanted, '.', '');
    $dot_pos = strpos($str, '.');
    $decs_here = ($dot_pos === false) ? 0 : (strlen($str) - $dot_pos - 1);
    if ($decs_here < $decs_wanted) {
        for ($i = 0; $i < $decs_wanted - $decs_here; $i++) {
            $str .= '0';
        }
    } elseif ($decs_here > $decs_wanted) {
        $str = substr($str, 0, strlen($str) - $decs_here + $decs_wanted);
        if ($decs_wanted == 0 && !$only_needed_decs) {
            $str = rtrim($str, '.');
        }
    }
    if ($only_needed_decs && $decs_wanted != 0) {
        $str = rtrim(rtrim($str, '0'), '.');
    }
    return $str;
}

/**
 * Format the given float number as a nicely formatted string (using the locale). Inverted with float_unformat.
 *
 * @param  float $val The value to format
 * @param  integer $decs_wanted The number of fractional digits
 * @param  boolean $only_needed_decs Whether to trim trailing zeros
 * @return string Nicely formatted string
 */
function float_format($val, $decs_wanted = 2, $only_needed_decs = false)
{
    $locale = localeconv();
    if ($locale['thousands_sep'] == '') {
        $locale['thousands_sep'] = ',';
    }
    $str = number_format($val, $decs_wanted, $locale['decimal_point'], $locale['thousands_sep']);
    $dot_pos = strpos($str, '.');
    $decs_here = ($dot_pos === false) ? 0 : (strlen($str) - $dot_pos - 1);
    if ($decs_here < $decs_wanted) {
        for ($i = 0; $i < $decs_wanted - $decs_here; $i++) {
            $str .= '0';
        }
    } elseif ($decs_here > $decs_wanted) {
        $str = substr($str, 0, strlen($str) - $decs_here + $decs_wanted);
        if ($decs_wanted == 0) {
            $str = rtrim($str, '.');
        }
    }
    if ($only_needed_decs && $decs_wanted != 0) {
        $str = rtrim(rtrim($str, '0'), '.');
    }
    return $str;
}

/**
 * Take the given formatted float number and convert it to a native float. The inverse of float_format.
 *
 * @param  string $str The formatted float number using the locale.
 * @param  boolean $no_thousands_sep Whether we do *not* expect a thousands separator, which means we can be a bit smarter.
 * @return float Native float
 */
function float_unformat($str, $no_thousands_sep = false)
{
    $locale = localeconv();

    // Simplest case?
    if (preg_match('#^\d+$#', $str) != 0) { // E.g. "123"
        return floatval($str);
    }

    if ($no_thousands_sep) {
        // We can assume a "." is a decimal point then?
        if (preg_match('#^\d+\.\d+$#', $str) != 0) { // E.g. "123.456"
            return floatval($str);
        }
    }

    // Looks like English-format? It couldn't be anything else because thousands_sep always comes before decimal_point
    if (preg_match('#^[\d,]+\.\d+$#', $str) != 0) { // E.g. "123,456.789"
        return floatval($str);
    }

    // Now it must e E.g. "123.456,789" or "123.456", or something from another language which uses other separators...

    if ($locale['thousands_sep'] != '') {
        $str = str_replace($locale['thousands_sep'], '', $str);
    }
    $str = str_replace($locale['decimal_point'], '.', $str);
    return floatval($str);
}

/**
 * Format the given integer number as a nicely formatted string (using the locale).
 *
 * @param  integer $val The value to format
 * @return string Nicely formatted string
 */
function integer_format($val)
{
    static $locale = null;
    if ($locale === null) {
        $locale = localeconv();
        if ($locale['thousands_sep'] == '') {
            $locale['thousands_sep'] = ',';
        }
    }
    return number_format($val, 0, $locale['decimal_point'], $locale['thousands_sep']);
}

/**
 * Sort a list of maps by the string length of a particular key ID in the maps.
 *
 * @param  array $rows List of maps to sort
 * @param  mixed $sort_key Either an integer sort key (to sort by integer key ID of contained arrays) or a String sort key (to sort by string key ID of contained arrays).
 */
function sort_maps_by__strlen($rows, $sort_key)
{
    global $M_SORT_KEY;
    $M_SORT_KEY = $sort_key;

    if (count($rows) < 2) {
        if (($GLOBALS['DEV_MODE']) && (count($rows) == 1)) {
            call_user_func('_strlen_sort', current($rows), current($rows)); // Just to make sure there's no crash bug in the sort function
        }

        return;
    }

    @uasort($rows, '_strlen_sort'); // LEGACY (< PHP 7): @ is to stop PHP bug (https://bugs.php.net/bug.php?id=50688 / https://github.com/php/php-src/commit/179c8a2a09a993bfa35f65828d40b68e3f69f64e#diff-497f073aa1ab88afcb8b248fc25d2a12) warning about altered array contents when Tempcode copies are evaluated internally
}

/**
 * Helper function for usort to sort a list by string length.
 *
 * @param  string $a The first string to compare
 * @param  string $b The second string to compare
 * @return integer The comparison result (0 for equal, -1 for less, 1 for more)
 * @ignore
 */
function _strlen_sort($a, $b)
{
    if (!isset($a)) {
        $a = '';
    }
    if (!isset($b)) {
        $b = '';
    }
    if ($a == $b) {
        return 0;
    }
    if (!is_string($a)) {
        global $M_SORT_KEY;
        return (strlen($a[$M_SORT_KEY]) < strlen($b[$M_SORT_KEY])) ? -1 : 1;
    }
    return (strlen($a) < strlen($b)) ? -1 : 1;
}

/**
 * Sort a list of maps by a particular key ID in the maps. Does not (and should not) preserve list indices, but does preserve associative key indices.
 *
 * @param  array $rows List of maps to sort
 * @param  mixed $sort_keys Either an integer sort key (to sort by integer key ID of contained arrays) or a Comma-separated list of sort keys (to sort by string key ID of contained arrays; prefix '!' a key to reverse the sort order for it).
 * @param  boolean $preserve_order_if_possible Don't shuffle order unnecessarily (i.e. do a merge sort)
 * @param  boolean $support_comma_delimiters Whether commas in $sort_keys delimit multiple sort keys
 */
function sort_maps_by(&$rows, $sort_keys, $preserve_order_if_possible = false, $support_comma_delimiters = true)
{
    if ($rows == array()) {
        return;
    }

    global $M_SORT_KEY;
    $M_SORT_KEY = ($support_comma_delimiters && is_string($sort_keys)) ? explode(',', $sort_keys) : array(@strval($sort_keys));
    if ($preserve_order_if_possible) {
        merge_sort($rows, '_multi_sort');
    } else {
        if (count($rows) < 2) {
            if (($GLOBALS['DEV_MODE']) && (count($rows) == 1)) {
                call_user_func('_multi_sort', current($rows), current($rows)); // Just to make sure there's no crash bug in the sort function
            }

            return;
        }

        $first_key = key($rows);
        if ((is_integer($first_key)) && (array_unique(array_map('is_integer', array_keys($rows))) === array(true))) {
            @usort($rows, '_multi_sort'); // LEGACY (< PHP 7): @ is to stop PHP bug (https://bugs.php.net/bug.php?id=50688 / https://github.com/php/php-src/commit/179c8a2a09a993bfa35f65828d40b68e3f69f64e#diff-497f073aa1ab88afcb8b248fc25d2a12) warning about altered array contents when Tempcode copies are evaluated internally
        } else {
            @uasort($rows, '_multi_sort'); // LEGACY (< PHP 7): @ is to stop PHP bug (https://bugs.php.net/bug.php?id=50688 / https://github.com/php/php-src/commit/179c8a2a09a993bfa35f65828d40b68e3f69f64e#diff-497f073aa1ab88afcb8b248fc25d2a12) warning about altered array contents when Tempcode copies are evaluated internally
        }
    }
}

/**
 * Do a user sort, preserving order where reordering not needed. Based on a PHP manual comment at http://php.net/manual/en/function.usort.php
 *
 * @param  array $array Sort array
 * @param  mixed $cmp_function Comparison function
 */
function merge_sort(&$array, $cmp_function = 'strcmp')
{
    // Arrays of size<2 require no action.
    if (count($array) < 2) {
        if (($GLOBALS['DEV_MODE']) && (count($array) == 1)) {
            call_user_func($cmp_function, current($array), current($array)); // Just to make sure there's no crash bug in the sort function
        }

        return;
    }

    // Split the array in half
    $halfway = intval(floatval(count($array)) / 2.0);
    $array1 = array_slice($array, 0, $halfway);
    $array2 = array_slice($array, $halfway);

    // Recurse to sort the two halves
    merge_sort($array1, $cmp_function);
    merge_sort($array2, $cmp_function);

    // If all of $array1 is <= all of $array2, just append them.
    if (call_user_func($cmp_function, end($array1), reset($array2)) < 1) {
        $array = array_merge($array1, $array2);
        return;
    }

    // Merge the two sorted arrays into a single sorted array
    $array = array();
    reset($array1);
    reset($array2);
    $ptr1 = 0;
    $ptr2 = 0;
    $cnt1 = count($array1);
    $cnt2 = count($array2);
    while (($ptr1 < $cnt1) && ($ptr2 < $cnt2)) {
        if (call_user_func($cmp_function, current($array1), current($array2)) < 1) {
            $key = key($array1);
            if (is_integer($key)) {
                $array[] = current($array1);
            } else {
                $array[$key] = current($array1);
            }
            $ptr1++;
            next($array1);
        } else {
            $key = key($array2);
            if (is_integer($key)) {
                $array[] = current($array2);
            } else {
                $array[$key] = current($array2);
            }
            $ptr2++;
            next($array2);
        }
    }

    // Merge the remainder
    while ($ptr1 < $cnt1) {
        $key = key($array1);
        if (is_integer($key)) {
            $array[] = current($array1);
        } else {
            $array[$key] = current($array1);
        }
        $ptr1++;
        next($array1);
    }
    while ($ptr2 < $cnt2) {
        $key = key($array2);
        if (is_integer($key)) {
            $array[] = current($array2);
        } else {
            $array[$key] = current($array2);
        }
        $ptr2++;
        next($array2);
    }
}

/**
 * Helper function to sort a list of maps by the value at $key in each of those maps.
 *
 * @param  array $a The first to compare
 * @param  array $b The second to compare
 * @return integer The comparison result (0 for equal, -1 for less, 1 for more)
 * @ignore
 */
function _multi_sort($a, $b)
{
    global $M_SORT_KEY;
    $keys = $M_SORT_KEY;
    $first_key = $keys[0];
    if ($first_key[0] === '!') {
        $first_key = substr($first_key, 1);
    }

    if (($a === null) && ($b === null)) {
        return 0;
    } elseif ($a === null) {
        return ($first_key[0] === '!') ? 1 : -1;
    } elseif ($b === null) {
        return ($first_key[0] === '!') ? -1 : 1;
    }

    if ((is_string($a[$first_key])) || (is_object($a[$first_key]))) {
        $ret = 0;
        do {
            $key = array_shift($keys);

            $backwards = ($key[0] === '!');
            if ($backwards) {
                $key = substr($key, 1);
            }

            $av = $a[$key];
            $bv = $b[$key];

            if (is_object($av)) {
                $av = $av->evaluate();
            }
            if (is_object($bv)) {
                $bv = $bv->evaluate();
            }

            if ($backwards) { // Flip around
                if ((is_numeric($av)) && (is_numeric($bv))) {
                    $ret = -strnatcasecmp($av, $bv);
                } else {
                    $ret = -strcasecmp($av, $bv);
                }
            } else {
                if ((is_numeric($av)) && (is_numeric($bv))) {
                    $ret = strnatcasecmp($av, $bv);
                } else {
                    $ret = strcasecmp($av, $bv);
                }
            }
        } while ((count($keys) !== 0) && ($ret === 0));
        return $ret;
    }

    do {
        $key = array_shift($keys);
        if ($key[0] === '!') { // Flip around
            $key = substr($key, 1);
            $ret = ($a[$key] > $b[$key]) ? -1 : (($a[$key] == $b[$key]) ? 0 : 1);
        } else {
            $ret = ($a[$key] > $b[$key]) ? 1 : (($a[$key] == $b[$key]) ? 0 : -1);
        }
    } while ((count($keys) !== 0) && ($ret == 0));
    return $ret;
}

/**
 * Require all code relating to the Conversr forum
 */
function cns_require_all_forum_stuff()
{
    require_lang('cns');

    require_code('cns_members');
    require_code('cns_topics');
    require_code('cns_posts');
    require_code('cns_moderation');
    require_code('cns_groups');
    require_code('cns_forums');
    require_code('cns_general');
}

/**
 * Create file with unique file name, but works around compatibility issues between servers. Note that the file is NOT automatically deleted. You should also delete it using "@unlink", as some servers have problems with permissions.
 *
 * @param  string $prefix The prefix of the temporary file name.
 * @return ~string The name of the temporary file (false: error).
 */
function cms_tempnam($prefix = 'cms')
{
    require_code('files2');
    return _cms_tempnam($prefix);
}

/**
 * Peek at a stack element.
 *
 * @param  array $array The stack to peek in
 * @param  integer $depth_down The depth into the stack we are peaking
 * @return mixed The result of the peeking
 */
function array_peek($array, $depth_down = 1)
{
    $count = count($array);
    if ($count - $depth_down < 0) {
        return null;
    }
    return $array[$count - $depth_down];
}

/**
 * Make a value suitable for use in an XML ID.
 *
 * @param  string $param The value to escape
 * @return string The escaped value
 */
function fix_id($param)
{
    if (preg_match('#^[A-Za-z][\w]*$#', $param) !== 0) {
        return $param; // Optimisation
    }

    $length = strlen($param);
    $new = '';
    for ($i = 0; $i < $length; $i++) {
        $char = $param[$i];
        switch ($char) {
            case '[':
                $new .= '_opensquare_';
                break;
            case ']':
                $new .= '_closesquare_';
                break;
            case '&#039;':
            case '\'':
                $new .= '_apostophe_';
                break;
            case '-':
                $new .= '_minus_';
                break;
            case ' ':
                $new .= '_space_';
                break;
            case '+':
                $new .= '_plus_';
                break;
            case '*':
                $new .= '_star_';
                break;
            case '/':
                $new .= '__';
                break;
            default:
                $ascii = ord($char);
                if ((($i !== 0) && ($char === '_')) || (($ascii >= 48) && ($ascii <= 57)) || (($ascii >= 65) && ($ascii <= 90)) || (($ascii >= 97) && ($ascii <= 122))) {
                    $new .= $char;
                } else {
                    $new .= '_' . strval($ascii) . '_';
                }
                break;
        }
    }
    if ($new === '') {
        $new = 'zero_length';
    }
    if ($new[0] === '_') {
        $new = 'und_' . $new;
    }
    return $new;
}

/**
 * See if the current URL matches the given Composr match-keys.
 *
 * @param  mixed $match_keys Match keys (comma-separated list of match-keys, or array of)
 * @param  boolean $support_post Check against POSTed data too
 * @param  ?array $current_params Parameters to check against (null: get from environment GET/POST) - if set, $support_post is ignored)
 * @param  ?ID_TEXT $current_zone_name Current zone name (null: get from environment)
 * @param  ?ID_TEXT $current_page_name Current page name (null: get from environment)
 * @return boolean Whether there is a match
 */
function match_key_match($match_keys, $support_post = false, $current_params = null, $current_zone_name = null, $current_page_name = null)
{
    $req_func = $support_post ? 'either_param_string' : 'get_param_string';

    if ($current_zone_name === null) {
        global $IN_SELF_ROUTING_SCRIPT;
        if (!$IN_SELF_ROUTING_SCRIPT) {
            return false;
        }

        $current_zone_name = get_zone_name();
    }
    if ($current_page_name === null) {
        $current_page_name = get_page_name();
    }

    $potentials = is_array($match_keys) ? $match_keys : explode(',', $match_keys);
    foreach ($potentials as $potential) {
        $parts = is_array($potential) ? $potential : explode(':', $potential);

        // Allow the first 2 parts to be omitted, but if so we need to insert them back in here
        $prepend_zone = ((!isset($parts[0])) || (strpos($parts[0], '=') !== false));
        if ($prepend_zone) {
            $parts = array_merge(array('_WILD'), $parts);
        }
        $prepend_page = ((!isset($parts[1])) || (strpos($parts[1], '=') !== false));
        if ($prepend_page) {
            $parts = array_merge(array_slice($parts, 0, 1), array('_WILD'), array_slice($parts, 1));
        }

        if (($parts[0] == '_WILD') || ($parts[0] == '_SEARCH')) {
            $parts[0] = $current_zone_name;
        }
        if (($parts[1] == '_WILD') || (($parts[1] == '_WILD_NOT_START') && ($current_page_name != get_zone_default_page($parts[0])))) {
            $parts[1] = $current_page_name;
        }
        if (($parts[0] == 'site') && (get_option('collapse_user_zones') == '1')) {
            $parts[0] = '';
        }
        $zone_matches = (($parts[0] == $current_zone_name) || ((strpos($parts[0], '*') !== false) && (simulated_wildcard_match($current_zone_name, $parts[0], true))));
        $page_matches = ((($parts[1] == '') && ($current_page_name == get_zone_default_page($current_zone_name))) || ($parts[1] == $current_page_name) || ((strpos($parts[1], '*') !== false) && (simulated_wildcard_match($current_page_name, $parts[1], true))));
        if (($zone_matches) && ($page_matches)) {
            $bad = false;
            for ($i = 2; $i < count($parts); $i++) {
                if ($parts[$i] != '') {
                    if (($i == 2) && (strpos($parts[$i], '=') === false)) {
                        $parts[$i] = 'type=' . $parts[$i];
                    } elseif (($i == 3) && (strpos($parts[$i], '=') === false)) {
                        $parts[$i] = 'id=' . $parts[$i];
                    }
                }

                $subparts = explode('=', $parts[$i]);
                if ($subparts[0] == 'type') {
                    $default = 'browse';
                } else {
                    $default = '';
                }
                if (count($subparts) != 2) {
                    $bad = true;
                    continue;
                }
                $env_val = ($current_params === null) ? call_user_func_array($req_func, array($subparts[0], null)) : (isset($current_params[$subparts[0]]) ? $current_params[$subparts[0]] : null);
                if ($subparts[1] == '_WILD') {
                    if ($env_val !== null) {
                        $subparts[1] = $env_val; // null won't match to a wildcard
                    }
                } else {
                    if ($env_val === null) {
                        $env_val = $default;
                    }
                }
                if ($env_val !== $subparts[1]) {
                    $bad = true;
                    continue;
                }
            }
            if (!$bad) {
                return true;
            }
        }
    }
    return false;
}

/**
 * Get the name of the page in the URL or active script.
 *
 * @return ID_TEXT The current page/script name
 */
function get_page_or_script_name()
{
    global $IN_SELF_ROUTING_SCRIPT;
    if ($IN_SELF_ROUTING_SCRIPT) {
        return get_page_name();
    }
    return current_script();
}

/**
 * Get the name of the page in the URL (by convention: the current page).
 * This works on the basis of the 'page' parameter and does not require index.php be the active script.
 * It will do dash to underscore substitution as required.
 *
 * @return ID_TEXT The current page name
 */
function get_page_name()
{
    global $PAGE_NAME_CACHE;
    if (isset($PAGE_NAME_CACHE)) {
        return $PAGE_NAME_CACHE;
    }
    global $ZONE, $GETTING_PAGE_NAME, $BOOTSTRAPPING;
    if ($GETTING_PAGE_NAME) {
        return 'unknown';
    }
    $GETTING_PAGE_NAME = true;
    $page = get_param_string('page', '', true);
    if (strlen($page) > 80) {
        warn_exit(do_lang_tempcode('INTERNAL_ERROR'));
    }
    if (($page == '') && ($ZONE !== null)) {
        $page = $ZONE['zone_default_page'];
        if ($page === null) {
            $page = '';
        }
    }
    if (strpos($page, '..') !== false) {
        $page = filter_naughty($page);
    }
    $simplified_algorithm = $BOOTSTRAPPING; // fix_page_name_dashing calls request_page, which won't work reliably during bootstrapping
    if ($simplified_algorithm) {
        $page = str_replace('-', '_', $page);
    } else {
        $page = fix_page_name_dashing(get_zone_name(), $page);
    }
    if (!$GETTING_PAGE_NAME) { // It's been changed by process_url_monikers, which was called indirectly by fix_page_name_dashing
        return $PAGE_NAME_CACHE;
    }
    if (($ZONE !== null) && (!$simplified_algorithm)) {
        $PAGE_NAME_CACHE = $page;
    }
    $GETTING_PAGE_NAME = false;
    return $page;
}

/**
 * Fix a page name that may have been given dashes for SEO reasons.
 *
 * @param  string $zone Zone.
 * @param  string $page Page.
 * @return string The fixed page name.
 */
function fix_page_name_dashing($zone, $page)
{
    if (strpos($page, '/') !== false) {
        return $page; // It's a moniker that hasn't been processed yet
    }

    // Fix page-name dashes if needed
    if (strpos($page, '-') !== false) {
        require_code('site');
        $test = _request_page($page, $zone, null, null, true);
        if ($test === false) {
            $_page = str_replace('-', '_', $page);
            $test = _request_page($_page, $zone);
            if ($test !== false) {
                $page = $_page;
            }
        }
    }
    return $page;
}

/**
 * Take a list of maps, and make one of the values of each array the index of a map to the map.
 *
 * list_to_map is very useful for handling query results.
 * Let's imagine you get the result of SELECT id,title FROM sometable.
 * list_to_map turns the array of rows into a map between the id key and each row.
 *
 * @param  string $map_value The key key of our maps that reside in our map
 * @param  array $list The list of maps
 * @return array The collapsed map
 */
function list_to_map($map_value, $list)
{
    $i = 0;

    $new_map = array();

    foreach ($list as $map) {
        $key = $map[$map_value];
        $new_map[$key] = $map;

        $i++;
    }

    if ($i > 0) {
        return $new_map;
    }
    return array();
}

/**
 * Take a list of maps of just two elements, and make it into a single map
 *
 * @param  string $key The key key of our maps that reside in our map
 * @param  string $value The value key of our maps that reside in our map
 * @param  array $list The map of maps
 * @return array The collapsed map
 */
function collapse_2d_complexity($key, $value, $list)
{
    $new_map = array();
    foreach ($list as $map) {
        $new_map[$map[$key]] = $map[$value];
    }

    return $new_map;
}

/**
 * Take a list of maps of just one element, and make it into a single map
 *
 * @param  ?string $key The key of our maps that reside in our map (null: first key)
 * @param  array $list The map of maps
 * @return array The collapsed map
 */
function collapse_1d_complexity($key, $list)
{
    $new_array = array();
    foreach ($list as $map) {
        if ($key === null) {
            $new_array[] = array_shift($map);
        } else {
            $new_array[] = $map[$key];
        }
    }

    return $new_array;
}

/**
 * Used by cms_strip_tags to handle whether to strip a tag.
 *
 * @param  array $matches Array of matches
 * @return string Substituted tag text
 *
 * @ignore
 */
function _cms_strip_tags_callback($matches)
{
    global $STRIP_TAGS_TAGS, $STRIP_TAGS_TAGS_AS_ALLOW;
    $tag_covered = stripos($STRIP_TAGS_TAGS, '<' . $matches[1] . '>');
    if ((($STRIP_TAGS_TAGS_AS_ALLOW) && ($tag_covered !== false)) || ((!$STRIP_TAGS_TAGS_AS_ALLOW) && ($tag_covered === false))) {
        return $matches[0];
    }
    return '';
}

/**
 * Strip HTML and PHP tags from a string.
 * Equivalent to PHP's strip_tags, whose $allowable_tags parameter is expected to be deprecated in PHP 7.3 (https://wiki.php.net/rfc/deprecations_php_7_3).
 *
 * @param  string $str Subject
 * @param  string $tags Comma-separated list of tags
 * @param  boolean $tags_as_allow Whether tags represents a whitelist (set for false to allow all by default and make $tags a blacklist)
 * @return string Result
 */
function cms_strip_tags($str, $tags, $tags_as_allow = true)
{
    global $STRIP_TAGS_TAGS, $STRIP_TAGS_TAGS_AS_ALLOW;
    $STRIP_TAGS_TAGS = $tags;
    $STRIP_TAGS_TAGS_AS_ALLOW = $tags_as_allow;

    return preg_replace_callback('#</?([^\s<>]+)(\s[^<>]*)?' . '>#', '_cms_strip_tags_callback', $str);
}

/**
 * Find whether an IP address is valid
 *
 * @param  IP $ip IP address to check.
 * @param  boolean $allow_wildcards Allow wildcards.
 * @return boolean Whether the IP address is valid.
 */
function is_valid_ip($ip, $allow_wildcards = false)
{
    $parts = array();

    if (strpos($ip, '.') !== false) {
        // ipv4

        if ($allow_wildcards) {
            if (preg_match('#^(\d+|\*)\.(\d+|\*)\.(\d+|\*)\.(\d+|\*)$#', $ip, $parts) == 0) {
                return false;
            }

            for ($i = 1; $i <= 4; $i++) {
                if ($parts[$i] == '*') {
                    for ($j = $i + 1; $j <= 4; $j++) {
                        if ($parts[$j] != '*') {
                            return false;
                        }
                    }
                }
            }
        } else {
            if (preg_match('#^(\d+)\.(\d+)\.(\d+)\.(\d+)$#', $ip, $parts) == 0) {
                return false;
            }
        }

        for ($i = 1; $i <= 4; $i++) {
            if ((is_numeric($parts[$i])) && (intval($parts[$i]) > 255)) {
                return false;
            }
        }

        return true;
    }

    if (strpos($ip, ':') !== false) {
        // ipv6

        if (substr_count($ip, '::') > 1) {
            return false;
        }

        $parts = explode(':', $ip);

        if (count($parts) > 8) {
            return false;
        }

        if ((count($parts) < 8) && (strpos($ip, '::') === false)) {
            return false;
        }

        foreach ($parts as $i => $part) {
            if ($allow_wildcards) {
                if (preg_match('#^(\*|[\dA-F]{0,4})$#i', $part) == 0) {
                    return false;
                }

                if ($part == '*') {
                    for ($j = $i + 1; $j < count($parts); $j++) {
                        if ($parts[$j] != '*') {
                            return false;
                        }
                    }
                }
            } else {
                if (preg_match('#^[\dA-F]{0,4}$#i', $part) == 0) {
                    return false;
                }
            }
        }

        return true;
    }

    return false;
}

/**
 * Attempt to get the clean IP address of the current user
 *
 * @param  integer $amount The number of groups to include in the IP address (rest will be replaced with *'s). For IP6, this is doubled.
 * @set    1 2 3 4
 * @param  ?IP $ip IP address to use, normally left null (null: current user's)
 * @return IP The users IP address (blank: could not find a valid one)
 */
function get_ip_address($amount = 4, $ip = null)
{
    require_code('config');

    if ((get_value('cloudflare_workaround') === '1') && (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) && (isset($_SERVER['REMOTE_ADDR']))) {
        $regexp = '^(173\.245\.|103\.21\.|103\.22\.|103\.31\.|141\.101\.|108\.162\.|190\.93\.|188\.114\.|197\.234\.|198\.41\.|162\.|104\.|172\.|131\.0\.)';
        if (preg_match('#' . $regexp . '#', $_SERVER['REMOTE_ADDR']) != 0) {
            $_SERVER['ORIGINAL_REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'];
            $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];
            unset($_SERVER['HTTP_X_FORWARDED_FOR']);
        }
    }

    if ($ip === null) {
        /* Presents too many security and maintenance problems. Can easily be faked, or changed.
        $fw = cms_srv('HTTP_X_FORWARDED_FOR');
        if (cms_srv('HTTP_CLIENT_IP') != '') {
            $fw = cms_srv('HTTP_CLIENT_IP');
        }
        if (($fw != '') && ($fw != '127.0.0.1') && (substr($fw, 0, 8) != '192.168.') && (substr($fw, 0, 3) != '10.') && (is_valid_ip($fw)) && ($fw != cms_srv('SERVER_ADDR'))) {
            $ip = $fw;
        } else
        */
        $ip = cms_srv('REMOTE_ADDR');
    }

    global $SITE_INFO;
    if (($amount == 3) && (!empty($SITE_INFO['full_ips'])) && ($SITE_INFO['full_ips'] == '1')) { // Extra configurable security
        $amount = 4;
    }

    return normalise_ip_address($ip, $amount);
}

/**
 * Normalise/fix a provided IP address, including wildcarding part of it if requested.
 *
 * @param  IP $ip The IP address to normalise
 * @param  ?integer $amount The number of groups to include in the IP address (rest will be replaced with *'s). For IP6, this is doubled. (null: wildcards not allowed)
 * @return IP The normalised IP address
 */
function normalise_ip_address($ip, $amount = null)
{
    if ($ip == '') {
        return '';
    }

    $raw_ip = $ip;

    static $ip_cache = array();
    if (isset($ip_cache[$raw_ip][$amount])) {
        return $ip_cache[$raw_ip][$amount];
    }

    // Bizarro-filter (found "in the wild")
    $pos = strpos($ip, ',');
    if ($pos !== false) {
        $ip = substr($ip, 0, $pos);
    }

    // ...and another
    if (strpos($ip, '%') !== false) {
        $ip = preg_replace('#%14$#', '', $ip);
    }

    // Normalise
    if (strpos($ip, '.') === false) { // IPv6
        if (substr_count($ip, ':') < 7) {
            $ip = str_replace('::', str_repeat(':', (7 - substr_count($ip, ':')) + 2), $ip);
        }
        $parts = explode(':', $ip);
        for ($i = 0; $i < (is_null($amount) ? 8 : ($amount * 2)); $i++) {
            if (isset($parts[$i])) {
                if ($parts[$i] != '*') {
                    $parts[$i] = strtoupper(str_pad($parts[$i], 4, '0', STR_PAD_LEFT));
                }
            } else {
                $parts[$i] = '0000';
            }
        }
        if (!is_null($amount)) {
            for ($i = $amount * 2; $i < 8; $i++) {
                $parts[$i] = '*';
            }
        }
        $ip = implode(':', $parts);
        $ip_cache[$raw_ip][$amount] = $ip;
    } else { // IPv4
        $parts = explode('.', $ip);
        for ($i = 0; $i < (is_null($amount) ? 4 : $amount); $i++) {
            if (!array_key_exists($i, $parts)) {
                $parts[$i] = '0';
            }
        }
        if (!is_null($amount)) {
            for ($i = $amount; $i < 4; $i++) {
                $parts[$i] = '*';
            }
        }
        $ip = implode('.', $parts);
        $ip_cache[$raw_ip][$amount] = $ip;
    }

    if (!is_valid_ip($ip_cache[$raw_ip][$amount], $amount !== null)) {
        // If still not valid after normalisation
        $ip_cache[$raw_ip][$amount] = '';
        return '';
    }

    return $ip_cache[$raw_ip][$amount];
}

/**
 * Exit with debug data, only for a specific IP address.
 *
 * @param  IP $ip IP address of tester
 * @param  mixed $data Data to display
 */
function me_debug($ip, $data)
{
    if (get_ip_address() == $ip) {
        @var_dump($data);
        exit();
    }
}

/**
 * Get a string of the users web browser
 *
 * @return string The web browser string
 */
function get_browser_string()
{
    return cms_srv('HTTP_USER_AGENT');
}

/**
 * Get the user's operating system
 *
 * @return string The operating system string
 */
function get_os_string()
{
    if (cms_srv('HTTP_UA_OS') != '') {
        return cms_srv('HTTP_UA_OS');
    } elseif (cms_srv('HTTP_USER_AGENT') != '') {
        // E.g. Mozilla/4.5 [en] (X11; U; Linux 2.2.9 i586)
        // We need to get the stuff in the brackets
        $matches = array();
        if (preg_match('#\(([^\)]*)\)#', cms_srv('HTTP_USER_AGENT'), $matches) != 0) {
            $ret = $matches[1];
            $ret = preg_replace('#^compatible; (MSIE[^;]*; )?#', '', $ret);
            return $ret;
        }
    }
    return '';
}

/**
 * Find if Cron is installed
 *
 * @param  boolean $absolutely_sure Whether Cron really needs to be installed (if set to false it will be assumed installed on dev-mode)
 * @return boolean Whether Cron is installed
 */
function cron_installed($absolutely_sure = false)
{
    $test = get_param_integer('keep_has_cron', null);
    if ($test !== null) {
        return $test == 1;
    }

    if (!$absolutely_sure) {
        if ($GLOBALS['DEV_MODE']) {
            return true;
        }
    }

    $last_cron = get_value('last_cron');
    if ($last_cron === null) {
        return false;
    }
    return intval($last_cron) > (time() - 60 * 60 * 5);
}

/**
 * Compare two IP addresses for potential correlation. Not as simple as equality due to '*' syntax.
 *
 * @param  string $wild The general IP address that is potentially wildcarded
 * @param  IP $full The specific IP address we are checking
 * @return boolean Whether the IP addresses correlate
 */
function compare_ip_address($wild, $full)
{
    $full = normalise_ip_address($full);
    if ($full == '') {
        return false;
    }
    if (strpos($full, '.') !== false) {
        return _compare_ip_address($wild, explode('.', $full), '.');
    }
    return _compare_ip_address($wild, explode(':', $full), ':');
}

/**
 * Compare two IP addresses for potential correlation, pre-exploded. Not as simple as equality due to '*' syntax.
 *
 * @param  string $wild The general IP address that is potentially wildcarded
 * @param  array $full_parts The exploded parts of the specific IP address we are checking
 * @param  string $delimiter The delimiter
 * @return boolean Whether the IP addresses correlate
 */
function _compare_ip_address($wild, $full_parts, $delimiter)
{
    $wild = normalise_ip_address($wild, 4);
    if ($wild == '') {
        return false;
    }
    $wild_parts = explode($delimiter, $wild);
    foreach ($wild_parts as $i => $wild_part) {
        if (($wild_part != '*') && ($wild_part != $full_parts[$i])) {
            return false;
        }
    }
    return true;
}

/**
 * Check to see if an IP address is banned.
 *
 * @param  string $ip The IP address to check for banning
 * @param  boolean $force_db Force check via database only
 * @param  boolean $handle_uncertainties Handle uncertainities (used for the external bans - if true, we may return null, showing we need to do an external check). Only works with $force_db.
 * @param  ?boolean $is_unbannable Whether the IP is unbannable (returned by reference) (null: not set yet by caller)
 * @param  ?integer $ban_until When the ban will expire, will always be more than the current timestamp (null: not set yet by caller / no expiration)
 * @param  boolean $check_caching Whether to check internal run-time caching (disable if doing automated tests)
 * @return ?boolean Whether the IP address is banned (null: unknown)
 */
function ip_banned($ip, $force_db = false, $handle_uncertainties = false, &$is_unbannable = null, &$ban_until = null, $check_caching = true)
{
    // NB: This function will make the first query called, so we will be a bit smarter, checking for errors

    $is_unbannable = false;
    $ban_until = null;

    static $cache = array();
    $sz = serialize(array($force_db, $handle_uncertainties));
    if ((isset($cache[$sz][$ip])) && ($check_caching)) {
        $is_unbannable = $cache[$sz][$ip]['is_unbannable'];
        $ban_until = $cache[$sz][$ip]['ban_until'];
        return $cache[$sz][$ip]['ret'];
    }

    // Edge case: No securitylogging addon
    if (!addon_installed('securitylogging')) {
        $ret = false;
        $cache[$sz][$ip] = array(
            'is_unbannable' => $is_unbannable,
            'ban_until' => $ban_until,
            'ret' => $ret,
        );
        return $ret;
    }

    // Edge case: Commonly invalid IP
    if ($ip == '') {
        $ret = false;
        $cache[$sz][$ip] = array(
            'is_unbannable' => $is_unbannable,
            'ban_until' => $ban_until,
            'ret' => $ret,
        );
        return $ret;
    }

    // Edge case: Excluded via config option (different from checking i_ban_positive===false, as this option is referenced in antispam.php too)
    $_exclusions = get_option('spam_check_exclusions');
    $exclusions = explode(',', $_exclusions);
    foreach ($exclusions as $exclusion) {
        if (trim($ip) == $exclusion) {
            $ret = false;
            $is_unbannable = true;
            $cache[$sz][$ip] = array(
                'is_unbannable' => $is_unbannable,
                'ban_until' => $ban_until,
                'ret' => $ret,
            );
            return $ret;
        }
    }

    // Read in IP bans - we need to check both DB and .htaccess for completeness (expiring bans will not be in .htaccess)
    $ip_bans = function_exists('persistent_cache_get') ? persistent_cache_get('IP_BANS') : null;
    if ($ip_bans === null) {
        $ip_bans = $GLOBALS['SITE_DB']->query_select('banned_ip', array('*'), null, '', null, null, true);
        if (!is_array($ip_bans)) { // LEGACY
            $ip_bans = $GLOBALS['SITE_DB']->query_select('usersubmitban_ip', array('*'), null, '', null, null, true);
        }
        if ($ip_bans !== null) {
            persistent_cache_set('IP_BANS', $ip_bans);
        }
    }
    if ($ip_bans === null) {
        critical_error('DATABASE_FAIL');
    }
    global $SITE_INFO;
    if ((!$force_db) && (((isset($SITE_INFO['known_suexec'])) && ($SITE_INFO['known_suexec'] == '1')) || (is_writable_wrap(get_file_base() . '/.htaccess')))) {
        $bans = array();
        $ban_count = preg_match_all('#\n(deny from|require not ip) (.*)#i', cms_file_get_contents_safe(get_file_base() . '/.htaccess'), $bans);
        for ($i = 0; $i < $ban_count; $i++) {
            $ip_bans[$bans[2][$i]] = array('ip' => ip_apache_to_wild($bans[2][$i]));
        }
    }

    // Get IP components, used for wildcard checks
    $ip4 = (strpos($ip, '.') !== false);
    if ($ip4) {
        $ip_parts = explode('.', $ip);
    } else {
        $ip_parts = explode(':', $ip);
    }

    // Check all bans in the database
    $self_ip = null;
    foreach ($ip_bans as $ban) {
        if ((isset($ban['i_ban_until'])) && ($ban['i_ban_until'] < time())) {
            $GLOBALS['SITE_DB']->query('DELETE FROM ' . get_table_prefix() . 'banned_ip WHERE i_ban_until IS NOT NULL AND i_ban_until<' . strval(time()));
            continue;
        }

        if ((($ip4) && (_compare_ip_address($ban['ip'], $ip_parts, '.'))) || ((!$ip4) && (_compare_ip_address($ban['ip'], $ip_parts, ':')))) {
            // Found a ban, which may be a positive or negative ban...

            // Edge case: Do not consider bans for anything that would match the server's own IP or a localhost IP (may only match as a wildcard)
            if ($self_ip === null) {
                $self_host = preg_replace('#:.*#', '', cms_srv('HTTP_HOST'));
                if (($self_host == '') || (preg_match('#^localhost\.?#', $self_host) != 0)) {
                    $self_ip = '';
                } else {
                    $self_ip = cms_gethostbyname($self_host);
                    if ($self_ip == $self_host) {
                        $self_ip = cms_srv('SERVER_ADDR');
                    }
                }
            }
            if (($self_ip != '') && (compare_ip_address($ban['ip'], $self_ip))) {
                continue;
            }
            if (compare_ip_address($ban['ip'], '127.0.0.1')) {
                continue;
            }
            if (compare_ip_address($ban['ip'], 'fe00:0000:0000:0000:0000:0000:0000:0000')) {
                continue;
            }

            // Get result
            if (array_key_exists('i_ban_positive', $ban)) {
                $ret = ($ban['i_ban_positive'] == 1);
                if (!$ret) {
                    $is_unbannable = true;
                }
                $ban_until = $ban['i_ban_until'];
            } else {
                // LEGACY
                $ret = true;
                $ban_until = null;
            }
            $cache[$sz][$ip] = array(
                'is_unbannable' => $is_unbannable,
                'ban_until' => $ban_until,
                'ret' => $ret,
            );
            return $ret;
        }
    }

    // Nothing found
    $ret = $handle_uncertainties ? null : false;
    $cache[$sz][$ip] = array(
        'is_unbannable' => $is_unbannable,
        'ban_until' => $ban_until,
        'ret' => $ret,
    );

    return $ret;
}

/**
 * Convert Apache netmask syntax in IP addresses to simple Composr wildcard syntax.
 *
 * @param  IP $ip The IP address (potentially encoded with netmask syntax)
 * @return string The Composr-style IP wildcard
 */
function ip_apache_to_wild($ip)
{
    $matches = [];
    if (preg_match('#^(.*)/(\d+)$#', $ip, $matches) == 0) {
        $ip = normalise_ip_address($ip, 4);
        if ($ip == '') {
            return '';
        }

        return $ip;
    }

    $ip_section = $matches[1];
    $range_bits = $matches[2];

    $ipv6 = (strpos($ip, ':') !== false);
    if ($ipv6) {
        $delimiter = ':';
        $bits_per_part = 16;
        $expected_blank_part = '0000';
    } else {
        $delimiter = '.';
        $bits_per_part = 8;
        $expected_blank_part = '0';
    }

    if (($range_bits % $bits_per_part) != 0) {
        return '';
    }

    $parts = explode($delimiter, $ip_section);
    $ip = '';
    $bits_used = 0;
    foreach ($parts as $part) {
        if ($ip != '') {
            $ip .= $delimiter;
        }
        if ($bits_used >= $range_bits) {
            if ($part != $expected_blank_part) {
                return '';
            }
            $ip .= '*';
        } else {
            $ip .= $part;
        }
        $bits_used += $bits_per_part;
    }
    return $ip;
}

/**
 * Log an action
 *
 * @param  ID_TEXT $type The type of activity just carried out (a language string ID)
 * @param  ?SHORT_TEXT $a The most important parameter of the activity (e.g. D) (null: none)
 * @param  ?SHORT_TEXT $b A secondary (perhaps, human readable) parameter of the activity (e.g. caption) (null: none)
 * @return ?AUTO_LINK Log ID (null: did not save a log)
 */
function log_it($type, $a = null, $b = null)
{
    require_code('global4');
    return _log_it($type, $a, $b);
}

/**
 * Escape a string to fit within PHP double quotes.
 *
 * @param  string $in String in
 * @return string Resultant string
 */
function php_addslashes($in)
{
    global $PHP_REP_FROM, $PHP_REP_TO;
    return str_replace($PHP_REP_FROM, $PHP_REP_TO, $in);
}

/**
 * Remove any duplication inside the list of rows (each row being a map). Duplication is defined by rows with correspinding IDs.
 *
 * @param  array $rows The rows to remove duplication of
 * @param  string $id_field The ID field
 * @return array The filtered rows
 */
function remove_duplicate_rows($rows, $id_field = 'id')
{
    $ids_seen = array();
    $rows2 = array();
    foreach ($rows as $row) {
        if (!array_key_exists($row[$id_field], $ids_seen)) {
            $rows2[] = $row;
        }

        $ids_seen[$row[$id_field]] = true;
    }

    return $rows2;
}

/**
 * Update the member tracker for the currently viewing user.
 *
 * @param  ID_TEXT $page The page
 * @param  ID_TEXT $type The type
 * @param  ID_TEXT $id The ID
 */
function member_tracking_update($page, $type, $id)
{
    if (get_value('no_member_tracking') === '1') {
        return;
    }

    if (!$GLOBALS['SITE_DB']->table_is_locked('member_tracking')) {
        $GLOBALS['SITE_DB']->query('DELETE FROM ' . get_table_prefix() . 'member_tracking WHERE mt_time<' . strval(time() - 60 * intval(get_option('users_online_time'))) . ' OR (mt_member_id=' . strval(get_member()) . ' AND ' . db_string_equal_to('mt_type', $type) . ' AND ' . db_string_equal_to('mt_id', $id) . ' AND ' . db_string_equal_to('mt_page', $page) . ')');
    }

    $GLOBALS['SITE_DB']->query_insert('member_tracking', array(
        'mt_member_id' => get_member(),
        'mt_cache_username' => $GLOBALS['FORUM_DRIVER']->get_username(get_member(), true),
        'mt_time' => time(),
        'mt_page' => $page,
        'mt_type' => $type,
        'mt_id' => $id
    ), false, true); // Ignore errors for race conditions
}

/**
 * Find whether the current user is invisible.
 *
 * @return boolean Whether the current user is invisible
 */
function is_invisible()
{
    global $SESSION_CACHE;
    $s = get_session_id();
    return ((isset($SESSION_CACHE[$s])) && ($SESSION_CACHE[$s]['session_invisible'] == 1));
}

/**
 * Get the number of users on the site in the last 5 minutes. The function also maintains the statistic via the sessions table.
 *
 * @return integer The number of users on the site
 */
function get_num_users_site()
{
    if (get_value('disable_user_online_counting') === '1') {
        return 1;
    }

    global $NUM_USERS_SITE_CACHE, $PEAK_USERS_EVER_CACHE, $PEAK_USERS_WEEK_CACHE;
    $users_online_time_seconds = 60 * intval(get_option('users_online_time'));
    $NUM_USERS_SITE_CACHE = get_value_newer_than('users_online', time() - $users_online_time_seconds / 2); /* Refreshes half way through the user online time, to approximate accuracy */
    if ($NUM_USERS_SITE_CACHE === null) {
        $NUM_USERS_SITE_CACHE = get_value('users_online');
        $count = 0;
        require_code('users2');
        get_users_online(false, null, $count);
        $NUM_USERS_SITE_CACHE = strval($count);
        if (!$GLOBALS['SITE_DB']->table_is_locked('values')) {
            set_value('users_online', $NUM_USERS_SITE_CACHE);
        }
    }
    if ((intval($NUM_USERS_SITE_CACHE) > intval(get_option('maximum_users'))) && (intval(get_option('maximum_users')) > 1) && (get_page_name() != 'login') && (!has_privilege(get_member(), 'access_overrun_site')) && (!running_script('cron_bridge'))) {
        set_http_status_code('503');

        critical_error('BUSY', do_lang('TOO_MANY_USERS'));
    }
    if (addon_installed('stats')) {
        // Store a peak record if there is one
        $PEAK_USERS_EVER_CACHE = get_value('user_peak');
        if (($PEAK_USERS_EVER_CACHE === null) || ($PEAK_USERS_EVER_CACHE == '')) {
            $_peak_users_user = $GLOBALS['SITE_DB']->query_select_value_if_there('usersonline_track', 'MAX(peak)', null, '', true);
            $PEAK_USERS_EVER_CACHE = ($_peak_users_user === null) ? $NUM_USERS_SITE_CACHE : strval($_peak_users_user);
            if (!$GLOBALS['SITE_DB']->table_is_locked('values')) {
                set_value('user_peak', $PEAK_USERS_EVER_CACHE);
            }
        }
        if (intval($NUM_USERS_SITE_CACHE) > intval($PEAK_USERS_EVER_CACHE)) {
            // New record
            $GLOBALS['SITE_DB']->query_insert('usersonline_track', array('date_and_time' => time(), 'peak' => intval($NUM_USERS_SITE_CACHE)), false, true);
            if (!$GLOBALS['SITE_DB']->table_is_locked('values')) {
                set_value('user_peak', $NUM_USERS_SITE_CACHE);
            }
        }

        // Store a 7-day-cycle peak record if we've made one
        $PEAK_USERS_WEEK_CACHE = get_value_newer_than('user_peak_week', time() - $users_online_time_seconds / 2);
        $store_anyway = false;
        if (($PEAK_USERS_WEEK_CACHE === null) || ($PEAK_USERS_WEEK_CACHE == '')) {
            $store_anyway = true;
        }
        if ((intval($NUM_USERS_SITE_CACHE) > intval($PEAK_USERS_WEEK_CACHE)) || ($store_anyway)) {
            $PEAK_USERS_WEEK_CACHE = $NUM_USERS_SITE_CACHE;

            // But also delete anything else in the last 7 days that was less than the new weekly peak record, to keep the stats clean (we only want 7 day peaks to be stored)
            $GLOBALS['SITE_DB']->query('DELETE FROM ' . get_table_prefix() . 'usersonline_track WHERE date_and_time>' . strval(time() - 60 * 60 * 24 * 7) . ' AND peak<=' . $PEAK_USERS_WEEK_CACHE, null, null, true);

            // Set record for week
            set_value('user_peak_week', $PEAK_USERS_WEEK_CACHE);
            $GLOBALS['SITE_DB']->query_insert('usersonline_track', array('date_and_time' => time(), 'peak' => intval($PEAK_USERS_WEEK_CACHE)), false, true);
        }
    }
    return intval($NUM_USERS_SITE_CACHE);
}

/**
 * Get the largest amount of users ever to be on the site at the same time.
 *
 * @return integer The number of peak users
 */
function get_num_users_peak()
{
    global $PEAK_USERS_EVER_CACHE;
    return intval($PEAK_USERS_EVER_CACHE);
}

/**
 * Get the specified string, but with all characters escaped.
 *
 * @param  mixed $string The input string
 * @return string The escaped string
 */
function escape_html($string)
{
    //if ($string === '') return $string; // Optimisation, but doesn't work well
    if (isset($string->codename)/*faster than is_object*/) {
        return $string;
    }

    /*if ($GLOBALS['XSS_DETECT']) {  Useful for debugging
        if (ocp_is_escaped($string)) {
            @var_dump(debug_backtrace());
            @exit('String double-escaped');
        }
    }*/

    global $XSS_DETECT, $ESCAPE_HTML_OUTPUT, $DECLARATIONS_STATE;

    $ret = @htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, get_charset());

    if (defined('I_UNDERSTAND_XSS') && !$DECLARATIONS_STATE[I_UNDERSTAND_XSS]) {
        $ESCAPE_HTML_OUTPUT[$ret] = true;
    }

    if ($XSS_DETECT) {
        ocp_mark_as_escaped($ret);
    }

    return $ret;
}

/**
 * See's if the current browser matches some special property code. Assumes users are keeping up on newish browsers (except for IE users, who are 6+)
 *
 * @param  string $code The property code
 * @set    android ios wysiwyg windows mac linux odd_os mobile ie ie8 ie8+ ie9 ie9+ gecko safari odd_browser chrome bot simplified_attachments_ui itunes
 * @param  ?string $comcode Comcode that might be WYSIWYG edited; used to determine whether WYSIWYG may load when we'd prefer it to not do so (null: none)
 * @return boolean Whether there is a match
 */
function browser_matches($code, $comcode = null)
{
    global $BROWSER_MATCHES_CACHE;
    if (isset($BROWSER_MATCHES_CACHE[$code])) {
        return $BROWSER_MATCHES_CACHE[$code];
    }

    $browser = strtolower(cms_srv('HTTP_USER_AGENT'));
    $os = strtolower(cms_srv('HTTP_UA_OS')) . ' ' . $browser;
    $is_safari = strpos($browser, 'applewebkit') !== false;
    $is_chrome = strpos($browser, 'chrome/') !== false;
    $is_gecko = (strpos($browser, 'gecko') !== false) && !$is_safari;
    $is_ie = ((strpos($browser, 'msie') !== false) || (strpos($browser, 'trident') !== false) || (strpos($browser, 'edge/') !== false));
    $is_ie8 = (strpos($browser, 'msie 8') !== false) && ($is_ie);
    $is_ie9 = (strpos($browser, 'msie 9') !== false) && ($is_ie);
    $is_ie8_plus = $is_ie; // Below IE8 not supported/recognised
    $is_ie9_plus = $is_ie && !$is_ie8;

    switch ($code) {
        case 'simplified_attachments_ui':
            $BROWSER_MATCHES_CACHE[$code] = !$is_ie8 && !$is_ie9 && get_option('simplified_attachments_ui') == '1' && get_option('complex_uploader') == '1' && has_js();
            return $BROWSER_MATCHES_CACHE[$code];
        case 'itunes':
            $BROWSER_MATCHES_CACHE[$code] = (get_param_integer('itunes', 0) == 1) || (strpos($browser, 'itunes') !== false);
            return $BROWSER_MATCHES_CACHE[$code];
        case 'bot':
            $BROWSER_MATCHES_CACHE[$code] = (get_bot_type() !== null);
            return $BROWSER_MATCHES_CACHE[$code];
        case 'android':
            $BROWSER_MATCHES_CACHE[$code] = strpos($browser, 'android') !== false;
            return $BROWSER_MATCHES_CACHE[$code];
        case 'ios':
            $BROWSER_MATCHES_CACHE[$code] = strpos($browser, 'iphone') !== false || strpos($browser, 'ipad') !== false;
            return $BROWSER_MATCHES_CACHE[$code];
        case 'wysiwyg':
            if ((get_option('wysiwyg') == '0') || ((is_mobile()) && ((is_null($comcode)) || (strpos($comcode, 'html]') === false)))) {
                $BROWSER_MATCHES_CACHE[$code] = false;
                return false;
            }
            $BROWSER_MATCHES_CACHE[$code] = (strpos($browser, 'android') === false); // Using CKEditor, which does not yet support Android
            return $BROWSER_MATCHES_CACHE[$code];
        case 'windows':
            $BROWSER_MATCHES_CACHE[$code] = (strpos($os, 'windows') !== false) || (strpos($os, 'win32') !== false);
            return $BROWSER_MATCHES_CACHE[$code];
        case 'mac':
            $BROWSER_MATCHES_CACHE[$code] = strpos($os, 'mac') !== false;
            return $BROWSER_MATCHES_CACHE[$code];
        case 'linux':
            $BROWSER_MATCHES_CACHE[$code] = strpos($os, 'linux') !== false;
            return $BROWSER_MATCHES_CACHE[$code];
        case 'odd_os':
            $BROWSER_MATCHES_CACHE[$code] = (strpos($os, 'windows') === false) && (strpos($os, 'mac') === false) && (strpos($os, 'linux') === false);
            return $BROWSER_MATCHES_CACHE[$code];
        case 'mobile':
            $BROWSER_MATCHES_CACHE[$code] = is_mobile();
            return $BROWSER_MATCHES_CACHE[$code];
        case 'ie':
            $BROWSER_MATCHES_CACHE[$code] = $is_ie;
            return $BROWSER_MATCHES_CACHE[$code];
        case 'ie8':
            $BROWSER_MATCHES_CACHE[$code] = $is_ie8;
            return $BROWSER_MATCHES_CACHE[$code];
        case 'ie8+':
            $BROWSER_MATCHES_CACHE[$code] = $is_ie8_plus;
            return $BROWSER_MATCHES_CACHE[$code];
        case 'ie9':
            $BROWSER_MATCHES_CACHE[$code] = $is_ie9;
            return $BROWSER_MATCHES_CACHE[$code];
        case 'ie9+':
            $BROWSER_MATCHES_CACHE[$code] = $is_ie9_plus;
            return $BROWSER_MATCHES_CACHE[$code];
        case 'chrome':
            $BROWSER_MATCHES_CACHE[$code] = $is_chrome;
            return $BROWSER_MATCHES_CACHE[$code];
        case 'gecko':
            $BROWSER_MATCHES_CACHE[$code] = $is_gecko;
            return $BROWSER_MATCHES_CACHE[$code];
        case 'safari':
            $BROWSER_MATCHES_CACHE[$code] = $is_safari;
            return $BROWSER_MATCHES_CACHE[$code];
        case 'odd_browser':
            $BROWSER_MATCHES_CACHE[$code] = !$is_safari && !$is_gecko && !$is_ie;
            return $BROWSER_MATCHES_CACHE[$code];
    }

    // Should never get here
    return false;
}

/**
 * Look at the user's browser, and decide if they are viewing on a mobile device or not.
 *
 * @param  ?string $user_agent The user agent (null: get from environment, current user's browser)
 * @param  boolean $truth Whether to always tell the truth (even if the current page does not have mobile support)
 * @return boolean Whether the user is using a mobile device
 */
function is_mobile($user_agent = null, $truth = false)
{
    $user_agent_given = ($user_agent !== null);

    global $IS_MOBILE_CACHE, $IS_MOBILE_TRUTH_CACHE;

    if (!$user_agent_given) {
        if (($truth ? $IS_MOBILE_TRUTH_CACHE : $IS_MOBILE_CACHE) !== null) {
            return $truth ? $IS_MOBILE_TRUTH_CACHE : $IS_MOBILE_CACHE;
        }
    }

    if ((!function_exists('get_option')) || (get_option('mobile_support') == '0')) {
        if (function_exists('get_option')) {
            $IS_MOBILE_CACHE = false;
            $IS_MOBILE_TRUTH_CACHE = false;
        }
        return false;
    }

    if ($user_agent === null) {
        $user_agent = cms_srv('HTTP_USER_AGENT');
    }

    global $SITE_INFO;
    if (((!isset($SITE_INFO['assume_full_mobile_support'])) || ($SITE_INFO['assume_full_mobile_support'] != '1')) && (isset($GLOBALS['FORUM_DRIVER'])) && (!$truth) && (running_script('index')) && (($theme = $GLOBALS['FORUM_DRIVER']->get_theme()) != 'default')) {
        $ini_path = (($theme == 'default' || $theme == 'admin') ? get_file_base() : get_custom_file_base()) . '/themes/' . $theme . '/theme.ini';
        if (is_file($ini_path)) {
            $page = get_param_string('page', ''); // We intentionally do not use get_page_name, as that requires URL Monikers to work, which are not available early in boot (as needed by static cache)

            require_code('files');
            $details = better_parse_ini_file($ini_path);
            if (!empty($details['mobile_pages'])) {
                if (substr($details['mobile_pages'], 0, 1) == '#' && substr($details['mobile_pages'], -1) == '#') {
                    if (preg_match($details['mobile_pages'], get_zone_name() . ':' . $page) == 0) {
                        $IS_MOBILE_CACHE = false;
                        return false;
                    }
                } else {
                    if (preg_match('#(^|,)\s*' . preg_quote($page, '#') . '\s*(,|$)#', $details['mobile_pages']) == 0 && preg_match('#(^|,)\s*' . preg_quote(get_zone_name() . ':' . $page, '#') . '\s*(,|$)#', $details['mobile_pages']) == 0) {
                        $IS_MOBILE_CACHE = false;
                        return false;
                    }
                }
            }
        }
    }

    if (!$user_agent_given && !$truth) {
        $val = get_param_integer('keep_mobile', null);
        if ($val !== null) {
            $result = ($val == 1);
            if (isset($GLOBALS['FORUM_DRIVER'])) {
                if ($truth) {
                    $IS_MOBILE_TRUTH_CACHE = $result;
                } else {
                    $IS_MOBILE_CACHE = $result;
                }
            }
            return $result;
        }
    }

    // The set of browsers (also change in static_cache.php)
    $browsers = array(
        // Implication by technology claims
        'WML',
        'WAP',
        'Wap',
        'MIDP', // Mobile Information Device Profile

        // Generics
        'Mobile',
        'Smartphone',
        'WebTV',

        // Well known/important browsers/brands
        'Mobile Safari', // Usually Android
        'Android',
        'iPhone',
        'iPod',
        'Opera Mobi',
        'Opera Mini',
        'BlackBerry',
        'Windows Phone',
        'nook browser', // Barnes and Noble
    );

    $exceptions = array(
        'iPad',
    );

    if (((!isset($SITE_INFO['no_extra_mobiles'])) || ($SITE_INFO['no_extra_mobiles'] != '1')) && (is_file(get_file_base() . '/text_custom/mobile_devices.txt'))) {
        require_code('files');
        $mobile_devices = better_parse_ini_file((get_file_base() . '/text_custom/mobile_devices.txt'));
        foreach ($mobile_devices as $key => $val) {
            if ($val == 1) {
                $browsers[] = $key;
            } else {
                $exceptions[] = $key;
            }
        }
    }

    // The test
    $result = (preg_match('/(' . implode('|', $browsers) . ')/i', $user_agent) != 0) && (preg_match('/(' . implode('|', $exceptions) . ')/i', $user_agent) == 0);
    if (!$user_agent_given) {
        if (isset($GLOBALS['FORUM_DRIVER'])) {
            if ($truth) {
                $IS_MOBILE_TRUTH_CACHE = $result;
            } else {
                $IS_MOBILE_CACHE = $result;
            }
        }
    }

    return $result;
}

/**
 * Get the name of a webcrawler bot, or null if no bot detected
 *
 * @param ?string $agent User agent (null: read from environment)
 * @return ?string Webcrawling bot name (null: not a bot)
 */
function get_bot_type($agent = null)
{
    $agent_given = ($agent !== null);

    if (!$agent_given) {
        global $BOT_TYPE_CACHE;
        if ($BOT_TYPE_CACHE !== false) {
            return $BOT_TYPE_CACHE;
        }

        $agent = cms_srv('HTTP_USER_AGENT');
    }

    if (strpos($agent, 'WebKit') !== false || strpos($agent, 'Trident') !== false || strpos($agent, 'MSIE') !== false || strpos($agent, 'Firefox') !== false || strpos($agent, 'Opera') !== false) {
        if (strpos($agent, 'bot') === false) {
            // Quick exit path
            if (!$agent_given) {
                $BOT_TYPE_CACHE = null;
            }
            return null;
        }
    }

    $agent = strtolower($agent);

    global $BOT_MAP_CACHE, $SITE_INFO;
    if ($BOT_MAP_CACHE === null) {
        if (((!isset($SITE_INFO['no_extra_bots'])) || ($SITE_INFO['no_extra_bots'] != '1')) && (is_file(get_file_base() . '/text_custom/bots.txt'))) {
            require_code('files');
            $BOT_MAP_CACHE = better_parse_ini_file(get_file_base() . '/text_custom/bots.txt');
        } else {
            $BOT_MAP_CACHE = array(
                'zyborg' => 'Looksmart',
                'googlebot' => 'Google',
                'mediapartners-google' => 'Google Adsense',
                'teoma' => 'Teoma',
                'jeeves' => 'Ask Jeeves',
                'ultraseek' => 'Infoseek',
                'ia_archiver' => 'Alexa/Archive.org',
                'msnbot' => 'Bing',
                'bingbot' => 'Bing',
                'mantraagent' => 'LookSmart',
                'wisenutbot' => 'Looksmart',
                'paros' => 'Paros',
                'sqworm' => 'Aol.com',
                'baidu' => 'Baidu',
                'facebookexternalhit' => 'Facebook',
                'yandex'=> 'Yandex',
                'daum' => 'Daum',
                'ahrefsbot' => 'Ahrefs',
                'mj12bot' => 'Majestic-12',
                'blexbot' => 'webmeup',
                'duckduckbot' => 'DuckDuckGo',
            );
        }
    }
    foreach ($BOT_MAP_CACHE as $id => $name) {
        if ($name == '') {
            continue;
        }
        if (strpos($agent, $id) !== false) {
            if (!$agent_given) {
                $BOT_TYPE_CACHE = $name;
            }

            return $name;
        }
    }

    if ((strpos($agent, 'bot') !== false) || (strpos($agent, 'spider') !== false)) {
        $to_a = strpos($agent, ' ');
        if ($to_a === false) {
            $to_a = strlen($agent);
        }
        $to_b = strpos($agent, '/');
        if ($to_b === false) {
            $to_b = strlen($agent);
        }

        $name = substr($agent, 0, min($to_a, $to_b));

        if (!$agent_given) {
            $BOT_TYPE_CACHE = $name;
        }

        return $name;
    }

    if (!$agent_given) {
        $BOT_TYPE_CACHE = null;
    }

    return null;
}

/**
 * Determine whether the user's browser supports cookies or not.
 * Unfortunately this function will only return true once a user has been to the site more than once... Composr will set a cookie, and if it perseveres, that indicates cookies work.
 *
 * @return boolean Whether the user has definitely got cookies
 */
function has_cookies() // Will fail on users first visit, but then will catch on
{
    global $HAS_COOKIES_CACHE;
    if ($HAS_COOKIES_CACHE !== null) {
        return $HAS_COOKIES_CACHE;
    }

    /*if (($GLOBALS['DEV_MODE']) && (get_param_integer('keep_debug_has_cookies', 0) == 0) && (!running_script('commandr')))   We know this works by now, was tested for years. Causes annoyance when developing
    {
        $_COOKIE = array();
        return false;
    }*/

    if (isset($_COOKIE['has_cookies'])) {
        $HAS_COOKIES_CACHE = true;
        return true;
    }
    require_code('users_active_actions');
    cms_setcookie('has_cookies', '1');
    $HAS_COOKIES_CACHE = false;
    return false;
}

/**
 * Determine whether the user's browser supports JavaScript or not.
 * Unfortunately this function will only return true once a user has been to the site more than once... JavaScript will set a cookie, indicating it works.
 *
 * @return boolean Whether the user has definitely got JavaScript
 */
function has_js()
{
    if (!function_exists('get_option')) {
        return true;
    }
    if (get_option('detect_javascript') == '0') {
        return true;
    }
    if (get_param_integer('keep_has_js', 0) == 1) {
        return true;
    }
    if (get_param_integer('keep_has_js', null) === 0) {
        return false;
    }
    return ((array_key_exists('js_on', $_COOKIE)) && ($_COOKIE['js_on'] == '1'));
}

/**
 * Turn an array into a humanely readable string.
 *
 * @param  array $array Array to convert
 * @param  boolean $already_stripped Whether PHP magic-quotes have already been cleaned out for the array
 * @return string A humanely readable version of the array.
 */
function flatten_slashed_array($array, $already_stripped = false)
{
    $ret = '';
    foreach ($array as $key => $val) {
        if (is_array($val)) {
            $val = flatten_slashed_array($val);
        }

        if (!$already_stripped && @get_magic_quotes_gpc()) {
            $val = stripslashes($val);
        }

        $ret .= '<param>' . (is_integer($key) ? strval($key) : $key) . '=' . $val . '</param>' . "\n"; // $key may be integer, due to recursion line for list fields, above
    }
    return $ret;
}

/**
 * Get a word-filtered version of the specified text.
 *
 * @param  string $text Text to filter
 * @return string Filtered version of the input text
 */
function wordfilter_text($text)
{
    if (!addon_installed('wordfilter')) {
        return $text;
    }

    require_code('wordfilter');
    return check_wordfilter($text, null, true);
}

/**
 * Assign this to explicitly declare that a variable may be of mixed type, and initialise to null.
 *
 * @return ?mixed Of mixed type (null: default)
 */
function mixed()
{
    return null;
}

/**
 * Get meta information for specified resource
 *
 * @param  ID_TEXT $type The type of resource (e.g. download)
 * @param  ID_TEXT $id The ID of the resource
 * @return array A pair: The first element is the meta keyword string for the specified resource, and the other is the meta description string.
 */
function seo_meta_get_for($type, $id)
{
    $cache = function_exists('persistent_cache_get') ? persistent_cache_get(array('seo', $type, $id)) : null;
    if ($cache !== null) {
        return $cache;
    }

    $where = array('meta_for_type' => $type, 'meta_for_id' => $id);

    $cache = array('', '');

    $rows = $GLOBALS['SITE_DB']->query_select('seo_meta_keywords', array('meta_keyword'), $where, 'ORDER BY id');
    foreach ($rows as $row) {
        if ($cache[0] != '') {
            $cache[0] .= ',';
        }
        $cache[0] .= get_translated_text($row['meta_keyword']);
    }

    $rows = $GLOBALS['SITE_DB']->query_select('seo_meta', array('meta_description'), $where, '', 1);
    if (array_key_exists(0, $rows)) {
        $cache[1] = get_translated_text($rows[0]['meta_description']);
    }

    persistent_cache_set(array('seo', $type, $id), $cache);

    return $cache;
}

/**
 * Load the specified resource's meta information into the system for use on this page.
 * Also, if the title is specified then this is used for the page title.
 *
 * @sets_output_state
 *
 * @param  ID_TEXT $type The type of resource (e.g. download)
 * @param  ID_TEXT $id The ID of the resource
 * @param  ?string $title The page-specific title to use, in Comcode or plain-text format with possible HTML entities included [Comcode will later be stripped] (null: none)
 */
function seo_meta_load_for($type, $id, $title = null)
{
    if (!$GLOBALS['IS_VIRTUALISED_REQUEST']) {
        $result = seo_meta_get_for($type, $id);
        global $SEO_KEYWORDS, $SEO_DESCRIPTION, $SHORT_TITLE;
        if ($result[0] != '') {
            $SEO_KEYWORDS = array_map('trim', explode(',', trim($result[0], ',')));
        }
        if ($result[1] != '') {
            $SEO_DESCRIPTION = $result[1];
        }
        if ($title !== null) {
            set_short_title(str_replace('&ndash;', '-', str_replace('&copy;', '(c)', str_replace('&#039;', '\'', $title))));
        }
    } // Otherwise don't bother (this is an optimisation)
}

/**
 * Get Tempcode for tags, based on loaded up from SEO keywords (seo_meta_load_for).
 *
 * @param  ?ID_TEXT $limit_to The search code for this tag content (e.g. downloads) (null: there is none)
 * @param  ?array $the_tags Explicitly pass a list of tags instead (null: use loaded ones)
 * @return Tempcode Loaded tag output (or blank if there are none)
 */
function get_loaded_tags($limit_to = null, $the_tags = null)
{
    if (get_value('no_tags') === '1') {
        return new Tempcode();
    }
    if (!addon_installed('search')) {
        return new Tempcode();
    }

    if ($the_tags === null) {
        global $SEO_KEYWORDS;
        $the_tags = $SEO_KEYWORDS;
    }

    $tags = array();
    if ($the_tags !== null) {
        $search_limiter_no = array('all_defaults' => '1');
        if ($limit_to !== null) {
            $search_limiter_no['search_' . $limit_to] = '1';
            $search_limiter_no['all_defaults'] = '0';
        }

        if ($limit_to !== null) {
            $search_limiter_yes = array();
            $search_limiter_yes['search_' . $limit_to] = '1';
            $search_limiter_yes['all_defaults'] = '0';
        } else {
            $search_limiter_yes = $search_limiter_no;
        }

        foreach ($the_tags as $tag) {
            $tag = trim($tag);
            if ($tag == '') {
                continue;
            }

            $tags[] = array(
                'TAG' => $tag,
                'LINK_LIMITEDSCOPE' => build_url(array('page' => 'search', 'type' => 'results', 'content' => '"' . $tag . '"', 'only_search_meta' => '1', 'boolean_search' => '1') + $search_limiter_yes, get_module_zone('search')),
                'LINK_FULLSCOPE' => build_url(array('page' => 'search', 'type' => 'results', 'content' => '"' . $tag . '"', 'only_search_meta' => '1', 'boolean_search' => '1') + $search_limiter_no, get_module_zone('search')),
            );
        }
    }

    return do_template('TAGS', array('_GUID' => '2cd542a245bc7d1c3f10e858e8fc5159', 'TAGS' => $tags, 'TYPE' => ($limit_to === null) ? '' : $limit_to));
}

/**
 * Get the default page for a zone.
 *
 * @param  ID_TEXT $zone_name Zone name
 * @return ID_TEXT Default page
 */
function get_zone_default_page($zone_name)
{
    if ($zone_name == '_SELF') {
        $zone_name = get_zone_name();
    }

    /*$p_test = function_exists('persistent_cache_get') ? persistent_cache_get(array('ZONE', $zone_name)) : null;       Better to get from ALL_ZONES_TITLED, less cache volume
    if ($p_test !== null) {
        return $p_test['zone_default_page'];
    }*/

    global $ZONE;
    if (($ZONE !== null) && ($ZONE['zone_name'] == $zone_name) && ($ZONE['zone_default_page'] !== null)) {
        return $ZONE['zone_default_page'];
    } else {
        global $ZONE_DEFAULT_PAGES_CACHE;
        if (!isset($ZONE_DEFAULT_PAGES_CACHE[$zone_name])) {
            $_zone_default_page = null;
            if (function_exists('persistent_cache_get')) {
                $temp = persistent_cache_get('ALL_ZONES_TITLED');
                if ($temp !== null) {
                    $_zone_default_page = array();
                    foreach ($temp as $_temp) {
                        list($_zone_name, , $zone_default_page) = $_temp;
                        $_zone_default_page[] = array('zone_name' => $_zone_name, 'zone_default_page' => $zone_default_page);
                    }
                }
            }
            if ($_zone_default_page === null) {
                $_zone_default_page = $GLOBALS['SITE_DB']->query_select('zones', array('zone_name', 'zone_default_page'), null/*Load multiple so we can cache for performance array('zone_name' => $zone_name)*/, 'ORDER BY zone_title', 50/*reasonable limit; zone_title is sequential for default zones*/);
            }
            $ZONE_DEFAULT_PAGES_CACHE[$zone_name] = 'start';
            $ZONE_DEFAULT_PAGES_CACHE['collaboration'] = 'start'; // Set this in case collaboration zone removed but still referenced. Performance tweak!
            foreach ($_zone_default_page as $zone_row) {
                $ZONE_DEFAULT_PAGES_CACHE[$zone_row['zone_name']] = $zone_row['zone_default_page'];
            }
        }

        return $ZONE_DEFAULT_PAGES_CACHE[$zone_name];
    }
}

/**
 * Turn a boring codename, into a "pretty" title.
 *
 * @param  ID_TEXT $boring The codename
 * @return string The title
 */
function titleify($boring)
{
    $ret = $boring;

    if (strpos($ret, '/') !== false || strpos($ret, '\\') !== false) {
        $ret = preg_replace('#([/\\\\])#', '${1} ', $ret);
    }

    $ret = ucwords(trim(str_replace('_', ' ', $boring)));

    $acronyms = array(
        'CMS',
        'CNS',
        'URL',
        'ID',
        'UI',
        'HTML',
        'MSN',
        'LDAP',
        'SMS',
        'SSL',
        'XML',
        'HPHP',
        'CSS',
        'SEO',
        'JavaScript',
    );
    foreach ($acronyms as $acronym) {
        if (stripos($ret, $acronym) !== false) {
            $ret = cms_preg_replace_safe('#(^|\s)' . preg_quote($acronym, '#') . '(\s|$)#i', '$1' . $acronym . '$2', $ret);
        }
    }

    if (strpos($ret, 'Ecommerce') !== false) {
        $ret = str_replace('Ecommerce', addon_installed('ecommerce') ? do_lang('ecommerce:ECOMMERCE') : 'eCommerce', $ret);
    }
    if (strpos($ret, 'Cpfs') !== false) {
        $ret = str_replace('Cpfs', do_lang('cns:CUSTOM_PROFILE_FIELDS'), $ret);
    }
    if (strpos($ret, 'Captcha') !== false) {
        $ret = str_replace('Captcha', addon_installed('captcha') ? do_lang('captcha:CAPTCHA') : 'CAPTCHA', $ret);
    }
    $ret = str_replace('Adminzone', do_lang('ADMIN_ZONE'), $ret);
    $ret = str_replace('Emails', do_lang('EMAILS'), $ret);
    $ret = str_replace('Phpinfo', 'PHP-Info', $ret);
    $ret = str_replace('CNS', 'Conversr', $ret);
    if (strpos($ret, 'Default Set') !== false) {
        $ret = str_replace('Default Set/cartoons', do_lang('cns:AVATARS_CARTOONS'), $ret);
        $ret = str_replace('Default Set/thematic', do_lang('cns:AVATARS_THEMATIC'), $ret);
        $ret = str_replace('Default Set', do_lang('cns:AVATARS_MISC'), $ret);
    }

    if ($GLOBALS['XSS_DETECT'] && ocp_is_escaped($boring)) {
        ocp_mark_as_escaped($ret);
    }

    return $ret;
}

/**
 * Propagate Filtercode through links.
 *
 * @param  ID_TEXT $prefix Prefix for main filter environment variable
 * @return array Extra URL mappings
 */
function propagate_filtercode($prefix = '')
{
    $active_filter = either_param_string(($prefix == '') ? 'active_filter' : ($prefix . '_active_filter'), '');
    $map = array();
    if ($active_filter != '') {
        $map['active_filter'] = $active_filter;
        foreach (array_keys($_GET + $_POST) as $key) {
            if (substr($key, 0, 7) == 'filter_') {
                $map[$key] = either_param_string($key, '');
            }
        }
    }
    return $map;
}

/**
 * Propagate Filtercode through page-links.
 *
 * @return string Extra page-link mappings
 */
function propagate_filtercode_page_link()
{
    $map = propagate_filtercode();
    $_map = '';
    foreach ($map as $key => $val) {
        $_map .= ':' . $key . '=' . urlencode($val);
    }
    return $_map;
}

/**
 * Make some text fractionably editable (i.e. inline editable).
 *
 * @param  ID_TEXT $content_type Content type
 * @param  mixed $id Content ID
 * @param  mixed $title Content title (either unescaped string, or Compiled Comcode [i.e. Tempcode])
 * @return Tempcode Inline editable HTML to put into output
 */
function make_fractionable_editable($content_type, $id, $title)
{
    require_code('content');
    $ob = get_content_object($content_type);
    $info = $ob->info();
    if ($info === null) {
        return is_object($title) ? $title : make_string_tempcode(escape_html($title));
    }

    $parameters = array(
        is_object($title) ? $title->evaluate() : $title,
        array_key_exists('edit_page_link_field', $info) ? $info['edit_page_link_field'] : preg_replace('#^\w\w?_#', '',
        array_key_exists('title_field_post', $info) ? $info['title_field_post'] : $info['title_field']),
        array_key_exists('edit_page_link_pattern_post', $info) ? str_replace('_WILD', is_integer($id) ? strval($id) : $id,
        $info['edit_page_link_pattern_post']) : preg_replace('#:_(.*)#', ':__${1}', str_replace('_WILD',
        is_integer($id) ? strval($id) : $id, $info['edit_page_link_pattern'])),
        (array_key_exists('title_field_supports_comcode', $info) && $info['title_field_supports_comcode']) ? '1' : '0',
    );
    return directive_tempcode('FRACTIONAL_EDITABLE', is_object($title) ? $title : escape_html($title), $parameters);
}

/**
 * Find whether a fractional edit is underway.
 *
 * @return boolean Whether a fractional edit is underway
 */
function fractional_edit()
{
    return post_param_integer('fractional_edit', 0) == 1;
}

/**
 * Convert some HTML to plain text.
 *
 * @param  string $in HTML
 * @return string Plain text
 */
function strip_html($in)
{
    if ((strpos($in, '<') === false) && (strpos($in, '&') === false)) {
        return $in; // Optimisation
    }

    $search = array(
        '#<script[^>]*?' . '>.*?</script>#si',  // Strip out JavaScript
        '#<style[^>]*?' . '>.*?</style>#siU',   // Strip style tags properly
        '#<![\s\S]*?--[ \t\n\r]*>#',            // Strip multi-line comments including CDATA
    );
    $in = preg_replace($search, '', $in);
    if (get_charset() != 'utf-8') {
        $in = str_replace(array('&ndash;', '&mdash;', '&middot;', '&ldquo;', '&rdquo;', '&lsquo;', '&rsquo;'), array('-', '-', '|', '"', '"', "'", "'"), $in);
    }
    $in = str_replace('><', '> <', $in);
    $in = strip_tags($in);
    return @html_entity_decode($in, ENT_QUOTES, get_charset());
}

/**
 * Find the base URL for documentation.
 *
 * @return URLPATH The base URL for documentation
 */
function get_brand_base_url()
{
    $value = function_exists('get_value') ? get_value('rebrand_base_url') : null;
    if (($value === null) || ($value == '')) {
        $value = 'https://compo.sr';
    }
    return $value;
}

/**
 * Get a URL to a Composr tutorial.
 *
 * @param  ?ID_TEXT $tutorial Name of a tutorial (null: don't include the page part)
 * @return URLPATH URL to a tutorial
 */
function get_tutorial_url($tutorial)
{
    $ret = get_brand_page_url(array('page' => is_null($tutorial) ? 'abcdef' : $tutorial), 'docs' . strval(cms_version()));
    if (is_null($tutorial)) {
        $ret = str_replace('abcdef.htm', '', $ret);
    }
    return $ret;
}

/**
 * Get a URL to a compo.sr page.
 *
 * @param  array $params URL map
 * @param  ID_TEXT $zone Zone
 * @return URLPATH URL to page
 */
function get_brand_page_url($params, $zone)
{
    // Assumes brand site supports .htm URLs, which it should
    return get_brand_base_url() . (($zone == '') ? '' : '/') . $zone . '/' . urlencode(str_replace('_', '-', $params['page'])) . '.htm';
}

/**
 * Get the brand name.
 *
 * @return string The brand name
 */
function brand_name()
{
    $value = function_exists('get_value') ? get_value('rebrand_name') : null;
    if ($value === null) {
        $value = 'Composr';
    }
    return $value;
}

/**
 * Find if we're on an Conversr satellite site.
 *
 * @return boolean If we are
 */
function is_cns_satellite_site()
{
    if (get_forum_type() != 'cns') {
        return false;
    }
    return (isset($GLOBALS['FORUM_DB'])) && ((get_db_site() != get_db_forums()) || (get_db_site_host() != get_db_forums_host()) || (get_db_site_user() != get_db_forums_user()));
}

/**
 * Convert GUIDs to IDs in some text.
 *
 * @param  string $text Input text
 * @return string Output text
 */
function convert_guids_to_ids($text)
{
    if (!addon_installed('commandr')) {
        return $text;
    }

    $matches = array();
    $num_matches = preg_match_all('#^{?([0-9a-fA-F]){8}(-([0-9a-fA-F]){4}){3}-([0-9a-fA-F]){12}}?$#', $text, $matches);
    if ($num_matches != 0) {
        require_code('resource_fs');
        $guids = array();
        for ($i = 0; $i < $num_matches; $i++) {
            $guids[] = $matches[0][$i];
        }
        $mappings = find_ids_via_guids($guids);
        foreach ($mappings as $guid => $id) {
            $text = str_replace($guid, $id, $text);
        }
    }
    return $text;
}

/**
 * Set if a mass-import is in progress.
 *
 * @param  boolean $doing_mass_import If it is
 */
function set_mass_import_mode($doing_mass_import = true)
{
    global $MASS_IMPORT_HAPPENING;
    $MASS_IMPORT_HAPPENING = $doing_mass_import;
}

/**
 * Find if a mass-import is in progress.
 *
 * @return boolean If it is
 */
function get_mass_import_mode()
{
    global $MASS_IMPORT_HAPPENING;
    return $MASS_IMPORT_HAPPENING;
}

/**
 * Prepare an argument for use literally in a command. Works around common PHP restrictions.
 *
 * @param  string $arg The argument.
 * @return string Escaped.
 */
function escapeshellarg_wrap($arg)
{
    if (php_function_allowed('escapeshellarg')) {
        return escapeshellarg($arg);
    }
    return "'" . addslashes(str_replace(array(chr(0), "'"), array('', "'\"'\"'"), $arg)) . "'";
}

/**
 * Find whether Composr is running on a local network, rather than a live-site.
 *
 * @return boolean If it is running locally
 */
function running_locally()
{
    return
        (substr(cms_srv('HTTP_HOST'), 0, 8) == '192.168.') ||
        (substr(cms_srv('HTTP_HOST'), 0, 7) == '10.0.0.') ||
        (in_array(cms_srv('HTTP_HOST'), array('localhost')));
}

/**
 * Exit if we are running on a Google App Engine application (live or development).
 */
function appengine_general_guard()
{
    if (GOOGLE_APPENGINE) {
        warn_exit(do_lang_tempcode('NOT_ON_GOOGLE_APPENGINE'));
    }
}

/**
 * Exit if we are running on a live Google App Engine application.
 */
function appengine_live_guard()
{
    if (appengine_is_live()) {
        warn_exit(do_lang_tempcode('NOT_ON_LIVE_GOOGLE_APPENGINE'));
    }
}

/**
 * Check serialized data for objects, as a security measure.
 *
 * @param string $data &$data Serialized data
 * @param ?mixed $safe_replacement What to substitute if objects are contained (null: substitute null)
 */
function secure_serialized_data(&$data, $safe_replacement = null)
{
    // Security check, unserialize can result in unchecked magic method invocation on defined objects
    //  Would be a vulnerability if there's a defined class where such method invocation has dangerous side-effects

    $matches = array();
    $num_matches = preg_match_all('#(^|;)O:[\d\+\-\.]+:"([^"]+)"#', $data, $matches);
    for ($i = 0; $i < $num_matches; $i++) {
        $harsh = true; // Could be turned into a method parameter later, if needed
        if ($harsh) {
            $bad_methods = array(
                '__.*',
                'code_to_preexecute',
            );
        } else {
            $bad_methods = array(
                '__sleep',
                '__wakeup',
                '__destruct',
                '__toString',
                '__set_state',
                '__isset',
                '__get',
                '__set',
                '__call',
                '__callStatic',
                'code_to_preexecute',
            );
        }

        $class_name = $matches[2][$i];
        $methods = get_class_methods($class_name);

        foreach ($bad_methods as $bad_method) {
            foreach ($methods as $method) {
                if (preg_match('#^' . $bad_method . '$#', $method) != 0) {
                    $data = serialize($safe_replacement);
                    return;
                }
            }
        }
    }
}

/**
 * Creates a PHP value from a stored representation.
 * Wraps the fact that new versions of PHP have better security, but old ones won't let you pass the extra parameter.
 *
 * @param  string $data Serialized string.
 * @return ~mixed What was originally serialised (false: bad data given, or actually false was serialized).
 */
function cms_unserialize($data)
{
    if (version_compare(PHP_VERSION, '7.0.0') >= 0) {
        return unserialize($data, array('allowed_classes' => false));
    }
    return unserialize($data);
}

/**
 * Update a catalogue content field reference, to a new value.
 *
 * @param ID_TEXT $type Content type
 * @param ID_TEXT $from Old value
 * @param ID_TEXT $to New value
 */
function update_catalogue_content_ref($type, $from, $to)
{
    if (strpos(get_db_type(), 'mysql') !== false) {
        $GLOBALS['SITE_DB']->query_update('catalogue_fields f JOIN ' . $GLOBALS['SITE_DB']->get_table_prefix() . 'catalogue_efv_short v ON v.cf_id=f.id', array('cv_value' => $to), array('cv_value' => $from, 'cf_type' => $type));
    } else {
        $fields = $GLOBALS['SITE_DB']->query_select('catalogue_fields', array('id'), array('cf_type' => $type));
        foreach ($fields as $field) {
            $GLOBALS['SITE_DB']->query_update('catalogue_efv_short', array('cv_value' => $to), array('cv_value' => $from, 'cf_id' => $field['id']));
        }
    }
}

/**
 * Start a profiling block, for a specified identifier (of your own choosing).
 *
 * @param  ID_TEXT $identifier Identifier
 */
function cms_profile_start_for($identifier)
{
    require_code('profiler');
    _cms_profile_start_for($identifier);
}

/**
 * End a profiling block, for a specified identifier (of your own choosing - but you must have started it with cms_profile_start_for).
 *
 * @param  ID_TEXT $identifier Identifier
 * @param  ?string $specifics Longer details of what happened (e.g. a specific SQL query that ran) (null: none provided)
 */
function cms_profile_end_for($identifier, $specifics = null)
{
    require_code('profiler');
    _cms_profile_end_for($identifier, $specifics);
}

/**
 * Put out some benign HTTP output.
 * FastCGI seems to have a weird issue with 'slowish spiky process not continuing with output' - this works around it. Not ideal as would break headers in any subsequent code.
 */
function send_http_output_ping()
{
    global $DOING_OUTPUT_PINGS;
    $DOING_OUTPUT_PINGS = true;

    if (running_script('index')) {
        if (!headers_sent()) {
            safe_ini_set('zlib.output_compression', 'Off'); // Otherwise it can compress all the spaces to nothing
            cms_ob_end_clean(); // Otherwise flushing won't help
        }

        echo ' ';
        flush();
    }
}

/**
 * Improve security by turning on a strict CSP that only allows stuff from partner sites and disables frames and forms.
 * Must be called before page output starts.
 *
 * @param  ?MEMBER $enable_more_open_html_for Allow more open HTML for a particular member ID (null: no member). It still will use the HTML blacklist functionality (unless they have even higher access already), but will remove the more restrictive whitelist functionality. Use of set_high_security_csp here is further decreasing the risk from dangerous HTML, even though the risk should be very low anyway due to the blacklist filter.
 */
function set_high_security_csp($enable_more_open_html_for = null)
{
    require_code('input_filter');
    $_partners = get_allowed_partner_sites();
    if ($_partners == array()) {
        $partners = '';
    } else {
        $partners = ' ' . implode(' ', $_partners);
        $partners .= ' https://' . implode(' https://', $_partners);
        $partners .= ' http://' . implode(' http://', $_partners);
    }

    $value = "";
    $value .= "script-src 'self'{$partners}; "; // browser will check mime-type, so okay for self
    $value .= "style-src 'self'{$partners}; "; // browser will check mime-type, so okay for self
    $value .= "object-src 'none'; "; // browser may not check mime-type, so none
    $value .= "frame-src 'none'; child-src 'none'; ";
    $value .= "form-action 'self'; ";
    $value .= "base-uri 'self'; ";
    $value .= "frame-ancestors 'self'{$partners}; ";

    header('Content-Security-Policy:' . trim($value));

    if ($enable_more_open_html_for !== null) {
        global $PRIVILEGE_CACHE;
        has_privilege($enable_more_open_html_for, 'allow_html'); // Force loading, so we can amend the cached value cleanly
        $PRIVILEGE_CACHE[$enable_more_open_html_for]['allow_html'][''][''][''] = 1;
    }
}

/**
 * Set a CSP header to not allow any frames to include us.
 */
function set_no_clickjacking_csp()
{
    require_code('input_filter');
    $_partners = get_allowed_partner_sites();
    if ($_partners == array()) {
        $partners = '';
    } else {
        $partners = ' ' . implode(' ', $_partners);
        $partners .= ' https://' . implode(' https://', $_partners);
        $partners .= ' http://' . implode(' http://', $_partners);
    }

    $value = "";
    $value .= "frame-ancestors 'self'{$partners}; ";

    @header('Content-Security-Policy:' . trim($value));
}

/**
 * Stop the web browser trying to save us, and breaking some requests in the process.
 */
function disable_browser_xss_detection()
{
    @header('X-XSS-Protection: 0');
}

/**
 * Whether smart decaching is enabled. It is slightly inefficient but makes site development easier for people.
 *
 * @param  boolean $support_temporary_disable Support it being temporarily disabled
 * @return boolean If smart decaching is enabled
 */
function support_smart_decaching($support_temporary_disable = false)
{
    if ($support_temporary_disable) {
        global $DISABLE_SMART_DECACHING_TEMPORARILY;
        if ($DISABLE_SMART_DECACHING_TEMPORARILY) {
            return false;
        }
    }

    static $has_in_url = null;
    if ($has_in_url === null) {
        $has_in_url = (get_param_integer('keep_smart_decaching', 0) == 1);
    }
    if ($has_in_url) {
        return true;
    }

    global $SITE_INFO;
    if (!empty($SITE_INFO['disable_smart_decaching'])) {
        if ($SITE_INFO['disable_smart_decaching'] == '1') {
            return false;
        }

        static $has_temporary = null;
        if ($has_temporary === null) {
            $has_temporary = false;
            $matches = array();
            if (preg_match('#^(\d+):(.*)$#', $SITE_INFO['disable_smart_decaching'], $matches) != 0) {
                $time = intval($matches[1]);
                $path = $matches[2];
                if (is_file($path) && filemtime($path) > time() - $time) {
                    $has_temporary = true;
                }
            }
        }
        return $has_temporary;
    }

    return true; // By default it is on
}

/**
 * For performance reasons disable smart decaching for cases that allow it to be disabled temporarily (it does a lot of file system checks).
 */
function disable_smart_decaching_temporarily()
{
    global $DISABLE_SMART_DECACHING_TEMPORARILY;
    $DISABLE_SMART_DECACHING_TEMPORARILY = true;
}

/**
 * Find if the current request has POST fields worth considering/propagating. Very standard framework fields will be ignored.
 *
 * @return boolean Whether it does
 */
function has_interesting_post_fields()
{
    $post = $_POST;
    $to_ignore = array(
        'csrf_token',
        'y' . md5(get_site_name() . ': antispam'),
        'login_username',
        'password',
        'remember_me',
        'login_invisible',
        'redirect',
        'redirect_passon',
    );
    foreach ($to_ignore as $field) {
        unset($post[$field]);
    }
    return (count($post) !== 0);
}

/**
 * Apply escaping for an HTTP header.
 *
 * @param string $str Text to insert into header
 * @param boolean $within_quotes Text is between quotes
 * @return string Escaped text
 */
function escape_header($str, $within_quotes = false)
{
    if ($within_quotes) {
        $str = addslashes($str);
    }
    return str_replace(array("\r", "\n"), array('', ''), $str);
}

/**
 * Find if a forum post is a spacer post.
 *
 * @param string $post The spacer post
 * @return array A pair: Whether it is, and the language it is in
 */
function is_spacer_post($post)
{
    if (substr($post, 0, 10) == '[semihtml]') {
        $post = substr($post, 10);
    }

    $langs = find_all_langs();
    foreach (array_keys($langs) as $lang) {
        $matcher = do_lang('SPACER_POST_MATCHER', null, null, null, $lang);
        if (substr($post, 0, strlen($matcher)) == $matcher) {
            return array(true, $lang);
        }
    }
    return array(false, get_site_default_lang());
}

/**
 * Get the Internet host name corresponding to a given IP address.
 *
 * @param  string $ip_address IP address
 * @return string Host name OR IP address if failed to look up
 */
function cms_gethostbyaddr($ip_address)
{
    $hostname = '';

    if ((php_function_allowed('shell_exec')) && (function_exists('get_value')) && (get_value('slow_php_dns') === '1')) {
        $hostname = trim(preg_replace('#^.* #', '', shell_exec('host ' . escapeshellarg_wrap($ip_address))));
    }

    if ($hostname == '') {
        if (php_function_allowed('gethostbyaddr')) {
            $hostname = @gethostbyaddr($ip_address);
        }
    }

    if ($hostname == '') {
        $hostname = $ip_address;
    }

    return $hostname;
}

/**
 * Get the IP address corresponding to a given Internet host name.
 *
 * @param  string $hostname Host name
 * @return string IP address OR host name if failed to look up
 */
function cms_gethostbyname($hostname)
{
    $ip_address = '';

    if ((php_function_allowed('shell_exec')) && (function_exists('get_value')) && (get_value('slow_php_dns') === '1')) {
        $ip_address = preg_replace('#^.*has address (\d+\.\d+\.\d+).*#s', '$1', shell_exec('host ' . escapeshellarg_wrap($hostname)));
    }

    if ($ip_address == '') {
        if (php_function_allowed('gethostbyname')) {
            $ip_address = @gethostbyname($hostname);
        }
    }

    if ($ip_address == '') {
        $ip_address = $hostname;
    }

    return $ip_address;
}

/**
 * Unpack some bytes to an integer, so we can do some bitwise arithmetic on them.
 * Assumes unsigned, unless you request 4 bytes.
 *
 * @param  string $str Input string
 * @param  ?integer $bytes How many bytes to read (null: as many as there are in $str)
 * @set 1 2 4
 * @param  boolean $little_endian Whether to use little endian (Intel order) as opposed to big endian (network/natural order)
 * @return integer Read integer
 */
function cms_unpack_to_uinteger($str, $bytes = null, $little_endian = false)
{
    if ($bytes === null) {
        $bytes = strlen($str);
    }

    switch ($bytes) {
        case 1:
            $result = unpack('C', $str);
            break;
        case 2:
            $result = unpack($little_endian ? 'v' : 'n', $str);
            break;
        case 4:
            $result = unpack($little_endian ? 'V' : 'N', $str);
            break;
        default:
            warn_exit(do_lang_tempcode('INTERNAL_ERROR'));
    }
    return $result[1];
}

/**
 * Perform a regular expression match.
 * Automatically applies utf-8 if possible and appropriate. \s is not actually Unicode-safe, for example (as it matches non-breaking-spaces).
 *
 * @param  string $pattern The pattern.
 * @param  string $subject The subject string.
 * @param  ?array $matches Where matches will be put (note that it is a list of maps, except the arrays are turned inside out) (null: do not store matches). Note that this is actually passed by reference, but is also optional. (null: don't gather)
 * @param  integer $flags Either 0, or PREG_OFFSET_CAPTURE.
 * @param  integer $offset Offset to start from. Usually use with 'A' modifier to anchor it (using '^' in the pattern will not work)
 * @return ~integer The number of matches (false: error).
 */
function cms_preg_match_safe($pattern, $subject, &$matches = null, $flags = 0, $offset = 0)
{
    if (get_charset() == 'utf-8') {
        $result = @preg_match($pattern . 'u', $subject, $matches, $flags, $offset);
        if ($result !== false) {
            return $result;
        }
    }
    return preg_match($pattern, $subject, $matches, $flags, $offset);
}

/**
 * Array entries that match the pattern.
 * Automatically applies utf-8 if possible and appropriate. \s is not actually Unicode-safe, for example (as it matches non-breaking-spaces).
 *
 * @param  string $pattern The pattern.
 * @param  array $subject The subject strings.
 * @param  integer $flags Either 0, or PREG_GREP_INVERT.
 * @return array Matches.
 */
function cms_preg_grep_safe($pattern, $subject, $flags = 0)
{
    if (get_charset() == 'utf-8') {
        $result = @preg_grep($pattern . 'u', $subject, $flags);
        if ($result !== false) {
            return $result;
        }
    }
    return preg_grep($pattern, $subject, $flags);
}

/**
 * Perform a global regular expression match.
 * Automatically applies utf-8 if possible and appropriate. \s is not actually Unicode-safe, for example (as it matches non-breaking-spaces).
 *
 * @param  string $pattern The pattern.
 * @param  string $subject The subject string.
 * @param  ?array $matches Where matches will be put (note that it is a list of maps, except the arrays are turned inside out). Note that this is actually passed by reference, but is also optional. (null: don't gather)
 * @param  integer $flags Either 0, or PREG_OFFSET_CAPTURE.
 * @return ~integer The number of matches (false: error).
 */
function cms_preg_match_all_safe($pattern, $subject, &$matches, $flags = 0)
{
    if (get_charset() == 'utf-8') {
        $result = @preg_match_all($pattern . 'u', $subject, $matches, $flags);
        if ($result !== false) {
            return $result;
        }
    }
    return preg_match_all($pattern, $subject, $matches, $flags);
}

/**
 * Perform a regular expression search and replace.
 * Automatically applies utf-8 if possible and appropriate. \s is not actually Unicode-safe, for example (as it matches non-breaking-spaces).
 *
 * @param  mixed $pattern The pattern (string or array).
 * @param  mixed $replacement The replacement string (string or array).
 * @param  string $subject The subject string.
 * @param  integer $limit The limit of replacements (-1: no limit).
 * @return ~string The string with replacements made (false: error).
 */
function cms_preg_replace_safe($pattern, $replacement, $subject, $limit = -1)
{
    if (get_charset() == 'utf-8') {
        $result = @preg_replace($pattern . 'u', $replacement, $subject, $limit);
        if ($result !== false) {
            return $result;
        }
    }
    return preg_replace($pattern, $replacement, $subject, $limit);
}

/**
 * Perform a regular expression search and replace using a callback.
 * Automatically applies utf-8 if possible and appropriate. \s is not actually Unicode-safe, for example (as it matches non-breaking-spaces).
 *
 * @param  string $pattern The pattern.
 * @param  mixed $callback The callback.
 * @param  string $subject The subject string.
 * @param  integer $limit The limit of replacements (-1: no limit).
 * @return ~string The string with replacements made (false: error).
 */
function cms_preg_replace_callback_safe($pattern, $callback, $subject, $limit = -1)
{
    if (get_charset() == 'utf-8') {
        $result = @preg_replace_callback($pattern . 'u', $callback, $subject, $limit);
        if ($result !== false) {
            return $result;
        }
    }
    return preg_replace_callback($pattern, $callback, $subject, $limit);
}

/**
 * Split string by a regular expression.
 * Automatically applies utf-8 if possible and appropriate. \s is not actually Unicode-safe, for example (as it matches non-breaking-spaces).
 *
 * @param  string $pattern The pattern.
 * @param  string $subject The subject.
 * @param  integer $max_splits The maximum number of splits to make (-1: no limit).
 * @param  ?integer $mode The special mode (null: none).
 * @return array The array due to splitting.
 */
function cms_preg_split_safe($pattern, $subject, $max_splits = -1, $mode = null)
{
    if (get_charset() == 'utf-8') {
        $result = @preg_split($pattern . 'u', $subject, $max_splits, $mode);
        if ($result !== false) {
            return $result;
        }
    }
    return preg_split($pattern, $subject, $max_splits, $mode);
}
