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

/**
 * Standard code module initialisation function.
 *
 * @ignore
 */
function init__zones()
{
    global $BLOCK_CACHE_ON_CACHE, $CLASS_CACHE;
    $BLOCK_CACHE_ON_CACHE = null;
    $CLASS_CACHE = array();

    global $ARB_COUNTER;
    $ARB_COUNTER = 1;

    global $DO_NOT_CACHE_THIS;
    $DO_NOT_CACHE_THIS = false;

    global $MODULES_ZONES_CACHE, $MODULES_ZONES_CACHE_DEFAULT;
    $MODULES_ZONES_CACHE = function_exists('persistent_cache_get') ? persistent_cache_get('MODULES_ZONES') : null;

    global $SITE_INFO;
    $hardcoded = (isset($SITE_INFO['hardcode_common_module_zones'])) && ($SITE_INFO['hardcode_common_module_zones'] == '1');
    if (get_forum_type() == 'cns') {
        if ($hardcoded) {
            $MODULES_ZONES_CACHE_DEFAULT = array( // Breaks redirects etc, but handy optimisation if you have a vanilla layout
                                                  'forumview' => 'forum',
                                                  'topicview' => 'forum',
                                                  'topics' => 'forum',
                                                  'vforums' => 'forum',
                                                  'points' => (get_option('collapse_user_zones') == '1') ? '' : 'site',
                                                  'members' => (get_option('collapse_user_zones') == '1') ? '' : 'site',
                                                  'catalogues' => (get_option('collapse_user_zones') == '1') ? '' : 'site',
                                                  'join' => '',
                                                  'login' => '',
                                                  'recommend' => '',
            );
        } else {
            $MODULES_ZONES_CACHE_DEFAULT = array(
                'join' => '',
                'login' => '',
            );
        }
    } else {
        $MODULES_ZONES_CACHE_DEFAULT = array(
            'join' => '',
        );
    }

    global $VIRTUALISED_ZONES_CACHE;
    $VIRTUALISED_ZONES_CACHE = null;
    if (is_null($MODULES_ZONES_CACHE)) {
        foreach ($MODULES_ZONES_CACHE_DEFAULT as $key => $val) {
            if ((!$hardcoded) && (!is_file(get_file_base() . (($val == '') ? '' : ('/' . $val)) . '/pages/modules/' . $key . '.php'))) {
                unset($MODULES_ZONES_CACHE_DEFAULT[$key]);
            }
        }
        $MODULES_ZONES_CACHE = array(get_zone_name() => array('modules' => $MODULES_ZONES_CACHE_DEFAULT));
    }

    global $ALL_ZONES_CACHE, $ALL_ZONES_TITLED_CACHE;
    $ALL_ZONES_CACHE = null;
    $ALL_ZONES_TITLED_CACHE = null;

    global $REDIRECT_CACHE;
    $REDIRECT_CACHE = null;

    global $MODULE_INSTALLED_CACHE;
    $MODULE_INSTALLED_CACHE = array();

    global $HOOKS_CACHE;
    $HOOKS_CACHE = function_exists('persistent_cache_get') ? persistent_cache_get('HOOKS') : array();
    if ($HOOKS_CACHE === null) {
        $HOOKS_CACHE = array();
    }

    if (!defined('FIND_ALL_PAGES__PERFORMANT')) {
        define('FIND_ALL_PAGES__PERFORMANT', 0);
        define('FIND_ALL_PAGES__NEWEST', 1);
        define('FIND_ALL_PAGES__ALL', 2);
    }

    global $BLOCKS_AT_CACHE;
    $BLOCKS_AT_CACHE = function_exists('persistent_cache_get') ? persistent_cache_get('BLOCKS_AT') : array();
    if ($BLOCKS_AT_CACHE === null) {
        $BLOCKS_AT_CACHE = array();
    }

    // "Kid Gloves Modes" tracking
    if (!defined('I_UNDERSTAND_SQL_INJECTION')) {
        define('I_UNDERSTAND_SQL_INJECTION', 1);
        define('I_UNDERSTAND_XSS', 2);
        define('I_UNDERSTAND_PATH_INJECTION', 4);
    }
    global $DECLARATIONS_STACK, $DECLARATIONS_STATE, $DECLARATIONS_STATE_DEFAULT;
    $DECLARATIONS_STACK = array();
    $DECLARATIONS_STATE_DEFAULT = array(
        I_UNDERSTAND_SQL_INJECTION => true,
        I_UNDERSTAND_XSS => true,
        I_UNDERSTAND_PATH_INJECTION => true,
    );
    $DECLARATIONS_STATE = $DECLARATIONS_STATE_DEFAULT;
    array_push($DECLARATIONS_STACK, $DECLARATIONS_STATE);
}

/**
 * Pre-load used blocks in bulk.
 */
function preload_block_internal_caching()
{
    global $SMART_CACHE;
    if (has_caching_for('block')) {
        $blocks_needed = $SMART_CACHE->get('blocks_needed');
        if ($blocks_needed !== null && $blocks_needed !== false) {
            $bulk = array();

            foreach ($blocks_needed as $param => $_) {
                $bulk[] = unserialize($param);
            }

            if ($GLOBALS['PERSISTENT_CACHE'] === null) {
                _get_cache_entries($bulk); // Will cache internally so that block loads super-quick
            }
        }
    }
}

/**
 * Declare what security properties the programmer understands. i.e. Self-certification.
 * A good programmer will understand the correct data conversions to undergo in order to write secure/correct/reliable code.
 * A newbie programmer likely will not, sloppiness or a lack of understanding could lead to critical mistakes.
 * If declarations aren't made then extra security precautions are taken, which may interfere with normal processing in limited cases.
 * Declarations should be made whenever entering a custom block or module.
 *
 * @param  integer $declarations Bitmask of declarations (I_UNDERSTAND_* constants).
 */
function i_solemnly_declare($declarations)
{
    global $DECLARATIONS_STACK, $DECLARATIONS_STATE_DEFAULT, $DECLARATIONS_STATE;
    array_pop($DECLARATIONS_STACK);
    $new_state = array();
    foreach (array_keys($DECLARATIONS_STATE_DEFAULT) as $property) {
        $new_state[$property] = (($declarations & $property) != 0);
    }
    $DECLARATIONS_STACK[] = $new_state;
    $DECLARATIONS_STATE = $new_state;
}

/**
 * Enter a new security scope (i.e. a custom block or module).
 *
 * @ignore
 */
function _solemnly_enter()
{
    if (in_safe_mode()) {
        // No custom code actually running
        return;
    }

    global $DECLARATIONS_STACK, $DECLARATIONS_STATE_DEFAULT, $DECLARATIONS_STATE;
    $new_state = array();
    foreach (array_keys($DECLARATIONS_STATE_DEFAULT) as $property) {
        $new_state[$property] = false;
    }
    $DECLARATIONS_STACK[] = $new_state;
    $DECLARATIONS_STATE = $new_state;
}

/**
 * Leave the most recent security scope (i.e. a custom block or module).
 *
 * @param  ?string $out Output to filter, if I_UNDERSTAND_XSS is not set (null: nothing to filter).
 *
 * @ignore
 */
function _solemnly_leave(&$out = null)
{
    if (in_safe_mode()) {
        // No custom code actually running
        return;
    }

    if ((!has_solemnly_declared(I_UNDERSTAND_XSS)) && ($out !== null)) {
        foreach (array_merge(array_values($_POST), array_values($_GET)) as $before) {
            if (is_string($before)) {
                $after = $before;
                kid_gloves_html_escaping_singular($after);
                if ($after !== $before) {
                    $out = str_replace($before, $after, $out);
                }
            }
        }
    }

    if (!has_solemnly_declared(I_UNDERSTAND_PATH_INJECTION)) {
        foreach ($_GET as $param) {
            if (is_string($param)) {
                filter_naughty($param);
            }
        }
    }

    global $DECLARATIONS_STACK, $DECLARATIONS_STATE;
    array_pop($DECLARATIONS_STACK);
    $DECLARATIONS_STATE = array_pop($DECLARATIONS_STACK);
    array_push($DECLARATIONS_STACK, $DECLARATIONS_STATE);
}

/**
 * Find if a security property has been declared as being understood.
 *
 * @param  integer $declaration The property.
 * @return boolean Whether it is understood.
 */
function has_solemnly_declared($declaration)
{
    global $DECLARATIONS_STATE;
    return $DECLARATIONS_STATE[$declaration];
}

/**
 * Consider virtual zone merging, where paths are not necessarily where you'd expect for pages in zones.
 *
 * @param  PATH $path The path, assuming in the obvious place.
 * @param  boolean $relative Where the passed path is relative.
 * @return PATH The fixed path.
 */
function zone_black_magic_filterer($path, $relative = false)
{
    static $no_collapse_zones = null;
    if ($no_collapse_zones === null) {
        $no_collapse_zones = (get_option('collapse_user_zones') !== '1');
    }
    if ($no_collapse_zones) {
        return $path;
    }

    static $zbmf_cache = null;
    if ($zbmf_cache === null) {
        $zbmf_cache = function_exists('persistent_cache_get') ? persistent_cache_get('ZBMF_CACHE') : array();
        if ($zbmf_cache === null) {
            $zbmf_cache = array();
        }
    }

    if (isset($zbmf_cache[$path])) {
        return $zbmf_cache[$path];
    }

    if ($relative) {
        $stripped = $path;
    } else {
        $cfb = get_custom_file_base();
        if (substr($path, 0, strlen($cfb)) === $cfb) {
            $stripped = substr($path, strlen($cfb) + 1);
        } else {
            $fb = get_file_base();
            $stripped = substr($path, strlen($fb) + 1);
        }
    }

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

        if (($stripped[0] === 'p') && (substr($stripped, 0, 6) === 'pages/')) { // Ah, need to do some checks as we are looking in the welcome zone
            $full = $relative ? (get_file_base() . '/' . $path) : $path;
            if (!@is_file($full)) {
                $site_equiv = get_file_base() . '/site/' . $stripped;

                if (@is_file($site_equiv)) {
                    $ret = $relative ? ('site/' . $stripped) : $site_equiv;
                    $zbmf_cache[$path] = $ret;
                    if (function_exists('persistent_cache_set')) {
                        persistent_cache_set('ZBMF_CACHE', $zbmf_cache);
                    }
                    return $ret;
                }
            }
        }
    }

    $zbmf_cache[$path] = $path;
    if (function_exists('persistent_cache_set')) {
        persistent_cache_set('ZBMF_CACHE', $zbmf_cache);
    }
    return $path;
}

