<?php /*

 Composr
 Copyright (c) Christopher Graham, 2004-2024

 See docs/LICENSE.md for full licensing 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  Christopher Graham
 * @package    core
 */

/*EXTRA FUNCTIONS: ucwords*/

/*
    main bootstrapping and library code.
    NB: Make sure to update the version in minikernel.php too if you add new common functions or change behaviours
*/

/**
 * Standard code module initialisation function.
 *
 * @ignore
 */
function init__global2()
{
    // Input filtering constants (filters to apply)
    //Basic ones (rarely directly used)
    define('INPUT_FILTER_WORDFILTER', 1); // Apply wordfilter. Applies to POST-only
    define('INPUT_FILTER_WYSIWYG_TO_COMCODE', 2); // Convert WYSIWYG to Comcode if WYSIWYG marker present. Applies to POST-only
    define('INPUT_FILTER_COMCODE_CLEANUP', 4); // Cleanup Comcode. Applies to POST-only
    define('INPUT_FILTER_DOWNLOAD_ASSOCIATED_MEDIA', 8); // Download referenced media locally. Applies to POST-only
    define('INPUT_FILTER_FIELDS_XML', 16); // Pass through fields.xml system. Applies to POST-only
    define('INPUT_FILTER_URL_SCHEMES', 32); // Decode URL scheme encoding. Applies to POST/GET
    define('INPUT_FILTER_URL_DESTINATION', 64); // Change non-trusted URL destinations to local base URL. Applies to GET
    define('INPUT_FILTER_JS_URLS', 128); // Disallow JS URLs. Applies to POST/GET
    define('INPUT_FILTER_VERY_STRICT', 256); // Very strict filtering for codename-like values. Applies to POST/GET
    define('INPUT_FILTER_SPAM_HEURISTIC', 512); // Look for spam. Applies to POST/GET
    define('INPUT_FILTER_EARLY_XSS', 1024); // Look for possible XSS attacks. Applies to POST for non-privileged/GET
    define('INPUT_FILTER_DYNAMIC_FIREWALL', 2048); // Check against dynamic firewall. Applies to POST/GET
    define('INPUT_FILTER_TRUSTED_SITES', 4096); // Only allow a POST request from a trusted site. Applies to POST
    define('INPUT_FILTER_MODSECURITY_URL_PARAMETER', 8192); // Decode a URL-in-URL parameter that was encoded to bypass ModSecurity protection. Applies to POST/GET
    define('INPUT_FILTER_URL_RECODING', 16384); // Re-encodes Unicode to %-encoding and Punycode for a valid URL
    define('INPUT_FILTER_TRIMMED', 32768); // Trimmed as user would expect, intended for critical data where white-space could cause a problem
    define('INPUT_FILTER_EMAIL_ADDRESS', 65536); // Enforces e-mail address validation and warn_exit when invalid
    //Compound ones intended for direct use
    define('INPUT_FILTER_DEFAULT_POST', INPUT_FILTER_WORDFILTER | INPUT_FILTER_WYSIWYG_TO_COMCODE | INPUT_FILTER_COMCODE_CLEANUP | INPUT_FILTER_DOWNLOAD_ASSOCIATED_MEDIA | INPUT_FILTER_FIELDS_XML | INPUT_FILTER_URL_SCHEMES | INPUT_FILTER_JS_URLS | INPUT_FILTER_SPAM_HEURISTIC | INPUT_FILTER_EARLY_XSS | INPUT_FILTER_DYNAMIC_FIREWALL | INPUT_FILTER_TRUSTED_SITES);
    define('INPUT_FILTER_DEFAULT_GET', INPUT_FILTER_JS_URLS | INPUT_FILTER_VERY_STRICT | INPUT_FILTER_SPAM_HEURISTIC | INPUT_FILTER_EARLY_XSS | INPUT_FILTER_DYNAMIC_FIREWALL | INPUT_FILTER_URL_SCHEMES);
    define('INPUT_FILTER_GET_COMPLEX', INPUT_FILTER_JS_URLS | INPUT_FILTER_SPAM_HEURISTIC | INPUT_FILTER_EARLY_XSS | INPUT_FILTER_DYNAMIC_FIREWALL | INPUT_FILTER_URL_SCHEMES);
    define('INPUT_FILTER_URL_GENERAL', INPUT_FILTER_JS_URLS | INPUT_FILTER_EARLY_XSS | INPUT_FILTER_DYNAMIC_FIREWALL | INPUT_FILTER_URL_SCHEMES | INPUT_FILTER_MODSECURITY_URL_PARAMETER | INPUT_FILTER_URL_RECODING);
    define('INPUT_FILTER_URL_INTERNAL', INPUT_FILTER_JS_URLS | INPUT_FILTER_URL_DESTINATION | INPUT_FILTER_EARLY_XSS | INPUT_FILTER_DYNAMIC_FIREWALL | INPUT_FILTER_URL_SCHEMES | INPUT_FILTER_MODSECURITY_URL_PARAMETER | INPUT_FILTER_URL_RECODING);
    define('INPUT_FILTER_NONE', 0);
    define('INPUT_FILTER_GET_IDENTIFIER', INPUT_FILTER_GET_COMPLEX | INPUT_FILTER_TRIMMED);
    define('INPUT_FILTER_POST_IDENTIFIER', INPUT_FILTER_DEFAULT_POST | INPUT_FILTER_TRIMMED);
    define('INPUT_FILTER_PASSWORD', INPUT_FILTER_TRIMMED);

    define('BACKEND_RESPONSE_NOINDEX', 1); // on by default
    define('BACKEND_RESPONSE_CSP_SUPER_STRICT', 2); // on by default
    define('BACKEND_RESPONSE_CSP_STRICT', 4);
    define('BACKEND_RESPONSE_CSP_OPEN', 8);
    define('BACKEND_RESPONSE_CHARSET_UTF8', 16);
    define('BACKEND_RESPONSE_AJAX', 32); // on by default
    define('BACKEND_RESPONSE_CACHE_BREAK', 64); // on by default

    fixup_bad_php_env_vars();

    cms_ini_set('log_errors', '1');
    if ((GOOGLE_APPENGINE) && (!appengine_is_live())) {
        @mkdir(get_custom_file_base() . '/data_custom', 0755);
    }
    $error_log_path = get_custom_file_base() . '/data_custom/errorlog.php';
    cms_ini_set('error_log', $error_log_path);
    if ((is_file($error_log_path)) && (filesize($error_log_path) < 17)) {
        @file_put_contents($error_log_path, "<" . "?php return; ?" . ">\n", LOCK_EX);
        if (fileperms(get_custom_file_base() . '/caches/lang') === 0777) { // Try and get permissions correct in a simple way (too early in boot to do properly)
            chmod($error_log_path, 0666);
        }
    }

    global $BOOTSTRAPPING, $SUPPRESS_ERROR_DEATH, $CHECKING_SAFEMODE, $RELATIVE_PATH, $RUNNING_SCRIPT_CACHE, $SERVER_TIMEZONE_CACHE, $HAS_SET_ERROR_HANDLER, $DYING_BADLY, $XSS_DETECT, $SITE_INFO, $IN_MINIKERNEL_VERSION, $EXITING, $FILE_BASE, $CACHE_TEMPLATES, $WORDS_TO_FILTER_CACHE, $VALID_ENCODING, $CONVERTED_ENCODING, $MICRO_BOOTUP, $MICRO_AJAX_BOOTUP, $QUERY_LOG, $CURRENT_SHARE_USER, $WHAT_IS_RUNNING_CACHE, $DEV_MODE, $SEMI_DEV_MODE, $IS_VIRTUALISED_REQUEST, $FILE_ARRAY, $DIR_ARRAY, $JAVASCRIPTS_DEFAULT, $JAVASCRIPTS, $KNOWN_AJAX, $KNOWN_UTF8, $CSRF_TOKENS, $STATIC_CACHE_ENABLED, $IN_SELF_ROUTING_SCRIPT, $INVALIDATED_FAST_SPIDER_CACHE, $CURRENTLY_LOGGING_IN, $DISABLED_MEMORY_LIMIT;;

    $CURRENTLY_LOGGING_IN = false;

    $INVALIDATED_FAST_SPIDER_CACHE = false;

    cms_ob_end_clean(); // Reset to have no output buffering by default (we'll use it internally, taking complete control)

    // Don't want the browser caching PHP output, explicitly say this
    set_http_caching(null);

    if (php_function_allowed('session_cache_limiter')) {
        @session_cache_limiter('');
    }

    // Closed site message
    if ((is_file(get_file_base() . '/closed.html')) && (get_param_integer('keep_force_open', 0) == 0)) {
        if ((strpos($_SERVER['SCRIPT_NAME'], 'upgrader.php') === false) && (strpos($_SERVER['SCRIPT_NAME'], 'execute_temp.php') === false) && (strpos($_SERVER['SCRIPT_NAME'], '_tests') === false) && ((!isset($SITE_INFO['no_extra_closed_file'])) || ($SITE_INFO['no_extra_closed_file'] != '1'))) {
            if (@strpos($_SERVER['SERVER_SOFTWARE'], 'IIS') === false) {
                http_response_code(503);
            }

            if ($GLOBALS['DEV_MODE']) {
                header('Redirect-Reason: Closed site message');
            }

            header('Location: ' . (is_file($RELATIVE_PATH . 'closed.html') ? 'closed.html' : '../closed.html')); // assign_refresh not used, as it is a pre-page situation

            $aaf = ini_get('auto_append_file');
            if (!empty($aaf)) {
                @include($aaf); // Because exit() avoids running this
            }
            exit();
        }
    }

    // Initialise some globals
    $SUPPRESS_ERROR_DEATH = [false];
    $DISABLED_MEMORY_LIMIT = false;
    $JAVASCRIPTS_DEFAULT = [
        'global' => true,
    ];
    $RUNNING_SCRIPT_CACHE = [];
    $WHAT_IS_RUNNING_CACHE = current_script();
    $WORDS_TO_FILTER_CACHE = null;
    $VALID_ENCODING = false;
    $CONVERTED_ENCODING = false;
    $KNOWN_AJAX = false;
    /** Whether we are loading up in micro-bootup mode (reduced amount of loading for quicker simple responses - mainly no member logins, and assumed non-page output and non-generative code).
     *
     * @global boolean $MICRO_BOOTUP
     */
    if (!isset($MICRO_BOOTUP)) {
        $MICRO_BOOTUP = false;
    }
    /** Whether we are loading up in micro-ajax-bootup mode (reduced amount of loading for quicker simple AJAX responses).
     *
     * @global boolean $MICRO_AJAX_BOOTUP
     */
    if (!isset($MICRO_AJAX_BOOTUP)) {
        $MICRO_AJAX_BOOTUP = false;
    }
    /** Whether we know input text is in UTF8 because it came from an AJAX call (which is always UTF).
     *
     * @global boolean $KNOWN_UTF8
     */
    if (!isset($KNOWN_UTF8)) {
        $KNOWN_UTF8 = false;
    }
    /** Whether we know we need to do CSRF token checks.
     *
     * @global boolean $CSRF_TOKENS
     */
    if (!isset($CSRF_TOKENS)) {
        $CSRF_TOKENS = false;
    }
    /** Whether we enable the static cache for this request.
     *
     * @global boolean $STATIC_CACHE_ENABLED
     */
    if (!isset($STATIC_CACHE_ENABLED)) {
        $STATIC_CACHE_ENABLED = false;
    }
    /** Whether build_url requests point through the running script, as opposed to pointing to an index.php call.
     *
     * @global boolean $IN_SELF_ROUTING_SCRIPT
     */
    if (!isset($IN_SELF_ROUTING_SCRIPT)) {
        $IN_SELF_ROUTING_SCRIPT = (current_script() == 'index')/*LEGACY - ideally just have as false but old zone index.php files exist on sites*/;
    }
    $CACHE_TEMPLATES = true;
    $IS_VIRTUALISED_REQUEST = false;
    /** On the quick installer, this presents manifest information about files that exist in the virtual filesystem.
     *
     * @global ?array $FILE_ARRAY
     */
    $FILE_ARRAY = null;
    /** On the quick installer, this presents manifest information about directories that exist in the virtual filesystem.
     *
     * @global ?array $DIR_ARRAY
     */
    $DIR_ARRAY = null;

    // Keep check of our bootstrapping
    $BOOTSTRAPPING = true;
    $CHECKING_SAFEMODE = false;

    // CSP nice and early
    require_code('csp');
    load_csp([]); // We start maximally strict, then we will reduce down based on config (config system not loaded yet)

    // Set cross-domain headers (COR)
    if (isset($_SERVER['HTTP_ORIGIN'])) {
        require_code('ajax');
        cor_prepare();
    }

    if ((running_script('messages')) && (get_param_string('action', 'new') == 'new')) { // Architecturally hackerish chat message precheck (for extra efficiency)
        require_code('chat_poller');
        chat_poller();
    }
    if ((running_script('notifications')) && (get_param_integer('time_barrier', null) !== null) && (@filemtime(get_custom_file_base() . '/data_custom/modules/web_notifications/latest.bin') <= get_param_integer('time_barrier')) && (get_param_string('type', '') == 'poller')) {
        prepare_backend_response();

        //  encoding="' . escape_html(get_charset()) . '" not needed due to no data in it
        $output = '<?xml version="1.0" ?' . '><response><result><time>' . strval(time()) . '</time></result></response>';
        echo $output;

        cms_safe_exit_flow();
        return;
    }

    // Initialise timezones
    $SERVER_TIMEZONE_CACHE = @date_default_timezone_get();
    if ($SERVER_TIMEZONE_CACHE != 'UTC') {
        date_default_timezone_set('UTC');
    }
    cms_ini_set('date.timezone', 'UTC'); // In case PHP does not have it configured, would produce a warning

    // Initialise some error handling
    error_reporting(E_ALL);
    $HAS_SET_ERROR_HANDLER = false;
    $DYING_BADLY = false; // If Composr is bailing out uncontrollably, setting this will make sure the error hander does not try and suppress

    if (!function_exists('git_repos')) {
        /**
         * Find the Git branch name. This is useful for making this config file context-adaptive (i.e. dev settings vs production settings).
         *
         * @return ?ID_TEXT Branch name (null: not in Git)
         */
        function git_repos() : ?string
        {
            $path = __DIR__ . '/.git/HEAD';
            if (!is_file($path)) {
                return '';
            }
            $lines = file($path);
            $parts = explode('/', $lines[0]);
            return trim(end($parts));
        }
    }

    // Dev mode stuff

    /** Whether Composr is running in development mode
     *
     * @global boolean $DEV_MODE
     */
    $DEV_MODE = (((!array_key_exists('dev_mode', $SITE_INFO) || ($SITE_INFO['dev_mode'] == '1')) && (is_dir(get_file_base() . '/.git') || (function_exists('ocp_mark_as_escaped')))) && ((!array_key_exists('keep_dev_mode', $_GET) || ($_GET['keep_dev_mode'] == '1'))));
    /** Whether the ocProducts version of PHP is running, and hence whether XSS-detection is enabled, and hence whether we may need to carry through additional metadata to make sure it operates correctly. Stored in a global for quick check (good performance).
     *
     * @global boolean $XSS_DETECT
     */
    $XSS_DETECT = function_exists('ocp_mark_as_escaped') && $DEV_MODE;
    /** Whether Composr is running in a more limited development mode, which may make things a bit slower and more verbose, but won't run such severe standard enforcement tricks
     *
     * @global boolean $SEMI_DEV_MODE
     */
    $SEMI_DEV_MODE = (((!array_key_exists('dev_mode', $SITE_INFO) || ($SITE_INFO['dev_mode'] == '1')) && (is_dir(get_file_base() . '/.git') || (function_exists('ocp_mark_as_escaped')))));
    if (php_function_allowed('set_time_limit')) {
        @set_time_limit(isset($SITE_INFO['max_execution_time']) ? intval($SITE_INFO['max_execution_time']) : 60);
    }
    if ($DEV_MODE) {
        if (php_function_allowed('set_time_limit')) {
            @set_time_limit(10);
        }
        if (get_param_integer('keep_type_strictness', null) !== 0) {
            cms_ini_set('ocproducts.type_strictness', '1');
        }
        if (get_param_integer('keep_xss_detect', null) !== 0) {
            cms_ini_set('ocproducts.xss_detect', '1');
        }
        array_splice($_POST, 500, count($_POST) - 500, []); // Simulate max_input_vars
        foreach ($_POST as $val) {
            if (is_string($val)) {
                if (strpos($val, '<script') !== false) {
                    warn_exit('ModSecurity dev-mode check failed, raw <script> tag POSTed');
                }
                if (strpos($val, '/bin') !== false) {
                    warn_exit('ModSecurity dev-mode check failed, raw path POSTed');
                }
            }
        }
    }
    if ($DEV_MODE || $SEMI_DEV_MODE) {
        require_code('developer_tools');
    }

    // Load most basic config
    /** Whether Composr is currently running from the 'minikernel' used during installation
     *
     * @global boolean $IN_MINIKERNEL_VERSION
     */
    $IN_MINIKERNEL_VERSION = false;
    $EXITING = 0;
    if ((array_key_exists('use_cns', $_GET)) && (running_script('upgrader'))) {
        $SITE_INFO['forum_type'] = 'cns';
        $SITE_INFO['cns_table_prefix'] = $SITE_INFO['table_prefix'];
    }

    require_code('version');
    if (!headers_sent()) {
        if (!empty($SITE_INFO['no_nosniff_header'])) {
            @header('X-Content-Type-Options: nosniff');
        }
        @header('X-XSS-Protection: 1');
    }
    if ((!$MICRO_BOOTUP) && (!$MICRO_AJAX_BOOTUP)) {
        // Marker that Composr running
        //@header('X-Powered-By: Composr ' . cms_version_pretty() . ' (PHP ' . phpversion() . ')');
        if (!headers_sent()) {
            header('X-Powered-By: Composr'); // Better to keep it vague, for security reasons
        }

        // Get ready for query logging if requested
        $QUERY_LOG = false;
        if ((isset($_REQUEST['special_page_type'])) && ($_REQUEST['special_page_type'] == 'query')) {
            $QUERY_LOG = true;
        }
    }

    // Most critical things
    require_code('global3'); // A lot of support code is present in this
    require_code('web_resources');
    if ((!running_script('webdav')) && (!running_script('endpoint'))) {
        $http_method = $_SERVER['REQUEST_METHOD'];
        if ($http_method != 'GET' && $http_method != 'POST' && $http_method != 'HEAD' && $http_method != '') {
            http_response_code(405);
            exit();
        }
    }

    // Static caching checks (early)
    define('STATIC_CACHE__FAST_SPIDER', 1);
    define('STATIC_CACHE__GUEST', 2);
    define('STATIC_CACHE__FAILOVER_MODE', 4);
    $force_failover = get_param_integer('keep_failover', null);
    if (((isset($SITE_INFO['failover_mode'])) && ($SITE_INFO['failover_mode'] == 'on' || $SITE_INFO['failover_mode'] == 'auto_on') && ($force_failover !== 0)) || ($force_failover === 1)) {
        if (!running_script('execute_temp')) {
            // Forced static cache, due to failover mode
            $bot_type = get_bot_type();
            require_code('static_cache');
            static_cache((($bot_type !== null) ? STATIC_CACHE__FAST_SPIDER : 0) | STATIC_CACHE__FAILOVER_MODE);
        }
    }
    $static_cache_mode = null;
    if (web_client_may_use_static_cache(true, $static_cache_mode)) {
        load_csp(['csp_enabled' => '0']);
        require_code('static_cache');
        static_cache($static_cache_mode);
    }

    // More critical things
    require_code('caches');
    require_code('database'); // There's nothing without the database
    require_code('config'); // Config is needed for much active stuff
    if ((!isset($SITE_INFO['known_suexec'])) || ($SITE_INFO['known_suexec'] == '0')) {
        if (ip_banned(get_ip_address())) {
            critical_error('BANNED');
        }
    }

    // Set CSP to what is truly configured
    load_csp();

    // Member startup takes some time
    if (!$MICRO_BOOTUP) {
        load_user_stuff();
    }

    // For any kind of niceness we need these. The order is chosen for complex dependency reasons - don't mess with it
    if (!$MICRO_AJAX_BOOTUP) {
        require_code('themes'); // Output needs to know about themes
        require_code('templates'); // So that we can do error templates
        require_code('tempcode'); // Output is done with Tempcode
        if (!$MICRO_BOOTUP) {
            require_code('comcode'); // Much output goes through Comcode
        }
    }
    require_code('zones'); // Zone is needed because zones are where all Composr pages reside

    if ((get_option('single_public_zone') == '1') && ($RELATIVE_PATH == 'site')) {
        get_base_url();/*force calculation first*/
        $RELATIVE_PATH = '';
    }

    require_code('users'); // Users are important due to permissions

    // Static caching checks (late)
    if (web_client_may_use_static_cache(false, $static_cache_mode)) {
        require_code('static_cache');
        static_cache($static_cache_mode);
    }

    if (get_param_integer('keep_debug_fs', 0) != 0) {
        require_code('debug_fs');
        enable_debug_fs();
    }

    $CACHE_TEMPLATES = has_caching_for('template');

    require_code('lang'); // So that we can do language stuff (e.g. errors). Note that even though we have included a lot so far, we can't really use any of it until lang is loaded. Lang isn't loaded earlier as it itself has a dependency on Tempcode.

    if (!$MICRO_AJAX_BOOTUP) {
        require_code('temporal'); // Date/time functions

        convert_request_data_encodings(get_param_integer('known_utf8', 0) == 1);

        if (!$MICRO_BOOTUP) {
            // FirePHP console support, only for administrators
            if ((get_param_integer('keep_firephp', 0) == 1) && (($GLOBALS['FORUM_DRIVER']->is_super_admin(get_member())) || ($GLOBALS['IS_ACTUALLY_ADMIN']))) {
                require_code('firephp');
            }

            require_code('permissions'); // So we can check access
        }
    }

    // At this point we can display errors nicely
    set_error_handler('composr_error_handler');
    if (function_exists('register_shutdown_function')) {
        register_shutdown_function('catch_fatal_errors');
    }
    $HAS_SET_ERROR_HANDLER = true;

    // Initialise members
    if (!$MICRO_BOOTUP) {
        $GLOBALS['FORUM_DRIVER']->forum_layer_initialise();
    }

    // More things to initialise
    if (!$MICRO_BOOTUP) {
        if ((!$IN_MINIKERNEL_VERSION) && (!$MICRO_AJAX_BOOTUP)) {
            has_cookies(); // Will determine at early point whether we have cookie support
            get_num_users_site(); // Will kill site if there are too many users
        }
    }
    require_code('urls'); // URL building is crucial

    handle_bad_access_context();

    // Register Internationalisation settings
    @header('Content-Type: text/html; charset=' . get_charset());
    $locale_str = do_lang('locale');
    if ($locale_str != '') {
        $locale_sections = explode(';', $locale_str);
        foreach ($locale_sections as $locale_section) {
            $parts = explode(':', $locale_section, 2);
            if (count($parts) == 2) {
                setlocale(@constant($parts[0]), explode(',', $parts[1]));
            }
        }
    }

    // Check RBLs
    $spam_check_level = get_option('spam_check_level');
    if (($spam_check_level == 'EVERYTHING') || (($_SERVER['REQUEST_METHOD'] == 'POST') && (($spam_check_level == 'ACTIONS') || ($spam_check_level == 'GUESTACTIONS') && (is_guest())))) {
        require_code('antispam');
        check_for_spam(null, null, true);
    }

    // Output compression?
    require_code('output_compression');

    if ((!$MICRO_AJAX_BOOTUP) && (!$MICRO_BOOTUP)) {
        // Before anything gets outputted
        handle_logins();

        require_code('site'); // This powers the site (top level page generation)
    }

    // Make sure POST fields readable
    global $MODSECURITY_WORKAROUND_ENABLED;
    $MODSECURITY_WORKAROUND_ENABLED = false;
    if (get_param_integer('_corrected_comcode', 0) == 1) {
        require_code('input_filter_2');
        modsecurity_workaround_enable();
    }

    // Okay, we've loaded everything critical. Don't need to tell Composr to be paranoid now.
    $BOOTSTRAPPING = false;

    if ((!$MICRO_AJAX_BOOTUP) && (!$MICRO_BOOTUP)) {
        do_site_prep();
        check_has_page_access(); // Make sure we're authorised
    }

    // Check installer not left behind
    if ((!$MICRO_AJAX_BOOTUP) && (!$MICRO_BOOTUP) && ((!isset($SITE_INFO['no_installer_checks'])) || ($SITE_INFO['no_installer_checks'] != '1'))) {
        if ((is_file(get_file_base() . '/install.php')) && (!is_file(get_file_base() . '/install_ok')) && (running_script('index'))) {
            if (get_param_integer('came_from_installer', 0) == 1) {
                @unlink(get_file_base() . '/install.php');
            }
            if (is_file(get_file_base() . '/install.php')) {
                warn_exit(do_lang_tempcode('MUST_DELETE_INSTALLER'));
            }
        }
    }

    if ((!$MICRO_AJAX_BOOTUP) && (!$MICRO_BOOTUP)) {
        // Clear caching if needed
        $changed_base_url = (get_value('last_base_url', null) !== get_base_url()) && (get_value('disable_base_check') !== '1');
        if ((running_script('index')) && ((is_browser_decaching()) || ($changed_base_url))) {
            require_code('caches3');
            auto_decache($changed_base_url);
        }

        // Load requirements for admins
        if (has_zone_access(get_member(), 'adminzone')) {
            $JAVASCRIPTS_DEFAULT['staff'] = true;
            $JAVASCRIPTS_DEFAULT['themeing'] = true;
            if (get_option('bottom_show_commandr_button', true) === '1') {
                $JAVASCRIPTS_DEFAULT['button_commandr'] = true;
            }
        }
        if (get_option('bottom_show_realtime_rain_button', true) === '1') {
            $JAVASCRIPTS_DEFAULT['button_realtime_rain'] = true;
        }
        $JAVASCRIPTS += $JAVASCRIPTS_DEFAULT;
    }
    /*cms_memory_profile('startup');   If debugging with inbuilt profiler
    $func = get_defined_functions();
    print_r($func['user']);*/

    // Pre-load used blocks in bulk
    preload_block_internal_caching();

    if (($SEMI_DEV_MODE) && (!$MICRO_AJAX_BOOTUP)) { // Lots of code that only runs if you're a programmer. It tries to make sure coding standards are met.
        semi_dev_mode_startup();
    }

    // Reduce down memory limit / raise if requested
    $default_memory_limit = get_value('memory_limit');
    if ((empty($default_memory_limit)) || ($default_memory_limit == '-1')) {
        $default_memory_limit = '64M';
    } else {
        if (substr($default_memory_limit, -2) == 'MB') {
            $default_memory_limit = substr($default_memory_limit, 0, strlen($default_memory_limit) - 1);
        }
        if ((is_numeric($default_memory_limit)) && (intval($default_memory_limit) < 1024 * 1024 * 16)) {
            $default_memory_limit .= 'M';
        }
    }
    cms_ini_set('memory_limit', $default_memory_limit);
    if ($GLOBALS['RELATIVE_PATH'] == 'adminzone' || $GLOBALS['RELATIVE_PATH'] == 'cms') {
        raise_php_memory_limit();
    }
    memory_limit_for_max_param('max');
    if (((isset($GLOBALS['FORUM_DRIVER'])) && ($GLOBALS['FORUM_DRIVER']->is_super_admin(get_member()))) || ($GLOBALS['DEV_MODE'])) {
        if (get_param_integer('keep_memory_limit', null) === 0) {
            disable_php_memory_limit();
        } else {
            $memory_test = get_param_integer('keep_memory_limit_test', 0);
            if (($memory_test != 0) && ($memory_test <= 32)) {
                cms_ini_set('memory_limit', strval($memory_test) . 'M');
            }
        }
    }
    $memory_tracking = get_value('memory_tracking');
    if (!empty($memory_tracking)) {
        if (function_exists('register_shutdown_function')) {
            register_shutdown_function('memory_tracking');
        }
    }

    if (!empty(array_diff(array_keys($_POST), ['x', 'y', 'http_referer'/*added by our JS*/]))) {
        // Detect and deal with spammers that triggered the spam blackhole
        if (get_option('spam_blackhole_detection') == '1') {
            $blackhole = post_param_string('y' . md5(get_site_name() . ': antispam'), '');
            if ($blackhole != '') {
                log_hack_attack_and_exit('BLACKHOLE_SPAM_HACK', '<blackhole>' . $blackhole . '</blackhole>');
            }
        }

        // Check security token, if necessary
        if ($CSRF_TOKENS) {
            require_code('csrf_filter');
            check_csrf_token(post_param_string('csrf_token', null));
        }
    }

    $member_id = get_member();
    if ((!is_guest($member_id)) && (!$GLOBALS['IS_VIA_BACKDOOR'])) { // All hands to the guns
        $reasoned_ban = null;
        if ($GLOBALS['FORUM_DRIVER']->is_banned($member_id, $reasoned_ban)) {
            require_code('failure');
            banned_exit($reasoned_ban);
        }
    }

    if (!running_script('upgrader')) {
        // Startup hooks
        $startup_hooks = find_all_hook_obs('systems', 'startup', 'Hook_startup_');
        foreach ($startup_hooks as $hook => $ob) {
            $ob->run($MICRO_BOOTUP, $MICRO_AJAX_BOOTUP, 0);
        }

        // Auto-upgrade
        if (($CURRENT_SHARE_USER !== null) && (float_to_raw_string(cms_version_number()) != get_value('version'))) {
            require_code('upgrade_shared_installs');
            automate_upgrade__safe();
        }
    }

    // For performance testing
    if (get_value('monitor_slow_urls', '0') !== '0') {
        if (function_exists('register_shutdown_function')) {
            register_shutdown_function('monitor_slow_urls');
        }
    }
}

