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

/*
Conversions...

Page-link -> Structs : page_link_decode
Page-link -> Tempcode : (none)
Page-link -> URL : page_link_to_url
Structs -> Page-link : build_page_link
Structs -> Tempcode : build_url
Structs -> URL : _build_url
Tempcode -> Page-link : (none)
Tempcode -> Structs : (none)
Tempcode -> URL : static_evaluate_tempcode
URL -> Page-link : url_to_page_link
URL -> Structs : parse_url
URL -> Tempcode : N/A

(Structs aren't consistent, and just refer to some kind of PHP data structure involving arrays)
*/

/**
 * Standard code module initialisation function.
 *
 * @ignore
 */
function init__urls()
{
    global $HTTPS_PAGES_CACHE;
    $HTTPS_PAGES_CACHE = null;

    global $CAN_TRY_URL_SCHEMES_CACHE;
    $CAN_TRY_URL_SCHEMES_CACHE = null;

    global $HAS_KEEP_IN_URL_CACHE;
    $HAS_KEEP_IN_URL_CACHE = null;

    global $URL_REMAPPINGS;
    $URL_REMAPPINGS = null;

    global $CONTENT_OBS;
    $CONTENT_OBS = null;

    global $SMART_CACHE, $LOADED_MONIKERS_CACHE;
    if ($SMART_CACHE !== null) {
        $test = $SMART_CACHE->get('NEEDED_MONIKERS');
        if ($test === null) {
            $LOADED_MONIKERS_CACHE = array();
        } else {
            foreach ($test as $c => $_) {
                list($url_parts, $zone, $effective_id) = unserialize($c);

                $LOADED_MONIKERS_CACHE[$url_parts['type']][$url_parts['page']][$effective_id] = true;
            }
        }
    }

    global $SELF_URL_CACHED;
    $SELF_URL_CACHED = null;

    global $HAS_NO_KEEP_CONTEXT, $NO_KEEP_CONTEXT_STACK;
    $HAS_NO_KEEP_CONTEXT = false;
    $NO_KEEP_CONTEXT_STACK = array();

    if (!defined('SELF_REDIRECT')) {
        define('SELF_REDIRECT', '!--:)defUNLIKELY');
    }
}

/**
 * Get a well formed URL equivalent to the current URL. Reads direct from the environment and does no clever mapping at all. This function should rarely be used.
 *
 * @param boolean $script_name_if_cli Return the script name instead of a URL, if running on the CLI. If this is set to false it will return the base URL instead.
 * @return URLPATH The URL
 */
function get_self_url_easy($script_name_if_cli = false)
{
    $cli = ((php_function_allowed('php_sapi_name')) && (php_sapi_name() == 'cli') && (cms_srv('REMOTE_ADDR') == ''));
    if ($cli) {
        if ($script_name_if_cli) {
            return $_SERVER['argv'][0];
        }
        return get_base_url();
    }

    $protocol = tacit_https() ? 'https' : 'http';
    $self_url = $protocol . '://' . cms_srv('HTTP_HOST');
    $self_url .= cms_srv('REQUEST_URI');
    return $self_url;
}

/**
 * Get a well formed URL equivalent to the current URL.
 *
 * @param  boolean $evaluate Whether to evaluate the URL (so as we don't return Tempcode)
 * @param  boolean $root_if_posted Whether to direct to the default page if there was a POST request leading to where we are now (i.e. to avoid missing post fields when we go to this URL)
 * @param  ?array $extra_params A map of extra parameters for the URL (null: none)
 * @param  boolean $posted_too Whether to also keep POSTed data, in the GET request (useful if either_param_string is used to get the data instead of post_param_string - of course the POST data must be of the not--persistent-state-changing variety)
 * @param  boolean $avoid_remap Whether to avoid URL Schemes (sometimes essential so we can assume the standard URL parameter addition scheme in templates)
 * @return mixed The URL (Tempcode or string)
 */
function get_self_url($evaluate = false, $root_if_posted = false, $extra_params = null, $posted_too = false, $avoid_remap = false)
{
    if ($extra_params === null) {
        $extra_params = array();
    }

    global $SELF_URL_CACHED, $IN_SELF_ROUTING_SCRIPT;
    $cacheable = ($evaluate) && (!$root_if_posted) && ($extra_params === array()) && (!$posted_too) && (!$avoid_remap);
    if (($cacheable) && ($SELF_URL_CACHED !== null)) {
        return $SELF_URL_CACHED;
    }

    if (running_script('execute_temp')) {
        return get_base_url();
    }

    if ($posted_too) {
        static $mq = null;
        if ($mq === null) {
            $mq = @get_magic_quotes_gpc();
        }
        $post_array = array();
        foreach ($_POST as $key => $val) {
            if (is_array($val)) {
                continue;
            }
            if ($mq) {
                $val = stripslashes($val);
            }
            $post_array[$key] = $val;
        }
        $extra_params = array_merge($post_array, $extra_params);
    }
    $page = '_SELF';
    $zone = '_SELF';
    if (($root_if_posted) && (has_interesting_post_fields()) || !$IN_SELF_ROUTING_SCRIPT) {
        $page = '';
        $zone = 'site';
        unset($extra_params['page']);
    }
    $params = array('page' => $page);
    $skip = array();
    foreach ($extra_params as $key => $val) {
        if ($val === null) {
            $skip[$key] = true;
        } else {
            $params[$key] = $val;
        }
    }

    $url = build_url($params, $zone, $skip, true, $avoid_remap);
    if ($evaluate) {
        $ret = $url->evaluate();
        if ($cacheable) {
            $SELF_URL_CACHED = $ret;
        }
        return $ret;
    }

    return $url;
}

/**
 * Encode a URL component in such a way that it won't get nuked by Apache %2F blocking security and url encoded '&' screwing. The get_param_string function will map it back. Hackerish but necessary.
 *
 * @param  URLPATH $url_part The URL to encode
 * @param  ?boolean $can_try_url_schemes Whether we have to consider URL Schemes (null: don't know, look up)
 * @return URLPATH The encoded result
 */
function cms_url_encode($url_part, $can_try_url_schemes = null)
{
    // Slipstream for 99.99% of data
    $url_part_encoded = urlencode($url_part);
    if ($url_part_encoded === $url_part) {
        return $url_part_encoded;
    }

    if ($can_try_url_schemes === null) {
        $can_try_url_schemes = can_try_url_schemes();
    }
    if ($can_try_url_schemes) { // These interfere with URL Scheme processing because they get pre-decoded and make things ambiguous
        //$url_part = str_replace(':', '(colon)', $url_part); We'll ignore theoretical problem here- we won't expect there to be a need for encodings within redirect URL paths (params is fine, handles naturally)
        $url_part = str_replace(array('/', '&', '#', '+', ' '), array(':slash:', ':amp:', ':uhash:', ':plus:', ':space:'), $url_part); // Blocked by mod_rewrite if not within the query string
    }
    $url_part = str_replace(array('?', '='), array(':ques:', ':equals:'), $url_part); // ModSecurity blocks these
    $url_part = urlencode($url_part);
    return $url_part;
}

/**
 * Encode a URL component, as per cms_url_encode but without slashes being encoded, and with rawurlencode.
 *
 * @param  URLPATH $url_part The URL to encode
 * @param  ?boolean $can_try_url_schemes Whether we have to consider URL Schemes (null: don't know, look up)
 * @return URLPATH The encoded result
 */