/**
 * Find the filebase-relative path of a Comcode page.
 *
 * @param  LANGUAGE_NAME $lang The language most preferable
 * @param  ID_TEXT $file The page name
 * @param  ID_TEXT $zone The zone
 * @return array A triple: The file base, The path (blank: not found), Combined path (blank: not found)
 */
function find_comcode_page($lang, $file, $zone)
{
    $file_path = zone_black_magic_filterer(filter_naughty($zone . (($zone == '') ? '' : '/') . 'pages/comcode_custom/' . $lang . '/' . $file . '.txt'), true);

    $file_base = null;
    if (is_file(get_custom_file_base() . '/' . $file_path)) {
        $file_base = get_custom_file_base();
    } elseif (is_file(get_file_base() . '/' . $file_path)) {
        $file_base = get_file_base();
    }

    if ($file_base === null) {
        $page_request = _request_page($file, $zone);
        if ($page_request === false || strpos($page_request[0], 'COMCODE') === false) {
            return array(get_file_base(), '', '');
        }
        $file_path = $page_request[count($page_request) - 1];

        $file_base = get_custom_file_base();
        if (!is_file($file_base . '/' . $file_path)) {
            $file_base = get_file_base();
        }
    }

    return array($file_base, $file_path, ($file_path == '') ? '' : ($file_base . '/' . $file_path));
}

/**
 * Get the name of the zone the current page request is coming from.
 *
 * @return ID_TEXT The current zone
 */
function get_zone_name()
{
    global $ZONE, $RELATIVE_PATH, $SITE_INFO, $VIRTUALISED_ZONES_CACHE;
    if ($ZONE !== null) {
        return $ZONE['zone_name'];
    }
    if ($VIRTUALISED_ZONES_CACHE !== false) {
        $VIRTUALISED_ZONES_CACHE = false;
        $url_path = str_replace('\\', '/', dirname(cms_srv('SCRIPT_NAME')));
        $host = preg_replace('#:\d+$#', '', cms_srv('HTTP_HOST'));
        foreach ($SITE_INFO as $key => $val) {
            if (($key[0] === 'Z') && (substr($key, 0, 13) === 'ZONE_MAPPING_') && (is_array($val))) {
                $VIRTUALISED_ZONES_CACHE = true;
                if (($host === $val[0]) && (preg_match('#^' . (($val[1] === '') ? '' : ('/' . preg_quote($val[1]))) . '(/|$)#', $url_path) != 0)) {
                    return @strval(substr($key, 13));
                }
            }
        }
        if (($VIRTUALISED_ZONES_CACHE) && (substr($host, 0, 4) === 'www.')) {
            $host = substr($host, 4);
            foreach ($SITE_INFO as $key => $val) {
                if (($key[0] === 'Z') && (substr($key, 0, 13) === 'ZONE_MAPPING_') && (is_array($val))) {
                    if (($host === $val[0]) && (preg_match('#^' . (($val[1] === '') ? '' : ('/' . preg_quote($val[1]))) . '(/|$)#', $url_path) != 0)) {
                        require_code('urls');
                        set_http_status_code('301');
                        header('Location: ' . escape_header(str_replace('://www.', '://', get_self_url_easy())));
                        exit();
                    }
                }
            }
        }
    }
    $real_zone = (($RELATIVE_PATH === 'data') || ($RELATIVE_PATH === 'data_custom')) ? get_param_string('zone', '') : $RELATIVE_PATH;

    return $real_zone;
}

/**
 * Load up redirect cache.
 */
function load_redirect_cache()
{
    global $REDIRECT_CACHE;

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

    if (addon_installed('redirects_editor')) {
        $redirect = persistent_cache_get('REDIRECT');
        if ($redirect === null) {
            $redirect = $GLOBALS['SITE_DB']->query_select('redirects', array('*')/*Actually for performance we will load all and cache them , array('r_from_zone' => get_zone_name())*/);
            persistent_cache_set('REDIRECT', $redirect);
        }
        foreach ($redirect as $r) {
            if (($r['r_from_zone'] == $r['r_to_zone']) && ($r['r_from_page'] == $r['r_to_page'])) {
                continue;
            }

            $REDIRECT_CACHE[$r['r_from_zone']][strtolower($r['r_from_page'])] = $r;
        }
    }
}

/**
 * Find the zone a page is in.
 *
 * @param  ID_TEXT $module_name The page name to find
 * @param  ID_TEXT $type The type of the page we are looking for
 * @param  ?string $dir2 The special subcategorisation of page we are looking for (e.g. 'EN' for a Comcode page) (null: none)
 * @param  string $ftype The file extension for the page type
 * @param  boolean $error Whether Composr should bomb out if the page was not found
 * @param  boolean $check_redirects Whether to check against redirects
 * @param  ?ID_TEXT $first_zone_to_check First zone to check (used for an optimisation) (null: current zone)
 * @return ?ID_TEXT The zone the page is in (null: not found)
 */