/**
 * PHP's environment can be a real mess across servers. Cleanup the best we can.
 * See phpstub.php for info on what environmental data we can rely on.
 * See Chris's own comments on http://php.net/manual/en/reserved.variables.server.php also.
 * Also see fixup_bad_php_env_vars_pre.
 */
function fixup_bad_php_env_vars()
{
    static $real_get = null;
    if ($real_get === null) {
        $real_get = $_GET;
    }

    // We can trust these to be there
    $script_filename = $_SERVER['SCRIPT_FILENAME']; // If was not here, was added by our front-end controller script

    // Now derive missing ones...

    if (empty($_SERVER['HTTPS'])) { // Maybe on command line
        $_SERVER['HTTPS'] = '';
    }

    if ((empty($_SERVER['SERVER_ADDR'])) && (!empty($_SERVER['LOCAL_ADDR']))) {
        $_SERVER['SERVER_ADDR'] = $_SERVER['LOCAL_ADDR'];
    }

    $_SERVER['HTTP_USER_AGENT'] = urldecode($_SERVER['HTTP_USER_AGENT']);

    $document_root = empty($_SERVER['DOCUMENT_ROOT']) ? '' : $_SERVER['DOCUMENT_ROOT'];
    if (empty($document_root)) {
        // Note on Windows we really do use '/' in DOCUMENT_ROOT

        $document_root = '';

        global $SITE_INFO;
        if (isset($SITE_INFO['base_url'])) {
            // Algorithm: backwards from URL-path in base URL
            $base_url_path = @parse_url($SITE_INFO['base_url'], PHP_URL_PATH);
            if (($base_url_path !== null) && (substr(get_file_base(), -strlen($base_url_path)) == $base_url_path)) {
                $document_root = substr(get_file_base(), 0, strlen(get_file_base()) - strlen($base_url_path));
            }
        }
        if ($document_root == '') {
            // Algorithm: up until a known document-root directory
            $path_components = explode(DIRECTORY_SEPARATOR, get_file_base());
            foreach ($path_components as $i => $path_component) {
                $document_root .= $path_component . '/';
                if (in_array($path_component, ['public_html', 'www', 'webroot', 'httpdocs', 'httpsdocs', 'wwwroot', 'Documents'])) {
                    break;
                }
            }
            $document_root = substr($document_root, 0, strlen($document_root) - strlen('/'));
        }

        $_SERVER['DOCUMENT_ROOT'] = $document_root;
    }

    $php_self = @cms_empty_safe($_SERVER['PHP_SELF']) ? '' : $_SERVER['PHP_SELF'];
    if ((cms_empty_safe($php_self)) || (/*or corrupt*/strpos($php_self, '.php') === false)) {
        // We're really desperate if we have to derive this, but here we go
        $regexp = '#^' . preg_quote(str_replace('/', DIRECTORY_SEPARATOR, $document_root) . DIRECTORY_SEPARATOR, '#') . '#';
        $_SERVER['PHP_SELF'] = '/' . str_replace(DIRECTORY_SEPARATOR, '/', preg_replace($regexp, '', str_replace('/', DIRECTORY_SEPARATOR, $script_filename)));
        $path_info = @cms_empty_safe($_SERVER['PATH_INFO']) ? '' : $_SERVER['PATH_INFO'];
        if (!cms_empty_safe($path_info)) { // Add in path-info if we have it
            $_SERVER['PHP_SELF'] .= $path_info;
        }
        $php_self = $_SERVER['PHP_SELF'];
    }

    if (@cms_empty_safe($_SERVER['SCRIPT_NAME'])) {
        $_SERVER['SCRIPT_NAME'] = preg_replace('#\.php/.*#', '.php', $php_self); // Same as PHP_SELF except without path-info on the end
    }

    if (@cms_empty_safe($_SERVER['REQUEST_URI'])) {
        if (isset($_SERVER['REDIRECT_URL'])) {
            $_SERVER['REQUEST_URI'] = $_SERVER['REDIRECT_URL'];
            if (strpos($_SERVER['REQUEST_URI'], '?') === false) {
                if (!empty($real_get)) {
                    $_SERVER['REQUEST_URI'] .= '?' . str_replace('/', '%2F', http_build_query($real_get)); // Messy as rewrite URL-embedded parameters will be doubled, but if you've got a broken server don't push it to do rewrites
                }
            }
        } else {
            $_SERVER['REQUEST_URI'] = $php_self; // Same as PHP_SELF, but...
            if (!empty($real_get)) { // add in query string data if we have it
                $_SERVER['REQUEST_URI'] .= '?' . str_replace('/', '%2F', http_build_query($real_get));
            }

            // ^ NB: May be slight deviation. Default directory index files not considered, i.e. index.php may have been omitted in URL
        }
    }

    if (@cms_empty_safe($_SERVER['QUERY_STRING'])) {
        $_SERVER['QUERY_STRING'] = str_replace('/', '%2F', http_build_query($real_get));
    }

    if (@cms_empty_safe($_SERVER['PHP_AUTH_USER'])) {
        if (empty($_SERVER['HTTP_AUTHORIZATION'])) {
            if (!empty($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
                $_SERVER['HTTP_AUTHORIZATION'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
            }
        }

        if ((!empty($_SERVER['HTTP_AUTHORIZATION'])) && (preg_match('#^Basic #', $_SERVER['HTTP_AUTHORIZATION'])) != 0) {
            $bits = explode(':', base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)), 2);
            if (count($bits) == 2) {
                list($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) = $bits;
            }
        } elseif (!@cms_empty_safe($_SERVER['REDIRECT_REMOTE_USER'])) {
            $_SERVER['PHP_AUTH_USER'] = $_SERVER['REDIRECT_REMOTE_USER'];
        } elseif (!@cms_empty_safe($_SERVER['REMOTE_USER'])) {
            $_SERVER['PHP_AUTH_USER'] = $_SERVER['REMOTE_USER'];
        }
    }
}