function cms_raw_url_encode($url_part, $can_try_url_schemes = null) // TODO: Rename function in v11 and document in codebook
{
    // Slipstream for 99.99% of data
    $url_part_encoded = rawurlencode($url_part);
    if ($url_part_encoded === $url_part) {
        return $url_part_encoded;
    }

    if ($can_try_url_schemes === null) {
        $can_try_url_schemes = can_try_url_schemes();
    }
    if ($can_try_url_schemes) { // These interfere with URL Scheme processing because they get pre-decoded and make things ambiguous
        //$url_part = str_replace(':', '(colon)', $url_part); We'll ignore theoretical problem here- we won't expect there to be a need for encodings within redirect URL paths (params is fine, handles naturally)
        $url_part = str_replace(array('&', '#', ' '), array(':amp:', ':uhash:', ':space:'), $url_part); // Blocked by mod_rewrite if not within the query string
    }
    $url_part = str_replace('%2F', '/', rawurlencode($url_part));
    return $url_part;
}

/**
 * Decode a URL component that was encoded with hackerish_url_encode
 *
 * @param  URLPATH $url_part The URL to encode
 * @return URLPATH The encoded result
 */
function cms_url_decode_post_process($url_part)
{
    if (strpos($url_part, ':') !== false) {
        if (can_try_url_schemes()) {
            $url_part = str_replace(array(':uhash:', ':amp:', ':slash:', ':plus:', ':space:'), array('#', '&', '/', '+', ' '), $url_part);
            //$url_part = str_replace('(colon)', ':', $url_part);
        }
        $url_part = str_replace(array(':ques:', ':equals:'), array('?', '='), $url_part);
    }
    return $url_part;
}

/**
 * Place a global marker as to whether we're skipping keep parameters.
 *
 * @param  boolean $setting Temporary setting
 */
function push_no_keep_context($setting = true)
{
    global $HAS_NO_KEEP_CONTEXT, $NO_KEEP_CONTEXT_STACK;
    array_push($NO_KEEP_CONTEXT_STACK, $HAS_NO_KEEP_CONTEXT);
    $HAS_NO_KEEP_CONTEXT = $setting;
}

/**
 * Remove the global marker as to whether we're skipping keep parameters. Never call this more than you've called push_no_keep_context().
 */
function pop_no_keep_context()
{
    global $HAS_NO_KEEP_CONTEXT, $NO_KEEP_CONTEXT_STACK;
    $HAS_NO_KEEP_CONTEXT = array_pop($NO_KEEP_CONTEXT_STACK);
}

/**
 * Find whether we can skip the normal preservation of a keep value, for whatever reason.
 *
 * @param  string $key Parameter name
 * @param  string $val Parameter value
 * @return boolean Whether we can skip it
 */
function skippable_keep($key, $val)
{
    global $BOT_TYPE_CACHE, $HAS_NO_KEEP_CONTEXT;
    if ($HAS_NO_KEEP_CONTEXT) {
        return true;
    }
    if ($BOT_TYPE_CACHE === false) {
        get_bot_type();
    }
    if ($BOT_TYPE_CACHE !== null) {
        return true;
    }

    static $nkp = null;
    if ($nkp === null) {
        $nkp = (isset($GLOBALS['SITE_INFO']['no_keep_params'])) && ($GLOBALS['SITE_INFO']['no_keep_params'] === '1');
    }
    if ($nkp) {
        return true;
    }

    return ((($key === 'keep_session') && (($val == '') || (isset($_COOKIE['has_cookies'])))) || (($key === 'keep_has_js') && ($val === '1'))) && ((isset($_COOKIE['js_on'])) || (get_option('detect_javascript') === '0'));
}

/**
 * Find whether the specified page is to use HTTPS (if not -- it will use HTTP).
 * All images (etc) on a HTTPS page should use HTTPS to avoid mixed-content browser notices.
 *
 * @param  ID_TEXT $zone The zone the page is in
 * @param  ID_TEXT $page The page codename
 * @return boolean Whether the page is to run across an HTTPS connection
 */
function is_page_https($zone, $page)
{
    static $off = null;
    if ($off === null) {
        global $SITE_INFO;
        $off = (!addon_installed('ssl')) || (in_safe_mode()) || (!function_exists('persistent_cache_get') || (!empty($SITE_INFO['no_ssl'])));
    }
    if ($off) {
        return false;
    }

    if (($page === 'login') && (get_page_name() === 'login')) { // Because how login can be called from any arbitrary page, which may or may not be on HTTPS. We want to maintain HTTPS if it is there to avoid warning on form submission
        if (tacit_https()) {
            return true;
        }
    }

    global $HTTPS_PAGES_CACHE;
    if (($HTTPS_PAGES_CACHE === null) && (function_exists('persistent_cache_get'))) {
        $HTTPS_PAGES_CACHE = persistent_cache_get('HTTPS_PAGES_CACHE');
    }
    if ($HTTPS_PAGES_CACHE === null) {
        if (isset($GLOBALS['SITE_DB'])) {
            $results = $GLOBALS['SITE_DB']->query_select('https_pages', array('*'), null, '', null, null, true);
            $HTTPS_PAGES_CACHE = array();
            if ($results !== null) {
                foreach ($results as $r) {
                    $HTTPS_PAGES_CACHE[$r['https_page_name']] = true;
                }
            }
            if (function_exists('persistent_cache_set')) {
                persistent_cache_set('HTTPS_PAGES_CACHE', $HTTPS_PAGES_CACHE);
            }
        }
    }
    return isset($HTTPS_PAGES_CACHE[$zone . ':' . $page]);
}

/**
 * Find if a URL Scheme is in use
 *
 * @param  boolean $avoid_remap Whether to explicitly avoid using URL Schemes. While it might seem weird to put this in as a function parameter, it removes duplicated logic checks in the code.
 * @return boolean Whether a URL Scheme is in use
 */
function can_try_url_schemes($avoid_remap = false)
{
    if (!function_exists('get_option')) {
        return false;
    }
    $url_scheme = get_option('url_scheme');
    return (($url_scheme !== 'RAW') && (get_param_integer('keep_no_url_scheme', 0) === 0) && ((empty($GLOBALS['SITE_INFO']['block_url_schemes'])) || ($GLOBALS['SITE_INFO']['block_url_schemes'] !== '1')) && (!$avoid_remap)); // If we don't have the option on or are not using Apache, return
}

/**
 * Find if keep_ parameters are in use
 *
 * @return boolean Whether they are
 */
function has_keep_parameters()
{
    static $answer = null;
    if ($answer !== null) {
        return $answer;
    }

    foreach (array_keys($_GET) as $key) {
        if (
            isset($key[0]) &&
            $key[0] == 'k' &&
            substr($key, 0, 5) == 'keep_'
            //&& $key != 'keep_devtest' && $key != 'keep_show_loading'/*If testing memory use we don't want this to trigger it as it breaks the test*/
        ) {
            $answer = true;
            return $answer;
        }
    }
    $answer = false;
    return $answer;
}