function get_module_zone($module_name, $type = 'modules', $dir2 = null, $ftype = 'php', $error = true, $check_redirects = true, $first_zone_to_check = null)
{
    if ($module_name === '') {
        return null;
    }

    $_zone = get_zone_name();
    if ($first_zone_to_check === null) {
        $first_zone_to_check = $_zone;
    }

    global $MODULES_ZONES_CACHE;
    if ((isset($MODULES_ZONES_CACHE[$check_redirects][$_zone][$type][$module_name])) || ((!$error) && (isset($MODULES_ZONES_CACHE[$check_redirects][$_zone][$type])) && (array_key_exists($module_name, $MODULES_ZONES_CACHE[$check_redirects][$_zone][$type])) && ($type === 'modules')/*don't want to look at cached failure for different page type*/)) {
        if (is_string($MODULES_ZONES_CACHE[$check_redirects][$_zone][$type][$module_name])/*should always be a string, but possible weird bug*/) {
            return $MODULES_ZONES_CACHE[$check_redirects][$_zone][$type][$module_name];
        }
    }

    $error = false; // hack for now

    if (($module_name === get_page_name()) && (running_script('index')) && ($module_name !== 'login')) {
        $MODULES_ZONES_CACHE[$check_redirects][$_zone][$type][$module_name] = $_zone;
        return $_zone;
    }

    if (get_value('allow_admin_in_other_zones') !== '1') {
        if (($type === 'modules') && ($module_name[0] === 'a') && (($module_name == 'admin') || (substr($module_name, 0, 6) === 'admin_'))) {
            $zone = 'adminzone';
            $MODULES_ZONES_CACHE[$check_redirects][$_zone][$type][$module_name] = $zone;
            return $zone;
        }
        if (($type === 'modules') && ($module_name[0] === 'c') && (($module_name == 'cms') || (substr($module_name, 0, 4) === 'cms_'))) {
            $zone = 'cms';
            $MODULES_ZONES_CACHE[$check_redirects][$_zone][$type][$module_name] = $zone;
            return $zone;
        }
    }

    $check_redirects = $check_redirects && (get_value('no_priority_redirects') !== '1');

    $likely_non_custom = ($type == 'minimodules') || ($type == 'html');

    global $REDIRECT_CACHE;
    if ($check_redirects && $REDIRECT_CACHE === null) {
        load_redirect_cache();
    }
    if (($module_name[0] == 'a') && (($module_name == 'admin') || (substr($module_name, 0, 6) === 'admin_'))) {
        $first_zones = array('adminzone');
    } elseif (($module_name[0] == 'c') && (($module_name == 'cms') || (substr($module_name, 0, 4) === 'cms_'))) {
        $first_zones = array('cms');
    } else {
        $first_zones = array($first_zone_to_check);
        if ($first_zone_to_check !== '') {
            $first_zones[] = '';
        }
        if (($first_zone_to_check !== 'site') && (get_option('collapse_user_zones') !== '1')/* && (is_file(get_file_base().'/site/index.php'))*/) {
            $first_zones[] = 'site';
        }
    }
    foreach ($first_zones as $zone) {
        if (($check_redirects) && ((isset($REDIRECT_CACHE[$zone][strtolower($module_name)])) && ($REDIRECT_CACHE[$zone][strtolower($module_name)]['r_is_transparent'] === 1) || (isset($REDIRECT_CACHE['*'][$module_name])) && ($REDIRECT_CACHE['*'][$module_name]['r_is_transparent'] === 1))) { // Only needs to actually look for redirections in first zones until end due to the way precedences work (we know the current zone will be in the first zones)
            $MODULES_ZONES_CACHE[$check_redirects][$_zone][$type][$module_name] = $zone;
            if (function_exists('persistent_cache_set')) {
                persistent_cache_set('MODULES_ZONES', $MODULES_ZONES_CACHE);
            }
            return $zone;
        }

        $a = zone_black_magic_filterer(get_file_base() . '/' . $zone . (($zone == '') ? '' : '/') . 'pages/' . $type . '/' . (($dir2 === null) ? '' : ($dir2 . '/')) . $module_name . '.' . $ftype);
        $b = zone_black_magic_filterer(get_file_base() . '/' . $zone . (($zone == '') ? '' : '/') . 'pages/' . $type . '_custom/' . (($dir2 === null) ? '' : ($dir2 . '/')) . $module_name . '.' . $ftype);
        if ((($likely_non_custom) && ((is_file($b)) || (is_file($a)))) || ((!$likely_non_custom) && ((is_file($a)) || (is_file($b))))) { // heavily optimised based on most likely path coming first
            if (($check_redirects) && (isset($REDIRECT_CACHE[$zone][strtolower($module_name)])) && ($REDIRECT_CACHE[$zone][strtolower($module_name)]['r_is_transparent'] === 0) && ($REDIRECT_CACHE[$zone][strtolower($module_name)]['r_to_page'] === $module_name)) {
                $zone = $REDIRECT_CACHE[$zone][strtolower($module_name)]['r_to_zone'];
            }
            $MODULES_ZONES_CACHE[$check_redirects][$_zone][$type][$module_name] = $zone;
            if (function_exists('persistent_cache_set')) {
                persistent_cache_set('MODULES_ZONES', $MODULES_ZONES_CACHE);
            }
            return $zone;
        }
    }
    $start = 0;
    $max = 50;
    $first_zones_flip = array_flip($first_zones);
    do {
        $zones = find_all_zones(false, false, false, $start, $max);
        foreach ($zones as $zone) {
            if (!array_key_exists($zone, $first_zones_flip)) {
                $a = zone_black_magic_filterer(get_file_base() . '/' . $zone . (($zone == '') ? '' : '/') . 'pages/' . $type . '/' . (($dir2 === null) ? '' : ($dir2 . '/')) . $module_name . '.' . $ftype);
                $b = zone_black_magic_filterer(get_file_base() . '/' . $zone . (($zone == '') ? '' : '/') . 'pages/' . $type . '_custom/' . (($dir2 === null) ? '' : ($dir2 . '/')) . $module_name . '.' . $ftype);
                if ((($likely_non_custom) && ((is_file($b)) || (is_file($a)))) || ((!$likely_non_custom) && ((is_file($a)) || (is_file($b))))) { // heavily optimised based on most likely path coming first
                    if (($check_redirects) && (isset($REDIRECT_CACHE[$zone][strtolower($module_name)])) && ($REDIRECT_CACHE[$zone][strtolower($module_name)]['r_is_transparent'] === 0) && ($REDIRECT_CACHE[$zone][strtolower($module_name)]['r_to_page'] === $module_name)) {
                        $zone = $REDIRECT_CACHE[$zone][strtolower($module_name)]['r_to_zone'];
                    }
                    $MODULES_ZONES_CACHE[$check_redirects][$_zone][$type][$module_name] = $zone;
                    if (function_exists('persistent_cache_set')) {
                        persistent_cache_set('MODULES_ZONES', $MODULES_ZONES_CACHE);
                    }
                    return $zone;
                }
            }
        }
        $start += 50;
    }
    while (count($zones) == $max);

    foreach ($zones as $zone) { // Okay, finally check for redirects
        if (($check_redirects) && (isset($REDIRECT_CACHE[$zone][strtolower($module_name)])) && ($REDIRECT_CACHE[$zone][strtolower($module_name)]['r_is_transparent'] === 1)) {
            $MODULES_ZONES_CACHE[$check_redirects][$_zone][$type][$module_name] = $zone;
            if (function_exists('persistent_cache_set')) {
                persistent_cache_set('MODULES_ZONES', $MODULES_ZONES_CACHE);
            }
            return $zone;
        }
    }

    if (!$error) {
        $MODULES_ZONES_CACHE[$check_redirects][$_zone][$type][$module_name] = null;
        return null;
    }
    warn_exit(do_lang_tempcode('MISSING_MODULE_REFERENCED', escape_html($module_name)));
    return null;
}

/**
 * Find the zone a Comcode page is in.
 *
 * @param  ID_TEXT $page_name The Comcode page name to find
 * @param  boolean $error Whether Composr should bomb out if the page was not found
 * @param  ?ID_TEXT $first_zone_to_check First zone to check (used for an optimisation) (null: current zone)
 * @return ?ID_TEXT The zone the Comcode page is in (null: missing)
 */
function get_comcode_zone($page_name, $error = true, $first_zone_to_check = null)
{
    $test = get_module_zone($page_name, 'comcode', user_lang(), 'txt', false, true, $first_zone_to_check);
    if ($test !== null) {
        return $test;
    }
    if (get_site_default_lang() != user_lang()) {
        $test = get_module_zone($page_name, 'comcode', get_site_default_lang(), 'txt', false, true, $first_zone_to_check);
        if ($test !== null) {
            return $test;
        }
    }
    if (fallback_lang() != get_site_default_lang()) {
        $test = get_module_zone($page_name, 'comcode', fallback_lang(), 'txt', false, true, $first_zone_to_check);
        if ($test !== null) {
            return $test;
        }
    }
    if ($error) {
        warn_exit(do_lang_tempcode('MISSING_MODULE_REFERENCED', escape_html($page_name)));
    }
    return null;
}

/**
 * Find the zone a page is in.
 *
 * @param  ID_TEXT $page_name The page name to find
 * @param  boolean $error Whether Composr should bomb out if the page was not found
 * @param  ?ID_TEXT $first_zone_to_check First zone to check (used for an optimisation) (null: current zone)
 * @param  ?ID_TEXT $type Page type (null: check all)
 * @return ?ID_TEXT The zone the page is in (null: missing)
 */
function get_page_zone($page_name, $error = true, $first_zone_to_check = null, $type = null)
{
    if (($type === null) || ($type == 'comcode')) {
        // Optimisation for pages known to default as Comcode pages
        if (in_array($page_name, array('privacy', 'sitemap', 'feedback', 'panel_top', 'panel_bottom', 'panel_left', 'panel_right', 'rules', 'keymap', 'start'/*TODO: change in v11*/))) {
            $test = get_comcode_zone($page_name, false);
            if ($test !== null) {
                return $test;
            }
        }
    }

    if (($type === null) || ($type == 'modules')) {
        $test = get_module_zone($page_name, 'modules', null, 'php', false, true, $first_zone_to_check);
        if ($test !== null) {
            return $test;
        }
    }
    if (($type === null) || ($type == 'comcode')) {
        $test = get_module_zone($page_name, 'comcode', get_site_default_lang(), 'txt', false, true, $first_zone_to_check);
        if ($test !== null) {
            return $test;
        }
        if (fallback_lang() != get_site_default_lang()) {
            $test = get_module_zone($page_name, 'comcode', fallback_lang(), 'txt', false, true, $first_zone_to_check);
            if ($test !== null) {
                return $test;
            }
        }
    }
    if (($type === null) || ($type == 'html')) {
        $test = get_module_zone($page_name, 'html', get_site_default_lang(), 'htm', false, true, $first_zone_to_check);
        if ($test !== null) {
            return $test;
        }
        if (fallback_lang() != get_site_default_lang()) {
            $test = get_module_zone($page_name, 'html', fallback_lang(), 'htm', false, true, $first_zone_to_check);
            if ($test !== null) {
                return $test;
            }
        }
    }
    if (($type === null) || ($type == 'minimodules')) {
        $test = get_module_zone($page_name, 'minimodules', null, 'php', false, true, $first_zone_to_check);
        if ($test !== null) {
            return $test;
        }
    }
    if ($error) {
        warn_exit(do_lang_tempcode('MISSING_MODULE_REFERENCED', escape_html($page_name)));
    }
    return null;
}

/**
 * Runs the specified mini-module.
 * The module result is returned.
 *
 * @param  PATH $string The relative path to the module file
 * @param  ?object $out Semi-filled output template (null: definitely not doing output streaming)
 * @return Tempcode The result of executing the module
 */
function load_minimodule_page($string, &$out = null)
{
    global $PAGE_STRING;
    if (is_null($PAGE_STRING)) {
        $PAGE_STRING = $string;
    }

    /*if (($GLOBALS['OUTPUT_STREAMING']) && ($out !== null))  Actually we cannot do this, as some minimodules don't return HTML and exit themselves (e.g. CSV downloads)
        $out->evaluate_echo(null, true);*/

    return _load_mini_code($string);
}

/**
 * Runs the specified mini-module/mini-block (actually, any simply-written PHP code).
 * The returned/output result is returned, in Tempcode form.
 *
 * @param  PATH $string The relative path to the code file
 * @param  ?array $map The block parameters (null: none)
 * @return Tempcode The result of executing the code
 *
 * @ignore
 */
function _load_mini_code($string, $map = null)
{
    require_code('developer_tools');
    destrictify();

    if (strpos($string, '_custom/') !== false) {
        _solemnly_enter();
    }

    ob_start();
    $test1 = require(get_file_base() . '/' . $string);
    $test2 = ob_get_contents();
    if ($GLOBALS['XSS_DETECT']) {
        ocp_mark_as_escaped($test2);
    }
    ob_end_clean();
    if ($test2 == '') {
        if (is_object($test1)) {
            if (strpos($string, '_custom/') !== false) {
                $_test1 = $test1->evaluate();
                _solemnly_leave($_test1);
                if (!has_solemnly_declared(I_UNDERSTAND_XSS)) {
                    $test1 = make_string_tempcode($_test1);
                }
            }

            $out = $test1;
        } else {
            $out = new Tempcode();
            if ((!is_bool($test1)) && (!is_integer($test1))) { // Not an automatic return code
                $_test1 = is_string($test1) ? $test1 : strval($test1);

                if (strpos($string, '_custom/') !== false) {
                    _solemnly_leave($_test1);
                }

                $out->attach($_test1);
            } else {
                if (strpos($string, '_custom/') !== false) {
                    _solemnly_leave();
                }
            }
        }
    } else {
        if (strpos($string, '_custom/') !== false) {
            _solemnly_leave($test2);
        }

        $out = new Tempcode();
        $out->attach($test2);
    }

    restrictify();

    return $out;
}