/**
 * Find if the current web client can use the static cache.
 * This doesn't do checks to see if the whole web request is cachable (see can_static_cache_request), just the web client.
 *
 * @param  boolean $early_boot_check Whether this is an check before the user subsystem is initiated (stricter conditions, but good to detect early)
 * @param  ?integer $mode A STATIC_CACHE__* constant (null: not yet set)
 * @param  ?string $reason Reason for a false result (null: not yet set)
 * @param  boolean $consider_failover_mode Whether to consider potential of cache being needed for failover mode
 * @return boolean Whether the web client can use static caching
 */
function web_client_may_use_static_cache(bool $early_boot_check = false, ?int &$mode = null, ?string &$reason = null, bool $consider_failover_mode = false) : bool
{
    global $SITE_INFO, $MICRO_BOOTUP, $MICRO_AJAX_BOOTUP, $STATIC_CACHE_ENABLED, $IS_ACTUALLY_ADMIN;

    if (empty($SITE_INFO['static_caching_hours'])) {
        $reason = 'Not enabled in _config.php';
        return false;
    }

    if ($MICRO_BOOTUP) {
        $reason = 'Micro bootup';
        return false;
    }

    if ($MICRO_AJAX_BOOTUP) {
        $reason = 'Micro-AJAX bootup';
        return false;
    }

    if (!$STATIC_CACHE_ENABLED) {
        $reason = '$STATIC_CACHE_ENABLED is false';
        return false;
    }

    if ($_SERVER['REQUEST_METHOD'] != 'GET') {
        $reason = 'Request method is ' . $_SERVER['REQUEST_METHOD'];
        return false;
    }

    if (!@cms_empty_safe($_SERVER['PHP_AUTH_USER'])) {
        $reason = 'HTTP-auth active';
        return false;
    }

    $bot_type = get_bot_type();
    if ($bot_type !== null) {
        $mode = STATIC_CACHE__FAST_SPIDER;
        return true;
    }

    $supports_failover_mode = (isset($SITE_INFO['failover_mode'])) && ($SITE_INFO['failover_mode'] != 'off');
    $supports_guest_caching = (!empty($SITE_INFO['any_guest_cached_too']));
    if ((!$supports_guest_caching) && ((!$consider_failover_mode) || (!$supports_failover_mode))) {
        $reason = 'Enabled for bots only';
        return false;
    }

    $mode = STATIC_CACHE__GUEST;

    if ($early_boot_check) {
        if (has_backdoor_ip_triggered()) {
            $reason = '[Conservative early boot check] Authorised by backdoor_ip';
            return false;
        }

        if (isset($_GET['keep_session'])) {
            $reason = '[Conservative early boot check] May have session via URL';
            return false;
        }

        if (get_forum_type() == 'cns') {
            if (isset($_COOKIE[$SITE_INFO['user_cookie']])) {
                $reason = '[Conservative early boot check] A login cookie is present';
                return false;
            }

            if (isset($_COOKIE[$SITE_INFO['session_cookie']])) {
                $reason = '[Conservative early boot check] A session cookie is present';
                return false;
            }
        } else {
            if (!empty(array_diff_key($_COOKIE, ['__utma' => 0, '__utmc' => 0, '__utmz' => 0, 'has_cookies' => 0, 'last_visit' => 0]))) {
                $reason = '[Conservative early boot check] Unknown cookies (which may be login cookies) are present';
                return false;
            }
        }
    } else {
        if (!is_guest(null, true)) {
            $reason = '[Later boot check]  Logged in';
            return false;
        }

        if ($IS_ACTUALLY_ADMIN) {
            $reason = '[Later boot check]  Using SU';
            return false;
        }
    }

    return true;
}