/**
 * Build and return a proper URL, from the $vars array.
 * Note: URL parameters should always be in lower case (one of the coding standards)
 *
 * @param  array $vars A map of parameter names to parameter values. E.g. array('page'=>'example','type'=>'foo','id'=>2). Values may be strings or integers, or Tempcode, or null. null indicates "skip this". 'page' cannot be null.
 * @param  ID_TEXT $zone_name The zone the URL is pointing to. YOU SHOULD NEVER HARD CODE THIS- USE '_SEARCH', '_SELF' (if you're self-referencing your own page) or the output of get_module_zone.
 * @param  ?array $skip Variables to explicitly not put in the URL (perhaps because we have $keep_all set, or we are blocking certain keep_ values). The format is of a map where the keys are the names, and the values are true. (null: don't skip any)
 * @param  boolean $keep_all Whether to keep all non-skipped parameters that were in the current URL, in this URL
 * @param  boolean $avoid_remap Whether to avoid URL Schemes (sometimes essential so we can assume the standard URL parameter addition scheme in templates)
 * @param  boolean $skip_keep Whether to skip actually putting on keep_ parameters (rarely will this skipping be desirable)
 * @param  string $hash Hash portion of the URL (blank: none). May or may not start '#' - code will put it on if needed
 * @return Tempcode The URL in Tempcode format.
 */
function build_url($vars, $zone_name = '', $skip = null, $keep_all = false, $avoid_remap = false, $skip_keep = false, $hash = '')
{
    if (empty($vars['page']) && running_script('index')) { // For SEO purposes we need to make sure we get the right URL
        $vars['page'] = get_zone_default_page($zone_name);
        if ($vars['page'] === null) {
            $vars['page'] = 'start';
        }
    }

    $id = isset($vars['id']) ? $vars['id'] : null;

    global $SITE_INFO;
    if (
        (isset($SITE_INFO['no_keep_params'])) &&
        ($SITE_INFO['no_keep_params'] === '1') &&
        ((get_option('url_monikers_enabled') === '0') || (!is_numeric($id)/*i.e. not going to trigger a URL moniker query*/) && ((is_null($id)) || (strpos($id, '/') !== false)))
    ) {
        if (($id === null) && (isset($vars['type'])) && ($vars['type'] === 'browse') && (!$keep_all)) {
            unset($vars['type']); // Redundant, let it default, this is our convention
        }

        if ($vars['page'] === '_SELF') {
            $vars['page'] = get_page_name();
        }
        if ($zone_name === '_SELF') {
            $zone_name = get_zone_name();
        }
        if ($zone_name === '_SEARCH') {
            $zone_name = get_page_zone($vars['page']);
        }
        if (($hash !== '') && ($hash[0] !== '#')) {
            $hash = '#' . $hash;
        }
        return make_string_tempcode(_build_url($vars, $zone_name, $skip, $keep_all, $avoid_remap, true, $hash));
    }

    $page_link = build_page_link($vars, $zone_name, $skip, $hash);

    $arr = array(
        $page_link,
        $avoid_remap ? '1' : '0',
        $skip_keep ? '1' : '0',
        $keep_all ? '1' : '0'
    );
    if ($skip !== null) {
        $arr[] = implode('|', array_keys($skip));
    }

    $ret = symbol_tempcode('PAGE_LINK', $arr);

    return $ret;
}

/**
 * Build and return a proper page-link, from the $vars array.
 * Note: URL parameters should always be in lower case (one of the coding standards)
 *
 * @param  array $vars A map of parameter names to parameter values. E.g. array('page'=>'example','type'=>'foo','id'=>2). Values may be strings or integers, or Tempcode, or null. null indicates "skip this". 'page' cannot be null.
 * @param  ID_TEXT $zone_name The zone the URL is pointing to. YOU SHOULD NEVER HARD CODE THIS- USE '_SEARCH', '_SELF' (if you're self-referencing your own page) or the output of get_module_zone.
 * @param  ?array $skip Variables to explicitly not put in the URL (perhaps because we have $keep_all set, or we are blocking certain keep_ values). The format is of a map where the keys are the names, and the values are 1. (null: don't skip any)
 * @param  string $hash Hash portion of the URL (blank: none). May or may not start '#' - code will put it on if needed
 * @return string The page-link.
 */
function build_page_link($vars, $zone_name = '', $skip = null, $hash = '')
{
    $id = isset($vars['id']) ? $vars['id'] : null;

    $page_link = $zone_name . ':' . /*urlencode not needed in reality, performance*/(isset($vars['page']) ? $vars['page'] : '');
    if ((isset($vars['type'])) || (array_key_exists('type', $vars))) {
        if (isset($vars['type']->codename/*faster than is_object*/)) {
            $page_link .= ':';
            $page_link .= $vars['type']->evaluate();
        } else {
            $page_link .= ':' . (($vars['type'] === null) ? '<null>' : urlencode($vars['type']));
        }
        unset($vars['type']);
        if ((isset($id)) || (array_key_exists('id', $vars))) {
            if (is_integer($id)) {
                $page_link .= ':' . strval($id);
            } elseif (isset($id->codename/*faster than is_object*/)) {
                $page_link .= ':';
                $page_link .= $id->evaluate();
            } else {
                $page_link .= ':' . (($id === null) ? '<null>' : urlencode($id));
            }
            unset($vars['id']);
        }
    } else {
        if (false) {
            $val = mixed();
        }
    }

    foreach ($vars as $key => $val) {
        if (!is_string($key)) {
            $key = strval($key);
        }
        if (is_integer($val)) {
            $val = strval($val);
        }

        if ($key !== 'page') {
            if (is_integer($key)) {
                $key = strval($key);
            }

            if (isset($val->codename/*faster than is_object*/)) {
                $page_link .= ':' . $key . '=';
                $page_link .= urlencode($val->evaluate());
            } else {
                $page_link .= ':' . $key . '=' . (($val === null) ? '<null>' : urlencode($val));
            }
        }
    }

    if (($hash !== '') && ($hash[0] !== '#')) {
        $hash = '#' . $hash;
    }

    $page_link .= $hash;

    return $page_link;
}

/**
 * Find whether URL monikers are enabled.
 *
 * @return boolean Whether URL monikers are enabled.
 */
function url_monikers_enabled()
{
    if (!function_exists('get_option')) {
        return false;
    }
    if (get_param_integer('keep_urlmonikers', null) === 0) {
        return false;
    }
    if (get_option('url_monikers_enabled') !== '1') {
        return false;
    }
    return true;
}

/**
 * Build and return a proper URL, from the $vars array.
 * Note: URL parameters should always be in lower case (one of the coding standards)
 *
 * @param  array $vars A map of parameter names to parameter values. Values may be strings or integers, or null. null indicates "skip this". 'page' cannot be null.
 * @param  ID_TEXT $zone_name The zone the URL is pointing to. YOU SHOULD NEVER HARD CODE THIS- USE '_SEARCH', '_SELF' (if you're self-referencing your own page) or the output of get_module_zone.
 * @param  ?array $skip Variables to explicitly not put in the URL (perhaps because we have $keep_all set, or we are blocking certain keep_ values). The format is of a map where the keys are the names, and the values are 1. (null: don't skip any)
 * @param  boolean $keep_all Whether to keep all non-skipped parameters that were in the current URL, in this URL
 * @param  boolean $avoid_remap Whether to avoid URL Schemes (sometimes essential so we can assume the standard URL parameter addition scheme in templates)
 * @param  boolean $skip_keep Whether to skip actually putting on keep_ parameters (rarely will this skipping be desirable)
 * @param  string $hash Hash portion of the URL (blank: none). May or may not start '#' - code will put it on if needed
 * @return string The URL in string format.
 *
 * @ignore
 */