/**
 * Runs the specified module, but also update any stats for the module, and check to see if it needs upgrading or reinstalling.
 * The module result is returned.
 *
 * @param  PATH $string The relative path to the module file
 * @param  ID_TEXT $codename The page name to load
 * @param  ?object $out Semi-filled output template (null: definitely not doing output streaming)
 * @return Tempcode The result of executing the module
 */
function load_module_page($string, $codename, &$out = null)
{
    global $PAGE_STRING;
    if (is_null($PAGE_STRING)) {
        $PAGE_STRING = $string;
    }

    if ((strpos($string, '_custom/') !== false) && (!is_file(str_replace('_custom/', '/', $string)))) {
        _solemnly_enter();
    }

    require_code(filter_naughty($string));
    if (class_exists('Mx_' . filter_naughty_harsh($codename))) {
        $object = object_factory('Mx_' . filter_naughty_harsh($codename));
    } else {
        $object = object_factory('Module_' . filter_naughty_harsh($codename));
    }

    // Get info about what is installed and what is on disk
    if (get_value('assume_modules_correct') !== '1') {
        $rows = persistent_cache_get('MODULES');
        if ($rows === null) {
            $rows = list_to_map('module_the_name', $GLOBALS['SITE_DB']->query_select('modules', array('*'), is_null($GLOBALS['PERSISTENT_CACHE']) ? array('module_the_name' => $codename) : null));
            persistent_cache_set('MODULES', $rows);
        }
        if (array_key_exists($codename, $rows)) {
            $info = $object->info();
            $installed_version = $rows[$codename]['module_version'];
            $installed_hack_version = $rows[$codename]['module_hack_version'];
            $installed_hacked_by = $rows[$codename]['module_hacked_by'];
            if (is_null($installed_hacked_by)) {
                $installed_hacked_by = '';
            }
            $this_version = $info['version'];
            $this_hack_version = $info['hack_version'];
            $this_hacked_by = $info['hacked_by'];
            if (is_null($this_hacked_by)) {
                $this_hacked_by = '';
            }

            // See if we need to do an upgrade
            if (($installed_version < $this_version) && (array_key_exists('update_require_upgrade', $info))) {
                require_code('database_action');
                require_code('config2');
                require_code('menus2');
                $GLOBALS['SITE_DB']->query_update('modules', array('module_version' => $this_version, 'module_hack_version' => $this_hack_version, 'module_hacked_by' => $this_hacked_by), array('module_the_name' => $codename), '', 1); // Happens first so if there is an error it won't loop (if we updated install code manually there will be an error)
                $object->install($installed_version, $installed_hack_version, $installed_hacked_by);

                persistent_cache_delete('MODULES');
            } elseif (($installed_hack_version < $this_hack_version) && (array_key_exists('hack_require_upgrade', $info))) {
                require_code('database_action');
                require_code('config2');
                require_code('menus2');
                /*if (($installed_hacked_by!=$this_hacked_by) && (!is_null($installed_hacked_by)))
                    {
                            fatal_exit('Managed by different author');
                    } Probably better we leave the solution to this to modders rather than just block the potential for there even to be a solution   */

                $GLOBALS['SITE_DB']->query_update('modules', array('module_version' => $this_version, 'module_hack_version' => $this_hack_version, 'module_hacked_by' => $this_hacked_by), array('module_the_name' => $codename), '', 1);
                $object->install($installed_version, $installed_hack_version, $installed_hacked_by);

                persistent_cache_delete('MODULES');
            }
        } else {
            require_code('zones2');
            $zone = substr($string, 0, strpos($string, '/'));
            if ($zone == 'pages') {
                $zone = '';
            }
            reinstall_module($zone, $codename);
        }
    }

    if (($GLOBALS['OUTPUT_STREAMING']) && ($out !== null)) {
        $GLOBALS['TEMPCODE_CURRENT_PAGE_OUTPUTTING'] = $out;
    }

    if (method_exists($object, 'pre_run')) {
        $exceptional_output = $object->pre_run();
        if ($exceptional_output !== null) {
            if ((strpos($string, '_custom/') !== false) && (!is_file(str_replace('_custom/', '/', $string)))) {
                $_exceptional_output = $exceptional_output->evaluate();
                _solemnly_leave($_exceptional_output);
                if (!has_solemnly_declared(I_UNDERSTAND_XSS)) {
                    $exceptional_output = make_string_tempcode($_exceptional_output);
                }
            }

            return $exceptional_output;
        }

        if (($GLOBALS['OUTPUT_STREAMING']) && ($out !== null)) {
            /* Breaks output streaming
            if ((strpos($string, '_custom/') !== false) && (!is_file(str_replace('_custom/', '/', $string)))) {
                $_out = $out->evaluate();
                _solemnly_leave($_out);
                if (!has_solemnly_declared(I_UNDERSTAND_XSS)) {
                    $out = make_string_tempcode($_out);
                }
                _solemnly_enter();
            }
            */

            $out->evaluate_echo(null, true);
        }
    }

    $ret = $object->run();

    if ((strpos($string, '_custom/') !== false) && (!is_file(str_replace('_custom/', '/', $string)))) {
        $_ret = $ret->evaluate();
        _solemnly_leave($_ret);
        if (!has_solemnly_declared(I_UNDERSTAND_XSS)) {
            $ret = make_string_tempcode($_ret);
        }
    }

    return $ret;
}

/**
 * Find the installed zones, up to the first $max installed
 *
 * @param  boolean $search Whether to search the file system and return zones that might not be fully in the system (otherwise will just use the database)
 * @param  boolean $get_titles Whether to get titles for the zones as well. Only if !$search
 * @param  boolean $force_all Whether to insist on getting all zones without $start/$max parameters (there could be thousands in theory...)
 * @param  integer $start Start position to get results from (ignored if $force_all is on)
 * @param  integer $max Maximum zones to get
 * @return array A list of zone names / a list of quartets (name, title, default page, zone row)
 */
function find_all_zones($search = false, $get_titles = false, $force_all = false, $start = 0, $max = 50)
{
    $collapse_user_zones = (get_option('collapse_user_zones') == '1');

    if ($search) {
        $out = array('');

        $dh = opendir(get_file_base());
        while (($file = readdir($dh)) !== false) {
            if (($file != '.') && ($file != '..') && (is_dir($file)) && (is_readable(get_file_base() . '/' . $file)) && (is_file(get_file_base() . '/' . $file . '/index.php')) && (is_dir(get_file_base() . '/' . $file . '/pages/modules'))) {
                if (($collapse_user_zones) && ($file == 'site')) {
                    continue;
                }

                if ((get_forum_type() != 'cns') && ($file == 'forum')) {
                    continue;
                }

                $out[] = $file;

                if ((!$force_all) && (count($out) == $max)) {
                    break;
                }
            }
        }
        closedir($dh);

        return $out;
    }

    global $ALL_ZONES_CACHE, $ALL_ZONES_TITLED_CACHE, $ZONE_DEFAULT_PAGES_CACHE, $SITE_INFO;

    $using_default_params = (!$force_all) && ($start == 0) && (($max == 50) || (($max > 50) && ($ALL_ZONES_CACHE !== null) && (count($ALL_ZONES_CACHE) < 30)));
    if ($using_default_params) {
        if ($get_titles) {
            if ($ALL_ZONES_TITLED_CACHE === null) {
                $ALL_ZONES_TITLED_CACHE = function_exists('persistent_cache_get') ? persistent_cache_get('ALL_ZONES_TITLED') : null;
            }
            if (!is_array($ALL_ZONES_TITLED_CACHE)) {
                $ALL_ZONES_TITLED_CACHE = null; // Cache corruption?
            }
            if ($ALL_ZONES_TITLED_CACHE !== null) {
                return $ALL_ZONES_TITLED_CACHE;
            }
        } else {
            if ($ALL_ZONES_CACHE === null) {
                $ALL_ZONES_CACHE = function_exists('persistent_cache_get') ? persistent_cache_get('ALL_ZONES') : null;
            }
            if (!is_array($ALL_ZONES_CACHE)) {
                $ALL_ZONES_CACHE = null; // Cache corruption?
            }
            if ($ALL_ZONES_CACHE !== null) {
                return $ALL_ZONES_CACHE;
            }
        }
    }

    $rows = $GLOBALS['SITE_DB']->query_select('zones', array('*'), null, 'ORDER BY zone_name', $force_all ? null : $max, $start);
    if ((!$force_all) && (count($rows) == $max)) {
        $rows = $GLOBALS['SITE_DB']->query_select('zones', array('*'), null, 'ORDER BY zone_title', $max/*reasonable limit; zone_title is sequential for default zones*/);
    }
    $zones_titled = array();
    $zones = array();
    foreach ($rows as $zone) {
        if (($collapse_user_zones) && ($zone['zone_name'] == 'site')) {
            continue;
        }

        if ((get_forum_type() != 'cns') && ($zone['zone_name'] == 'forum')) {
            continue;
        }

        $zone['_zone_title'] = function_exists('get_translated_text') ? get_translated_text($zone['zone_title']) : $zone['zone_name'];

        if (((isset($SITE_INFO['no_disk_sanity_checks'])) && ($SITE_INFO['no_disk_sanity_checks'] == '1')) || (is_file(get_file_base() . '/' . $zone['zone_name'] . (($zone['zone_name'] == '') ? '' : '/') . 'index.php'))) {
            $zones[] = $zone['zone_name'];
            $zones_titled[$zone['zone_name']] = array($zone['zone_name'], $zone['_zone_title'], $zone['zone_default_page'], $zone);
        }

        $ZONE_DEFAULT_PAGES_CACHE[$zone['zone_name']] = $zone['zone_default_page'];
    }

    if ($using_default_params) {
        $ALL_ZONES_TITLED_CACHE = $zones_titled;
        if (function_exists('persistent_cache_set')) {
            persistent_cache_set('ALL_ZONES_TITLED', $ALL_ZONES_TITLED_CACHE);
        }
        $ALL_ZONES_CACHE = $zones;
        if (function_exists('persistent_cache_set')) {
            persistent_cache_set('ALL_ZONES', $ALL_ZONES_CACHE);
        }
    }

    return $get_titles ? $zones_titled : $zones;
}