/**
 * Find whether the whole site is HTTPS.
 * Also see tacit_https.
 *
 * @return boolean Whether it is
 */
function whole_site_https() : bool
{
    global $SITE_INFO;
    if (empty($SITE_INFO['base_url'])) {
        return tacit_https();
    }
    return (strpos($SITE_INFO['base_url'], 'https://') !== false);
}

/**
 * Redirect away if the domain or protocol is incorrect.
 * This may be a security measure - in case someone links the site on the wrong protocol in order to tap network traffic.
 * Also see do_site_prep function.
 */
function handle_bad_access_context()
{
    if (is_cli()) {
        return;
    }

    if (strpos(get_self_url_easy(), '/webdav/') !== false) {
        return;
    }

    if ((!empty($_SERVER['REQUEST_METHOD'])) && ($_SERVER['REQUEST_METHOD'] != 'GET')) {
        return;
    }

    if (!running_script('index')) {
        return; // get_self_url only works for index.php
    }

    $request_hostname = get_request_hostname();

    // Detect bad access domain
    global $SITE_INFO;
    if (($request_hostname != '') && (isset($_SERVER['HTTP_HOST'])) && (empty($GLOBALS['EXTERNAL_CALL']))) {
        $base_url_hostname = get_base_url_hostname();

        if ($base_url_hostname != $request_hostname) {
            if (empty($SITE_INFO['ZONE_MAPPING_' . get_zone_name()])) {
                if (($GLOBALS['FORUM_DRIVER'] !== null) && ($GLOBALS['FORUM_DRIVER']->is_super_admin(get_member()))) {
                    require_code('site');
                    attach_message(do_lang_tempcode('BAD_ACCESS_DOMAIN', escape_html($base_url_hostname), escape_html($request_hostname)), 'warn');
                }

                set_http_status_code(301);
                if ($GLOBALS['DEV_MODE']) {
                    header('Redirect-Reason: Bad access domain');
                }
                header('Location: ' . escape_header(get_self_url(true, false))); // assign_refresh not used, as it is a pre-page situation
                exit();
            }
        }
    }

    // Detect bad access protocol
    if (((whole_site_https()) && (!tacit_https()) || ((!whole_site_https()) && (tacit_https())))) {
        // Note we don't output Strict-Transport-Security as that is a domain-level signal and we cannot assume we control the whole domain - it may be set in .htaccess if desired

        set_http_status_code(301);
        if ($GLOBALS['DEV_MODE']) {
            header('Redirect-Reason: Bad access protocol');
        }
        header('Location: ' . escape_header(get_self_url(true, false))); // assign_refresh not used, as it is a pre-page situation
        exit();
    }
}

/**
 * Get server hostname as used in the base URL.
 * See also get_domain() and get_request_hostname() and get_server_names().
 *
 * @return string The hostname
 */
function get_base_url_hostname() : string
{
    global $SITE_INFO;
    if (!empty($SITE_INFO['base_url'])) {
        $ret = parse_url($SITE_INFO['base_url'], PHP_URL_HOST);
        if ($ret !== null) {
            return $ret;
        }
    }
    if (!empty($_SERVER['HTTP_HOST'])) {
        return preg_replace('#:.*#', '', $_SERVER['HTTP_HOST']);
    }
    return gethostname();
}

/**
 * Get server hostname as used in the URL.
 * See also get_domain() and get_base_url_hostname() and get_server_names().
 *
 * @return string The hostname
 */
function get_request_hostname() : string
{
    if (!empty($_SERVER['HTTP_HOST'])) {
        return preg_replace('#:.*#', '', $_SERVER['HTTP_HOST']);
    }
    global $SITE_INFO;
    if (!empty($SITE_INFO['base_url'])) {
        $ret = parse_url($SITE_INFO['base_url'], PHP_URL_HOST);
        if ($ret !== null) {
            return $ret;
        }
    }
    return gethostname();
}

/**
 * Get the domain the website is installed on (preferably, without any www). The domain is used for e-mail defaults among other things.
 * Also see get_request_hostname() and get_base_url_hostname() and get_server_names().
 *
 * @return string The domain of the website
 */
function get_domain() : string
{
    global $SITE_INFO;
    $ret = (!empty($SITE_INFO['domain'])) ? $SITE_INFO['domain'] : '';

    // Ah, no explicit setting, so derive...
    if (empty($ret)) {
        // Derive from base URL
        if (!empty($SITE_INFO['base_url'])) {
            $ret = parse_url($SITE_INFO['base_url'], PHP_URL_HOST);
        }
    }
    if (empty($ret)) {
        if (!empty($_SERVER['HTTP_HOST'])) {
            $ret = preg_replace('#:.*#', '', $_SERVER['HTTP_HOST']);
        }
    }
    if (empty($ret)) {
        if (!empty($_SERVER['SERVER_NAME'])) {
            $ret = $_SERVER['SERVER_NAME'];
        }
    }
    if (empty($ret)) {
        $ret = gethostname();
    }
    return preg_replace('#^www\.#', '', $ret);
}

/**
 * Set HTTP caching in a conclusive and simple way.
 *
 * @param  ?TIME $last_modified When the resource was last modified (null: dynamic non-cached request)
 * @param  boolean $public Whether the request is public (can be cached in public proxy caches)
 * @param  TIME $expiry_seconds Seconds until cache expires (only applicable if $last_modified is not null)
 */
function set_http_caching(?int $last_modified, bool $public = false, int $expiry_seconds = 604800/*1 week*/)
{
    if ($last_modified === null) {
        @header('Cache-Control: no-store');
    } else {
        @header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $last_modified) . ' GMT');
        @header('Cache-Control: ' . ($public ? 'public' : 'private') . '; max-age=' . strval($expiry_seconds));

        // Default from PHP
        @header_remove('Pragma');
        @header_remove('Expires');
    }
}

/**
 * Use with register_shutdown_function to log slow URLs.
 */
function monitor_slow_urls()
{
    if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
        $time = microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'];
        $slow = ($time > floatval(get_value('monitor_slow_urls')));
    } else {
        $time = time() - $_SERVER['REQUEST_TIME'];
        $slow = ($time > intval(get_value('monitor_slow_urls')));
    }
    if ($slow) {
        require_code('urls');
        if (php_function_allowed('error_log')) {
            error_log('Profiling: Over time limit @ ' . get_self_url_easy(true) . "\t" . strval($time) . ' secs' . "\t" . date('Y-m-d H:i:s', time()), 0);
        }
    }
}

/**
 * Log excessive memory usage.
 */
function memory_tracking()
{
    $memory_tracking = intval(get_value('memory_tracking'));
    if (memory_get_peak_usage() > 1024 * 1024 * $memory_tracking) {
        if (php_function_allowed('error_log')) {
            error_log('Profiling: Memory usage above memory_tracking (' . strval($memory_tracking) . 'MB) @ ' . get_self_url_easy(true), 0);
        }
    }
}

/**
 * Prepare what we need for a web browser's behind-the-scenes (e.g. AJAX) response.
 *
 * @param  ?string $content_type The MIME content type (null: don't output one)
 * @param  integer $settings Bitmask of BACKEND_RESPONSE_* settings
 */
function prepare_backend_response(?string $content_type = 'text/xml', int $settings = 101)
{
    if (get_param_integer('keep_show_loading', 0) == 1) {
        return;
    }

    if (($settings & BACKEND_RESPONSE_NOINDEX) != 0) {
        header('X-Robots-Tag: noindex');
    }

    // Don't allow special execution via a vector of namespace-injected HTML
    if (($settings & BACKEND_RESPONSE_CSP_SUPER_STRICT) != 0) {
        header("Content-Security-Policy: default-src 'none'");
    }
    if (($settings & BACKEND_RESPONSE_CSP_STRICT) != 0) {
        header("Content-Security-Policy: default-src 'self'; style-src 'self' data: 'unsafe-inline'");
    }
    if (($settings & BACKEND_RESPONSE_CSP_OPEN) != 0) {
        header("Content-Security-Policy: default-src *  data: blob: 'unsafe-inline'");
    }

    if ($content_type !== null) {
        $charset = (($settings & BACKEND_RESPONSE_CHARSET_UTF8) != 0) ? 'utf-8' : get_charset();
        header('Content-Type: ' . $content_type . '; charset=' . $charset);
    }

    if (($settings & BACKEND_RESPONSE_CACHE_BREAK) != 0) {
        set_http_caching(null);
    }

    if (($settings & BACKEND_RESPONSE_AJAX) != 0) {
        convert_request_data_encodings(true);

        global $KNOWN_AJAX;
        $KNOWN_AJAX = true;
    }
}

/**
 * Raise the PHP memory limit to cater for a requested large result set.
 *
 * @param  ID_TEXT $max_param The max parameter name
 */
function memory_limit_for_max_param(string $max_param)
{
    $max = get_param_integer($max_param, null); // If making a large request and are an admin, raise PHP memory limit
    if (($max !== null) && ($max > 80) && (function_exists('has_privilege'))) {
        if (has_privilege(get_member(), 'remove_page_split')) {
            $shl = @ini_get('suhosin.memory_limit');
            if (($shl === false) || ($shl == '') || ($shl == '0')) {
                raise_php_memory_limit();
            }
        }
    }
}

/**
 * Find if running on lower memory than we'd like.
 *
 * @return boolean Whether we are
 */
function has_low_memory() : bool
{
    $ml = php_return_bytes(ini_get('memory_limit'));
    return ($ml != -1) && ($ml < 64 * 1024 * 1024);
}

/**
 * Raise the PHP memory limit to the documented minimum.
 * By default we keep the memory limit lower than that to mitigate the effect of crashes.
 */
function raise_php_memory_limit()
{
    global $DISABLED_MEMORY_LIMIT;
    if ($DISABLED_MEMORY_LIMIT) {
        return;
    }

    cms_ini_set('memory_limit', '128M');
}

/**
 * Disable the PHP memory limit. Do not use this carelessly, use it if a screen is a bit fat or in an importer, don't use it assuming memory is infinite.
 * Mar 2021: Actually we will still cap it, as nobody needs more than 512MB of RAM (for a PHP process) and bugs can crash a machine.
 */
function disable_php_memory_limit()
{
    if ((function_exists('get_value')) && (get_value('memory_limit_simulate_hard') === '1')) {
        return;
    }

    global $DISABLED_MEMORY_LIMIT;
    $DISABLED_MEMORY_LIMIT = true;

    $shl = @ini_get('suhosin.memory_limit');
    if (($shl === false) || ($shl == '') || ($shl == '0')) {
        // Progressively relax more and more (some PHP installs may block at some point)
        cms_ini_set('memory_limit', '128M');
        cms_ini_set('memory_limit', '256M');
        cms_ini_set('memory_limit', '512M');
        //cms_ini_set('memory_limit', '-1');
    } else {
        if (is_numeric($shl)) {
            $shl .= 'M'; // Units are in MB for this, while PHP's memory limit setting has it in bytes
        }
        cms_ini_set('memory_limit', $shl);
    }
}

/**
 * Get the character set to use. We try and be clever to allow AJAX scripts to avoid loading up language.
 * We may assume utf-8 is lower case, but any other character set may be of any case.
 *
 * @return string The character set
 */