function _build_url($vars, $zone_name = '', $skip = null, $keep_all = false, $avoid_remap = false, $skip_keep = false, $hash = '')
{
    global $HAS_KEEP_IN_URL_CACHE, $CAN_TRY_URL_SCHEMES_CACHE, $BOT_TYPE_CACHE, $WHAT_IS_RUNNING_CACHE, $KNOWN_AJAX, $IN_SELF_ROUTING_SCRIPT;

    $has_page = isset($vars['page']);

    if (($hash !== '') && ($hash[0] !== '#')) {
        $hash = '#' . $hash;
    }

    // Build up our URL base
    $stub = get_base_url(is_page_https($zone_name, $has_page ? $vars['page'] : ''), $zone_name);
    $stub .= '/';

    // For bots we explicitly unset skippable injected 'keep_' params because it bloats the crawl-space
    if (($BOT_TYPE_CACHE !== null) && (get_bot_type() !== null)) {
        foreach ($vars as $key => $val) {
            if ($key === 'redirect') {
                unset($vars[$key]);
            }
            if ((substr($key, 0, 5) === 'keep_') && (skippable_keep($key, $val))) {
                unset($vars[$key]);
            }
        }
    }

    // Things we need to keep in the url
    $keep_actual = array();
    if (($HAS_KEEP_IN_URL_CACHE === null) || ($HAS_KEEP_IN_URL_CACHE) || ($keep_all)) {
        static $mc = null;
        if ($mc === null) {
            $mc = @get_magic_quotes_gpc();
        }

        $keep_cant_use = array();
        $HAS_KEEP_IN_URL_CACHE = false;
        foreach ($_GET as $key => $val) {
            if (is_array($val)) {
                if ($keep_all) {
                    if ((!array_key_exists($key, $vars)) && (!isset($skip[$key]))) {
                        _handle_array_var_append($key, $val, $vars);
                    }
                }
                continue;
            }

            $is_keep = false;
            $appears_keep = ((is_string($key)) && (isset($key[0])) && ($key[0] === 'k') && (substr($key, 0, 5) === 'keep_'));
            if ($appears_keep) {
                if ((!$skip_keep) && (!skippable_keep($key, $val))) {
                    $is_keep = true;
                }
                $HAS_KEEP_IN_URL_CACHE = true;
            }
            if (((($keep_all) && (!$appears_keep)) || ($is_keep)) && (!array_key_exists($key, $vars)) && (!isset($skip[$key]))) {
                if ($mc) {
                    $val = stripslashes($val);
                }
                if ($is_keep) {
                    $keep_actual[$key] = $val;
                } else {
                    $vars[$key] = $val;
                }
            } elseif ($is_keep) {
                if ($mc) {
                    $val = stripslashes($val);
                }
                $keep_cant_use[$key] = $val;
            }
        }

        $vars += $keep_actual;
    }

    if ((!isset($vars['id'])) && (isset($vars['type'])) && ($vars['type'] === 'browse') && (!$keep_all)) {
        unset($vars['type']); // Redundant, let it default, this is our convention
    }

    global $URL_MONIKERS_ENABLED_CACHE;
    if ($URL_MONIKERS_ENABLED_CACHE === null) {
        $URL_MONIKERS_ENABLED_CACHE = url_monikers_enabled();
    }
    if ($URL_MONIKERS_ENABLED_CACHE) {
        $test = find_id_moniker($vars, $zone_name, false);
        if ($test !== null) {
            if (substr($test, 0, 1) === '/') { // relative to zone root
                $parts = explode('/', substr($test, 1), 3);
                $vars['page'] = $parts[0];
                if (isset($parts[1])) {
                    $vars['type'] = $parts[1];
                } else {
                    unset($vars['type']);
                }
                if (isset($parts[2])) {
                    $vars['id'] = $parts[2];
                } else {
                    unset($vars['id']);
                }
            } else { // relative to content module
                if (array_key_exists('id', $vars)) {
                    $vars['id'] = $test;
                } else {
                    $vars['page'] = $test;
                }
            }
        }
    }

    // Apply dashes if needed
    if ($has_page) {
        if ((strpos($vars['page'], '_') !== false) && ($vars['page'] !== '_SELF')) {
            $vars['page'] = str_replace('_', '-', $vars['page']);
        }
    }

    // We either use a URL Scheme, or return a standard parameterisation
    if (($CAN_TRY_URL_SCHEMES_CACHE === null) || ($avoid_remap)) {
        $can_try_url_schemes = can_try_url_schemes($avoid_remap);
        if (!$avoid_remap) {
            $CAN_TRY_URL_SCHEMES_CACHE = $can_try_url_schemes;
        }
    } else {
        $can_try_url_schemes = $CAN_TRY_URL_SCHEMES_CACHE;
    }
    $_what_is_running = $WHAT_IS_RUNNING_CACHE;
    if (!$IN_SELF_ROUTING_SCRIPT && $has_page) {
        $_what_is_running = 'index';
    }
    $test_rewrite = null;
    $self_page = ((!$has_page) || ((function_exists('get_zone_name')) && (get_zone_name() === $zone_name) && (($vars['page'] === '_SELF') || ($vars['page'] === get_page_name())))) && ((!isset($vars['type'])) || ($vars['type'] === get_param_string('type', 'browse', true))) && ($hash !== '#_top') && (!$KNOWN_AJAX);
    if ($can_try_url_schemes) {
        if ((!$self_page) || ($_what_is_running === 'index')) {
            $test_rewrite = _url_rewrite_params($zone_name, $vars, count($keep_actual) > 0);
        }
    }
    if ($test_rewrite === null) {
        $url = (($self_page) && ($_what_is_running !== 'index')) ? find_script($_what_is_running) : ($stub . 'index.php');

        // Fix sort order
        if (isset($vars['id'])) {
            $_vars = $vars;
            unset($_vars['id']);
            $vars = array('id' => $vars['id']) + $_vars;
        }
        if (isset($vars['type'])) {
            $_vars = $vars;
            unset($_vars['type']);
            $vars = array('type' => $vars['type']) + $_vars;
        }
        if ($has_page) {
            $_vars = $vars;
            unset($_vars['page']);
            $vars = array('page' => $vars['page']) + $_vars;
        }

        // Build up the URL string
        $symbol = '?';
        foreach ($vars as $key => $val) {
            if ($val === null) {
                continue; // null means skip
            }

            if (!isset($key[0]/*Faster than is_string*/) && $key !== '') {
                $key = strval($key);
            }

            if ($val === SELF_REDIRECT) {
                $val = get_self_url(true, true);
            }

            // Add in
            $url .= $symbol . $key . '=' . (is_integer($val) ? strval($val) :/*cms_*/urlencode($val/*,false*/));
            $symbol = '&';
        }
    } else {
        $url = $stub . $test_rewrite;
    }

    // Done
    return $url . $hash;
}

/**
 * Recursively put array parameters into a flat array for use in a query string.
 *
 * @param  ID_TEXT $key Primary field name
 * @param  array $val Array
 * @param  array $vars Flat array to write into
 *
 * @ignore
 */
function _handle_array_var_append($key, $val, &$vars)
{
    $val2 = mixed();

    foreach ($val as $key2 => $val2) {
        if (!is_string($key2)) {
            $key2 = strval($key2);
        }

        if (is_array($val2)) {
            _handle_array_var_append($key . '[' . $key2 . ']', $val2, $vars);
        } else {
            if (@get_magic_quotes_gpc()) {
                $val2 = stripslashes($val2);
            }

            $vars[$key . '[' . $key2 . ']'] = $val2;
        }
    }
}