/**
 * Look up and remember what modules are installed.
 */
function cache_module_installed_status()
{
    global $MODULE_INSTALLED_CACHE;
    $rows = $GLOBALS['SITE_DB']->query_select('modules', array('module_the_name'));
    foreach ($rows as $row) {
        $MODULE_INSTALLED_CACHE[$row['module_the_name']] = true;
    }
}

/**
 * Check to see if a module is installed.
 *
 * @param  ID_TEXT $module The module name
 * @return boolean Whether it is
 */
function module_installed($module)
{
    global $MODULE_INSTALLED_CACHE;
    if (array_key_exists($module, $MODULE_INSTALLED_CACHE)) {
        return $MODULE_INSTALLED_CACHE[$module];
    }
    $test = $GLOBALS['SITE_DB']->query_select_value_if_there('modules', 'module_the_name', array('module_the_name' => $module));
    $answer = !is_null($test);
    $MODULE_INSTALLED_CACHE[$module] = $answer;
    return $answer;
}

/**
 * Get the path to a module known to be in a certain zone.
 *
 * @param  ID_TEXT $zone The zone name
 * @param  ID_TEXT $module The module name
 * @return PATH The module path
 *
 * @ignore
 */
function _get_module_path($zone, $module)
{
    $module_path = zone_black_magic_filterer(($zone == '') ? ('pages/modules_custom/' . filter_naughty_harsh($module) . '.php') : (filter_naughty($zone) . '/pages/modules_custom/' . filter_naughty_harsh($module) . '.php'), true);
    if ((in_safe_mode()) || (!is_file(get_file_base() . '/' . $module_path))) {
        $module_path = zone_black_magic_filterer(($zone == '') ? ('pages/modules/' . filter_naughty_harsh($module) . '.php') : (filter_naughty($zone) . '/pages/modules/' . filter_naughty_harsh($module) . '.php'), true);
    }
    return $module_path;
}

/**
 * Get an array of all the hook implementations for a hook class.
 *
 * @param  ID_TEXT $type The type of hook
 * @set    blocks endpoints modules systems
 * @param  ID_TEXT $entry The hook class to find hook implementations for (e.g. the name of a module)
 * @return array A map of hook implementation name to [sources|sources_custom]
 */
function find_all_hooks($type, $entry)
{
    global $HOOKS_CACHE;
    if (isset($HOOKS_CACHE[$type . '/' . $entry])) {
        return $HOOKS_CACHE[$type . '/' . $entry];
    }

    $out = array();

    if (strpos($type, '..') !== false) {
        $type = filter_naughty($type);
    }
    if (strpos($entry, '..') !== false) {
        $entry = filter_naughty($entry);
    }
    $dir = get_file_base() . '/sources/hooks/' . $type . '/' . $entry;
    $dh = @scandir($dir);
    if ($dh !== false) {
        foreach ($dh as $file) {
            $basename = basename($file, '.php');
            if (($file[0] != '.') && ($file === $basename . '.php')/* && (preg_match('#^[\w\-]*$#', $basename) != 0) Let's trust - performance*/) {
                $out[$basename] = 'sources';
            }
        }
    }

    if ((!isset($GLOBALS['DOING_USERS_INIT'])) && ((!in_safe_mode()) || ($GLOBALS['RELATIVE_PATH'] === '_tests') && ($entry === 'addon_registry'))) { // The !isset is because of if the user init causes a DB query to load sessions which loads DB hooks which checks for safe mode which leads to a permissions check for safe mode and thus a failed user check (as sessions not loaded yet)
        $dir = get_file_base() . '/sources_custom/hooks/' . $type . '/' . $entry;
        $dh = @scandir($dir);
        if ($dh !== false) {
            foreach ($dh as $file) {
                $basename = basename($file, '.php');
                if (($file[0] != '.') && ($file === $basename . '.php')/* && (preg_match('#^[\w\-]*$#', $basename) != 0) Let's trust - performance*/) {
                    $out[$basename] = 'sources_custom';
                }
            }
        }
    }

    // Optimisation, so that hooks with same name as our page get loaded first
    $page = get_param_string('page', '', true); // Not get_page_name for bootstrap order reasons
    if (array_key_exists($page, $out)) {
        $_out = array($page => $out[$page]);
        unset($out[$page]);
        $out = array_merge($_out, $out);
    }

    if (!isset($GLOBALS['DOING_USERS_INIT'])) {
        $HOOKS_CACHE[$type . '/' . $entry] = $out;
    }

    if (function_exists('persistent_cache_set')) {
        persistent_cache_set('HOOKS', $HOOKS_CACHE);
    }

    return $out;
}

/**
 * Find the default caching setting for a block.
 *
 * @param  ID_TEXT $codename The block name
 * @return ID_TEXT The default caching setting
 */
function block_cache_default($codename)
{
    if (cron_installed(true)) {
        if ($codename === 'side_rss' || $codename === 'main_rss') { // Special cases to stop external dependencies causing slowdowns
            return '2';
        }
    }
    return '1'; // NB: If the block doesn't support caching then nothing will be cached even if it is set to 1, UNLESS quick caching is also requested
}

/**
 * Get a unique ID representing a block call.
 *
 * @param  array $map The block parameter map
 * @return ID_TEXT The block ID
 */
function get_block_id($map)
{
    if (isset($map['block_id'])) {
        return $map['block_id'];
    }
    ksort($map);
    unset($map['raw']);
    unset($map['cache']);
    unset($map['start']);
    unset($map['max']);
    return substr(md5(serialize($map)), 0, 6);
}

/**
 * Get the processed Tempcode for the specified block. Please note that you pass multiple parameters in as an array, but single parameters go in as a string or other flat variable.
 *
 * @param  ID_TEXT $codename The block name
 * @param  ?array $map The block parameter map (null: no parameters)
 * @param  ?integer $ttl The TTL to use in minutes (null: block default)
 * @return Tempcode The generated Tempcode
 */