function get_charset() : string
{
    static $charset_cache = null;
    global $XSS_DETECT;
    if (isset($charset_cache)) {
        return $charset_cache;
    }

    global $SITE_INFO;
    if (!empty($SITE_INFO['charset'])) { // An optimisation, if you want to put it in here
        $charset_cache = $SITE_INFO['charset'];
        if ($XSS_DETECT) {
            ocp_mark_as_escaped($charset_cache);
        }
        return $charset_cache;
    }

    global $LANGS_REQUESTED;
    if ((function_exists('do_lang')) && (function_exists('user_lang')) && (isset($LANGS_REQUESTED['critical_error'], $LANGS_REQUESTED['global'])) && (!in_safe_mode())) {
        $attempt = do_lang('charset', null, null, null, null, false);
        if ($attempt !== null) {
            $charset_cache = trim($attempt);
            return $attempt;
        }
    }

    static $temp_charset_cache = null;
    if (isset($temp_charset_cache)) {
        return $temp_charset_cache;
    }

    global $SITE_INFO;
    $lang = (!empty($SITE_INFO['default_lang'])) ? $SITE_INFO['default_lang'] : 'EN';
    $path = get_file_base() . '/lang_custom/' . $lang . '/global.ini';
    if (!is_file($path)) {
        $path = get_file_base() . '/lang/' . $lang . '/global.ini';
    }
    if (!is_file($path)) {
        $path = get_file_base() . '/lang/EN/global.ini';
    }

    global $FILE_ARRAY;
    $contents = '';
    if (@is_array($FILE_ARRAY)) {
        if (file_array_exists('lang/' . $lang . '/global.ini')) {
            $lang_file = 'lang/' . $lang . '/global.ini';
        } elseif (file_array_exists('lang/EN/global.ini')) {
            $lang_file = 'lang/EN/global.ini';
        } else {
            fatal_exit('Internal Error');
        }
        $contents = str_replace("\r", "\n", file_array_get($lang_file));
    } else {
        $file = fopen($path, 'rb');
        $contents = str_replace("\r", "\n", fread($file, 3000));
        fclose($file);
    }

    $matches = [];
    if (preg_match('#\[strings\].*charset=([\w\-]+)\n#s', $contents, $matches) != 0) {
        $temp_charset_cache = $matches[1];
        if ($XSS_DETECT) {
            ocp_mark_as_escaped($temp_charset_cache);
        }
        return $temp_charset_cache;
    }
    $temp_charset_cache = 'utf-8';
    return $temp_charset_cache;
}

/**
 * Load stuff that allows user code to work.
 */
function load_user_stuff()
{
    if ((!array_key_exists('FORUM_DRIVER', $GLOBALS)) || ($GLOBALS['FORUM_DRIVER'] === null)) { // Second clause is for Quercus, as it pre-nulls referenced variables
        global $SITE_INFO, $FORUM_DRIVER, $SITE_DB, $FORUM_DB;

        require_code('forum_stub');
        require_code('users');

        if (empty($SITE_INFO['forum_type'])) {
            $SITE_INFO['forum_type'] = 'cns';
        }
        require_code('forum/' . $SITE_INFO['forum_type']);     // So we can at least get user details
        $class = 'Forum_driver_' . filter_naughty_harsh($SITE_INFO['forum_type']);
        if (class_exists($class . '_sub')) {
            $class .= '_sub';
        }
        /** The active forum driver, through which member and forum interfacing should be done (apart from code that is explicitly only written as part of Conversr)
         *
         * @global object $FORUM_DRIVER
         */
        $FORUM_DRIVER = object_factory($class);
        if (($SITE_INFO['forum_type'] == 'cns') && (!is_on_multi_site_network()) && (!$GLOBALS['DEV_MODE'])) { // NB: In dev mode needs separating so we can properly test our boundaries
            $FORUM_DRIVER->db = &$SITE_DB;
        } elseif ($SITE_INFO['forum_type'] != 'none') {
            $FORUM_DRIVER->db = new DatabaseConnector(get_db_forums(), get_db_forums_host(), get_db_forums_user(), get_db_forums_password(), $FORUM_DRIVER->get_drivered_table_prefix());
        }
        $FORUM_DRIVER->MEMBER_ROWS_CACHED = [];
        /** The connector to the active forum database.
         *
         * @global object $FORUM_DB
         */
        $FORUM_DB = null;
        $GLOBALS['FORUM_DB'] = &$FORUM_DRIVER->db; // Done like this to workaround that PHP can't put a reference in a global'd variable
        if (is_on_multi_site_network()) {
            reload_lang_fields(false, 'f_member_custom_fields');
        }
    }
}

/**
 * Add new suppress error death setting. Whether error display is suppressed.
 * Suppressed errors will always be logged and be shown depending on error_handling_* config (unlike with '@'), they just don't cause a fatal exit.
 * So we use this function over '@' when an error is real and wants logging/possibly-showing.
 *
 * @param  boolean $setting New setting
 */
function push_suppress_error_death(bool $setting)
{
    global $SUPPRESS_ERROR_DEATH;
    array_push($SUPPRESS_ERROR_DEATH, $setting);
}

/**
 * Remove last suppress error death setting.
 */
function pop_suppress_error_death()
{
    global $SUPPRESS_ERROR_DEATH;
    array_pop($SUPPRESS_ERROR_DEATH);
}

/**
 * See suppress error death setting.
 *
 * @return boolean Last setting
 */
function peek_suppress_error_death() : bool
{
    global $SUPPRESS_ERROR_DEATH;
    return end($SUPPRESS_ERROR_DEATH);
}

/**
 * Composr error catcher for fatal versions.
 *
 * @ignore
 */
function catch_fatal_errors()
{
    $error = error_get_last(); // If the last error is E_*_ERROR then it would have been fatal, so we should show it via this function
    if ($error !== null) {
        // @var_dump($error);@debug_print_backtrace(); // Useful for debugging

        // Smart refresh when we timed out
        if (substr($error['message'], 0, 26) == 'Maximum execution time of ') {
            if (function_exists('i_force_refresh')) {
                i_force_refresh();
            }
        }

        // Useful for debugging memory problems, finding unneeded stuff that is loaded
        //$tmp = $GLOBALS;unset($tmp['GLOBALS']);@var_dump($tmp);
        //@var_dump(get_defined_functions());
        //exit();

        if ((error_reporting() & $error['type']) === 0) {
            return;
        }

        switch ($error['type']) {
            case E_ERROR:
            case E_CORE_ERROR:
            case E_COMPILE_ERROR:
            case E_USER_ERROR:
            case E_PARSE:

            // While not fatalistic in PHP, these error types are not supported by user-defined error handlers in PHP. So we also have to catch them here. We will treat these as fatalistic anyway.
            case E_CORE_WARNING: // Error during bootstrap, so before set_error_handler could have been called
            case E_COMPILE_WARNING:
            case E_STRICT: // Not used much in PHP, as most of this has now been enforced in the language and E_DEPRECATED tends to be used now.
                push_suppress_error_death(false); // We can't recover as we've lost our execution track. Force a nice death rather than trying to display a recoverable error.
                $GLOBALS['DYING_BADLY'] = true; // Tells composr_error_handler to roll through, definitely an error.
                $GLOBALS['EXITING'] = 2; // Fudge to force a critical error, we're too desperate to show a Tempcode stack trace.
                composr_error_handler($error['type'], $error['message'], $error['file'], $error['line']);
                break;
        }
    }
}

/**
 * Composr error handler (hooked into PHP error system).
 *
 * @param  integer $errno The error type-number
 * @param  PATH $errstr The error message
 * @param  string $errfile The file the error occurred in
 * @param  integer $errline The line the error occurred on
 * @return boolean Mark error handled, so PHP's native error handling code does not execute. i.e. false => bubble, true => handled. For errors we intercept we don't return at all so bubbling never happens in such a case.
 *
 * @ignore
 */
function composr_error_handler(int $errno, string $errstr, string $errfile, int $errline) : bool
{
    if (((error_reporting() & $errno) !== 0) && (strpos($errstr, 'Illegal length modifier specified')/*Weird random error in dev PHP version*/ === false) || ($GLOBALS['DYING_BADLY'])) {
        $GLOBALS['DYING_BADLY'] = false;

        // Work out the simplified error type and how to handle it
        switch ($errno) {
            case E_RECOVERABLE_ERROR:
            case E_USER_ERROR:
            case E_CORE_ERROR:
            case E_COMPILE_ERROR:
            case E_ERROR:
            case E_PARSE:
                if (function_exists('set_throw_errors')) {
                    set_throw_errors(false);
                }
                $type = 'error';
                $syslog_type = LOG_ERR;
                $handling_method = 'FATAL';
                break;

            case -123: // Hacked in for the memtrack extension, which was buggy
            case E_USER_WARNING:
            case E_CORE_WARNING:
            case E_COMPILE_WARNING:
            case E_WARNING:
                $type = 'warning';
                $syslog_type = LOG_WARNING;
                $handling_method = 'FATAL';
                break;

            case E_USER_NOTICE:
            case E_NOTICE:
                $type = 'notice';
                $syslog_type = LOG_NOTICE;
                $handling_method = 'ATTACH';
                break;

            case E_STRICT:
            case E_USER_DEPRECATED:
            case E_DEPRECATED:
            default:
                $type = 'deprecated';
                $syslog_type = LOG_INFO;
                $handling_method = 'SKIP'; // We always skip these, as they should be fixed during the Composr development cycle and should not concern users. Often we cannot even deal with them until we remove support for an old version, so have to leave a transition period.
                break;
        }
        if (function_exists('get_option')) {
            if ($type == 'deprecated') {
                $handling_method = get_option('error_handling_' . $type);
            } else {
                $handling_method = get_option('error_handling_' . $type . 's');
            }
        }
        if ($GLOBALS['DEV_MODE']) {
            $handling_method = 'FATAL';
        }

        // It's incredibly minor, so it's probably best to continue
        if ($handling_method == 'SKIP') {
            return true; // No bubbling back to PHP
        }

        // So error suppress works again
        $GLOBALS['DYING_BADLY'] = false;

        // Strip down path for security
        if (substr(str_replace(DIRECTORY_SEPARATOR, '/', $errfile), 0, strlen(get_file_base() . '/')) == str_replace(DIRECTORY_SEPARATOR, '/', get_file_base() . '/')) {
            $errfile = substr($errfile, strlen(get_file_base() . '/'));
        }

        // Special handling for memory errors
        if (strpos($errstr, 'Allowed memory') !== false) {
            global $REQUIRED_CODE;
            if (!array_key_exists('failure', $REQUIRED_CODE)) {
                $php_error_label = $errstr . ' in ' . $errfile . ' on line ' . strval($errline) . ' @ ' . get_self_url_easy(true); // We really want to know the URL where this is happening (normal PHP error logging does not include it)!
                if ((!empty($_SERVER['REQUEST_METHOD'])) && ($_SERVER['REQUEST_METHOD'] != 'GET')) {
                    $php_error_label .= ' [' . $_SERVER['REQUEST_METHOD'] . ']';
                }

                if ((function_exists('syslog')) && (GOOGLE_APPENGINE)) {
                    syslog($syslog_type, $php_error_label);
                }
                if (php_function_allowed('error_log')) {
                    @error_log('PHP ' . (function_exists('cms_ucwords_ascii') ? cms_ucwords_ascii($type) : ucwords($type)) . ': ' . $php_error_label, 0);
                }

                critical_error('EMERGENCY', $errstr . escape_html(' [' . $errfile . ' at ' . strval($errline) . ']'));
            }
        }

        // Handle the error
        require_code('failure');
        _composr_error_handler($type, $errno, $errstr, $errfile, $errline, $syslog_type, $handling_method);
        return true; // No bubbling back to PHP
    }

    return false; // Bubbles back to PHP, which will do nothing if "@" was used
}

/**
 * Find whether the browser session is set to be doing a hard cache-empty refresh.
 *
 * @return boolean Whether the browser session is set to be doing a hard cache-empty refresh
 */
function is_browser_decaching() : bool
{
    static $browser_decaching_cache = null;
    if ($browser_decaching_cache !== null) {
        return $browser_decaching_cache;
    }

    if (GOOGLE_APPENGINE) {
        $browser_decaching_cache = false;
        return false; // Decaching by mistake is real-bad when Google Cloud Storage is involved
    }

    if ((defined('DO_PLANNED_DECACHE')) && (is_writable(get_file_base() . '/_config.php'))) { // Used by decache.php
        $config_file_orig = cms_file_get_contents_safe(get_file_base() . '/_config.php', FILE_READ_LOCK);
        $config_file = $config_file_orig;
        $config_file = rtrim(str_replace(['if (!defined(\'DO_PLANNED_DECACHE\')) ', 'define(\'DO_PLANNED_DECACHE\', true);'], ['', ''], $config_file)) . "\n\n";
        require_code('files');
        cms_file_put_contents_safe(get_file_base() . '/_config.php', $config_file, FILE_WRITE_FIX_PERMISSIONS);
        $browser_decaching_cache = true;
        return true;
    }

    if (get_value('ran_once') === null) { // Track whether Composr has run at least once
        set_value('ran_once', '1');
        $browser_decaching_cache = true;
        return true;
    }

    return false; // The below technique stopped working well, Chrome sends cache-control too freely

    /*
    $header_method = ($_SERVER['HTTP_CACHE_CONTROL'] == 'no-cache') && ($_SERVER['REQUEST_METHOD'] != 'POST') && ((!function_exists('browser_matches')));
    $BROWSER_DECACHING = (($header_method) && ((array_key_exists('FORUM_DRIVER', $GLOBALS)) && (has_actual_page_access(get_member(), 'admin_cleanup')) || ($GLOBALS['IS_ACTUALLY_ADMIN'])));
    return $BROWSER_DECACHING;
    */
}