/**
 * Attempt to use a URL Scheme to improve this URL.
 *
 * @param  ID_TEXT $zone_name The name of the zone for this
 * @param  array $vars A map of variables to include in our URL
 * @param  boolean $force_index_php Force inclusion of the index.php name into a URL Scheme, so something may tack on extra parameters to the result here
 * @return ?URLPATH The improved URL (null: couldn't do anything)
 * @ignore
 */
function _url_rewrite_params($zone_name, $vars, $force_index_php = false)
{
    global $URL_REMAPPINGS;
    if ($URL_REMAPPINGS === null) {
        require_code('url_remappings');
        $URL_REMAPPINGS = get_remappings(get_option('url_scheme'));
        foreach ($URL_REMAPPINGS as $i => $_remapping) {
            $URL_REMAPPINGS[$i][3] = count($_remapping[0]);
        }
    }

    static $url_scheme = null;
    if ($url_scheme === null) {
        $url_scheme = get_option('url_scheme');
    }

    // Find mapping
    foreach ($URL_REMAPPINGS as $_remapping) {
        list($remapping, $target, $require_full_coverage, $last_key_num) = $_remapping;
        $good = true;

        $loop_cnt = 0;
        foreach ($remapping as $key => $val) {
            $loop_cnt++;
            $last = ($loop_cnt == $last_key_num);

            if ((isset($vars[$key])) && (is_integer($vars[$key]))) {
                $vars[$key] = strval($vars[$key]);
            }

            if (!(((isset($vars[$key])) || (($val === null) && ($key === 'type') && ((isset($vars['id'])) || (array_key_exists('id', $vars))))) && (($key !== 'page') || ($vars[$key] != '') || ($val === '')) && ((!isset($vars[$key]) && !array_key_exists($key, $vars)/*NB this is just so the next clause does not error, we have other checks for non-existence*/) || ($vars[$key] != '') || (!$last)) && (($val === null) || ($vars[$key] === $val)))) {
                $good = false;
                break;
            }
        }

        if ($require_full_coverage) {
            foreach ($_GET as $key => $val) {
                if (!is_string($val)) {
                    continue;
                }

                if ((substr($key, 0, 5) === 'keep_') && (!skippable_keep($key, $val))) {
                    $good = false;
                }
            }
            foreach ($vars as $key => $val) {
                if ((!array_key_exists($key, $remapping)) && ($val !== null) && (($key !== 'page') || ($vars[$key] != ''))) {
                    $good = false;
                }
            }
        }
        if ($good) {
            // We've found one, now let's sort out the target
            $makeup = $target;
            if ($GLOBALS['DEV_MODE']) {
                foreach ($vars as $key => $val) {
                    if (is_integer($val)) {
                        $vars[$key] = strval($val);
                    }
                }
            }

            $extra_vars = array();
            foreach ($remapping as $key => $_) {
                if (!isset($vars[$key])) {
                    continue;
                }

                $val = $vars[$key];
                unset($vars[$key]);

                switch ($key) {
                    case 'page':
                        $key = 'PAGE';
                        break;
                    case 'type':
                        $key = 'TYPE';
                        break;
                    case 'id':
                        $key = 'ID';
                        break;
                    default:
                        $key = strtoupper($key);
                        break;
                }
                $makeup = str_replace($key, cms_raw_url_encode($val, true), $makeup);
            }
            if (!$require_full_coverage) {
                $extra_vars += $vars;
            }
            $makeup = str_replace('TYPE', 'browse', $makeup);
            if ($makeup === '') {
                switch ($url_scheme) {
                    case 'HTM':
                        $makeup .= get_zone_default_page($zone_name) . '.htm';
                        break;

                    case 'SIMPLE':
                        $makeup .= get_zone_default_page($zone_name);
                        break;
                }
            }
            if (($extra_vars !== array()) || ($force_index_php)) {
                $first = true;
                $_makeup = '';
                foreach ($extra_vars as $key => $val) { // Add these in explicitly
                    if ($val === null) {
                        continue;
                    }
                    if (is_integer($key)) {
                        $key = strval($key);
                    }
                    if ($val === SELF_REDIRECT) {
                        $val = get_self_url(true, true);
                    }
                    $_makeup .= ($first ? '?' : '&') . $key . '=' . cms_url_encode($val, true);
                    $first = false;
                }
                if ($_makeup !== '') {
                    $makeup .= $_makeup;
                }
            }

            return $makeup;
        }
    }

    return null;
}

/**
 * Find if the specified URL is local or not (actually, if it is relative). This is often used by code that wishes to use file system functions on URLs (Composr will store such relative local URLs for uploads, etc)
 *
 * @param  URLPATH $url The URL to check
 * @return boolean Whether the URL is local
 */
function url_is_local($url)
{
    if ($url === '') {
        return true;
    }

    if ($url[0] === 't' && substr($url, 0, 7) === 'themes/' || $url[0] === 'u' && substr($url, 0, 8) === 'uploads/') {
        return true;
    }
    if ($url[0] === 'h' && (substr($url, 0, 7) === 'http://' || substr($url, 0, 8) === 'https://')) {
        return false;
    }

    if (preg_match('#^[^:\{%]*$#', $url) !== 0) {
        return true;
    }
    $first_char = $url[0];
    return (strpos($url, '://') === false) && ($first_char !== '{') && (substr($url, 0, 7) !== 'mailto:') && (substr($url, 0, 5) !== 'data:') && (substr($url, 0, 8) !== 'debugfs:') && ($first_char !== '%');
}

/**
 * Find if a value appears to be some kind of URL (possibly a Composrised Comcode one).
 *
 * @param  string $value The value to check
 * @param  boolean $lax Whether to be a bit lax in the check
 * @return boolean Whether the value appears to be a URL
 */
function looks_like_url($value, $lax = false)
{
    if ($lax) {
        if (strpos($value, '/') !== false) {
            return true;
        }
        $at = substr($value, 0, 1);
        if ($at === '%' || $at === '{') {
            return true;
        }
    }
    return
        (
        ((strpos($value, '.php') !== false) ||
         (strpos($value, '.htm') !== false) ||
         (substr($value, 0, 1) === '#') ||
         (substr($value, 0, 15) === '{$TUTORIAL_URL') ||
         (substr($value, 0, 13) === '{$FIND_SCRIPT') ||
         (substr($value, 0, 17) === '{$BRAND_BASE_URL') ||
         (substr($value, 0, 10) === '{$BASE_URL') ||
         (substr($value, 0, 3) === '../') ||
         (substr(strtolower($value), 0, 11) === 'javascript:') ||
         (substr($value, 0, 4) === 'tel:') ||
         (substr($value, 0, 7) === 'mailto:') ||
         (substr($value, 0, 7) === 'http://') ||
         (substr($value, 0, 8) === 'https://') ||
         (substr($value, 0, 7) === 'sftp://') ||
         (substr($value, 0, 6) === 'ftp://'))
        ) && (strpos($value, '<') === false);
}

/**
 * Get hidden fields for a form representing 'keep_x'. If we are having a GET form instead of a POST form, we need to do this. This function also encodes the page name, as we'll always want that.
 *
 * @param  ID_TEXT $page The page for the form to go to (blank: don't attach)
 * @param  boolean $keep_all Whether to keep all elements of the current URL represented in this form (rather than just the keep_ fields, and page)
 * @param  ?array $exclude A list of parameters to exclude (null: don't exclude any)
 * @return Tempcode The builtup hidden form fields
 */