function do_block($codename, $map = null, $ttl = null)
{
    global $LANGS_REQUESTED, $REQUIRED_ALL_LANG, $JAVASCRIPTS, $CSSS, $DO_NOT_CACHE_THIS, $SMART_CACHE;

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

    $map['block'] = $codename;

    if (!isset($map['cache'])) {
        $map['cache'] = block_cache_default($codename);
    }

    if (!$GLOBALS['OUTPUT_STREAMING']) {
        push_output_state(false, true);
    }

    $DO_NOT_CACHE_THIS = ($map['cache'] === '0');

    $object = null;
    $new_security_scope = null;
    if (has_caching_for('block')) {
        // See if the block may be cached (else cannot, or is yet unknown)
        if ($map['cache'] === '0') {
            $row = null;
        } else { // We may allow it to be cached but not store the cache signature, as it is too complex
            $row = get_block_info_row($codename, $map, $object, $new_security_scope);
        }
        if ($row !== null) {
            $cache_identifier = do_block_get_cache_identifier($row['cache_on'], $map);
            $special_cache_flags = array_key_exists('special_cache_flags', $row) ? $row['special_cache_flags'] : CACHE_AGAINST_DEFAULT;

            // See if it actually is cached
            if ($cache_identifier !== null) {
                if ($ttl === null) {
                    $ttl = $row['cache_ttl'];
                }
                $cache = get_cache_entry($codename, $cache_identifier, $special_cache_flags, $ttl, true, $map['cache'] === '2', $map);
                if ($cache === null) {
                    $nql_backup = $GLOBALS['NO_QUERY_LIMIT'];
                    $GLOBALS['NO_QUERY_LIMIT'] = true;

                    if ($object === null) {
                        list($object, $new_security_scope) = do_block_hunt_file($codename, $map);
                    }
                    if (!is_object($object)) {
                        // This probably happened as we uninstalled a block, and now we're getting a "missing block" message back.

                        // Removed outdated cache-on information
                        $GLOBALS['SITE_DB']->query_delete('cache_on', array('cached_for' => $codename), '', 1);
                        persistent_cache_delete('BLOCK_CACHE_ON_CACHE');

                        $out = new Tempcode();
                        $out->attach(@strval($object));
                        if (!$GLOBALS['OUTPUT_STREAMING']) {
                            restore_output_state(false, true);
                        }

                        return $out;
                    }
                    $backup_langs_requested = $LANGS_REQUESTED;
                    $backup_required_all_lang = $REQUIRED_ALL_LANG;
                    $LANGS_REQUESTED = array();
                    $REQUIRED_ALL_LANG = array();
                    if ((isset($map['quick_cache'])) && ($map['quick_cache'] === '1')) { // because we know we will not do this often we can allow this to work as a vector for doing highly complex activity
                        global $MEMORY_OVER_SPEED;
                        $MEMORY_OVER_SPEED = true; // Let this eat up some CPU in order to let it save RAM,
                        disable_php_memory_limit();
                        if (php_function_allowed('set_time_limit')) {
                            @set_time_limit(200);
                        }
                    }
                    if ($new_security_scope) {
                        _solemnly_enter();
                    }
                    if (isset($SMART_CACHE)) {
                        $SMART_CACHE->paused = true;
                    }
                    $cache = $object->run($map);
                    if ($new_security_scope) {
                        $_cache = $cache->evaluate();
                        _solemnly_leave($_cache);
                        if (!has_solemnly_declared(I_UNDERSTAND_XSS)) {
                            $cache = make_string_tempcode($_cache);
                        }
                    }
                    $cache->evaluate(); // To force lang files to load, etc
                    if (isset($SMART_CACHE)) {
                        $SMART_CACHE->paused = false;
                    }
                    if (!$DO_NOT_CACHE_THIS) {
                        require_code('caches2');
                        if ((isset($map['quick_cache'])) && ($map['quick_cache'] === '1')/* && (has_cookies())*/) {
                            $cache = apply_quick_caching($cache);
                            $LANGS_REQUESTED = array();
                            $REQUIRED_ALL_LANG = array();
                        }
                        require_code('temporal');
                        $staff_status = (($special_cache_flags & CACHE_AGAINST_STAFF_STATUS) !== 0) ? ($GLOBALS['FORUM_DRIVER']->is_staff(get_member()) ? 1 : 0) : null;
                        $member = (($special_cache_flags & CACHE_AGAINST_MEMBER) !== 0) ? get_member() : null;
                        $groups = (($special_cache_flags & CACHE_AGAINST_PERMISSIVE_GROUPS) !== 0) ? permissive_groups_cache_signature() : '';
                        $is_bot = (($special_cache_flags & CACHE_AGAINST_BOT_STATUS) !== 0) ? (is_null(get_bot_type()) ? 0 : 1) : null;
                        $timezone = (($special_cache_flags & CACHE_AGAINST_TIMEZONE) !== 0) ? get_users_timezone(get_member()) : '';
                        put_into_cache($codename, $ttl, $cache_identifier, $staff_status, $member, $groups, $is_bot, $timezone, $cache, array_keys($LANGS_REQUESTED), array_keys($JAVASCRIPTS), array_keys($CSSS), true);
                    } elseif (($ttl !== -1) && ($cache->is_empty())) { // Try again with no TTL, if we currently failed but did impose a TTL
                        $LANGS_REQUESTED += $backup_langs_requested;
                        $REQUIRED_ALL_LANG = $backup_required_all_lang;
                        if (!$GLOBALS['OUTPUT_STREAMING']) {
                            restore_output_state(false, true);
                        }
                        return do_block($codename, $map, -1);
                    }
                    $LANGS_REQUESTED += $backup_langs_requested;
                    $REQUIRED_ALL_LANG += $backup_required_all_lang;

                    $GLOBALS['NO_QUERY_LIMIT'] = $nql_backup;
                }
                if (!$GLOBALS['OUTPUT_STREAMING']) {
                    restore_output_state(false, true);
                }
                return $cache;
            }
        }
    }

    // NB: If we've got this far cache="2" is ignored. But later on (for normal expiries, different contexts, etc) cache_on will be known so not an issue.

    // We will need to load the actual file
    if ($object === null) {
        list($object, $new_security_scope) = do_block_hunt_file($codename, $map);
    }
    if (is_object($object)) {
        $nql_backup = $GLOBALS['NO_QUERY_LIMIT'];
        $GLOBALS['NO_QUERY_LIMIT'] = true;
        $backup_langs_requested = $LANGS_REQUESTED;
        $backup_required_all_lang = $REQUIRED_ALL_LANG;
        $LANGS_REQUESTED = array();
        $REQUIRED_ALL_LANG = array();
        if ($new_security_scope) {
            _solemnly_enter();
        }
        $cache = $object->run($map);
        if ($new_security_scope) {
            $_cache = $cache->evaluate();
            _solemnly_leave($_cache);
            if (!has_solemnly_declared(I_UNDERSTAND_XSS)) {
                $cache = make_string_tempcode($_cache);
            }
        }

        $GLOBALS['NO_QUERY_LIMIT'] = $nql_backup;
    } else {
        $out = new Tempcode();
        $out->attach(@strval($object));
        if (!$GLOBALS['OUTPUT_STREAMING']) {
            restore_output_state(false, true);
        }
        return $out;
    }

    // May it be added to cache_on?
    if ((!$DO_NOT_CACHE_THIS) && (method_exists($object, 'caching_environment')) && (has_caching_for('block'))) {
        $info = $object->caching_environment($map);
        if ($info !== null) {
            $cache_identifier = do_block_get_cache_identifier($info['cache_on'], $map);
            if ($cache_identifier !== null) {
                $special_cache_flags = array_key_exists('special_cache_flags', $info) ? $info['special_cache_flags'] : CACHE_AGAINST_DEFAULT;

                require_code('caches2');
                require_code('temporal');
                $staff_status = (($special_cache_flags & CACHE_AGAINST_STAFF_STATUS) !== 0) ? ($GLOBALS['FORUM_DRIVER']->is_staff(get_member()) ? 1 : 0) : null;
                $member = (($special_cache_flags & CACHE_AGAINST_MEMBER) !== 0) ? get_member() : null;
                $groups = (($special_cache_flags & CACHE_AGAINST_PERMISSIVE_GROUPS) !== 0) ? permissive_groups_cache_signature() : '';
                $is_bot = (($special_cache_flags & CACHE_AGAINST_BOT_STATUS) !== 0) ? (is_null(get_bot_type()) ? 0 : 1) : null;
                $timezone = (($special_cache_flags & CACHE_AGAINST_TIMEZONE) !== 0) ? get_users_timezone(get_member()) : '';
                put_into_cache($codename, $info['ttl'], $cache_identifier, $staff_status, $member, $groups, $is_bot, $timezone, $cache, array_keys($LANGS_REQUESTED), $GLOBALS['OUTPUT_STREAMING'] ? array() : array_keys($JAVASCRIPTS), $GLOBALS['OUTPUT_STREAMING'] ? array() : array_keys($CSSS), true);
            }
        }
    }
    $LANGS_REQUESTED += $backup_langs_requested;
    $REQUIRED_ALL_LANG += $backup_required_all_lang;

    if (!$GLOBALS['OUTPUT_STREAMING']) {
        restore_output_state(false, true);
    }
    return $cache;
}

/**
 * Simplify some Tempcode (losing dynamicness), for the quick cache option.
 * Includes remove of  contextual URL parameters for neutrality within quick cache.
 *
 * @param  Tempcode $_cache Input Tempcode
 * @return Tempcode Output Tempcode
 */
function apply_quick_caching($_cache)
{
    $cache = $_cache->evaluate();

    $new_tempcode = new Tempcode();
    $prior_offset = 0;

    $has_keep_parameters = has_keep_parameters();

    if ($has_keep_parameters) {
        $keep_first_has_escaping = symbol_tempcode('KEEP', array('0'), array(ENTITY_ESCAPED));
        $keep_non_first_has_escaping = symbol_tempcode('KEEP', array('1'), array(ENTITY_ESCAPED));

        $keep_first_has_no_escaping = symbol_tempcode('KEEP', array('0'), array(NULL_ESCAPED));
        $keep_non_first_has_no_escaping = symbol_tempcode('KEEP', array('1'), array(NULL_ESCAPED));
    }

    $matches = array();
    $num_matches = preg_match_all('#(((\?)|(&(amp;)?))keep\_[^="\']*=[^\#&"\']*)+#', $cache, $matches, PREG_OFFSET_CAPTURE); // We assume that the keep_* parameters always come last, which holds true in Composr
    for ($i = 0; $i < $num_matches; $i++) {
        $new_offset = $matches[0][$i][1];

        $portion = substr($cache, $prior_offset, $new_offset - $prior_offset);
        if ($GLOBALS['XSS_DETECT'] && ocp_is_escaped($cache)) {
            ocp_mark_as_escaped($portion);
        }

        $new_tempcode->attach($portion);

        $has_escaping = (preg_match('#&\w+;#', $matches[0][$i][0]) !== 0);

        if ($has_keep_parameters) { // NB: has_keep_parameters() is in cache signature of 'menu' block, so this is safe for menus, keep_* will still work with this quick caching when both on and off
            if ($matches[0][$i][0][0] === '&') { // Other parameters are non-keep, but as they come first we can just strip the keep_* ones off
                $keep = $has_escaping ? $keep_first_has_escaping : $keep_first_has_no_escaping;
            } else { // All parameters are keep_*
                $keep = $has_escaping ? $keep_non_first_has_escaping : $keep_non_first_has_no_escaping;
            }
            $new_tempcode->attach($keep);
        }

        $prior_offset = $new_offset + strlen($matches[0][$i][0]);
    }

    $portion = substr($cache, $prior_offset);
    if ($portion !== '') {
        if ($GLOBALS['XSS_DETECT'] && ocp_is_escaped($cache)) {
            ocp_mark_as_escaped($portion);
        }

        $new_tempcode->attach($portion);
    }

    return $new_tempcode;
}

/**
 * Get Comcode used for a block to submit back to itself via AJAX.
 *
 * @param  array $map The parameters
 * @return string Parameters for a Comcode block tag
 */
function get_block_ajax_submit_map($map)
{
    $map_comcode = '';
    foreach ($map as $key => $val) {
        if ($key != 'defer') {
            $map_comcode .= ' ' . $key . '="' . addslashes($val) . '"';
        }
    }
    return $map_comcode;
}