/**
 * Find out what script is running.
 *
 * @return ID_TEXT The script running (usually 'index')
 */
function current_script() : string
{
    // Strip down current URL so we can do a simple compare
    global $WHAT_IS_RUNNING_CACHE;
    if ($WHAT_IS_RUNNING_CACHE === null) {
        $script_name = $_SERVER['SCRIPT_NAME'];
        $stripped_current_url = basename($script_name);
        $WHAT_IS_RUNNING_CACHE = substr($stripped_current_url, 0, strpos($stripped_current_url, '.'));
    }
    return $WHAT_IS_RUNNING_CACHE;
}

/**
 * Find whether a certain script is being run to get here.
 *
 * @param  string $is_this_running Script filename (canonically we want NO .php file type suffix)
 * @return boolean Whether the script is running
 */
function running_script(string $is_this_running) : bool
{
    if (strpos($_SERVER['SCRIPT_NAME'], '/_tests/') !== false) {
        return false;
    }

    // First check cache
    global $RUNNING_SCRIPT_CACHE;
    if (isset($RUNNING_SCRIPT_CACHE[$is_this_running . '.php'])) {
        return $RUNNING_SCRIPT_CACHE[$is_this_running . '.php'];
    }
    if (isset($RUNNING_SCRIPT_CACHE[$is_this_running])) {
        return $RUNNING_SCRIPT_CACHE[$is_this_running];
    }

    // Do the stem compare
    $answer = (current_script() === $is_this_running);

    // Cache and return result
    $RUNNING_SCRIPT_CACHE[$is_this_running] = $answer;
    return $answer;
}

/**
 * This is a intended to output an informational exit at the same time as terminating execution.
 *
 * @param  mixed $text The error message, provided in plain-text format or as HTML via do_lang_tempcode/protect_from_escaping (string or Tempcode)
 * @param  ?boolean $support_match_key_messages Whether match key messages / redirects should be supported (null: detect)
 * @exits
 */
function inform_exit($text, ?bool $support_match_key_messages = null)
{
    require_code('failure'); // It's in failure.php although this isn't REALLY failure. Still it's an exceptional event so we can't justify loading the code as global.
    _generic_exit($text, 'INFORM_SCREEN', $support_match_key_messages);
}

/**
 * This is a less-revealing alternative to fatal_exit, that is used for user-errors/common-corruption-scenarios.
 *
 * @param  mixed $text The error message, provided in plain-text format or as HTML via do_lang_tempcode/protect_from_escaping (string or Tempcode)
 * @param  boolean $support_match_key_messages Whether match key messages / redirects should be supported
 * @param  boolean $log_error Whether to log the error
 * @param  ?integer $http_status HTTP status to set (null: none)
 * @param  ?Tempcode $title Title to use show (null: default)
 * @param  ?URLPATH $image_url Image to show (null: default)
 * @exits
 */
function warn_exit($text, bool $support_match_key_messages = false, bool $log_error = false, ?int $http_status = 500, ?object $title = null, ?string $image_url = null)
{
    require_code('failure');
    suggest_fatalistic();
    _generic_exit($text, 'WARN_SCREEN', $support_match_key_messages, $log_error, $http_status, $title, $image_url);
    if (running_script('cron_bridge')) {
        relay_error_notification(is_object($text) ? $text->evaluate() : escape_html($text), false, 'error_occurred_cron');
    }
}

/**
 * Do a fatal exit, echo the header (if possible) and an error message, followed by a debugging back-trace.
 * It also adds an entry to the error log, for reference.
 *
 * @param  mixed $text The error message, provided in plain-text format or as HTML via do_lang_tempcode/protect_from_escaping (string or Tempcode)
 * @param  boolean $log_error Whether to log the error
 * @param  integer $http_status HTTP status to set
 * @exits
 */
function fatal_exit($text, bool $log_error = true, int $http_status = 500)
{
    require_code('failure');
    _generic_exit($text, 'FATAL_SCREEN', false, $log_error, $http_status);
}

/**
 * Log a hackattack, then displays an error message. It also attempts to send an e-mail to the staff alerting them of the hackattack.
 *
 * @param  ID_TEXT $reason The reason for the hack-attack. This has to be a language string codename
 * @param  SHORT_TEXT $reason_param_a A parameter for the hack-attack language string (this should be based on a unique ID, preferably)
 * @param  SHORT_TEXT $reason_param_b A more illustrative parameter, which may be anything (e.g. a title)
 * @exits
 */
function log_hack_attack_and_exit(string $reason, string $reason_param_a = '', string $reason_param_b = '')
{
    require_code('failure');
    _log_hack_attack_and_exit($reason, $reason_param_a, $reason_param_b);
}

/**
 * Get the major version of your installation.
 *
 * @return integer The major version number of your installation
 */
function cms_version() : int
{
    if (!function_exists('cms_version_number')) {
        return -1;
    }
    return intval(cms_version_number());
}

/**
 * Get the full string version of Composr that you are running, in 'pretty' format.
 * This is (and must be kept) equivalent to get_version_pretty__from_dotted(get_version_dotted()).
 *
 * @return string The string saying the full Composr version number
 */
function cms_version_pretty() : string
{
    if (!function_exists('cms_version_minor')) {
        return 'unknown';
    }
    $minor = cms_version_minor();
    $dotted = strval(cms_version()) . (($minor == '') ? '' : ('.' . $minor));
    return preg_replace('#\.(alpha|beta|RC)#', ' ${1}', $dotted);
}

/**
 * Get the type of forums installed.
 *
 * @return string The type of forum installed
 */
function get_forum_type() : string
{
    global $SITE_INFO;
    if (!isset($SITE_INFO['forum_type'])) {
        $SITE_INFO['forum_type'] = 'cns';
    }
    if ($SITE_INFO['forum_type'] === 'ocf') {
        $SITE_INFO['forum_type'] = 'cns'; // LEGACY
    }
    return $SITE_INFO['forum_type'];
}

/**
 * Get the installed forum base URL.
 *
 * @param  boolean $forum_base Whether to get the base directory of the forum. Unless running Conversr, this makes no difference - if possibly running Conversr, you need to think about this parameter: are you trying to reach the MSN-central-site or just a link to the forums?
 * @return URLPATH The installed forum base URL
 */
function get_forum_base_url(bool $forum_base = false) : string
{
    global $SITE_INFO;

    if (empty($SITE_INFO['forum_base_url'])) {
        if (empty($SITE_INFO['board_prefix'])) {
            $SITE_INFO['forum_base_url'] = get_base_url();
        } else {
            $SITE_INFO['forum_base_url'] = $SITE_INFO['board_prefix']; // LEGACY
        }
    }

    switch (get_forum_type()) {
        case 'none':
            return '';

        case 'cns':
            $needs_forum_strip = (substr($SITE_INFO['forum_base_url'], -6) === '/forum') && (substr(get_base_url(), -6) !== '/forum');
            if ((!$forum_base) && ($needs_forum_strip)) {
                return substr($SITE_INFO['forum_base_url'], 0, strlen($SITE_INFO['forum_base_url']) - 6);
            }
            if (($forum_base) && (!$needs_forum_strip)) {
                return $SITE_INFO['forum_base_url'] . '/forum';
            }
            break;
    }

    return $SITE_INFO['forum_base_url'];
}

/**
 * Get the Composr cookie path.
 *
 * @return ?string The Composr cookie path (null: no special path, global)
 */
function get_cookie_path() : ?string
{
    global $SITE_INFO;
    $ret = array_key_exists('cookie_path', $SITE_INFO) ? $SITE_INFO['cookie_path'] : '/';
    return ($ret == '') ? null : $ret;
}

/**
 * Get the Composr cookie domain.
 *
 * @return string The Composr cookie domain (blank: current domain)
 */
function get_cookie_domain() : string
{
    global $SITE_INFO;
    $ret = array_key_exists('cookie_domain', $SITE_INFO) ? $SITE_INFO['cookie_domain'] : '';
    return ($ret == '') ? '' : $ret;
}

/**
 * Get the number of days to store our cookies.
 *
 * @return float The number of days to store our cookies
 */
function get_cookie_days() : float
{
    global $SITE_INFO;
    return array_key_exists('cookie_days', $SITE_INFO) ? floatval($SITE_INFO['cookie_days']) : 1825.0;
}

/**
 * Get the site name.
 *
 * @return string The name of the site
 */
function get_site_name() : string
{
    return get_option('site_name');
}

/**
 * Find whether we are running in safe mode.
 *
 * @return boolean Whether we are in safe mode
 */
function in_safe_mode() : bool
{
    global $SITE_INFO;
    if (!empty($SITE_INFO['safe_mode'])) {
        if (!isset($_GET['keep_safe_mode'])) {
            return ($SITE_INFO['safe_mode'] == '1'); // Generally more robust and fast than specifying as a URL parameter
        }
    }

    $backdoor_ip = has_backdoor_ip_triggered();

    global $CHECKING_SAFEMODE, $REQUIRED_CODE;
    if (!$backdoor_ip) {
        if (!isset($REQUIRED_CODE['lang']) || !$REQUIRED_CODE['lang']) {
            return false; // Too early. We can get in horrible problems when doing get_member() below if lang hasn't loaded yet
        }
        if ($CHECKING_SAFEMODE) {
            return false; // Stops infinite loops (e.g. Check safe mode > Check access > Check usergroups > Check implicit usergroup hooks > Check whether to look at custom implicit usergroup hooks [i.e. if not in safe mode])
        }
    }
    $CHECKING_SAFEMODE = true;
    static $url_says = null;
    if ($url_says === null) {
        $url_says = (get_param_integer('keep_safe_mode', 0) == 1);
    }
    $ret = (($url_says) && (
        $backdoor_ip ||
        (isset($GLOBALS['IS_ACTUALLY_ADMIN']) && ($GLOBALS['IS_ACTUALLY_ADMIN'])) ||
        (strpos($_SERVER['SCRIPT_NAME'], '/_tests/') !== false) ||
        (!array_key_exists('FORUM_DRIVER', $GLOBALS)) ||
        ($GLOBALS['FORUM_DRIVER'] === null) ||
        (!function_exists('get_member')) ||
        (@cms_empty_safe($GLOBALS['MEMBER_CACHED'])) ||
        ($GLOBALS['FORUM_DRIVER']->is_super_admin(get_member())))
    );
    $CHECKING_SAFEMODE = false;
    return $ret;
}

/**
 * Find the URL to a certain entry point script, located in the root directory, top level of a zone directory, data directory, or data_custom directory.
 * Why this function? Because Composr allows these to be moved around between zone directories, to suit site .htaccess requirements).
 *
 * @param  string $name The codename of the needed script
 * @param  boolean $append_keep Whether to append keep variables
 * @return URLPATH The URL to the script
 */
function find_script(string $name, bool $append_keep = false) : string
{
    $append = '';
    if ($append_keep) {
        $keep = symbol_tempcode('KEEP', ['1']);
        $append .= $keep->evaluate();
    }

    static $find_script_cache = [];
    if (empty($find_script_cache)) {
        if (function_exists('persistent_cache_get')) {
            $find_script_cache = persistent_cache_get('SCRIPT_PLACES');
        }
        if ($find_script_cache === null) {
            $find_script_cache = [];
        }
    }
    if (isset($find_script_cache[$name][$append_keep])) {
        return $find_script_cache[$name][$append_keep] . $append;
    }

    $zones = [];
    if (function_exists('get_zone_name')) {
        $zones[] = get_zone_name();
    }
    if (!in_safe_mode()) {
        $zones[] = 'data_custom';
    }
    $zones[] = 'data';
    if (function_exists('find_all_zones')) {
        $zones = array_merge($zones, find_all_zones());
    }
    foreach ($zones as $zone) {
        if (is_file(get_file_base() . '/' . $zone . (($zone == '') ? '' : '/') . $name . '.php')) {
            $ret = get_base_url() . '/' . $zone . (($zone == '') ? '' : '/') . $name . '.php';
            $find_script_cache[$name][$append_keep] = $ret;
            if (function_exists('persistent_cache_set')) {
                persistent_cache_set('SCRIPT_PLACES', $find_script_cache);
            }
            return $ret . $append;
        }
    }
    $ret = get_base_url() . '/site/' . $name . '.php';
    $find_script_cache[$name][$append_keep] = $ret;
    if (function_exists('persistent_cache_set')) {
        persistent_cache_set('SCRIPT_PLACES', $find_script_cache);
    }
    return $ret . $append;
}