function build_keep_form_fields($page = '', $keep_all = false, $exclude = null)
{
    require_code('urls2');
    return _build_keep_form_fields($page, $keep_all, $exclude);
}

/**
 * Relay all POST variables for this URL, to the URL embedded in the form.
 *
 * @param  ?array $exclude A list of parameters to exclude (null: exclude none)
 * @param  boolean $force_everything Force field labels and descriptions to copy through even when there are huge numbers of parameters
 * @return Tempcode The builtup hidden form fields
 */
function build_keep_post_fields($exclude = null, $force_everything = false)
{
    require_code('urls2');
    return _build_keep_post_fields($exclude, $force_everything);
}

/**
 * Takes a URL, and converts it into a file system storable filename. This is used to cache URL contents to the servers filesystem.
 *
 * @param  URLPATH $url_full The URL to convert to an encoded filename
 * @return string A usable filename based on the URL
 */
function url_to_filename($url_full)
{
    require_code('urls2');
    return _url_to_filename($url_full);
}

/**
 * Take a URL and base-URL, and fully qualify the URL according to it.
 *
 * @param  URLPATH $url The URL to fully qualified
 * @param  URLPATH $url_base The base-URL
 * @param  boolean $base_is_full_url Whether the base-URL is actually a full URL which needs stripping back
 * @return URLPATH Fully qualified URL
 */
function qualify_url($url, $url_base, $base_is_full_url = false)
{
    require_code('urls2');
    return _qualify_url($url, $url_base, $base_is_full_url);
}

/**
 * Take a page-link and convert to attributes and zone.
 *
 * @param  SHORT_TEXT $page_link The page-link
 * @return array Triple: zone, attribute-array, hash part of a URL including the hash (or blank)
 */
function page_link_decode($page_link)
{
    if (strpos($page_link, '#') === false) {
        $hash = '';
    } else {
        $hash_pos = strpos($page_link, '#');
        $hash = substr($page_link, $hash_pos + 1);
        $page_link = substr($page_link, 0, $hash_pos);
    }
    if (strpos($page_link, "\n") === false) {
        $bits = explode(':', $page_link);
    } else { // If there's a line break then we ignore any colons after that line-break. It's to allow complex stuff to be put on the end of the page-link
        $term_pos = strpos($page_link, "\n");
        $bits = explode(':', substr($page_link, 0, $term_pos));
        $bits[count($bits) - 1] .= substr($page_link, $term_pos);
    }
    $zone = $bits[0];
    if ($zone === '_SEARCH') {
        if (isset($bits[1])) {
            $zone = get_page_zone($bits[1], false);
            if ($zone === null) {
                $zone = '';
            }
        } else {
            $zone = '';
        }
    } elseif (($zone === 'site') && (get_option('collapse_user_zones') === '1')) {
        $zone = '';
    } elseif ($zone === '_SELF') {
        $zone = get_zone_name();
    }
    if ((isset($bits[1])) && (strpos($bits[1], '=') === false)) {
        if ($bits[1] !== '') {
            if ($bits[1] === '_SELF') {
                $attributes = array('page' => get_page_name());
            } else {
                $attributes = array('page' => $bits[1]);
            }
        } else {
            $attributes = array('page' => get_zone_default_page($zone));
        }
        unset($bits[1]);
    } else {
        $attributes = array('page' => get_zone_default_page($zone));
    }
    unset($bits[0]);
    $i = 0;
    foreach ($bits as $bit) {
        if (($bit !== '') || ($i === 1)) {
            if (($i === 0) && (strpos($bit, '=') === false)) {
                $_bit = array('type', $bit);
            } elseif (($i === 1) && (strpos($bit, '=') === false)) {
                $_bit = array('id', $bit);
            } else {
                $_bit = explode('=', $bit, 2);
            }
        } else {
            $_bit = array($bit, '');
        }
        if (isset($_bit[1])) {
            $decoded = urldecode($_bit[1]);
            if (($decoded !== '') && ($decoded[0] === '{') && (strlen($decoded) > 2) && (intval($decoded[1]) > 51)) { // If it is in template format (symbols)
                require_code('tempcode_compiler');
                $_decoded = template_to_tempcode($decoded);
                $decoded = $_decoded->evaluate();
            }
            if ($decoded === '<null>') {
                $attributes[$_bit[0]] = null;
            } else {
                $attributes[$_bit[0]] = $decoded;
            }
        }

        $i++;
    }

    return array($zone, $attributes, $hash);
}

/**
 * Convert a URL to a local file path.
 *
 * @param  URLPATH $url The value to convert
 * @return ?PATH File path (null: is not local)
 */
function convert_url_to_path($url)
{
    require_code('urls2');
    return _convert_url_to_path($url);
}

/**
 * Sometimes users don't enter full URLs but do intend for them to be absolute. This code tries to see what relative URLs are actually absolute ones, via an algorithm. It then fixes the URL.
 *
 * @param  URLPATH $in The URL to fix
 * @return URLPATH The fixed URL (or original one if no fix was needed)
 */
function fixup_protocolless_urls($in)
{
    require_code('urls2');
    $ret = _fixup_protocolless_urls($in);
    if ((strlen($ret) >= 255) && (strlen($in) < 255)) {
        return $in; // Don't allow transformations to create a fatal error by a URL too long (this is worse than just leaving a broken URL)
    }
    return $ret;
}

/**
 * Convert a local URL to a page-link.
 *
 * @param  URLPATH $url The URL to convert. Note it may not be for a URL Scheme, and it must be based on the local base URL (else failure WILL occur).
 * @param  boolean $abs_only Whether to only convert absolute URLs. Turn this on if you're not sure what you're passing is a URL not and you want to be extra safe.
 * @param  boolean $perfect_only Whether to only allow perfect conversions.
 * @return string The page-link (blank: could not convert).
 */
function url_to_page_link($url, $abs_only = false, $perfect_only = true)
{
    require_code('urls2');
    return _url_to_page_link($url, $abs_only, $perfect_only);
}

/**
 * Given a URL or page-link, return an absolute URL.
 *
 * @param  string $url URL or page-link
 * @param  boolean $skip_keep Whether to skip actually putting on keep_ parameters (rarely will this skipping be desirable)
 * @return URLPATH URL
 */
function page_link_to_url($url, $skip_keep = false)
{
    $parts = array();
    if ((preg_match('#([' . URL_CONTENT_REGEXP . ']*):([' . URL_CONTENT_REGEXP . ']+|[^/]|$)((:(.*))*)#', $url, $parts) != 0) && ($parts[1] != 'mailto')) { // Specially encoded page-link. Complex regexp to make sure URLs do not match
        list($zone, $map, $hash) = page_link_decode($url);
        $url = static_evaluate_tempcode(build_url($map, $zone, array(), false, false, $skip_keep, $hash));
    } else {
        $url = qualify_url($url, get_base_url());
    }
    return $url;
}

/**
 * Given a page-link, return an absolute URL.
 *
 * @param  string $page_link Page-link
 * @return Tempcode URL
 */
function page_link_to_tempcode_url($page_link)
{
    list($zone, $map, $hash) = page_link_decode($page_link);
    return build_url($map, $zone, array(), false, false, false, $hash);
}