/**
 * Convert a parameter set from a an array (for PHP code) to a string (for templates).
 *
 * @param  array $map The parameters / acceptable parameter pattern
 * @return string The parameters / acceptable parameter pattern, as template safe parameter
 */
function block_params_arr_to_str($map)
{
    ksort($map);

    $_map = '';

    foreach ($map as $key => $val) {
        if ($_map != '') {
            $_map .= ',';
        }
        if ((is_integer($key)) && (strpos($val, '=') !== false)) { // {$BLOCK} style, i.e. a list not a map
            $_map .= str_replace(',', '\,', $val);
        } else {
            $_map .= $key . '=' . str_replace(',', '\,', $val);
        }
    }

    return $_map;
}

/**
 * Convert a parameter set from a string (for templates) to an array (for PHP code).
 *
 * @param  string $_map The parameters / acceptable parameter pattern, as template safe parameter
 * @param  boolean $block_symbol_style Whether to leave in block symbol style (i.e. like {$BLOCK} would take, a list not a map)
 * @return array The parameters / acceptable parameter pattern
 */
function block_params_str_to_arr($_map, $block_symbol_style = false)
{
    $map = array();
    $param = preg_split('#((?<!\\\\)|(?<=\\\\\\\\)|(?<=^)),#', $_map);
    foreach ($param as $x) {
        if ($block_symbol_style) {
            $map[] = $x;
        } else {
            $result = explode('=', $x, 2);
            if (isset($result[1])) {
                list($a, $b) = $result;
                $map[$a] = str_replace('\,', ',', $b);
            }
        }
    }

    ksort($map);

    return $map;
}

/**
 * Get the block object for a given block codename.
 *
 * @param  ID_TEXT $codename The block name
 * @param  ?array $map The block parameter map (null: no parameters)
 * @return array A pair: Either the block object, or the string output of a miniblock ; and whether we entered a new security scope
 */
function do_block_hunt_file($codename, $map = null)
{
    global $BLOCKS_AT_CACHE;

    $codename = filter_naughty_harsh($codename);

    $file_base = get_file_base();

    $new_security_scope = false;

    global $REQUIRED_CODE;
    if ((!in_safe_mode()) && (((isset($BLOCKS_AT_CACHE[$codename])) && ($BLOCKS_AT_CACHE[$codename] === 'sources_custom/blocks')) || ((!isset($BLOCKS_AT_CACHE[$codename])) && (is_file($file_base . '/sources_custom/blocks/' . $codename . '.php'))))) {
        if (!isset($REQUIRED_CODE['blocks/' . $codename])) {
            require_once($file_base . '/sources_custom/blocks/' . $codename . '.php');
        }
        $REQUIRED_CODE['blocks/' . $codename] = true;

        if (!isset($BLOCKS_AT_CACHE[$codename])) {
            $BLOCKS_AT_CACHE[$codename] = 'sources_custom/blocks';
            if (function_exists('persistent_cache_set')) {
                persistent_cache_set('BLOCKS_AT', $BLOCKS_AT_CACHE);
            }
        }

        if (!is_file($file_base . '/sources/blocks/' . $codename . '.php')) {
            $new_security_scope = true;
        }
    } elseif (((isset($BLOCKS_AT_CACHE[$codename])) && ($BLOCKS_AT_CACHE[$codename] === 'sources/blocks')) || ((!isset($BLOCKS_AT_CACHE[$codename])) && (is_file($file_base . '/sources/blocks/' . $codename . '.php')))) {
        if (!isset($REQUIRED_CODE['blocks/' . $codename])) {
            require_once($file_base . '/sources/blocks/' . $codename . '.php');
        }
        $REQUIRED_CODE['blocks/' . $codename] = true;

        if (!isset($BLOCKS_AT_CACHE[$codename])) {
            $BLOCKS_AT_CACHE[$codename] = 'sources/blocks';
            if (function_exists('persistent_cache_set')) {
                persistent_cache_set('BLOCKS_AT', $BLOCKS_AT_CACHE);
            }
        }
    } else {
        if ((!in_safe_mode()) && (((isset($BLOCKS_AT_CACHE[$codename])) && ($BLOCKS_AT_CACHE[$codename] === 'sources_custom/miniblocks')) || ((!isset($BLOCKS_AT_CACHE[$codename])) && (is_file($file_base . '/sources_custom/miniblocks/' . $codename . '.php'))))) {
            $object = static_evaluate_tempcode(_load_mini_code('sources_custom/miniblocks/' . $codename . '.php', $map));

            if (!isset($BLOCKS_AT_CACHE[$codename])) {
                $BLOCKS_AT_CACHE[$codename] = 'sources_custom/miniblocks';
                if (function_exists('persistent_cache_set')) {
                    persistent_cache_set('BLOCKS_AT', $BLOCKS_AT_CACHE);
                }
            }

            $new_security_scope = true;
        } elseif (((isset($BLOCKS_AT_CACHE[$codename])) && ($BLOCKS_AT_CACHE[$codename] === 'sources/miniblocks')) || ((!isset($BLOCKS_AT_CACHE[$codename])) && (is_file($file_base . '/sources/miniblocks/' . $codename . '.php')))) {
            $object = static_evaluate_tempcode(_load_mini_code('sources/miniblocks/' . $codename . '.php', $map));

            if (!isset($BLOCKS_AT_CACHE[$codename])) {
                $BLOCKS_AT_CACHE[$codename] = 'sources/miniblocks';
                if (function_exists('persistent_cache_set')) {
                    persistent_cache_set('BLOCKS_AT', $BLOCKS_AT_CACHE);
                }
            }
        } elseif (($map === null) || (!isset($map['failsafe'])) || ($map['failsafe'] !== '1')) {
            $temp = do_template('WARNING_BOX', array('_GUID' => '09f1bd6e117693a85fb69bfb52ea1799', 'WARNING' => do_lang_tempcode('MISSING_BLOCK_FILE', escape_html($codename))));
            $object = $temp->evaluate();
        } else {
            $object = '';
        }
        return array($object, $new_security_scope);
    }

    $_object = object_factory('Block_' . $codename);
    return array($_object, $new_security_scope);
}

/**
 * Get standardised info about a block.
 *
 * @param  ID_TEXT $codename The block name
 * @param  array $map The block parameter map
 * @param  ?mixed $object Object/string of the block (null: not looked up)
 * @param  ?boolean $new_security_scope Whether the block is in a new security scope (null: not looked up)
 * @return ?array The block info (null: cannot cache for some reason)
 */
function get_block_info_row($codename, $map, &$object = null, &$new_security_scope = null)
{
    static $cache = array();
    $sz = serialize(array($codename, $map));
    if (isset($cache[$sz])) {
        return $cache[$sz];
    }

    $row = find_cache_on($codename);
    if ($row === null) {
        list($object, $new_security_scope) = do_block_hunt_file($codename, $map);
        if ((is_object($object)) && (method_exists($object, 'caching_environment'))) {
            $info = $object->caching_environment($map);
            if ($info !== null) {
                $special_cache_flags = array_key_exists('special_cache_flags', $info) ? $info['special_cache_flags'] : CACHE_AGAINST_DEFAULT;
                $row = array(
                    'cached_for' => $codename,
                    'cache_on' => $info['cache_on'],
                    'special_cache_flags' => $special_cache_flags,
                    'cache_ttl' => $info['ttl'],
                );

                if (!is_array($info['cache_on'])) {
                    $GLOBALS['SITE_DB']->query_insert('cache_on', $row, false, true); // Allow errors in case of race conditions
                    global $BLOCK_CACHE_ON_CACHE;
                    $BLOCK_CACHE_ON_CACHE[$codename] = $row;
                    persistent_cache_set('BLOCK_CACHE_ON_CACHE', $BLOCK_CACHE_ON_CACHE);
                }
            }
        }
    }
    if (($row === null) && (isset($map['quick_cache'])) && ($map['quick_cache'] === '1')) {
        $row = array('cached_for' => $codename, 'cache_on' => 'array($map,$GLOBALS[\'FORUM_DRIVER\']->get_members_groups(get_member()))', 'cache_ttl' => 60);
    }

    $cache[$sz] = $row;
    return $row;
}

/**
 * Takes a string which is a PHP expression over $map (parameter map), and returns a derived identifier.
 * We see if we have something cached by looking for a matching identifier.
 *
 * @param  mixed $cache_on PHP expression over $map (the parameter map of the block) OR a call_user_func specifier that will return a result (which will be used if caching is really very important, even for Hip Hop PHP)
 * @param  ?array $map The block parameter map (null: no parameters)
 * @return ?LONG_TEXT The derived cache identifier (null: the identifier is CURRENTLY null meaning cannot be cached)
 */
function do_block_get_cache_identifier($cache_on, $map)
{
    static $cache = array();
    $sz = serialize(array($cache_on, $map));
    if (isset($cache[$sz])) {
        return $cache[$sz];
    }

    $_cache_identifier = array();
    if (is_array($cache_on)) {
        $_cache_identifier = call_user_func($cache_on[0], $map);
    } else {
        if ($cache_on != '') {
            $block_id = mixed();
            if (strpos($cache_on, 'block_id') !== false) {
                $block_id = get_block_id($map);
            }

            $_cache_on = eval('return ' . $cache_on . ';'); // NB: This uses $map, as $map is referenced inside $cache_on
            if ($_cache_on === null) {
                return null;
            }
            foreach ($_cache_on as $on) {
                $_cache_identifier[] = $on;
            }
            require_code('urls');
        }
    }

    global $SITE_INFO;
    $_cache_identifier[] = (tacit_https()) || (!empty($SITE_INFO['base_url'])) && (substr($SITE_INFO['base_url'], 0, 8) == 'https://');

    if (!empty($map['raw'])) {
        $_cache_identifier[] = $map['raw'];
    }

    $cache_identifier = serialize($_cache_identifier);

    $cache[$sz] = $cache_identifier;

    return $cache_identifier;
}