/**
 * Get the base URL (the minimum fully qualified URL to our installation).
 *
 * @param  ?ID_TEXT $zone_for The zone the link is for (null: root zone)
 * @return URLPATH The base URL
 */
function get_base_url(?string $zone_for = null) : string
{
    global $VIRTUALISED_ZONES_CACHE;
    static $base_url_cache = null;

    if ($VIRTUALISED_ZONES_CACHE === null) {
        require_code('zones');
        get_zone_name();
    }

    if (($base_url_cache !== null) && ((!$VIRTUALISED_ZONES_CACHE) || ($zone_for === null))) {
        return $base_url_cache . (empty($zone_for) ? '' : ('/' . $zone_for));
    }

    global $SITE_INFO;
    if ((!isset($SITE_INFO)) || (empty($SITE_INFO['base_url']))) { // Try and autodetect the base URL if it's not configured
        $domain = get_domain();
        $script_name_path = dirname(isset($_SERVER['SCRIPT_NAME']) ? $_SERVER['SCRIPT_NAME'] : '');
        $script_name_path = str_replace(DIRECTORY_SEPARATOR, '/', $script_name_path);
        if (($GLOBALS['RELATIVE_PATH'] === '') || (strpos($script_name_path, $GLOBALS['RELATIVE_PATH']) !== false)) {
            $script_name_path = preg_replace('#(^|/)' . preg_quote($GLOBALS['RELATIVE_PATH'], '#') . '$#', '', $script_name_path);
        } else {
            $cnt = substr_count($GLOBALS['RELATIVE_PATH'], '/');
            for ($i = 0; $i <= $cnt; $i++) {
                $script_name_path = dirname($script_name_path);
            }
        }
        $base_url = (tacit_https() ? 'https://' : 'http://') . $domain;
        if (isset($_SERVER['SERVER_PORT'])) {
            $port = $_SERVER['SERVER_PORT'];
            if ($port != (tacit_https() ? '443' : '80')) {
                $base_url .= ':' . $port;
            }
        }
        $base_url .= str_replace('%2F', '/', rawurlencode($script_name_path));
        $SITE_INFO['base_url'] = $base_url;
    }

    // Lookup
    $base_url = $SITE_INFO['base_url'];
    global $CURRENT_SHARE_USER;
    if ($CURRENT_SHARE_USER !== null) {
        // Put in access domain, in case there is a custom domain attached to the site
        $domain = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '';
        $base_url = preg_replace('#^http(s)?://([\w]+\.)?' . preg_quote($SITE_INFO['custom_share_domain'], '#') . '#', 'http$1://' . $domain, $base_url);
    }
    $found_mapping = false;
    if ($VIRTUALISED_ZONES_CACHE) { // Special searching if we are doing a complex zone scheme
        $zone_doing = ($zone_for === null) ? '' : str_replace('/', '', $zone_for);

        if (!empty($SITE_INFO['ZONE_MAPPING_' . $zone_doing])) {
            $domain = $SITE_INFO['ZONE_MAPPING_' . $zone_doing][0];
            $path = $SITE_INFO['ZONE_MAPPING_' . $zone_doing][1];
            $base_url = ((whole_site_https()) ? 'https://' : 'http://') . $domain;
            if ($path !== '') {
                $base_url .= '/' . $path;
            }
            $found_mapping = true;
        }
    }

    // Work out correct variant
    if ((!$VIRTUALISED_ZONES_CACHE) || ($zone_for === null)) {
        $base_url_cache = $base_url;
    }

    if (!$found_mapping) { // Scope inside the correct zone
        $base_url .= (empty($zone_for) ? '' : ('/' . $zone_for));
    }

    // Done
    return $base_url;
}

/**
 * Get the base URL (the minimum fully qualified URL to our personal data installation). For a shared install, or a GAE-install, this is different to the base URL.
 *
 * @return URLPATH The base URL
 */
function get_custom_base_url() : string
{
    global $SITE_INFO;
    if (!empty($SITE_INFO['custom_base_url'])) {
        return $SITE_INFO['custom_base_url'];
    }
    if (empty($SITE_INFO['custom_base_url_stub'])) {
        return get_base_url();
    }

    // Note that HTTPS is not supported for shared installs
    $u = current_share_user();
    if ($u === null) {
        return get_base_url();
    }
    return $SITE_INFO['custom_base_url_stub'] . '/' . $u;
}

/**
 * Function to get a base URL for an Conversr relative-URL. The situation is complex as it needs to take into account Conversr multi-site-network's, locally defined theme images, and shared-installs (Demonstratr style).
 *
 * @param  URLPATH $at Short base URL we need to probe
 * @return URLPATH The appropriate base URL
 */
function get_complex_base_url(string $at) : string
{
    return ((get_forum_base_url() != get_base_url()) ? get_forum_base_url() : ((substr($at, 0, 22) === 'themes/default/images/') ? get_base_url() : get_custom_base_url()));
}

/**
 * Get a parameter value (either POST *or* GET, i.e. like $_REQUEST[$name]), or the default if neither can be found.
 * Implements additional security over the direct PHP access mechanism which should not be used.
 * If the requested POST parameter is an array, it will be returned as a comma-delimited string.
 * Use with caution, as this has very limited CSRF protection compared to post_param_string.
 *
 * @param  ID_TEXT $name The name of the parameter to get
 * @param  ?~mixed $default The default value to give the parameter if the parameter value is not defined (null: allow missing parameter) (false: give error on missing parameter)
 * @param  integer $filters A bitmask of INPUT_FILTER_* filters
 * @return ?string The parameter value (null: missing)
 */
function either_param_string(string $name, $default = false, int $filters = INPUT_FILTER_DEFAULT_POST) : ?string
{
    $ret = __param(array_merge($_POST, $_GET), $name, $default);
    if ($ret === null) {
        return null;
    }

    if ($ret === $default) {
        if ($default === null) {
            return null;
        }

        return $ret;
    }

    if ((($filters & INPUT_FILTER_URL_SCHEMES) != 0) && (strpos($ret, ':') !== false) && (function_exists('cms_urldecode_post_process'))) {
        $ret = cms_urldecode_post_process($ret);
    }

    // GET is prioritised over POST, so change our filters accordingly
    $is_get = isset($_GET[$name]);
    if ($is_get) {
        $filters &= ~INPUT_FILTER_DEFAULT_POST;
        $filters &= INPUT_FILTER_DEFAULT_GET;
    }

    require_code('input_filter');
    check_input_field_string($name, $ret, !$is_get, $filters);

    return $ret;
}

/**
 * Get the value of the specified POST parameter (i.e. like $_POST[$name]) if it is passed, or the default otherwise.
 * Implements additional security over the direct PHP access mechanism which should not be used.
 * If the requested POST parameter is an array, it will be returned as a comma-delimited string.
 *
 * @param  ID_TEXT $name The name of the parameter to get
 * @param  ?~mixed $default The default value to give the parameter if the parameter value is not defined (null: allow missing parameter) (false: give error on missing parameter)
 * @param  integer $filters A bitmask of INPUT_FILTER_* filters
 * @return ?string The parameter value (null: missing)
 */
function post_param_string(string $name, $default = false, int $filters = INPUT_FILTER_DEFAULT_POST) : ?string
{
    $ret = __param($_POST, $name, $default, false, true);

    if ($ret === null) {
        return null;
    }
    if ((trim($ret) === '') && ($default !== '') && (array_key_exists('require__' . $name, $_POST)) && ($_POST['require__' . $name] !== '0')) {
        // __param already triggered improperly_filled_in_post for completely missing or unparsable POST parameters.
        // This branch handles cases of BLANK parameters for when the POST environment said blank parameters were not acceptable.
        // A blank parameter is never remapped to $default - the blank is considered its true value, it is just a question of whether that is acceptable as a value vs an error.

        if ($default === null) {
            return null;
        }

        require_code('failure');
        improperly_filled_in_post($name);
    }

    if (($ret !== '') && (($filters & INPUT_FILTER_WORDFILTER) != 0) && (addon_installed('wordfilter'))) {
        require_code('wordfilter');
        if ($ret !== $default) {
            $ret = check_wordfilter($ret, $name);
        }
    }
    if ($ret !== null) {
        $ret = unixify_line_format($ret);

        require_code('comcode_cleanup');

        if ((($filters & INPUT_FILTER_DOWNLOAD_ASSOCIATED_MEDIA) != 0) && (post_param_integer($name . '_download_associated_media', 0) === 1)) {
            download_associated_media($ret);
        }

        if ((((($filters & INPUT_FILTER_WYSIWYG_TO_COMCODE) != 0) || ($filters & INPUT_FILTER_COMCODE_CLEANUP) != 0)) && (isset($_POST[$name . '__is_wysiwyg'])) && ($_POST[$name . '__is_wysiwyg'] === '1')) {
            $ret = cms_trim($ret, true); // We do the trimming because CKEditor adds a trailing nbsp
        }
        comcode_page_hints_post($name, $ret);
    }

    if ((($filters & INPUT_FILTER_WYSIWYG_TO_COMCODE) != 0) && (isset($_POST[$name . '__is_wysiwyg'])) && ($_POST[$name . '__is_wysiwyg'] === '1')) {
        if (trim($ret) === '') {
            $ret = '';
        } else {
            require_code('comcode_from_html');
            $ret = trim(semihtml_to_comcode($ret));

            // Now that we potentially stripped down our code a lot, we can try again and see if it is blank
            if ((trim($ret) === '') && ($default !== '') && (array_key_exists('require__' . $name, $_POST)) && ($_POST['require__' . $name] !== '0')) {
                if ($default === null) {
                    return null;
                }

                require_code('failure');
                improperly_filled_in_post($name);
            }
        }
    } else {
        if ((($filters & INPUT_FILTER_COMCODE_CLEANUP) != 0) && (substr($ret, 0, 10) === '[semihtml]') && (substr(trim($ret), -11) === '[/semihtml]')) {
            $_ret = trim($ret);
            $_ret = substr($_ret, 10, strlen($_ret) - 11 - 10);
            if (strpos($_ret, '[semihtml') === false) {
                require_code('comcode_from_html');
                $ret = trim(semihtml_to_comcode($_ret));
            }
        }
    }

    require_code('input_filter');

    if ((!$GLOBALS['BOOTSTRAPPING']) && (!$GLOBALS['MICRO_AJAX_BOOTUP'])) {
        if ($ret !== $default) {
            check_posted_field($name, $ret, $filters);
        }

        // Custom fields.xml filter system
        if (($filters & INPUT_FILTER_FIELDS_XML) != 0) {
            $ret = filter_form_field_default($name, $ret, true);
        }
    }

    if ($ret === $default) {
        return $ret;
    }

    if ((($filters & INPUT_FILTER_URL_SCHEMES) != 0) && (strpos($ret, ':') !== false) && (function_exists('cms_urldecode_post_process'))) {
        $ret = cms_urldecode_post_process($ret);
    }

    check_input_field_string($name, $ret, true, $filters);

    return $ret;
}

/**
 * Get the value of the specified GET parameter (i.e. like $_GET[$name]) if it is passed, or the default otherwise.
 * Implements additional security over the direct PHP access mechanism which should not be used.
 * If the requested POST parameter is an array, it will be returned as a comma-delimited string.
 *
 * @param  ID_TEXT $name The name of the parameter to get
 * @param  ?~mixed $default The default value to give the parameter if the parameter value is not defined (null: allow missing parameter) (false: give error on missing parameter)
 * @param  integer $filters A bitmask of INPUT_FILTER_* filters
 * @return ?string The parameter value (null: missing)
 */
function get_param_string(string $name, $default = false, int $filters = INPUT_FILTER_DEFAULT_GET) : ?string
{
    $ret = __param($_GET, $name, $default);
    if (($ret === '') && (isset($_GET['require__' . $name])) && ($default !== $ret) && ($_GET['require__' . $name] !== '0')) {
        // We didn't give some required input. See equivalent comments in the post_param_string function
        set_http_status_code(400);
        warn_exit(do_lang_tempcode('NO_PARAMETER_SENT', escape_html($name)));
    }

    if ($ret === $default) {
        return $ret;
    }

    if ((($filters & INPUT_FILTER_URL_SCHEMES) != 0) && (strpos($ret, ':') !== false) && (function_exists('cms_urldecode_post_process'))) {
        $ret = cms_urldecode_post_process($ret);
    }

    require_code('input_filter');
    check_input_field_string($name, $ret, false, $filters);

    if ($ret === false) { // Should not happen, but have seen in the wild via malicious bots sending corrupt URLs
        $ret = $default;
    }

    return $ret;
}