/**
 * Convert a local page file path to a written page-link.
 *
 * @param  string $page The path.
 * @return string The page-link (blank: could not convert).
 */
function page_path_to_page_link($page)
{
    require_code('urls2');
    return _page_path_to_page_link($page);
}

/**
 * Load up hooks needed to detect how to use monikers.
 */
function load_moniker_hooks()
{
    if (running_script('install')) {
        return;
    }

    global $CONTENT_OBS;
    if ($CONTENT_OBS === null) {
        $CONTENT_OBS = function_exists('persistent_cache_get') ? persistent_cache_get('CONTENT_OBS') : null;
        if ($CONTENT_OBS !== null) {
            foreach ($CONTENT_OBS as $ob_info) {
                if (($ob_info['title_field'] !== null) && (strpos($ob_info['title_field'], 'CALL:') !== false)) {
                    require_code('hooks/systems/content_meta_aware/' . $ob_info['_hook']);
                }
            }

            return;
        }

        $no_monikers_in = array( // FUDGE: Optimisation, not ideal! But it saves file loading and memory
            'author' => true,
            'banner' => true,
            'banner_type' => true,
            'calendar_type' => true,
            'catalogue' => true,
            'post' => true,
            'wiki_page' => true,
            'wiki_post' => true,
        );

        $CONTENT_OBS = array();
        $hooks = find_all_hooks('systems', 'content_meta_aware');
        foreach ($hooks as $hook => $sources_dir) {
            if (isset($no_monikers_in[$hook])) {
                continue;
            }

            $info_function = extract_module_functions(get_file_base() . '/' . $sources_dir . '/hooks/systems/content_meta_aware/' . $hook . '.php', array('info'), null, false, 'Hook_content_meta_aware_' . $hook);
            if ($info_function[0] !== null) {
                $ob_info = is_array($info_function[0]) ? call_user_func_array($info_function[0][0], $info_function[0][1]) : eval($info_function[0]);

                if ($ob_info === null) {
                    continue;
                }
                if (!isset($ob_info['view_page_link_pattern'])) {
                    continue;
                }
                $ob_info['_hook'] = $hook;
                $CONTENT_OBS[$ob_info['view_page_link_pattern']] = $ob_info;

                if (($ob_info['title_field'] !== null) && (strpos($ob_info['title_field'], 'CALL:') !== false)) {
                    require_code('hooks/systems/content_meta_aware/' . $hook);
                }
            }
        }

        if (function_exists('persistent_cache_set')) {
            persistent_cache_set('CONTENT_OBS', $CONTENT_OBS);
        }
    }
}

/**
 * Find the textual moniker for a typical Composr URL path. This will be called from inside build_url, based on details learned from a moniker hook (only if a hook exists to hint how to make the requested link SEO friendly).
 *
 * @param  array $url_parts The URL component map (must contain 'page', 'type', and 'id' if this function is to do anything).
 * @param  ID_TEXT $zone The URL zone name (only used for Comcode Page URL monikers).
 * @param  boolean $search_redirects Whether to consider that the page may have been redirected. We'll generally set this to false when linking, as we know that redirects will be considered elsewhere in the stack anyway.
 * @return ?string The moniker ID (null: could not find)
 */
function find_id_moniker($url_parts, $zone, $search_redirects = true)
{
    if (!isset($url_parts['page'])) {
        return null;
    }

    $page = $url_parts['page'];

    if (strpos($page, '[') !== false) {
        return null; // A regexp in a comparison URL, in breadcrumbs code
    }
    if ($zone == '[\w\-]*') {
        return null; // Part of a breadcrumbs regexp
    }

    // Does this URL arrangement support monikers?
    global $CONTENT_OBS;
    if ($CONTENT_OBS === null) {
        load_moniker_hooks();
    }
    if (!array_key_exists('id', $url_parts)) {
        if (($page != 'start'/*TODO: Change in v11*/) && (@is_file(get_file_base() . '/' . $zone . (($zone == '') ? '' : '/') . 'pages/modules/' . $page . '.php'))) { // Wasteful of resources
            return null;
        }
        if (($zone == '') && (get_option('collapse_user_zones') == '1')) {
            if (@is_file(get_file_base() . '/site/pages/modules/' . $page . '.php')) {// Wasteful of resources
                return null;
            }
        }

        // Moniker may be held the other side of a redirect
        if (!function_exists('_request_page')) {
            return null; // In installer
        }
        if ($search_redirects) {
            $page_place = _request_page(str_replace('-', '_', $page), $zone);
            if (($page_place !== false) && ($page_place[0] == 'REDIRECT')) {
                $page = $page_place[1]['r_to_page'];
                $zone = $page_place[1]['r_to_zone'];
            }
        }

        $url_parts['type'] = '';
        $effective_id = $zone;
        $url_parts['id'] = $zone;

        $looking_for = '_WILD:_WILD';
    } else {
        if (!isset($url_parts['type'])) {
            $url_parts['type'] = 'browse';
        }
        if ($url_parts['type'] === null) {
            $url_parts['type'] = 'browse'; // null means "do not take from environment"; so we default it to 'browse' (even though it might actually be left out when URL Schemes are off, we know it cannot be for URL Schemes)
        }

        if ($url_parts['id'] === null) {
            return null;
        }

        global $REDIRECT_CACHE;
        if ((isset($REDIRECT_CACHE[$zone][strtolower($page)])) && ($REDIRECT_CACHE[$zone][strtolower($page)]['r_is_transparent'] === 1)) {
            $new_page = $REDIRECT_CACHE[$zone][strtolower($page)]['r_to_page'];
            $new_zone = $REDIRECT_CACHE[$zone][strtolower($page)]['r_to_zone'];
            $page = $new_page;
            $zone = $new_zone;
        }

        $effective_id = $url_parts['id'];

        $looking_for = '_SEARCH:' . $page . ':' . $url_parts['type'] . ':_WILD';
    }
    $ob_info = isset($CONTENT_OBS[$looking_for]) ? $CONTENT_OBS[$looking_for] : null;
    if ($ob_info === null) {
        return null;
    }

    if ($ob_info['id_field_numeric']) {
        if (!is_numeric($effective_id)) {
            return null;
        }
    } else {
        if (strpos($effective_id, '/') !== false) {
            return null;
        }
    }

    if ($ob_info['support_url_monikers']) {
        global $SMART_CACHE;
        if ($SMART_CACHE !== null) {
            $SMART_CACHE->append('NEEDED_MONIKERS', serialize(array(array('page' => $page, 'type' => $url_parts['type']), $zone, $effective_id)));
        }

        // Has to find existing if already there
        global $LOADED_MONIKERS_CACHE;
        if (isset($LOADED_MONIKERS_CACHE[$url_parts['type']][$page][$effective_id])) {
            if (is_bool($LOADED_MONIKERS_CACHE[$url_parts['type']][$page][$effective_id])) { // Ok, none pre-loaded yet, so we preload all and replace the boolean values with actual results
                $or_list = '';
                foreach ($LOADED_MONIKERS_CACHE as $type => $pages) {
                    foreach ($pages as $_page => $ids) {
                        $first_it = true;

                        foreach ($ids as $id => $status) {
                            if ($status !== true) {
                                continue;
                            }

                            if ($first_it) {
                                if (!is_string($_page)) {
                                    $_page = strval($_page);
                                }

                                $first_it = false;
                            }

                            if (is_integer($id)) {
                                $id = strval($id);
                            }

                            if ($or_list != '') {
                                $or_list .= ' OR ';
                            }
                            $or_list .= '(' . db_string_equal_to('m_resource_page', $_page) . ' AND ' . db_string_equal_to('m_resource_type', $type) . ' AND ' . db_string_equal_to('m_resource_id', $id) . ')';

                            $LOADED_MONIKERS_CACHE[$_page][$type][$id] = $id; // Will be replaced with correct value if it is looked up
                        }
                    }
                }
                if ($or_list != '') {
                    $bak = $GLOBALS['NO_DB_SCOPE_CHECK'];
                    $GLOBALS['NO_DB_SCOPE_CHECK'] = true;
                    $query = 'SELECT m_moniker,m_resource_page,m_resource_type,m_resource_id FROM ' . get_table_prefix() . 'url_id_monikers WHERE m_deprecated=0 AND (' . $or_list . ')';
                    $results = $GLOBALS['SITE_DB']->query($query, null, null, false, true);
                    $GLOBALS['NO_DB_SCOPE_CHECK'] = $bak;
                    foreach ($results as $result) {
                        $LOADED_MONIKERS_CACHE[$result['m_resource_type']][$result['m_resource_page']][$result['m_resource_id']] = $result['m_moniker'];
                    }
                    foreach ($LOADED_MONIKERS_CACHE as $type => &$pages) {
                        foreach ($pages as $_page => &$ids) {
                            foreach ($ids as $id => $status) {
                                if (is_bool($status)) {
                                    $ids[$id] = false; // Could not look up, but we don't want to search for it again so mark as missing
                                }
                            }
                        }
                    }
                }
            }
            $test = $LOADED_MONIKERS_CACHE[$url_parts['type']][$page][$effective_id];
            if ($test === false) {
                $test = null;
            }
        } else {
            $bak = $GLOBALS['NO_DB_SCOPE_CHECK'];
            $GLOBALS['NO_DB_SCOPE_CHECK'] = true;
            $where = array(
                'm_deprecated' => 0,
                'm_resource_page' => $page,
                'm_resource_type' => $url_parts['type'],
                'm_resource_id' => is_integer($effective_id) ? strval($effective_id) : $effective_id,
            );
            $test = $GLOBALS['SITE_DB']->query_select_value_if_there('url_id_monikers', 'm_moniker', $where);
            $GLOBALS['NO_DB_SCOPE_CHECK'] = $bak;
            if ($test !== null) {
                $LOADED_MONIKERS_CACHE[$url_parts['type']][$page][$effective_id] = $test;
            } else {
                $LOADED_MONIKERS_CACHE[$url_parts['type']][$page][$effective_id] = false;
            }
        }

        if (is_string($test)) {
            return ($test == '') ? null : $test;
        }

        if ($looking_for == '_WILD:_WILD') {
            return null; // We don't generate these automatically
        }

        // Otherwise try to generate a new one
        require_code('urls2');
        $test = autogenerate_new_url_moniker($ob_info, $url_parts, $zone);
        if ($test === null) {
            $test = '';
        }
        $LOADED_MONIKERS_CACHE[$url_parts['type']][$page][$effective_id] = $test;
        return ($test == '') ? null : $test;
    }

    return null;
}

