<?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: strtoupper|strtolower|ucfirst*/

/*
    the equivalent of global2 for "Software not yet installed" situations like the installer or backup restorer

    The software can install basically from the real final code, except for...
    -- global.php
    -- global2.php
    -- users.php
    --  things that depend on functionality of those that hasn't been emulated here
    This file emulates cut-down versions of the code in those files, for the most part.
    Once the software is installed, this file is never used except by the upgrader.
*/

/**
 * Standard code module initialisation function.
 *
 * @ignore
 */
function init__minikernel()
{
    // Input filtering constants
    if (!defined('INPUT_FILTER_WORDFILTER')) {
        // 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);
    }

    fixup_bad_php_env_vars_pre();
    fixup_bad_php_env_vars();

    global $EXITING;
    $EXITING = null;

    global $MICRO_BOOTUP;
    $MICRO_BOOTUP = false;

    global $EXTERNAL_CALL;
    $EXTERNAL_CALL = false;

    global $IN_SELF_ROUTING_SCRIPT;
    $IN_SELF_ROUTING_SCRIPT = false;

    global $XSS_DETECT;
    $XSS_DETECT = false;

    global $CSP_ENABLED;
    $CSP_ENABLED = false;

    set_error_handler('cms_error_handler');
    if (function_exists('register_shutdown_function')) {
        register_shutdown_function('catch_fatal_errors');
    }
    global $SUPPRESS_ERROR_DEATH;
    $SUPPRESS_ERROR_DEATH = [false];

    cms_ini_set('ocproducts.type_strictness', '1');

    cms_ini_set('date.timezone', 'UTC');

    set_http_caching(null);

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

    global $DISABLED_MEMORY_LIMIT;
    $DISABLED_MEMORY_LIMIT = false;
}

/**
 * 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($last_modified, $public = false, $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));
        @header_remove('Pragma');
        @header_remove('Expires');
    }
}

/**
 * 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($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()
{
    global $SUPPRESS_ERROR_DEATH;
    return end($SUPPRESS_ERROR_DEATH);
}

/**
 * PHP's environment can be a real mess across servers. Cleanup $_SERVER from $_ENV for IIS if needed.
 * Also see fixup_bad_php_env_vars.
 */
function fixup_bad_php_env_vars_pre()
{
    // Variables may be defined in $_ENV on some servers
    $understood = [
        'DOCUMENT_ROOT',
        'HTTP_ACCEPT',
        'HTTP_ACCEPT_CHARSET',
        'HTTP_ACCEPT_LANGUAGE',
        'HTTP_CLIENT_IP',
        'HTTP_HOST',
        'HTTP_IF_MODIFIED_SINCE',
        'HTTP_ORIGIN',
        'PATH_INFO',
        'HTTP_PREFER',
        'HTTP_RANGE',
        'HTTP_REFERER',
        'HTTP_USER_AGENT',
        'HTTP_X_FORWARDED_FOR',
        'HTTP_X_FORWARDED_PROTO',
        'HTTP_CF_CONNECTING_IP',
        'PHP_SELF',
        'QUERY_STRING',
        'REMOTE_ADDR',
        'REQUEST_METHOD',
        'REQUEST_URI',
        'SCRIPT_FILENAME',
        'SCRIPT_NAME',
        'SERVER_ADDR',
        'SERVER_NAME',
        'SERVER_SOFTWARE',
        'HTTP_AUTHORIZATION',
        'REDIRECT_HTTP_AUTHORIZATION',
        'REMOTE_USER',
        'REDIRECT_REMOTE_USER',
        'PHP_AUTH_USER',
        'PHP_AUTH_PW',
    ];
    foreach ($understood as $key) {
        if (!isset($_SERVER[$key])) {
            if (isset($_ENV[$key])) {
                $_SERVER[$key] = $_ENV[$key];
            } else {
                $_SERVER[$key] = '';
            }
        }
    }
}

/**
 * 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.
 */