/**
 * Gets the path to a block code file for a block code name
 *
 * @param  ID_TEXT $block The name of the block
 * @return PATH The path to the block
 *
 * @ignore
 */
function _get_block_path($block)
{
    $block_path = get_file_base() . '/sources_custom/blocks/' . filter_naughty($block) . '.php';
    if ((in_safe_mode()) || (!is_file($block_path))) {
        $block_path = get_file_base() . '/sources/blocks/' . filter_naughty($block) . '.php';
        if (!is_file($block_path)) {
            $block_path = get_file_base() . '/sources_custom/miniblocks/' . filter_naughty($block) . '.php';
        }
    }
    return $block_path;
}

/**
 * Check to see if a block is installed.
 *
 * @param  ID_TEXT $block The module name
 * @return boolean Whether it is
 */
function block_installed($block)
{
    $test = $GLOBALS['SITE_DB']->query_select_value_if_there('blocks', 'block_name', array('block_name' => $block));
    return !is_null($test);
}

/**
 * Get an array of all the pages everywhere in the zone, limited by the selection algorithm (for small sites everything will be returned, for larger ones it depends on the show method).
 *
 * @param  ID_TEXT $zone The zone name
 * @param  boolean $keep_ext_on Whether to leave file extensions on the page name
 * @param  boolean $consider_redirects Whether to take transparent redirects into account
 * @param  integer $show_method Selection algorithm constant
 * @set 0 1 2
 * @param  ?ID_TEXT $page_type Page type to show (null: all)
 * @set    modules comcode html
 * @return array A map of page name to type (modules_custom, etc)
 */
function find_all_pages_wrap($zone, $keep_ext_on = false, $consider_redirects = false, $show_method = 0, $page_type = null)
{
    require_code('zones2');
    return _find_all_pages_wrap($zone, $keep_ext_on, $consider_redirects, $show_method, $page_type);
}

/**
 * Get an array of all the pages of the specified type (module, etc) and extension (for small sites everything will be returned, for larger ones it depends on the show method).
 *
 * @param  ID_TEXT $zone The zone name
 * @param  ID_TEXT $type The page type
 * @set    modules comcode html
 * @param  string $ext The file extension to limit us to (without a dot)
 * @param  boolean $keep_ext_on Whether to leave file extensions on the page name
 * @param  ?TIME $cutoff_time Only show pages newer than (null: no restriction)
 * @param  integer $show_method Selection algorithm constant
 * @set 0 1 2
 * @return array A map of page name to type (modules_custom, etc)
 */
function find_all_pages($zone, $type, $ext = 'php', $keep_ext_on = false, $cutoff_time = null, $show_method = 0)
{
    require_code('zones2');
    return _find_all_pages($zone, $type, $ext, $keep_ext_on, $cutoff_time, $show_method);
}

/**
 * Get an array of all the modules.
 *
 * @param  ID_TEXT $zone The zone name
 * @return array A map of page name to type (modules_custom, etc)
 */
function find_all_modules($zone)
{
    require_code('zones2');
    return _find_all_modules($zone);
}

/**
 * Extract code to execute the requested functions with the requested parameters from the module at the given path.
 * We used to actually load up the module, but it ate all our RAM when we did!
 *
 * @param  PATH $path The path to the module (or any PHP file with a class)
 * @param  array $functions Array of functions to be executing
 * @param  ?array $params A list of parameters to pass to our functions (null: none)
 * @param  boolean $prefer_direct_code_call Whether to do this "properly" (via proper OOP), which will consume more memory
 * @param  ?string $class_name Class name to use (null: autodetect, which is a little slower)
 * @return array A list of pieces of code to do the equivalent of executing the requested functions with the requested parameters
 */
function extract_module_functions($path, $functions, $params = null, $prefer_direct_code_call = false, $class_name = null)
{
    if ($params === null) {
        $params = array();
    }

    global $SITE_INFO;
    $prefer_direct_code_call = $prefer_direct_code_call || ((isset($SITE_INFO['prefer_direct_code_call'])) && ($SITE_INFO['prefer_direct_code_call'] === '1'));
    if ((HHVM) || ($prefer_direct_code_call)) {
        global $CLASS_CACHE;
        if (isset($CLASS_CACHE[$path])) {
            $new_classes = $CLASS_CACHE[$path];
        } else {
            if (!HHVM && $class_name === null) {
                $classes_before = get_declared_classes();
            }
            $require_path = preg_replace('#^' . preg_quote(get_file_base()) . '/#', '', preg_replace('#^' . preg_quote(get_file_base()) . '/((sources)|(sources\_custom))/(.*)\.php#', '${4}', $path));
            require_code($require_path);
            if (!HHVM && $class_name === null) {
                $classes_after = get_declared_classes();
            }
            if ($class_name === null) {
                $new_classes = HHVM ? array() : array_values(array_diff($classes_after, $classes_before));
                if (count($new_classes) === 0) { // Ah, HHVM's AllVolatile is probably not enabled *OR* maybe this module already had require_code run for it
                    $matches = array();
                    if ((running_script('install')) && (file_exists(preg_replace('#(sources|modules|minimodules)_custom#', '${1}', $path)))) {
                        $path = preg_replace('#(sources|modules|minimodules)_custom#', '${1}', $path);
                    }
                    if (preg_match('#^\s*class (\w+)#m', file_get_contents($path), $matches) !== 0) {
                        $new_classes = array($matches[1]);
                    }
                }
            } else {
                $new_classes = array($class_name);
            }
            $CLASS_CACHE[$path] = $new_classes;
        }
        if ((isset($new_classes[0])) && ($new_classes[0] === 'Standard_crud_module')) {
            array_shift($new_classes); // This is not the class we want
        }
        if ((isset($new_classes[0])) && ($new_classes[0] === 'non_overridden__Standard_crud_module')) {
            array_shift($new_classes); // This is not the class we want
        }
        if (isset($new_classes[0])) {
            $c = $new_classes[0];
            $new_ob = new $c;
        } else {
            $new_ob = null;
        }
        $ret = array();
        foreach ($functions as $function) {
            if (method_exists($new_ob, $function)) {
                $ret[] = array(array(&$new_ob, $function), $params);
            } else {
                $ret[] = null;
            }
        }
        return $ret;
    }

    if (!is_file($path)) {
        $ret = array();
        foreach ($functions as $function) {
            $ret[] = null;
        }
        return $ret;
    }

    $file = unixify_line_format(file_get_contents($path), null, false, true);
    if ((strpos($path, '/modules_custom/') !== false) && (is_file(str_replace('/modules_custom/', '/modules/', $path))) && (strpos($file, "\nclass ") === false)) {
        // Customised file is not a full class, so go to default file
        $path = str_replace('/modules_custom/', '/modules/', $path);
        $file = unixify_line_format(file_get_contents($path), null, false, true);
    }

    if (strpos($file, 'class Mx_') !== false) {
        unset($file); // To save memory

        return extract_module_functions($path, $functions, $params, true);
    }

    global $ARB_COUNTER;

    $r = preg_replace('#[^\w]#', '', basename($path, '.php')) . strval(mt_rand(0, mt_getrandmax())) . '_' . strval($ARB_COUNTER);
    $ARB_COUNTER++;
    $out = array();
    $_params = '';
    $pre = substr($file, 5, strpos($file, "\nclass ") - 5); // FUDGE. We assume any functions we need to pre-load precede any classes in the file
    $pre = preg_replace('#(^|\n)function (\w+)\(.*#s', 'if (!function_exists(\'${1}\')) { ${0} }', $pre); // In case we end up extracting from this file more than once across multiple calls to extract_module_functions
    if ($params !== null) {
        foreach ($params as $param) {
            if ($_params !== '') {
                $_params .= ',';
            }
            if (is_string($param)) {
                $_params .= '\'' . str_replace('\'', '\\\'', $param) . '\'';
            } elseif ($param === null) {
                $_params .= 'null';
            } elseif (is_bool($param)) {
                $_params .= $param ? 'true' : 'false';
            } else {
                $_params .= strval($param);
            }
        }
    }
    foreach ($functions as $function) {
        $start = strpos($file, 'function ' . $function . '(');

        $spaces = 1;
        if ($start === false) {
            $out[] = null;
        } else {
            while ($file[$start - $spaces - 1] !== "\n") {
                $spaces++;
            }
            $spaces -= strlen(ltrim(substr($file, $start - $spaces, $spaces))); // Remove length of stuff like 'public ' in front of 'function'

            $end1 = strpos($file, "\n" . str_repeat(' ', $spaces) . '}' . "\n", $start);
            $end2 = strpos($file, "\n" . str_repeat("\t", $spaces) . '}' . "\n", $start);
            if ($end1 === false) {
                $end1 = $end2;
            }
            if ($end2 === false) {
                $end2 = $end1;
            }
            $end = min($end1, $end2) + 2 + $spaces;
            $func = substr($file, $start, $end - $start);

            $new_func = str_replace('function ' . $function . '(', 'function ' . $function . $r . '(', $func) . 'return ' . filter_naughty_harsh($function) . $r . '(' . $_params . '); ';
            $out[] = $pre . "\n\n" . $new_func;

            if ((strpos($new_func, 'parent::') !== false) || (strpos($new_func, '$this->') !== false)) {
                return extract_module_functions($path, $functions, $params, true, $class_name);
            }

            $pre = ''; // Can only load that bit once
        }
    }

    unset($file); // To save memory

    return $out;
}