/**
 * Helper function to load up a GET/POST parameter.
 * If the requested POST parameter is an array, it will be returned as a comma-delimited string.
 *
 * @param  array $array The array we're extracting parameters from
 * @param  string $name The name of the parameter
 * @param  ?~mixed $default The default value to give the parameter if the parameter value is not defined (null: allow missing parameter) (false: give error on missing parameter)
 * @param  boolean $integer Whether the parameter has to be an integer
 * @param  ?boolean $posted Whether the parameter is a POST parameter (null: undetermined)
 * @return ?string The value of the parameter (null: missing)
 *
 * @ignore
 */
function __param(array $array, string $name, $default, bool $integer = false, ?bool $posted = false) : ?string
{
    if ((!isset($array[$name])) || ($array[$name] === false) || (($integer) && ($array[$name] === ''))) {
        if ($default !== false) {
            return $default;
        }

        require_code('failure');
        improperly_filled_in($name, $posted, $array);
    }

    $val = $array[$name];
    if (is_array($val)) {
        $val = @trim(implode(',', $val), ' ,'); // @ because it could be any complex arbitrary data structure (Composr does not do this, but bots may generate such URLs)
    }

    return $val;
}

/**
 * Do a wildcard match by converting to a regular expression.
 * Supports the '%' and '_' wildcards and '\' escaping of them (as per most SQL implementations of LIKE).
 *
 * @param  string $target The haystack
 * @param  string $expression The needle (a wildcard expression)
 * @param  boolean $full_cover Whether full-coverage is required
 * @param  boolean $case_sensitive Whether it is case sensitive
 * @return boolean Whether we have a match
 */
function simulated_wildcard_match(string $target, string $expression, bool $full_cover = false, bool $case_sensitive = false) : bool
{
    $rexp = '';
    $len = strlen($expression);
    $escape_flag = false;
    for ($i = 0; $i < $len; $i++) {
        $c = $expression[$i];
        if ($escape_flag) {
            $c = preg_quote($c, '#');
        } else {
            if ($c == '%' || $c == '*') {
                $c = '.*';
            } elseif ($c == '_' || $c == '?') {
                $c = '.';
            } elseif ($c == '\\') {
                $escape_flag = true;
                continue;
            } else {
                $c = preg_quote($c, '#');
            }
        }
        $rexp .= $c;
    }

    if ($full_cover) {
        $rexp = '^' . $rexp . '$';
    }

    $_rexp = '#' . $rexp . '#';
    if (!$case_sensitive) {
        $_rexp .= 'i';
    }

    return preg_match($_rexp, $target) != 0;
}

/**
 * This function is the integeric partner of either_param_string, as it returns the value as an integer.
 * You should always use integer specified versions when inputting integers, for the added security that type validation allows. If the value is of the wrong type, it indicates a hack attempt and will be logged.
 *
 * @param  ID_TEXT $name The name of the parameter to get
 * @param  ?~mixed $default The default value to give the parameter if the parameter value is not defined or the empty string (null: allow missing parameter) (false: give error on missing parameter)
 * @return ?integer The parameter value (null: not set, and null given as default)
 */
function either_param_integer(string $name, $default = false) : ?int
{
    $ret = __param(array_merge($_POST, $_GET), $name, ($default === false) ? $default : (($default === null) ? '' : strval($default)), true, null); // $_REQUEST contains cookies too, so can't use
    if (($default === null) && ($ret === '')) {
        return null;
    }
    if (!is_numeric($ret)) {
        require_code('failure');
        $ret = _param_invalid_integer($name, $ret, true);
    }
    $reti = intval($ret);
    if (($reti > 2147483647) || ($reti < -2147483648)) { // TODO: #3046 in tracker
        require_code('failure');
        _param_invalid_integer($name, null, true);
    }
    return $reti;
}

/**
 * This function is the integeric partner of post_param_string, as it returns the value as an integer.
 *
 * @param  ID_TEXT $name The name of the parameter to get
 * @param  ~?mixed $default The default value to give the parameter if the parameter value is not defined or the empty string (null: allow missing parameter) (false: give error on missing parameter)
 * @param  integer $filters A bitmask of INPUT_FILTER_* filters
 * @return ?integer The parameter value (null: not set, and null given as default)
 */
function post_param_integer(string $name, $default = false, int $filters = INPUT_FILTER_DEFAULT_POST) : ?int
{
    $ret = __param($_POST, $name, ($default === false) ? $default : (($default === null) ? '' : strval($default)), true, true);

    if ((!$GLOBALS['BOOTSTRAPPING']) && (!$GLOBALS['MICRO_AJAX_BOOTUP'])) {
        if (((is_string($default)) && ($ret != strval($default))) || (!is_string($default))) {
            check_posted_field($name, $ret, $filters);
        }

        // Custom fields.xml filter system
        $ret = filter_form_field_default($name, $ret, true);
    }

    if (($default === null) && ($ret === '')) {
        return null;
    }
    if (!is_numeric($ret)) {
        require_code('failure');
        $ret = _param_invalid_integer($name, $ret, true);
    }
    if ($ret === '0') {
        return 0;
    }
    if ($ret === '1') {
        return 1;
    }
    $reti = intval($ret);
    $retf = floatval($reti);
    if (($retf > 2147483647.0) || ($retf < -2147483648.0)) { // TODO: #3046 in tracker
        require_code('failure');
        _param_invalid_integer($name, null, true);
    }
    return $reti;
}

/**
 * This function is the integeric partner of get_param_string, as it returns the value as an integer.
 *
 * @param  ID_TEXT $name The name of the parameter to get
 * @param  ?~mixed $default The default value to give the parameter if the parameter value is not defined or the empty string (null: allow missing parameter) (false: give error on missing parameter)
 * @param  boolean $not_string_ok If a string is given, use the default parameter rather than giving an error (only use this if you are suffering from a parameter conflict situation between different parts of Composr)
 * @return ?integer The parameter value (null: not set, and null given as default)
 */
function get_param_integer(string $name, $default = false, bool $not_string_ok = false) : ?int
{
    $m_default = ($default === false) ? false : (isset($default) ? (($default === 0) ? '0' : strval($default)) : '');
    $ret = __param($_GET, $name, $m_default, true); // do not set $ret to mixed(), breaks bootstrapping
    if ((!isset($default)) && ($ret === '')) {
        return null;
    }
    if (!is_numeric($ret)) {
        if (substr($ret, -1) === '/') {
            $ret = substr($ret, 0, strlen($ret) - 1);
        }
        if (!is_numeric($ret)) {
            $matches = [];
            if (preg_match('#^(\d+)\#[\w]*$#', $ret, $matches) !== 0) { // Bizarre situation (bug in IIS?)
                $ret = $matches[1];
            } else {
                if ($not_string_ok) {
                    return $default;
                }
                require_code('failure');
                $ret = _param_invalid_integer($name, $ret, false);
            }
        }
    }
    if ($ret === '0') {
        return 0;
    }
    if ($ret === '1') {
        return 1;
    }
    $reti = intval($ret);
    $retf = floatval($reti);
    if (($retf > 2147483647.0) || ($retf < -2147483648.0)) { // TODO: #3046 in tracker
        require_code('failure');
        _param_invalid_integer($name, null, false);
    }
    return $reti;
}

/**
 * Make sure that lines are separated by "\n", with no "\r"'s there at all. For Mac data, this will be a flip scenario. For Linux data this will be a null operation. For windows data this will be change from "\r\n" to just "\n". For a realistic scenario, data could have originated on all kinds of platforms, with some editors converting, some situations being inter-platform, and general confusion. Don't make blind assumptions - use this function to clean data, then write clean code that only considers "\n"'s.
 *
 * @param  string $in The data to clean
 * @param  ?ID_TEXT $desired_charset The character set it should be in. We don't do any real conversions using this, only make sure that common problems with fed ISO-8859-1 data are resolved (null: output character set)
 * @return string The cleaned data
 */
function unixify_line_format(string $in, ?string $desired_charset = null) : string
{
    if ($in === '') {
        return $in;
    }

    if ($desired_charset === null) {
        $desired_charset = get_charset();
    }

    static $bom = null;
    if ($bom === null) {
        $bom = chr(0xEF) . chr(0xBB) . chr(0xBF);
    }
    if (substr($in, 0, 3) == $bom) {
        $in = substr($in, 3);
    }

    if (strpos($in, "\r") === false) {
        return $in;
    }

    static $from = null;
    if ($from === null) {
        $from = ["\r\n", '&#8298;', "\r"]; // &#8298; is very odd- seems to come from open office copy & paste
    }
    static $to = null;
    if ($to === null) {
        $to = ["\n", '', "\n"];
    }
    $in = str_replace($from, $to, $in);
    return $in;
}

/**
 * Provides an override point for file synchronisation between mirrored servers. Called after any file creation, deletion or edit.
 *
 * @param  PATH $filename File/directory name to sync on (full path)
 */
function sync_file(string $filename)
{
    global $FILE_BASE, $_MODIFIED_FILES;
    static $has_sync_script = null;
    if ($has_sync_script === null) {
        $has_sync_script = is_file($FILE_BASE . '/data_custom/sync_script.php');
    }
    if ((!$has_sync_script) && (!isset($_MODIFIED_FILES))) {
        return;
    }

    require_code('files2');
    _sync_file($filename);
}

/**
 * Provides an override point for file-move synchronisation between mirrored servers. Called after any rename or move action.
 *
 * @param  PATH $old File/directory name to move from (may be full or relative path)
 * @param  PATH $new File/directory name to move to (may be full or relative path)
 */
function sync_file_move(string $old, string $new)
{
    require_code('files2');
    _sync_file_move($old, $new);
}

/**
 * Performs lots of magic to make sure data encodings are converted correctly. Input, and output too (as often stores internally in UTF or performs automatic dynamic conversions from internal to external charsets).
 *
 * @param  boolean $known_utf8 Whether we know we are working in utf-8. This is the case for AJAX calls.
 */
function convert_request_data_encodings(bool $known_utf8 = false)
{
    global $VALID_ENCODING, $CONVERTED_ENCODING;
    $VALID_ENCODING = true;

    if ($CONVERTED_ENCODING) {
        return; // Already done it
    }

    if (preg_match('#^[\x00-\x7F]*$#', serialize($_POST) . serialize($_GET) . serialize($_FILES)) != 0) { // Simple case, all is ASCII
        $CONVERTED_ENCODING = true;
        return;
    }

    require_code('character_sets');
    _convert_request_data_encodings($known_utf8);
}

/**
 * Recursively clean (erase) the output buffer and turn off output buffering.
 */
function cms_ob_end_clean()
{
    while (ob_get_level() > 0) {
        if (!@ob_end_clean()) {
            break; // Cannot delete special buffer, likely output compression
        }
    }
    if (function_exists('init_output_compression')) {
        reinit_output_compression();
    }
}

/**
 * Check if a string starts with a substring.
 *
 * @param  string $haystack The haystack
 * @param  string $needle The needle
 * @return boolean Whether the haystack starts with the needle
 */
function starts_with(string $haystack, string $needle) : bool
{
    $length = strlen($needle);
    return (substr($haystack, 0, $length) === $needle);
}

/**
 * Check if a string ends with a substring.
 *
 * @param  string $haystack The haystack
 * @param  string $needle The needle
 * @return boolean Whether the haystack ends with the needle
 */
function ends_with(string $haystack, string $needle) : bool
{
    $length = strlen($needle);
    return ($length === 0) || (substr($haystack, -$length) === $needle);
}

/**
 * Get the date/time string as we log it. Designed to be consistent with how PHP puts dates into the error-log.
 *
 * @return string Date/time string
 */
function loggable_date() : string
{
    return gmdate('[d-M-Y H:i:s \U\T\C]');
}

/**
 * Determine whether a variable is empty (empty being defined differently for different types).
 * The string '0' is NOT considered empty, unlike the default PHP empty language construct.
 * Generally this function is used if you don't want to check for array index presence, non-nullness, and non-blank-stringness via individual ANDd clauses.
 * As this is a function and not a language construct, reference to non-set variables or array indices need guarding using @.
 *
 * @param  mixed $var Input
 * @return boolean Whether it is CONSIDERED empty
 */
function cms_empty_safe($var) : bool
{
    return (empty($var)) && ($var !== '0');
}

/**
 * Find if we are currently inside a logging in screen.
 *
 * @return boolean Whether we are
 */
function currently_logging_in() : bool
{
    global $CURRENTLY_LOGGING_IN;
    if ($CURRENTLY_LOGGING_IN) {
        return true;
    }
    $page = get_param_string('page', '', INPUT_FILTER_GET_COMPLEX); // Not get_page_name for bootstrap order reasons
    return ($page == 'login');
}