/**
 * Extend a URL with additional parameter(s). Does not handle URL encoding of the appended parameter, which you should do first if applicable.
 *
 * @param  string $url The URL to append to (returned by reference).
 * @param  string $append URL parameter(s) to append, with no leading or trailing ? or & characters.
 */
function extend_url(&$url, $append)
{
    if (($append != '') && (strpos($url, '?' . $append) === false) && (strpos($url, '&' . $append) === false)) {
        $url .= ((strpos($url, '?') === false) ? '?' : '&') . $append;
    }
}

/**
 * Ensure a URL can be embedded within our webpage context.
 * Currently this means making sure if we're on an HTTPS page, everything is HTTPS.
 *
 * @param  string $url The URL to check.
 * @return string $append The fixed URL.
 */
function ensure_protocol_suitability($url)
{
    if (!tacit_https()) { // Site not running HTTPS for this page
        return $url;
    }

    if (strpos($url, '://') === false) { // Protocol-relative URL, relative URL, or some other URL handler that we can't make conclusions about
        return $url;
    }

    if (substr($url, 0, 7) != 'http://') { // Already HTTPS
        return $url;
    }

    $https_url = 'https://' . substr($url, 7);

    $https_exists = check_url_exists($https_url, 60 * 60 * 24 * 31);

    if ($https_exists) {
        return $https_url;
    }

    return find_script('external_url_proxy') . '?url=' . urlencode($url);
}

/**
 * Check to see if a URL exists.
 *
 * @param  string $url The URL to check.
 * @param  integer $test_freq_secs Cache must be newer than this many seconds.
 * @return boolean Whether it does.
 */
function check_url_exists($url, $test_freq_secs)
{
    $test1 = $GLOBALS['SITE_DB']->query_select('urls_checked', array('url_check_time', 'url_exists'), array('url' => $url), 'ORDER BY url_check_time DESC', 1);

    if ((!isset($test1[0])) || ($test1[0]['url_check_time'] < time() - $test_freq_secs)) {
        $test2 = http_download_file($url, 0, false);
        if (($test2 === null) && (($GLOBALS['HTTP_MESSAGE'] == '401') || ($GLOBALS['HTTP_MESSAGE'] == '403') || ($GLOBALS['HTTP_MESSAGE'] == '405') || ($GLOBALS['HTTP_MESSAGE'] == '416') || ($GLOBALS['HTTP_MESSAGE'] == '500') || ($GLOBALS['HTTP_MESSAGE'] == '503') || ($GLOBALS['HTTP_MESSAGE'] == '520'))) {
            $test2 = http_download_file($url, 1, false); // Try without HEAD, sometimes it's not liked
        }
        $exists = (($test2 === null) && ($GLOBALS['HTTP_MESSAGE'] != 401)) ? 0 : 1;

        if (isset($test1[0])) {
            $GLOBALS['SITE_DB']->query_delete('urls_checked', array(
                'url' => $url,
            ));
        }

        $GLOBALS['SITE_DB']->query_insert('urls_checked', array(
            'url' => $url,
            'url_exists' => $exists,
            'url_check_time' => time(),
        ));
    } else {
        $exists = $test1[0]['url_exists'];
    }

    return ($exists == 1);
}

/**
 * Remove unnecessarily paranoid URL-encoding if needed, so the given URL will fit in the database.
 *
 * @param  URLPATH $url The URL
 * @param  boolean $force Whether to force a conversion even if the URL is not that long
 * @param  boolean $tolerate_errors If this is set to false then an error message will be shown if the URL is still too long after we do what we can; set to true if we have someway of further shortening the URL after this function is called
 * @return URLPATH The shortened URL
 */
function cms_rawurlrecode($url, $force = false, $tolerate_errors = false)
{
    if ((cms_mb_strlen($url) > 255) || ($force)) {
        require_code('urls_simplifier');
        $url = _cms_rawurlrecode($url, $tolerate_errors);
    }

    return $url;
}

/**
 * Take a URL, which may have a utf-8 domain name, and normalise it to normal IDN.
 *
 * @param  URLPATH $url The URL
 * @return URLPATH The normalised URL
 */
function normalise_idn_url($url)
{
    require_code('urls_simplifier');
    $coder_ob = new HarmlessURLCoder();
    return $coder_ob->encode($url);
}