function fixup_bad_php_env_vars()
{
    // 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['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 = '';
        $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', 'wwwroot', 'Documents'])) {
                break;
            }
        }
        $document_root = substr($document_root, 0, strlen($document_root) - strlen(DIRECTORY_SEPARATOR));
        $_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($_GET)) {
                    $_SERVER['REQUEST_URI'] .= '?' . str_replace('/', '%2F', http_build_query($_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($_GET)) { // add in query string data if we have it
                $_SERVER['REQUEST_URI'] .= '?' . str_replace('/', '%2F', http_build_query($_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($_GET));
    }
}

/**
 * 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()
{
    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().
 *
 * @return string The hostname
 */
function get_request_hostname()
{
    if (!empty($_SERVER['HTTP_HOST'])) {
        return preg_replace('#:.*#', '', $_SERVER['HTTP_HOST']);
    }
    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.
 * See also get_request_hostname() and get_base_url_hostname().
 *
 * @return string The domain of the website
 */
function get_domain()
{
    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);
}

/**
 * Get last error message.
 *
 * @return string Error message (blank: none)
 */
function cms_error_get_last()
{
    $error = error_get_last();
    if ($error === null) {
        return '';
    }

    switch ($error['type']) {
        case E_RECOVERABLE_ERROR:
        case E_USER_ERROR:
        case E_CORE_ERROR:
        case E_COMPILE_ERROR:
        case E_ERROR:
        case E_PARSE:
            $type = 'error';
            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';
            break;

        case E_USER_NOTICE:
        case E_NOTICE:
            $type = 'notice';
            break;

        case E_STRICT:
        case E_USER_DEPRECATED:
        case E_DEPRECATED:
        default:
            $type = 'deprecated';
            break;
    }

    return '<strong>' . (function_exists('cms_strtoupper_ascii') ? cms_strtoupper_ascii($type) : strtoupper($type)) . '</strong> [' . strval($error['type']) . '] ' . $error['message'] . ' in ' . $error['file'] . ' on line ' . strval($error['line']);
}

/**
 * Find if we are running on a live Google App Engine application.
 *
 * @return boolean If it is running as a live Google App Engine application
 */
function appengine_is_live()
{
    return false;
}

/**
 * Are we currently running HTTPS.
 * Also see whole_site_https.
 *
 * @return boolean If we are
 */
function tacit_https()
{
    $https = isset($_SERVER['HTTPS']) ? $_SERVER['HTTPS'] : '';
    return ($https != '') && ($https != 'off');
}

/**
 * Provides a hook 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($filename)
{
}

/**
 * Find whether a particular PHP function is blocked.
 *
 * @param  string $function Function name
 * @return boolean Whether it is
 */
function php_function_allowed($function)
{
    if (!in_array($function, /*These are actually language constructs rather than functions*/['eval', 'exit', 'include', 'include_once', 'isset', 'require', 'require_once', 'unset', 'empty', 'print',])) {
        if (!function_exists($function)) {
            return false;
        }
    }
    $to_block = ['disable_functions', 'suhosin.executor.func.blacklist', 'suhosin.executor.include.blacklist', 'suhosin.executor.eval.blacklist'];
    $_blocked = [];
    foreach ($to_block as $func) {
        $ini_val = ini_get($func);
        if ($ini_val !== false) {
            $_blocked[] = $ini_val;
        }
    }
    $blocked = implode(',', $_blocked);
    return (@preg_match('#(\s|,|^)' . preg_quote($function, '#') . '(\s|$|,)#i', $blocked) == 0);
}

/**
 * Return a debugging back-trace of the current execution stack. Use this for debugging purposes.
 *
 * @return Tempcode Debugging backtrace
 */
function get_html_trace()
{
    $x = @ob_get_contents();
    cms_ob_end_clean();
    if (is_string($x)) {
        @print($x);
    }

    push_suppress_error_death(true);

    $_trace = debug_backtrace();
    $trace = [];
    foreach ($_trace as $i => $stage) {
        $traces = [];
        //if (in_array($stage['function'], ['get_html_trace', 'cms_error_handler', 'fatal_exit'])) continue;
        $file = '';
        $line = '';
        $__value = mixed();
        foreach ($stage as $key => $__value) {
            if ($key == 'file') {
                $file = str_replace('\'', '', $__value);
            } elseif ($key == 'line') {
                $line = strval($__value);
            }
            if ($key == 'args') {
                $_value = new Tempcode();
                foreach ($__value as $param) {
                    if (!((is_array($param)) && (array_key_exists('GLOBALS', $param)))) { // Some versions of PHP give the full environment as parameters. This will cause a recursive issue when outputting due to GLOBALS->ENV chaining.
                        if ((is_object($param) && (is_a($param, 'Tempcode'))) || ($param === null)) {
                            $__value = gettype($param);
                        } else {
                            @ob_start();
                            var_export($param);
                            $__value = ob_get_clean();
                        }
                        if (strlen($__value) < 3000) {
                            $_value->attach(paragraph(escape_html($__value)));
                        } else {
                            $_value = make_string_tempcode(escape_html('...'));
                        }
                    }
                }
            } else {
                $value = mixed();
                if (is_float($__value)) {
                    $value = float_format($__value);
                } elseif (is_integer($__value)) {
                    $value = integer_format($__value);
                } else {
                    $value = $__value;
                }

                if ((is_object($value) && (is_a($value, 'Tempcode'))) || (is_array($value) && (strlen(serialize($value)) > 100))) {
                    $_value = make_string_tempcode(escape_html(gettype($value)));
                } else {
                    @ob_start();
                    var_export($value);
                    $_value = make_string_tempcode(escape_html(ob_get_contents()));
                    ob_end_clean();
                }
            }
            $traces[] = ['LINE' => $line, 'FILE' => $file, 'KEY' => function_exists('cms_ucfirst_ascii') ? cms_ucfirst_ascii($key) : ucfirst($key), 'VALUE' => $_value];
        }
        $trace[] = ['TRACES' => $traces];
    }

    pop_suppress_error_death();

    return do_template('STACK_TRACE', ['_GUID' => 'da6c0ef0d8d793807d22e51555d73929', 'TRACE' => $trace, 'POST' => '']);
}

/**
 * Do a clean 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
 * @exits
 */
function fatal_exit($text)
{
    //if (is_object($text)) $text = $text->evaluate();

    http_response_code(500);

    // To break any looping of errors
    global $EXITING;
    if (($EXITING !== null) || (!class_exists('Tempcode'))) {
        die_html_trace($text);
    }
    $EXITING = 1;

    $title = get_screen_title('ERROR_OCCURRED');

    $trace = get_html_trace();
    $echo = new Tempcode();
    $echo->attach(do_template('FATAL_SCREEN', ['_GUID' => '95877d427cf4e785b2f16cc71381e7eb', 'TITLE' => $title, 'TEXT' => $text, 'TRACE' => $trace, 'MAY_SEE_TRACE' => true,]));
    $version = strval(cms_version());
    $version .= (is_numeric(cms_version_minor()) ? '.' : ' ') . cms_version_minor();
    if (!array_key_exists('step', $_GET)) {
        $_GET['step'] = '1';
    }
    require_code('tempcode_compiler');
    $css_nocache = _do_template('default', '/css/', 'no_cache', 'no_cache', 'EN', '.css');
    if (running_script('restore')) {
        $out_final = do_template('RESTORE_HTML_WRAP', [
            '_GUID' => 'be91236730dafaab9c81e99aabac3198',
            'CSS_NOCACHE' => $css_nocache,
            'MESSAGE' => $echo,
            'ERROR' => true,
        ]);
    } else {
        $installer_js = new Tempcode();
        $installer_js->attach(do_template('global', [], null, false, null, '.js', 'javascript'));
        $installer_js->attach(do_template('installer', [], null, false, null, '.js', 'javascript'));
        $out_final = do_template('INSTALLER_HTML_WRAP', [
            '_GUID' => '990e78523cee0b6782e1e09d73a700a7',
            'CSS_NOCACHE' => $css_nocache,
            'DEFAULT_FORUM' => '',
            'PASSWORD_PROMPT' => '',
            'RESOURCE_BASE_URL' => 'install.php?type=',
            'STEP' => integer_format(intval($_GET['step'])),
            'CONTENT' => $echo,
            'VERSION' => $version,
            'INSTALLER_JS' => $installer_js,
        ]);
    }
    $out_final->evaluate_echo();

    echo '<!--ERROR-->';

    exit();
}

/**
 * Lookup error on the homesite, to see if there is more information.
 * (null implementation for minikernel)
 *
 * @param  mixed $error_message The error message (string or Tempcode)
 * @return ?string The result from the web service (null: no result)
 */
function get_webservice_result($error_message)
{
    return null;
}

/**
 * Software error catcher for fatal versions.
 */
function catch_fatal_errors()
{
    $error = error_get_last();

    if ($error !== null) {
        if (substr($error['message'], 0, 26) == 'Maximum execution time of ') {
            if (function_exists('i_force_refresh')) {
                i_force_refresh();
            }
        }

        switch ($error['type']) {
            case E_ERROR:
            case E_CORE_ERROR:
            case E_COMPILE_ERROR:
            case E_USER_ERROR:
                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; // Does not actually work unfortunately. @'d calls never get here at all.
                cms_error_handler($error['type'], $error['message'], $error['file'], $error['line']);
                break;
        }
    }
}

/**
 * Software error handler (hooked into PHP error system).
 *
 * @param  integer $errno The error code-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 Always false
 */
function cms_error_handler($errno, $errstr, $errfile, $errline)
{
    if (peek_suppress_error_death()) {
        return false;
    }

    if ((error_reporting() & $errno) === 0) {
        return false; // This actually tells if @ was used oddly enough. You wouldn't figure from the PHP docs.
    }

    if ($errno == E_USER_ERROR) {
        $errno = E_ERROR;
    }
    if ($errno == E_PARSE) {
        $errno = E_ERROR;
    }
    if ($errno == E_CORE_ERROR) {
        $errno = E_ERROR;
    }
    if ($errno == E_COMPILE_ERROR) {
        $errno = E_ERROR;
    }
    if ($errno == E_CORE_WARNING) {
        $errno = E_WARNING;
    }
    if ($errno == E_COMPILE_WARNING) {
        $errno = E_WARNING;
    }
    if ($errno == E_USER_WARNING) {
        $errno = E_WARNING;
    }
    if ($errno == E_USER_NOTICE) {
        $errno = E_NOTICE;
    }

    switch ($errno) {
        case E_ERROR:
        case E_WARNING:
        case E_NOTICE:
            cms_ob_end_clean(); // Emergency output, potentially, so kill off any active buffer
            fatal_exit('PHP [' . strval($errno) . '] ' . $errstr);
            break;
    }

    return false;
}

/**
 * Find whether the current member is a guest.
 *
 * @param  ?MEMBER $member_id Member ID to check (null: current user)
 * @return boolean Whether the current member is a guest
 */
function is_guest($member_id = null)
{
    return true;
}

/**
 * Find whether we are running in safe mode.
 *
 * @return boolean Whether we are in safe mode
 */
function in_safe_mode()
{
    // Force safe mode in the upgrader so corrupt non-bundled addons (which are not supported by the upgrader) do not break it.
    if (running_script('upgrader')) {
        return true;
    }

    return (get_param_integer('keep_safe_mode', 0) == 1);
}

/**
 * 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($is_this_running)
{
    if (substr($is_this_running, -4) != '.php') {
        $is_this_running .= '.php';
    }
    $script_name = isset($_SERVER['SCRIPT_NAME']) ? $_SERVER['SCRIPT_NAME'] : '';
    return (basename($script_name) == $is_this_running);
}

/**
 * Get the character set to use. We try and be clever to allow AJAX scripts to avoid loading up language.
 *
 * @return string The character set
 */
function get_charset()
{
    if (function_exists('do_lang')) {
        $ret = do_lang('charset', null, null, null, null, false);
        if ($ret !== null) {
            return $ret;
        }
    }
    global $SITE_INFO;
    $lang = (!empty($SITE_INFO['default_lang'])) ? $SITE_INFO['default_lang'] : 'EN';
    $path = get_file_base() . '/lang_custom/' . $lang . '/global.ini';
    if (!file_exists($path)) {
        $path = get_file_base() . '/lang/' . $lang . '/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 = unixify_line_format(file_array_get($lang_file));
    } else {
        $file = fopen($path, 'rb');
        $contents = unixify_line_format(fread($file, 100));
        fclose($file);
    }

    $matches = [];
    if (preg_match('#charset=([\w\-]+)\r?\n#', $contents, $matches) != 0) {
        return $matches[1];
    }
    return 'utf-8';
}

/**
 * Echo an error message, and a debug back-trace of the current execution stack. Use this for debugging purposes.
 *
 * @param  string $message An error message
 */
function die_html_trace($message)
{
    critical_error('PASSON', $message);
}

/**
 * This is a less-revealing alternative to fatal_exit, that is used for user-errors/common-corruption-scenarios.
 *
 * @param  mixed $text The error message
 * @exits
 */
function inform_exit($text)
{
    warn_exit($text);
}

/**
 * This is a less-revealing alternative to fatal_exit, that is used for user-errors/common-corruption-scenarios.
 *
 * @param  mixed $text The error message
 * @exits
 */
function warn_exit($text)
{
    // To break any looping of errors
    global $EXITING;
    if (($EXITING !== null) || (!class_exists('Tempcode'))) {
        die_html_trace($text);
    }
    $EXITING = 1;

    $title = get_screen_title('ERROR_OCCURRED');

    $echo = new Tempcode();
    $echo->attach(do_template('WARN_SCREEN', ['_GUID' => '723ede24462dfc4cd4485851819786bc', 'TITLE' => $title, 'TEXT' => $text, 'PROVIDE_BACK' => false]));
    $version = strval(cms_version());
    $version .= (is_numeric(cms_version_minor()) ? '.' : ' ') . cms_version_minor();
    if (!array_key_exists('step', $_GET)) {
        $_GET['step'] = '1';
    }
    require_code('tempcode_compiler');
    $css_nocache = _do_template('default', '/css/', 'no_cache', 'no_cache', 'EN', '.css');
    if (running_script('restore')) {
        $out_final = do_template('RESTORE_HTML_WRAP', [
            '_GUID' => '190e78523cee0b6782e1e09d73a700a7',
            'CSS_NOCACHE' => $css_nocache,
            'MESSAGE' => $echo,
            'ERROR' => true,
        ]);
    } else {
        $installer_js = new Tempcode();
        $installer_js->attach(do_template('global', [], null, false, null, '.js', 'javascript'));
        $installer_js->attach(do_template('installer', [], null, false, null, '.js', 'javascript'));
        $out_final = do_template('INSTALLER_HTML_WRAP', [
            '_GUID' => '710e7ea5c186b4c42bb3a5453dd915ed',
            'CSS_NOCACHE' => $css_nocache,
            'DEFAULT_FORUM' => '',
            'PASSWORD_PROMPT' => '',
            'RESOURCE_BASE_URL' => 'install.php?type=',
            'STEP' => integer_format(intval($_GET['step'])),
            'CONTENT' => $echo,
            'VERSION' => $version,
            'INSTALLER_JS' => $installer_js,
        ]);
    }
    $out_final->evaluate_echo();

    exit();
}

/**
 * Get the major version of your installation.
 *
 * @return integer The major version number of your installation
 */
function cms_version()
{
    return intval(cms_version_number());
}

/**
 * Get the full string version of the software that you are running.
 *
 * @return string The string saying the full software version number
 */
function cms_version_pretty()
{
    return '';
}

/**
 * Get the type of forums installed.
 *
 * @return string The type of forum installed
 */
function get_forum_type()
{
    global $SITE_INFO;
    if (empty($SITE_INFO['forum_type'])) {
        return 'none';
    }
    return $SITE_INFO['forum_type'];
}

/**
 * Get the installed forum base URL.
 *
 * @return URLPATH The installed forum base URL
 */
function get_forum_base_url()
{
    if (get_forum_type() == 'none') {
        return '';
    }
    global $SITE_INFO;
    if (empty($SITE_INFO['forum_base_url'])) {
        return get_base_url();
    }
    return $SITE_INFO['forum_base_url'];
}

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

/**
 * Get the base URL (the minimum fully qualified URL to our installation).
 *
 * @param  string $zone_for What zone this is running in
 * @return URLPATH The base URL
 */
function get_base_url($zone_for = '')
{
    global $SITE_INFO;
    if (empty($SITE_INFO['base_url'])) {
        $default_base_url = (tacit_https() ? 'https://' : 'http://') . get_request_hostname();
        if (isset($_SERVER['SERVER_PORT'])) {
            $port = $_SERVER['SERVER_PORT'];
            if ($port != (tacit_https() ? '443' : '80')) {
                $default_base_url .= ':' . $port;
            }
        }
        $default_base_url .= str_replace('%2F', '/', rawurlencode(str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME']))));

        $base_url = post_param_string('base_url', $default_base_url);
        if (substr($base_url, -1) == '/') {
            $base_url = substr($base_url, 0, strlen($base_url) - 1);
        }

        return $base_url . (($zone_for == '') ? '' : ('/' . $zone_for));
    }
    return $SITE_INFO['base_url'] . (($zone_for == '') ? '' : ('/' . $zone_for));
}

/**
 * Get the base URL (the minimum fully qualified URL to our personal data installation). For a shared install only, this is different to the base URL.
 *
 * @return URLPATH The base URL
 */
function get_custom_base_url()
{
    return get_base_url();
}

/**
 * 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)
 * @param  integer $risk The default risk score for this hack attack; could be overridden by advanced banning
 * @exits
 */
function log_hack_attack_and_exit($reason, $reason_param_a = '', $reason_param_b = '', int $risk_score = 10)
{
    exit('You should not see this message. If you do, contact the Core Development Team and tell them a \'lhaae\' showed during installation.');
}

/**
 * Get a value (either POST [u]or[/u] GET), or the default if neither can be found.
 *
 * @param  ID_TEXT $name The name of the parameter to get
 * @param  ?string $default The default value to give the parameter if the parameter value is not defined (null: give error on missing parameter)
 * @return ?string The value of the parameter (null: not there, and default was null)
 */
function either_param_string($name, $default = null)
{
    $a = __param($_REQUEST, $name, $default);
    return $a;
}

/**
 * Get the value of the specified POST key, if it is found, or the default otherwise.
 *
 * @param  ID_TEXT $name The name of the parameter to get
 * @param  ?string $default The default value to give the parameter if the parameter value is not defined (null: give error on missing parameter)
 * @return ?string The value of the parameter (null: not there, and default was null)
 */
function post_param_string($name, $default = null)
{
    $a = __param($_POST, $name, $default);
    return $a;
}

/**
 * Get the value of the specified GET key, if it is found, or the default otherwise.
 *
 * @param  ID_TEXT $name The name of the parameter to get
 * @param  ?string $default The default value to give the parameter if the parameter value is not defined (null: give error on missing parameter)
 * @return ?string The value of the parameter (null: not there, and default was null)
 */
function get_param_string($name, $default = null)
{
    $a = __param($_GET, $name, $default);
    return $a;
}

/**
 * Helper function to load up a GET/POST parameter.
 *
 * @param  array $array The array we're extracting parameters from
 * @param  ID_TEXT $name The name of the parameter
 * @param  ?mixed $default The default value to use for the parameter (null: no default)
 * @param  boolean $must_integer Whether the parameter has to be an integer
 * @param  boolean $is_post Whether the parameter is a POST parameter
 * @return ?string The value of the parameter (null: not there, and default was null)
 * @ignore
 */
function __param($array, string $name, $default, $must_integer = false, $is_post = false)
{
    if (!array_key_exists($name, $array)) {
        return $default;
    }
    $val = trim($array[$name]);

    return $val;
}

/**
 * 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 (null: give error on missing parameter)
 * @return integer The parameter value
 */
function either_param_integer($name, $default = null)
{
    $ret = __param($_REQUEST, $name, ($default === false) ? false : (($default === null) ? null : strval($default)));
    if (($default === null) && (($ret === '') || ($ret === null))) {
        return null;
    }
    return intval($ret);
}

/**
 * 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 (null: give error on missing parameter)
 * @return integer The parameter value
 */
function post_param_integer($name, $default = null)
{
    $ret = __param($_POST, $name, ($default === false) ? false : (($default === null) ? null : strval($default)));
    if (($default === null) && (($ret === '') || ($ret === null))) {
        return null;
    }
    return intval($ret);
}

/**
 * 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 (null: give error on missing parameter)
 * @return integer The parameter value
 */
function get_param_integer($name, $default = null)
{
    $ret = __param($_GET, $name, ($default === false) ? false : (($default === null) ? null : strval($default)));
    if (($default === null) && (($ret === '') || ($ret === null))) {
        return null;
    }
    return intval($ret);
}

/**
 * Get the file base for your installation of the software.
 *
 * @return PATH The file base, without a trailing slash
 */
function get_file_base()
{
    global $FILE_BASE;
    return $FILE_BASE;
}

/**
 * Get the file base for your installation of the software.
 * For a shared install only, this is different to the base URL.
 *
 * @return PATH The file base, without a trailing slash
 */
function get_custom_file_base()
{
    global $FILE_BASE;
    return $FILE_BASE;
}

/**
 * Get the parameter put into it, with no changes. If it detects that the parameter is naughty (i.e malicious, and probably from a hacker), it will log the hack-attack and output an error message.
 * This function is designed to be called on parameters that will be embedded in a path, and defines malicious as trying to reach a parent directory using '..'. All file paths in the software should be absolute
 *
 * @param  string $in String to test
 * @return string Same as input string
 */
function filter_naughty($in)
{
    if (strpos($in, '..') !== false) {
        exit();
    }
    return $in;
}

/**
 * This function is similar to filter_naughty, except it requires the parameter to be strictly alphanumeric. It is intended for use on text that will be put into an eval.
 *
 * @param  string $in String to test
 * @return string Same as input string
 */
function filter_naughty_harsh($in)
{
    if (preg_match('#^[\w\-]*$#', $in) != 0) {
        return $in;
    }
    exit();
}

/**
 * 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
 * @return string The cleaned data
 */
function unixify_line_format($in)
{
    $in = str_replace("\r\n", "\n", $in);
    return str_replace("\r", "\n", $in);
}

/**
 * Make sure that the given CSS file is loaded up.
 *
 * @sets_output_state
 *
 * @param  ID_TEXT $css The CSS file required
 */
function require_css($css)
{
}

/**
 * Make sure that the given JavaScript file is loaded up.
 *
 * @sets_output_state
 *
 * @param  ID_TEXT $css The JavaScript file required
 */
function require_javascript($css)
{
}

/**
 * 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($target, $expression, $full_cover = false, $case_sensitive = false)
{
    $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;
}

/**
 * Check to see if caching is enabled.
 *
 * @param  string $type Cache type
 * @set block lang comcode_page template
 * @param  ?string $name The name (filename? codename?) of what we are using the cache for, so we can support low-level cache avoidance via URL parameter (null: none)
 * @return boolean Whether it has the caching
 */
function has_caching_for($type, $name = null)
{
    return true;
}

/**
 * Get data from the persistent cache.
 *
 * @param  mixed $key Key
 * @param  ?TIME $min_cache_date Minimum timestamp that entries from the cache may hold (null: don't care)
 * @return ?mixed The data (null: not found / null entry)
 */
function persistent_cache_get($key, $min_cache_date = null)
{
    return null;
}

/**
 * Put data into the persistent cache.
 *
 * @param  mixed $key Key
 * @param  mixed $data The data
 * @param  boolean $server_wide Whether it is server-wide data
 * @param  ?integer $expire_secs The expiration time in seconds. (null: Default expiry in 60 minutes, or never if it is server-wide).
 */
function persistent_cache_set($key, $data, $server_wide = false, $expire_secs = null)
{
}

/**
 * Delete data from the persistent cache.
 *
 * @param  mixed $key Key name
 * @param  boolean $substring Whether we are deleting via substring
 */
function persistent_cache_delete($key, $substring = false)
{
}

/**
 * Recursively clean (erase) the output buffer and turn off output buffering.
 * Does not disable the output handler, you need to call disable_output_compression() for that.
 */
function cms_ob_end_clean()
{
    while (ob_get_level() > 0) {
        if (!@ob_end_clean()) {
            break; // Cannot delete special buffer, likely output compression
        }
    }
}

/**
 * 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.
 * 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)
{
    return (empty($var)) && ($var !== '0');
}

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

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

/**
 * Get the session cookie's name.
 *
 * @return string The session ID cookie's name
 */
function get_session_cookie()
{
    global $SITE_INFO;
    if (empty($SITE_INFO['session_cookie'])) {
        $SITE_INFO['session_cookie'] = '__Host-cms_session';
    }

    validate_special_cookie_prefix($SITE_INFO['session_cookie']);

    return $SITE_INFO['session_cookie'];
}

/**
 * Ensure that if we are using a special cookie name prefix that we can actually do so, otherwise strip it.
 *
 * @param ID_TEXT $cookie_name The name of the cookie (passed by reference; prefix will be stripped if it cannot be used)
 */
function validate_special_cookie_prefix(string &$cookie_name)
{
    global $SITE_INFO;

    // If __Host- prefixed, determine if we can use it
    if (strpos($cookie_name, '__Host-') === 0) {
        if (!empty(get_cookie_domain())) { // Cannot use __Host- if a domain is set
            $cookie_name = substr($cookie_name, 7);
            return;
        }

        if (strpos(get_base_url(), 'https://') !== 0) { // Cannot use __Host- if not running securely
            $cookie_name = substr($cookie_name, 7);
            return;
        }

        if (get_cookie_path() !== '/') { // Cannot use __Host- if path is not /
            $cookie_name = substr($cookie_name, 7);
            return;
        }
    }

    // If __Secure- prefixed, determine if we can use it
    if (strpos($cookie_name, '__Secure-') === 0) {
        if (strpos(get_base_url(), 'https://') !== 0) { // Cannot use __Secure- if not running securely
            $cookie_name = substr($cookie_name, 9);
            return;
        }
    }
}

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

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

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

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

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

        return true;
    }

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

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

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

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

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

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

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

        return true;
    }

    return false;
}

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

    $raw_ip = $ip;

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

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

    // Normalise
    if (strpos($ip, '.') === false) { // IPv6
        // Strip ipv6 scope ID
        if (strpos($ip, '%') !== false) {
            $ip = preg_replace('#%.*$#', '', $ip);
        }

        if ($amount !== null) {
            $amount += (8 - $amount);
        }

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

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

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

/**
 * Get possible IP addresses of a localhost machine.
 * Also see get_server_ips().
 *
 * @return array IP addresses
 */
function get_localhost_ips() : array
{
    return [
        '0000:0000:0000:0000:0000:0000:0000:0001',
        '::1',
        '127.0.0.1',
    ];
}

/**
 * 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]');
}

/**
 * Attach a message to the page output.
 * For response-embedded messaging where the code isn't failing per-se, but it's more like a predictable messaging situation occurred.
 * For failure-like messages call trigger_error which will in turn call attach_message itself.
 *
 * @sets_output_state
 *
 * @param  mixed $message The message to show, provided in plain-text format or HTML Tempcode
 * @param  ID_TEXT $type The 'template' to use
 * @set inform notice warn
 * @param  boolean $put_in_helper_panel Whether to put into the helper panel instead of the normal header area (true: minikernel suppresses the message completely)
 * @param  boolean $log_error Whether to log the error
 * @return string Blank string so it can be chained in the Tempcode compiler. You will rarely want to use this return value. It's kind of a failsafe.
 */
function attach_message($message, string $type = 'inform', bool $put_in_helper_panel = false, bool $log_error = false) : string
{
    if ($type == 'warn') {
        warn_exit($message);
    } else {
        echo $message . "<br />\n";
    }
    return '';
}

/**
 * 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');
}

/**
 * Find the current mode of fatalistic.
 *
 * @return integer 0 if fatalistic is off, 1 if it is on, or 2 if it is on with additional details
 */
function current_fatalistic() : int
{
    // _config.php takes priority
    global $SITE_INFO;
    if (!empty($SITE_INFO['keep_fatalistic'])) {
        switch ($SITE_INFO['keep_fatalistic']) { // This is to prevent invalid config values from breaking the site
            case '1':
                return 1;
            case '2':
                return 2;
            default:
                return 0;
        }
    }

    // URL parameter
    if (get_param_integer('keep_fatalistic', 0) != 0) {
        return get_param_integer('keep_fatalistic');
    }

    return 0;
}
