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

*/

/*EXTRA FUNCTIONS: strtoupper|strtolower|usleep*/

/*
    secondary bootstrap loaded after bootstrap.php by any front-end script
*/

/**
 * @license    http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright  Christopher Graham
 * @package    core
 */

/**
 * Require a software PHP file while, where applicable, compiling in overrides from *_custom folders and contentious overrides.
 * You should remember this function and use this opposed to require/include/require_once/include_once for proper modularity (except for bootstrap.php which has to be required directly).
 *
 * @param  string $codename The codename for the source module to load (or a full relative path, ending with .php; if custom checking is needed, this must be the custom version)
 * @param  boolean $light_exit Whether to cleanly fail when a source file is missing
 * @param  ?boolean $has_custom Whether this is going to be from under a custom directory (null: search). This is used for performance to avoid extra searching when we already know where a file is
 * @param  boolean $force_custom Whether to forcefully allow custom overrides even if in safe mode (still ignored for files which do not support overrides)
 */
function require_code(string $codename, bool $light_exit = false, ?bool $has_custom = null, bool $force_custom = false)
{
    // Handle if already required...
    global $REQUIRED_CODE, $REQUIRING_CODE, $FILE_BASE, $SITE_INFO;
    if (isset($REQUIRED_CODE[$codename])) {
        return;
    }

    // These scripts can be loaded in directly from outside software (but only the original code). For consistency, do not allow overrides on them.
    // Exception: if we are running in dev_mode, compile anyway because we need strict typing on them.
    $no_override_support = ['crypt_maintenance', 'version'];
    if (in_array($codename, $no_override_support) && (!$GLOBALS['DEV_MODE'])) {
        require_code_no_override($codename);
        return;
    }

    $ok_per_safe_mode = (($force_custom) || (!function_exists('in_safe_mode')) || ($REQUIRING_CODE) || (!in_safe_mode()));
    if (isset($REQUIRED_CODE[$codename])) {
        return; // In case it changed through the above safe mode check
    }
    $REQUIRED_CODE[$codename] = false; // unset means no, false means in-progress, true means done
    $REQUIRING_CODE = true;

    $shorthand = (strpos($codename, '.php') === false);
    if (!$shorthand) {
        $non_custom_codename = str_replace('_custom/', '/', $codename);
        $REQUIRED_CODE[$non_custom_codename] = true;
    }

    // Security...

    if (strpos($codename, '..') !== false) {
        $codename = filter_naughty($codename);
    }

    // Optional process tracking...

    if ((isset($_GET['keep_show_loading_code'])) && ($_GET['keep_show_loading_code'] === '1')) {
        $before = memory_get_usage();

        $contents = gmdate('[d-M-Y H:i:s \U\T\C]') . ': Requiring ' . $codename . ' (current memory at ' . @number_format($before) . ' bytes)' . "\n";
        @file_put_contents($FILE_BASE . '/data_custom/require_code.log', $contents, FILE_APPEND | LOCK_EX);
    }

    // Calculate custom/non-custom status...

    $path_custom = $FILE_BASE . '/' . ($shorthand ? ('sources_custom/' . $codename . '.php') : $codename);
    $path_orig = $FILE_BASE . '/' . ($shorthand ? ('sources/' . $codename . '.php') : $non_custom_codename);

    $has_orig = null;
    if (isset($GLOBALS['PERSISTENT_CACHE']) && !in_safe_mode()) {
        global $CODE_OVERRIDES;
        if (!isset($CODE_OVERRIDES)) {
            $CODE_OVERRIDES = persistent_cache_get('CODE_OVERRIDES');
            if ($CODE_OVERRIDES === null) {
                $CODE_OVERRIDES = [];
            }
        }
        if (isset($CODE_OVERRIDES[$codename])) {
            if ($has_custom === null) {
                $has_custom = $CODE_OVERRIDES[$codename];
                if ($has_custom) {
                    $has_custom = is_file($path_custom); // Double-check still there
                }
            }
            $has_orig = $CODE_OVERRIDES['!' . $codename];
            if ($has_orig) {
                $has_orig = is_file($path_orig); // Double-check still there
            }
        } else {
            if ($has_custom === null) {
                $has_custom = is_file($path_custom);
            }
            $has_orig = is_file($path_orig);
            $CODE_OVERRIDES[$codename] = $has_custom;
            $CODE_OVERRIDES['!' . $codename] = $has_orig;
            persistent_cache_set('CODE_OVERRIDES', $CODE_OVERRIDES);
        }
    } else {
        if ($has_custom === null) {
            $has_custom = is_file($path_custom);
        }
    }

    // Overrides are disabled in safe mode
    if ((isset($SITE_INFO['safe_mode'])) && ($SITE_INFO['safe_mode'] === '1')) {
        $has_custom = false;
    }

    // Load the code as appropriate...

    if (($has_custom) && (($ok_per_safe_mode) || (!is_file($path_orig)))) {
        $done_init = false;
        $init_func = 'init__' . str_replace('/', '__', str_replace('.php', '', $codename));

        if (!isset($has_orig)) {
            $has_orig = is_file($path_orig);
        }
        if (($path_custom !== $path_orig) && ($has_orig)) {
            // Have a custom and original (i.e. override)...

            $orig = clean_php_file_for_eval(file_get_contents($path_orig), $path_orig);
            $custom = clean_php_file_for_eval(file_get_contents($path_custom), $path_custom);

            if (strpos($custom, '/*FORCE_ORIGINAL_LOAD_FIRST*/') === false/*e.g. Cannot do code rewrite for a module override that includes an Mx, because the extends needs the parent class already defined - in such cases we put this comment in the code*/) {
                // We need to identify the new functions and classes. Ideally we'd use get_defined_functions and get_declared_classes, and do a diff before/after - but this does a massive amount of memory access
                $function_matches = [];
                $possible_new_functions = [];
                $has_new = false;
                $num_function_matches = preg_match_all('#\sfunction\s+(\w+)\(#', $custom, $function_matches);
                for ($i = 0; $i < $num_function_matches; $i++) {
                    $possible_new_functions[] = $function_matches[1][$i];
                }
                $class_matches = [];
                $possible_new_classes = [];
                $num_class_matches = preg_match_all('#\sclass\s+(\w+)#', $custom, $class_matches);
                for ($i = 0; $i < $num_class_matches; $i++) {
                    $possible_new_classes[] = $class_matches[1][$i];
                }

                // We need to also get the functions and classes from the original
                $function_matches = [];
                $orig_functions = [];
                $num_function_matches = preg_match_all('#\sfunction\s+(\w+)\(#', $orig, $function_matches);
                for ($i = 0; $i < $num_function_matches; $i++) {
                    $orig_functions[] = $function_matches[1][$i];
                }
                $class_matches = [];
                $orig_classes = [];
                $num_class_matches = preg_match_all('#\sclass\s+(\w+)#', $orig, $class_matches);
                for ($i = 0; $i < $num_class_matches; $i++) {
                    $orig_classes[] = $class_matches[1][$i];
                }

                $functions_diff = [];
                foreach ($possible_new_functions as $possible_new_function) {
                    if (in_array($possible_new_function, $orig_functions)) {
                        $functions_diff[] = $possible_new_function;
                    } else {
                        $has_new = true;
                    }
                }
                $classes_diff = [];
                foreach ($possible_new_classes as $possible_new_class) {
                    if (in_array($possible_new_class, $orig_classes)) {
                        $classes_diff[] = $possible_new_class;
                    } else {
                        $has_new = true;
                    }
                }

                $strpos_func = 'strpos';
                $str_replace_func = 'str_replace';
                $overlaps = false;
                foreach ($functions_diff as $function) { // Go through override's functions and make sure original doesn't have them: rename original's to non_overridden__ equivs.
                    if ($strpos_func($orig, 'function ' . $function . '(') !== false) { // NB: If this fails, it may be that "function\t" is in the file (you can't tell with a three-width proper tab)
                        $orig = $str_replace_func('function ' . $function . '(', 'function non_overridden__' . $function . '(', $orig);
                        $overlaps = true;
                    }
                }
                foreach ($classes_diff as $class) {
                    if (cms_strtolower_ascii(substr($class, 0, 6)) === 'module') {
                        $class = cms_ucfirst_ascii($class);
                    }
                    if (cms_strtolower_ascii(substr($class, 0, 4)) === 'hook') {
                        $class = cms_ucfirst_ascii($class);
                    }
                    if (strpos($orig, 'class ' . $class) !== false) {
                        $orig = str_replace('class ' . $class, 'class non_overridden__' . $class, $orig);
                        $overlaps = true;
                    }
                }

                $code_custom = null;
                $code_orig = null;

                // Compile in custom code (we do not need a _compiled file by default)
                $delete_custom = compile_included_code($path_custom, $codename, $light_exit, $code_custom);

                // Compile in original code (pass in $orig code if we modified it so a _compiled file is generated)
                if ($overlaps) {
                    $code_orig = $orig;
                    $delete_orig = compile_included_code($path_orig, $codename, $light_exit, $code_orig);
                } else {
                    $delete_orig = compile_included_code($path_orig, $codename, $light_exit, $code_orig);
                }

                // We are clear to call the compiled code now
                call_compiled_code($path_custom, $codename, $light_exit, $delete_custom, $code_custom);
                call_compiled_code($path_orig, $codename, $light_exit, $delete_orig, $code_orig);
            } else {
                // Note we load the original and then the override. This is so function_exists can be used in the overrides (as we can't support the re-definition) OR in the case of Mx_ class derivation, so that the base class is loaded first.
                $delete_orig = compile_included_code($path_orig, $codename, $light_exit);
                $delete_custom = compile_included_code($path_custom, $codename, $light_exit);

                // We are clear to call the compiled code now
                call_compiled_code($path_orig, $codename, $light_exit, $delete_orig);
                call_compiled_code($path_custom, $codename, $light_exit, $delete_custom);
            }
        } else {
            // Have a custom but no original...

            $delete_code = null;
            $delete_custom = compile_included_code($path_custom, $codename, $light_exit, $delete_code);

            call_compiled_code($path_custom, $codename, $light_exit, $delete_custom, $delete_code);
        }

        if ((isset($_GET['keep_show_loading_code'])) && ($_GET['keep_show_loading_code'] === '1')) {
            $contents = gmdate('[d-M-Y H:i:s \U\T\C]') . ': ' . $codename . ' (' . @number_format(memory_get_usage() - $before) . ' bytes used, now at ' . @number_format(memory_get_usage()) . ')' . "\n";
            @file_put_contents($FILE_BASE . '/data_custom/require_code.log', $contents, FILE_APPEND | LOCK_EX);
            if (function_exists('attach_message')) {
                attach_message('require_code: ' . $codename . ' (' . @number_format(memory_get_usage() - $before) . ' bytes used, now at ' . @number_format(memory_get_usage()) . ')', 'inform');
            } else {
                print('<!-- require_code: ' . htmlentities($codename) . ' (' . htmlentities(@number_format(memory_get_usage() - $before)) . ' bytes used, now at ' . htmlentities(@number_format(memory_get_usage())) . ') -->' . "\n");
                cms_flush_safe();
            }
        }

        if (!$done_init) {
            if (function_exists($init_func)) {
                call_user_func($init_func);
            }
        }
    } else {
        // Have an original and no custom (no override)...

        $code_orig = null;
        $delete_orig = compile_included_code($path_orig, $codename, $light_exit, $code_orig);

        call_compiled_code($path_orig, $codename, $light_exit, $delete_orig, $code_orig);

        // Optional process tracking (has to run before init function called)
        if ((isset($_GET['keep_show_loading_code'])) && ($_GET['keep_show_loading_code'] === '1')) {
            $contents = gmdate('[d-M-Y H:i:s \U\T\C]') . ': ' . $codename . ' (' . @number_format(memory_get_usage() - $before) . ' bytes used, now at ' . @number_format(memory_get_usage()) . ')' . "\n";
            @file_put_contents($FILE_BASE . '/data_custom/require_code.log', $contents, FILE_APPEND | LOCK_EX);
            if (function_exists('attach_message')) {
                attach_message('require_code: ' . $codename . ' (' . @number_format(memory_get_usage() - $before) . ' bytes used, now at ' . @number_format(memory_get_usage()) . ')', 'inform');
            } else {
                print('<!-- require_code: ' . htmlentities($codename) . ' (' . htmlentities(@number_format(memory_get_usage() - $before)) . ' bytes used, now at ' . htmlentities(@number_format(memory_get_usage())) . ') -->' . "\n");
                cms_flush_safe();
            }
        }

        $init_func = 'init__' . str_replace(['/', '.php'], ['__', ''], $codename);
        if (function_exists($init_func)) {
            call_user_func($init_func);
        }
    }

    // Done...

    $REQUIRED_CODE[$codename] = true;
    $REQUIRING_CODE = false;
}

/**
 * Require code from the software without considering or compiling overrides.
 * This also bypasses special DEV_MODE declare strict_types.
 *
 * @param  string $codename The codename for the source module to load
 */
function require_code_no_override(string $codename)
{
    global $REQUIRED_CODE;
    if (array_key_exists($codename, $REQUIRED_CODE)) {
        return;
    }
    $REQUIRED_CODE[$codename] = true;
    require_once get_file_base() . '/sources/' . filter_naughty($codename) . '.php';
    if (function_exists('init__' . str_replace('/', '__', $codename))) {
        call_user_func('init__' . str_replace('/', '__', $codename));
    }
}

/**
 * Compile some code into the _compiled directory if we need to for later inclusion by call_compiled_code.
 * Do not use this function directly; use require_code instead.
 *
 * @param  string $orig_path Path to the original or custom file (not _compiled one)
 * @param  string $codename The codename for the source module to load
 * @param  boolean $light_exit Whether to cleanly fail when an error occurs
 * @param  ?string $code Custom file contents, passed by reference (null: use contents from $orig_path and avoid making a _compiled file if we do not need one)
 * @return ?boolean Whether we should delete any existing _compiled files for this, through call_compiled_code (null: special case; we are ignoring _compiled files and evaluating $code instead)
 * @ignore
 */
function compile_included_code(string $orig_path, string $codename, bool $light_exit, ?string &$code = null) : ?bool
{
    global $SITE_INFO;

    // Files which have been compiled already will be tracked here; the next time this function runs, we assume we are appending onto the compiled file.
    // The files already defined do not support overrides and should have already been required / included by PHP at this point when necessary.
    static $already_compiled = ['sources/global.php', 'sources/minikernel.php', 'sources/bootstrap.php'];

    $path = $orig_path; // $path tracks the actual file for inclusion
    $relative_path = str_replace([get_custom_file_base() . '/', get_file_base() . '/', get_custom_file_base() . '\\', get_file_base() . '\\'], ['', '', '', ''], $orig_path);
    $compiled_relative_path = '_compiled/' . $relative_path;
    $compiled_path = get_custom_file_base() . '/' . $compiled_relative_path;

    // If we already compiled this file, exit out
    $done_this = isset($already_compiled[$relative_path]);
    if ($done_this) {
        return false;
    }

    // Untouchable third-party code should never be compiled by the software.
    $exceptions = list_untouchable_third_party_directories();
    if (preg_match('#^(' . implode('|', $exceptions) . ')/#', $relative_path) != 0) {
        return true;
    }
    $exceptions = list_untouchable_third_party_files();
    if (in_array($relative_path, $exceptions)) {
        return true;
    }

    // Run contentious overrides (but not on themselves)
    if ((function_exists('find_all_hook_obs')) && (strpos($codename, 'sources_custom/hooks/systems/contentious_overrides') !== 0)) {
        $override_hooks = find_all_hook_obs('systems', 'contentious_overrides', 'Hook_contentious_overrides_');
        foreach ($override_hooks as $hook_ob) {
            if (method_exists($hook_ob, 'compile_included_code')) {
                $hook_ob->compile_included_code($orig_path, $codename, /*Passed by reference*/$code);
            }
        }
        unset($override_hooks); // Free up memory when we are done
    }

    $prepend = ''; // NB: Must begin with a space if populated

    // In DEV_MODE, declare PHP strict_types at the top unless "No strict_types" is commented.
    if ($GLOBALS['DEV_MODE']) {
        if ($code === null) {
            $code = file_get_contents($path);
            $code = clean_php_file_for_eval($code, $path); // We will later put the PHP declaration back in, but we need to replace __FILE__ and __DIR__.
        }
        if (strpos($code, '/*No strict_types*/') === false) {
            $prepend .= ' declare(strict_types=1);';
        }
    }

    // We are not compiling to files according to site info
    if (isset($SITE_INFO['no_compiled_files']) && ($SITE_INFO['no_compiled_files'] == '1')) {
        return null;
    }

    // Code must be able to compile into a PHP file
    if (($code !== null) && (strpos('<' . '?php ', $code) === false)) {
        $code = '<' . '?php' . $code;
    }

    if ($code !== null) {
        $recompile = false;

        // Create missing directories
        if (!is_dir(dirname($compiled_path))) {
            if (@mkdir(dirname($compiled_path), 0777, true) === false) {
                if (!is_dir(dirname($compiled_path))) { // Maybe another process beat us to creating it?
                    $error_message = 'Cannot create directory ' . dirname($compiled_relative_path) . ' for compiling source PHP scripts. Maybe you do not have the correct file permissions?';
                    if (function_exists('fatal_exit')) {
                        fatal_exit($error_message, true);
                    } else {
                        require_code('critical_errors');
                        critical_error('PASSON', $error_message);
                    }
                }
            } else {
                file_put_contents(dirname($compiled_path) . '/index.html', '');
            }
        }

        // Prepare code
        $prepend .= ' /* DO NOT EDIT THIS FILE; this is a temporary compiled file. Instead, edit ' . $relative_path . ' and its overrides. */ ';
        $code = str_replace('<' . '?php ', '<' . '?php' . $prepend, $code);

        // Write operations are very time-consuming; don't write if the contents of the PHP file will not change.
        if (!is_file($compiled_path)) {
            $recompile = true;
        } else {
            $file_hash = @hash_file('crc32', $compiled_path); // Might throw a permission denied if locked
            if ((!is_string($file_hash)) || ($file_hash != hash('crc32', $code))) {
                $recompile = true;
            }
        }

        if ($recompile === true) {
            // Keep trying to write for 5 seconds; there might be parallel processes compiling files
            $time = microtime(true);
            while (file_put_contents($compiled_path, $code, LOCK_EX | LOCK_NB) === false) {
                if ((microtime(true) - $time) > 5.0) {
                    $error_message = 'Cannot write file ' . $compiled_relative_path;
                    if (function_exists('fatal_exit')) {
                        fatal_exit($error_message, true);
                    } else {
                        require_code('critical_errors');
                        critical_error('PASSON', $error_message);
                    }
                }

                if (php_function_allowed('usleep')) {
                    usleep(250000);
                }
            }

            clearstatcache(true, $compiled_path);
        }

        return false;
    }

    return true;
}

/**
 * Require some code for use within the software (after we ran compile_included_code on it), prioritising _compiled files if they exist.
 * Do not use this function directly; use require_code instead.
 *
 * @param  string $path The file path to the original file (not the compiled one)
 * @param  string $codename The codename for the source module to load
 * @param  boolean $light_exit Whether to cleanly fail when an error occurs
 * @param  ?boolean $delete_compiled Whether we should ignore, and delete, the _compiled file, if it exists (null: ignore only, and evaluate $code if not null)
 * @param  ?string $code If $delete_compiled is null, evaluate this code instead of loading in compiled code (null: no overridden code to evaluate; require in $path instead)
 * @ignore
 */
function call_compiled_code(string $path, string $codename, bool $light_exit, ?bool $delete_compiled = false, ?string $code = null)
{
    // The files already defined here are always required in before using the core software's methods of requiring code.
    static $already_called = ['sources/global.php', 'sources/minikernel.php', 'sources/bootstrap.php'];

    $do_sed = function_exists('push_suppress_error_death');
    if ($do_sed) {
        push_suppress_error_death(true);
    }

    if (function_exists('error_clear_last')) {
        error_clear_last();
    }
    $errormsg_before = error_get_last();

    $relative_path = str_replace([get_custom_file_base() . '/', get_file_base() . '/', get_custom_file_base() . '\\', get_file_base() . '\\'], ['', '', '', ''], $path);
    $compiled_relative_path = '_compiled/' . $relative_path;
    $compiled_path = get_custom_file_base() . '/' . $compiled_relative_path;

    if (is_file($compiled_path)) {
        if ($delete_compiled !== false) {
            if ($delete_compiled === true) {
                clearstatcache(true, $compiled_path);
                clearstatcache(true, $path);
                @unlink($compiled_path);
            }

            $calling_path = $path;
            $calling_relative_path = $relative_path;
        } else {
            $calling_path = $compiled_path;
            $calling_relative_path = $compiled_relative_path;
        }
    } else {
        $calling_path = $path;
        $calling_relative_path = $relative_path;
    }

    // If we already included this file, just exit
    if (isset($already_called[$calling_path])) {
        return;
    }

    try {
        $already_called[$calling_path] = true;

        if (($code === null) || ($delete_compiled !== null)) {
            // We need to wait (but not too long) for locks to be released before we can include the file
            $time = microtime(true);
            $file = fopen($calling_path, 'r');
            while (flock($file, LOCK_SH | LOCK_NB) === false) {
                if ((microtime(true) - $time) > 5.0) {
                    throw new \Exception('Cannot read file ' . $calling_relative_path . '; a lock was not released on the file in a timely manner.');
                }

                if (php_function_allowed('usleep')) {
                    usleep(100000);
                }
            }

            $result = require_once $calling_path;

            if ($file !== false) {
                fclose($file);
                $file = false;
            }
        } else {
            $result = eval($code);
        }

        if ($result === false) {
            $errormsg = cms_error_get_last();
            if (($errormsg == '') || ($errormsg === $errormsg_before)) {
                $errormsg = '';
            }
            if (stripos($errormsg, 'deprecated') !== false) {
                $errormsg = ''; // Deprecated errors can leak through because even though we return true in our error handler, error handlers won't run recursively, so if this code is loaded during an error it'll stream through deprecated stuff here
            }
        } else {
            $errormsg = '';
        }
    } catch (Exception $e) {
        $result = false;

        $errormsg = $e->getMessage();
        $errormsg .= ' (in ' . $e->getFile() . ', line ' . strval($e->getLine()) . ')';
    } catch (Error $e) {
        $result = false;

        $errormsg = $e->getMessage();
        $errormsg .= ' (in ' . $e->getFile() . ', line ' . strval($e->getLine()) . ')';
    }

    if ($do_sed) {
        pop_suppress_error_death();
    }

    if ($errormsg != '') {
        if (!function_exists('do_lang')) {
            if (!is_file($calling_path)) {
                require_code('critical_errors');
                critical_error('MISSING_SOURCE', $codename);
            }
        }

        if ((!function_exists('do_lang')) || (!function_exists('fatal_exit')) || ($codename === 'failure')) {
            require_code('critical_errors');
            critical_error('PASSON', $errormsg . ' in ' . $calling_relative_path);
        }

        // We need to be careful of the potential that a lot of these functions have not been loaded up yet
        $error_lang_str = (is_file($calling_path) ? 'CORRUPT_SOURCE_FILE' : 'MISSING_SOURCE_FILE');
        if (function_exists('do_lang_tempcode')) {
            $_error_message = do_lang_tempcode($error_lang_str, escape_html($codename), escape_html($calling_relative_path), escape_html($errormsg));
            $error_message = $_error_message->evaluate();
        } else {
            $error_message = $error_lang_str . ': ' . escape_html($codename) . '; ' . escape_html($calling_relative_path) . '; ' . escape_html($errormsg);
        }
        if ($light_exit) {
            if (function_exists('warn_exit')) {
                warn_exit($error_message, false, true);
            } else {
                require_code('critical_errors');
                critical_error('PASSON', $error_message . ' in ' . $calling_relative_path);
            }
        }
        if (function_exists('fatal_exit')) {
            fatal_exit($error_message, true);
        } else {
            require_code('critical_errors');
            critical_error('PASSON', $error_message . ' in ' . $calling_relative_path);
        }
    }
}

/**
 * Get last error message.
 *
 * @return string Error message (blank: none)
 */
function cms_error_get_last() : string
{
    $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_USER_DEPRECATED:
        case E_DEPRECATED:
        default:
            $type = 'deprecated';
            break;
    }

    $ret = '<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']);
    return $ret;
}

/**
 * Replace a limited number of occurrences of the search string with the replacement string.
 * If there are the wrong number of occurrences (including zero) an error is put out, as this indicates an override is broken.
 * The phrase "<ditto>" will repeat the original $search string back into $replace.
 *
 * @param  mixed $search What's being replaced (string or array)
 * @param  mixed $replace What's being replaced with (string or array)
 * @param  mixed $subject Subject (string or array)
 * @param  integer $times Number of times to replace (to expect to replace)
 * @param  boolean $fail_ok Whether a failure should attach a message and return the original code (false: a failure should cause a critical error)
 * @return mixed Result (string or array)
 */
function override_str_replace_exactly($search, $replace, $subject, int $times = 1, bool $fail_ok = false)
{
    // If in the upgrader, force $fail_ok to true so corrupt non-bundled addons (which are not supported by the upgrader) do not break it.
    if (running_script('upgrader')) {
        $fail_ok = true;
    }

    $cnt = substr_count($subject, $search);

    if ($cnt != $times) {
        $lines = debug_backtrace();
        $relay = _sanitise_error_msg(preg_replace('#^' . preg_quote(get_file_base() . '/') . '#', '', $lines[0]['file']) . ':' . strval($lines[0]['line']));
        if ($fail_ok) {
            attach_message('An override seems to no longer be compatible, ' . htmlentities($relay), (running_script('upgrader') ? 'notice' : 'warn'), false, true);
            return $subject; // Return original code
        }
        critical_error('CORRUPT_OVERRIDE', $relay);
    }

    $replace = str_replace('<ditto>', $search, $replace);

    return str_replace($search, $replace, $subject);
}

/**
 * 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() : bool
{
    return (GOOGLE_APPENGINE) && (!is_writable(get_file_base() . '/sources/bootstrap.php'));
}

/**
 * Are we currently running HTTPS.
 * Also see whole_site_https.
 *
 * @return boolean If we are
 */
function tacit_https() : bool
{
    static $tacit_https = null;
    if ($tacit_https === null) {
        $tacit_https = (($_SERVER['HTTPS'] != '') && ($_SERVER['HTTPS'] != 'off'/*IIS quirk*/));
    }
    return $tacit_https;
}

/**
 * Make an object of the given class.
 *
 * @param  string $class The class name
 * @param  boolean $failure_ok Whether to return null if there is no such class
 * @param  array $parameters Array of parameters
 * @return ?object The object (null: could not create)
 */
function object_factory(string $class, bool $failure_ok = false, array $parameters = []) : ?object
{
    if (!class_exists($class)) {
        if ($failure_ok) {
            return null;
        }
        fatal_exit(escape_html('Missing class: ' . $class));
    }
    return new $class(...$parameters);
}

/**
 * Find whether a particular PHP function is blocked.
 *
 * Note that you still need to put "@" before set_time_limit, as some webhost(s) have their own non-detectable block:
 *  "Cannot set max execution time limit due to system policy".
 *
 * @param  string $function Function name
 * @return boolean Whether it is
 */
function php_function_allowed(string $function) : bool
{
    static $cache = [];
    if (isset($cache[$function])) {
        return $cache[$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)) {
            $cache[$function] = false;
            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);
    $cache[$function] = (@preg_match('#(\s|,|^)' . preg_quote($function, '#') . '(\s|$|,)#i', $blocked) == 0);
    return $cache[$function];
}

/**
 * Sets the value of a configuration option, if the PHP environment allows it.
 *
 * @param  string $var Config option
 * @param  string $value New value of option
 * @return ~string Old value of option (false: error)
 */
function cms_ini_set(string $var, string $value)
{
    if (!php_function_allowed('ini_set')) {
        return false;
    }

    if ($var == 'memory_limit' && PHP_DEBUG == 1) {
        $value = '-1';
    }

    return @ini_set($var, $value);
}

/**
 * Flush but only if compression streams are not active.
 */
function cms_flush_safe()
{
    if ((ini_get('output_handler') == '') && (ini_get('brotli.output_compression') !== 'On') && (ini_get('zlib.output_compression') !== 'On') && (ob_get_level()) == 0) {
        flush();
    }
}

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

/**
 * Get the file base for your installation of the software.  For a shared install, or a GAE-install, this is different to the file-base.
 *
 * @return PATH The file base, without a trailing slash
 */
function get_custom_file_base() : string
{
    global $FILE_BASE, $SITE_INFO;
    if (!empty($SITE_INFO['custom_file_base'])) {
        return $SITE_INFO['custom_file_base'];
    }
    if (!empty($SITE_INFO['custom_file_base_stub'])) {
        require_code('shared_installs');
        $u = current_share_user();
        if ($u !== null) {
            return $SITE_INFO['custom_file_base_stub'] . '/' . $u;
        }
    }
    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
 * @param  boolean $preg Whether to just filter out the naughtiness
 * @return string Same as input string
 */
function filter_naughty(string $in, bool $preg = false) : string
{
    if (strpos($in, "\0") !== false) {
        log_hack_attack_and_exit('PATH_HACK');
    }

    if (strpos($in, '..') !== false) {
        if ($preg) {
            return str_replace('.', '', $in);
        }

        $in = str_replace('...', '', $in);
        if (strpos($in, '..') !== false) {
            log_hack_attack_and_exit('PATH_HACK');
        }
        warn_exit(do_lang_tempcode('INVALID_URL'));
    }
    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
 * @param  boolean $preg Whether to just filter out the naughtiness
 * @return string Same as input string
 */
function filter_naughty_harsh(string $in, bool $preg = false) : string
{
    if (preg_match('#^[' . URL_CONTENT_REGEXP . ']*$#D', $in) !== 0) {
        return $in;
    }
    if (preg_match('#^[\w\-]*/#', $in) !== 0) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE')); // Probably a relative URL underneath a URL Scheme short URL, should not really happen
    }

    if ($preg) {
        return preg_replace('#[^' . URL_CONTENT_REGEXP . ']#D', '', $in);
    }
    log_hack_attack_and_exit('EVAL_HACK', $in);
    return ''; // trick to make linters happy
}

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

/**
 * See if we should consider the user admin based on the backdoor_ip setting.
 * If the software is not fully bootstrapped then it will do simpler checks.
 *
 * @return boolean Whether backdoor is activated
 */
function has_backdoor_ip_triggered() : bool
{
    global $SITE_INFO;

    $backdoor_ip_address = []; // Enable to a real IP address to force login from FTP access (if lost admin password)
    if (!empty($SITE_INFO['backdoor_ip'])) {
        $backdoor_ip_addresses = array_map('trim', explode(',', $SITE_INFO['backdoor_ip']));
    }
    if (!empty($backdoor_ip_addresses)) {
        $user_ip = @strval($_SERVER['REMOTE_ADDR']);
        if (empty($user_ip)) {
            return false;
        }
        $localhost_ips = get_localhost_ips();

        $user_ip = normalise_ip_address($user_ip);

        foreach ($backdoor_ip_addresses as $backdoor_ip) {
            if ($backdoor_ip == '') {
                continue;
            }

            $cidr = (strpos($backdoor_ip, '/') !== false);
            if ($cidr) {
                // CIDR
                if (ip_cidr_check($user_ip, $backdoor_ip)) {
                    return true;
                }
            } else {
                if ((function_exists('is_valid_ip')) && (!is_valid_ip(normalise_ip_address($backdoor_ip))) && (!empty($_GET['keep_check_backdoor_ip_dns']))) {
                    // Host name
                    $backdoor_ip = gethostbyname($backdoor_ip);
                } else {
                    // IP
                    $backdoor_ip = normalise_ip_address($backdoor_ip);
                }

                if (!empty($backdoor_ip)) {
                    if ($backdoor_ip === $user_ip) {
                        return true;
                    }

                    if ((in_array($backdoor_ip, $localhost_ips)) && (in_array($user_ip, $localhost_ips))) {
                        return true;
                    }
                }
            }
        }
    }

    return false;
}

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

/**
 * Find if an IP address is within a CIDR range. Based on comment in PHP manual: http://php.net/manual/en/ref.network.php.
 *
 * @param  IP $ip IP address
 * @param  SHORT_TEXT $cidr CIDR range (e.g. 204.93.240.0/24)
 * @return boolean Whether it is
 */
function ip_cidr_check(string $ip, string $cidr) : bool
{
    if ((strpos($ip, ':') === false) !== (strpos($cidr, ':') === false)) {
        return false; // Different IP address type
    }

    if (strpos($ip, ':') === false) {
        // IPv4...

        list($net, $maskbits) = explode('/', $cidr, 2);

        $ip_net = ip2long($net);
        $ip_mask = ~((1 << (32 - intval($maskbits))) - 1);

        $ip_ip = ip2long($ip);

        return (($ip_ip & $ip_mask) == $ip_net);
    }

    // IPv6...

    $unpacked = unpack('A16', _inet_pton($ip));
    $binaryip = '';
    for ($i = 0; $i < strlen($unpacked[1]); $i++) {
        $char = $unpacked[1][$i];
        $binaryip .= str_pad(decbin(ord($char)), 8, '0', STR_PAD_LEFT);
    }

    list($net, $maskbits) = explode('/', $cidr, 2);
    $unpacked = unpack('A16', _inet_pton($net));
    $binarynet = '';
    for ($i = 0; $i < strlen($unpacked[1]); $i++) {
        $char = $unpacked[1][$i];
        $binarynet .= str_pad(decbin(ord($char)), 8, '0', STR_PAD_LEFT);
    }

    $ip_net_bits = substr($binaryip, 0, intval($maskbits));
    $net_bits = substr($binarynet, 0, intval($maskbits));
    return ($ip_net_bits == $net_bits);
}

/**
 * Normalise an IPv6 address.
 *
 * @param  IP $ip IP address
 * @return IP Normalised address
 *
 * @ignore
 */
function _inet_pton(string $ip) : string
{
    $_ip = explode(':', $ip);
    $normalised_ip = '';
    $normalised_ip .= str_pad('', (4 * (8 - count($_ip))), '0000', STR_PAD_LEFT); // Fill out trimmed 0's on left
    foreach ($_ip as $seg) { // Copy rest in
        $normalised_ip .= str_pad($seg, 4, '0', STR_PAD_LEFT); // Pad out each component in full, building up $normalised_ip
    }
    return $normalised_ip;
}

/**
 * Do what we need, if anything, to allow exiting without any extraneous output messing up a non-HTML request.
 * It is a hard assumption that if this function returns, exit will happen via a natural flow.
 */
function cms_safe_exit_flow()
{
    $aaf = @ini_get('auto_append_file');
    if (!empty($aaf)) {
        // Necessary to stop it corrupting our XML if it contains ad-crap
        exit();
    }
}

/**
 * Get a list of third-party directories we should not touch with standard software processes.
 *
 * @return array List of relative directories
 */
function list_untouchable_third_party_directories() : array
{
    return [
        'caches',
        'data/ace',
        'data/ckeditor',
        'data/ckeditor/plugins/codemirror',
        'data_custom/ckeditor',
        'data_custom/pdf_viewer',
        'data/polyfills',
        'docs/jsdoc',
        'exports',
        'imports',
        'mobiquo/lib',
        'mobiquo/smartbanner',
        'nbproject',
        '_old',
        //'sources/diff', We maintain this now
        'sources/isocodes',
        'sources/imap',
        'sources_custom/aws_ses',
        'sources_custom/Cloudinary',
        'sources_custom/composr_mobile_sdk',
        'sources_custom/imap',
        'sources_custom/geshi',
        'sources_custom/getid3',
        'sources_custom/sabredav',
        'sources_custom/openspout',
        'sources_custom/swift_mailer',
        'sources_custom/Transliterator',
        'sources_custom/hybridauth',
        'temp',
        '_tests/assets',
        '_tests/codechecker',
        '_tests/html_dump',
        '_tests/screens_tested',
        '_tests/simpletest',
        'themes/admin/templates_cached/EN',
        'themes/default/templates_cached/EN',
        'themes/_unnamed_/templates_cached/EN',
        'tracker',
        'uploads/website_specific/test',
        'vendor',
    ];
}

/**
 * Get a list of third-party files we should not touch with standard software processes.
 *
 * @return array List of relative files to not touch
 */
function list_untouchable_third_party_files() : array
{
    return [
        //'sources/crc24.php', We maintain this now
        '_config.php',
        'aps/test/TEST-META.xml',
        'aps/test/composrIDEtest.xml',
        'data/curl-ca-bundle.crt',
        'data_custom/errorlog.php',
        'data_custom/execute_temp.php',
        'data_custom/webfonts/adgs-icons.svg',
        // 'data/locations/IP_Country.txt', needs to be tracked by core_locations so the addon version bumps when this is changed (to trigger an update)
        'install.sql',
        'mobiquo/lib/xmlrpc.php',
        'mobiquo/lib/xmlrpcs.php',
        'mobiquo/license_agreement.txt',
        'mobiquo/smartbanner/appbanner.css',
        'mobiquo/smartbanner/appbanner.js',
        'mobiquo/tapatalkdetect.js',
        'sources_custom/browser_detect.php',
        'sources_custom/geshi.php',
        'sources/firephp.php',
        //'sources/jsmin.php', We maintain this now
        //'sources/lang_stemmer_EN.php', We maintain this now
        'sources/mail_dkim.php',
        '_tests/codechecker/codechecker.ini',
        '_tests/codechecker/nbactions.xml',
        '_tests/codechecker/pom.xml',
        '_tests/libs/mf_parse.php',
        'themes/default/css_custom/columns.css',
        'themes/default/css_custom/flip.css',
        'themes/default/css_custom/google_search.css',
        'themes/default/css/skitter.css',
        'themes/default/css_custom/sortable_tables.css',
        'themes/default/css_custom/unslider.css',
        'themes/default/css/mediaelementplayer.css',
        'themes/default/css/widget_color.css',
        'themes/default/css/widget_plupload.css',
        'themes/default/css/widget_select2.css',
        'themes/default/css/toastify.css',
        'themes/default/javascript/charts.js',
        'themes/default/javascript_custom/columns.js',
        'themes/default/javascript_custom/jquery_flip.js',
        'themes/default/javascript/skitter.js',
        'themes/default/javascript_custom/sortable_tables.js',
        'themes/default/javascript_custom/tag_cloud.js',
        'themes/default/javascript_custom/unslider.js',
        'themes/default/javascript/glide.js',
        'themes/default/javascript/jquery.js',
        'themes/default/javascript/webfontloader.js',
        'themes/default/javascript/_json5.js',
        'themes/default/javascript/masonry.js',
        'themes/default/javascript/mediaelement-and-player.js',
        'themes/default/javascript/modernizr.js',
        'themes/default/javascript/plupload.js',
        'themes/default/javascript/select2.js',
        'themes/default/javascript/sound.js',
        'themes/default/javascript/toastify.js',
        'themes/default/javascript/widget_color.js',
        'themes/default/javascript/xsl_mopup.js',
        'themes/default/javascript/cookie_consent.js',
        'themes/default/javascript/_polyfill_fetch.js',
        'themes/default/javascript/_polyfill_general.js',
        'themes/default/javascript/_polyfill_keyboardevent_key.js',
        'themes/default/javascript/_polyfill_url.js',
        'themes/default/javascript/_polyfill_web_animations.js',
        'themes/default/javascript/jquery_autocomplete.js',
        'themes/default/css/toastify.css',
    ];
}

// Useful for basic profiling
global $PAGE_START_TIME;
$PAGE_START_TIME = microtime(true);

// Are we in a special version of PHP?
define('GOOGLE_APPENGINE', isset($_SERVER['APPLICATION_ID']));

define('URL_CONTENT_REGEXP', '\w\-\x80-\xFF'); // PHP is done using ASCII (don't use the 'u' modifier). Note this doesn't include dots, this is intentional as they can cause problems in filenames
define('URL_CONTENT_REGEXP_JS', '\w\-\u0080-\uFFFF'); // JavaScript is done using Unicode

// Sanitise the PHP environment some more
if (!GOOGLE_APPENGINE) {
    cms_ini_set('include_path', '');
    cms_ini_set('allow_url_fopen', '0');
}
cms_ini_set('allow_url_include', '0');
cms_ini_set('display_errors', '0');
cms_ini_set('suhosin.executor.disable_emodifier', '1'); // Extra security if suhosin is available
cms_ini_set('suhosin.executor.multiheader', '1'); // Extra security if suhosin is available
cms_ini_set('suhosin.executor.disable_eval', '0');
cms_ini_set('suhosin.executor.eval.whitelist', '');
cms_ini_set('suhosin.executor.func.whitelist', '');
cms_ini_set('auto_detect_line_endings', '0'); // LEGACY: Remove when only supporting PHP 8.1+
cms_ini_set('default_socket_timeout', '60');
cms_ini_set('html_errors', '1');
cms_ini_set('docref_root', 'http://php.net/manual/en/');
cms_ini_set('docref_ext', '.php');
cms_ini_set('pcre.jit', '0'); // Compatibility issue in PHP 7.3, "JIT compilation failed: no more memory"
@header_remove('x-powered-by'); // Security

fixup_bad_php_env_vars_pre();

$script_name = isset($_SERVER['SCRIPT_NAME']) ? $_SERVER['SCRIPT_NAME'] : '';
if ((strpos($script_name, '/sources/') !== false) || (strpos($script_name, '/sources_custom/') !== false)) {
    header('Content-Type: text/plain; charset=utf-8');
    exit('May not be included directly');
}

// Get ready for some global variables
global $REQUIRED_CODE, $REQUIRING_CODE, $CURRENT_SHARE_USER, $PURE_POST, $IN_MINIKERNEL_VERSION;
/** Details of what code files have been loaded up.
 *
 * @global array $REQUIRED_CODE
 */
$REQUIRED_CODE = [];
$REQUIRING_CODE = false;
/** If running on a shared-install, this is the identifying name of the site that is being called up.
 *
 * @global ?ID_TEXT $CURRENT_SHARE_USER
 */
if ((!isset($CURRENT_SHARE_USER)) || (isset($_SERVER['REQUEST_METHOD']))) {
    $CURRENT_SHARE_USER = null;
}
/** A copy of the POST parameters, as passed initially to PHP (needed for hash checks with some IPN systems).
 *
 * @global array $PURE_POST
 */
$PURE_POST = $_POST;
$IN_MINIKERNEL_VERSION = false;

// Critical error reporting system
global $FILE_BASE;
if (is_file($FILE_BASE . '/sources_custom/critical_errors.php')) {
    require $FILE_BASE . '/sources_custom/critical_errors.php';
} else {
    if (function_exists('error_clear_last')) {
        error_clear_last();
    }
    $errormsg_before = error_get_last();
    $result = @include $FILE_BASE . '/sources/critical_errors.php';
    $errormsg = error_get_last();
    if ((!$result) && ($errormsg !== null) && ($errormsg !== $errormsg_before)) {
        exit('<!DOCTYPE html>' . "\n" . '<html lang="EN"><head><title>Critical startup error</title></head><body><h1>Composr startup error</h1><p>The third most basic Composr startup file, sources/critical_errors.php, could not be loaded (error: ' . $errormsg['message'] . '). This is almost always due to an incomplete upload of the Composr system, so please check all files are uploaded correctly.</p><p>Once all Composr files are in place, Composr must actually be installed by running the installer. You must be seeing this message either because your system has become corrupt since installation, or because you have uploaded some but not all files from our manual installer package: the quick installer is easier, so you might consider using that instead.</p><p>The core developers maintain full documentation for all procedures and tools, especially those for installation. These may be found on the <a href="https://composr.app">Composr website</a>. If you are unable to easily solve this problem, we may be contacted from our website and can help resolve it for you.</p><hr /><p style="font-size: 0.8em">Composr is a website engine created by Christopher Graham.</p></body></html>');
    }
}

// NB: bootstrap should have loaded $SITE_INFO already, but bootstrap does not actually check if it's missing because it's not critical until global.php
global $SITE_INFO;
if (!is_array($SITE_INFO) || (count($SITE_INFO) == 0) || (empty($SITE_INFO))) {
    if (!is_file($FILE_BASE . '/_config.php')) {
        critical_error('_CONFIG.PHP_MISSING');
    } elseif (strlen(trim(file_get_contents($FILE_BASE . '/_config.php'))) == 0) {
        critical_error('_CONFIG.PHP_EMPTY');
    } else {
        critical_error('_CONFIG.PHP_CORRUPTED');
    }
}

// Check if we might be proxying through Cloudflare (unsafe test as this does not check actual remote address against known Cloudflare IPs)
global $MIGHT_BE_USING_CF;
$MIGHT_BE_USING_CF = isset($_SERVER['HTTP_CF_RAY']);

// Make sure we have the correct IP address in REMOTE_ADDR. Note for Cloudflare checks, some webhosts might handle Cloudflare automatically, which prevents us from knowing the true REMOTE_ADDR of Cloudflare, thus we cannot compare it to trusted proxies.
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    if (empty($SITE_INFO['trusted_proxies'])) {
        $trusted_proxies = '173.245.48.0/20,103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,141.101.64.0/18,108.162.192.0/18,190.93.240.0/20,188.114.96.0/20,197.234.240.0/22,198.41.128.0/17,162.158.0.0/15,104.16.0.0/13,104.24.0.0/14,172.64.0.0/13,131.0.72.0/22,2400:cb00::/32,2606:4700::/32,2803:f800::/32,2405:b500::/32,2405:8100::/32,2a06:98c0::/29,2c0f:f248::/32';
        $might_be_cloudflare = true;
    } else {
        $trusted_proxies = $SITE_INFO['trusted_proxies'];
        $might_be_cloudflare = false;
    }
    foreach (explode(',', $trusted_proxies) as $proxy) {
        if (((strpos($proxy, '/') !== false) && (ip_cidr_check($_SERVER['REMOTE_ADDR'], $proxy))) || ($_SERVER['REMOTE_ADDR'] == $proxy)) {
            if (ip_cidr_check($_SERVER['REMOTE_ADDR'], $proxy)) {
                $_SERVER['REMOTE_ADDR'] = (($might_be_cloudflare) && (isset($_SERVER['HTTP_CF_CONNECTING_IP']))) ? $_SERVER['HTTP_CF_CONNECTING_IP'] : $_SERVER['HTTP_X_FORWARDED_FOR'];
                $_SERVER['HTTP_X_FORWARDED_FOR'] = '';
                if ($might_be_cloudflare) {
                    unset($_SERVER['HTTP_CF_CONNECTING_IP']);
                }
                if ((isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) && ($_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')) {
                    $_SERVER['HTTPS'] = 'on';
                    $_SERVER['SERVER_PORT'] = '443';
                }
                break;
            }
        }
    }
}

// Rate limiter, to stop aggressive bots
global $SITE_INFO;
$rate_limiting = !empty($SITE_INFO['rate_limiting']);
if ($rate_limiting) {
    if ((!empty($_SERVER['REMOTE_ADDR'])) && (basename($_SERVER['SCRIPT_NAME']) == 'index.php')) {
        // Basic context
        $ip = $_SERVER['REMOTE_ADDR'];
        $time = time();

        if (!(((!empty($_SERVER['SERVER_ADDR'])) && ($ip == $_SERVER['SERVER_ADDR'])) || ((!empty($_SERVER['LOCAL_ADDR'])) && ($ip == $_SERVER['LOCAL_ADDR'])))) {
            global $RATE_LIMITING_DATA;
            $RATE_LIMITING_DATA = [];

            // Read in state
            $rate_limiter_path = dirname(__DIR__) . '/data_custom/rate_limiter.php';
            if (is_file($rate_limiter_path)) {
                $fp = fopen($rate_limiter_path, 'rb');
                flock($fp, LOCK_SH);
                include $rate_limiter_path;
                flock($fp, LOCK_UN);
                fclose($fp);
            }

            // Filter to just times within our window
            $pertinent = [];
            $rate_limit_time_window = empty($SITE_INFO['rate_limit_time_window']) ? 10 : intval($SITE_INFO['rate_limit_time_window']);
            if (isset($RATE_LIMITING_DATA[$ip])) {
                foreach ($RATE_LIMITING_DATA[$ip] as $i => $old_time) {
                    if ($old_time >= $time - $rate_limit_time_window) {
                        $pertinent[] = $old_time;
                    }
                }
            }

            // Do we have to block?
            $rate_limit_hits_per_window = empty($SITE_INFO['rate_limit_hits_per_window']) ? 5 : intval($SITE_INFO['rate_limit_hits_per_window']);
            if (count($pertinent) >= $rate_limit_hits_per_window) {
                http_response_code(429);
                header('Content-Type: text/plain');
                exit('We only allow ' . strval($rate_limit_hits_per_window - 1) . ' page hits every ' . strval($rate_limit_time_window) . ' seconds. You\'re at ' . strval(count($pertinent)) . '.');
            }

            // Remove any old hits from other IPs
            foreach ($RATE_LIMITING_DATA as $_ip => $times) {
                if ($_ip != $ip) {
                    foreach ($times as $i => $old_time) {
                        if ($old_time < $time - $rate_limit_time_window) {
                            unset($RATE_LIMITING_DATA[$_ip][$i]);
                        }
                    }
                    if (empty($RATE_LIMITING_DATA[$_ip])) {
                        unset($RATE_LIMITING_DATA[$_ip]);
                    }
                }
            }

            // Write out new state
            $RATE_LIMITING_DATA[$ip] = $pertinent;
            $RATE_LIMITING_DATA[$ip][] = $time;
            file_put_contents($rate_limiter_path, '<' . '?php' . "\n\n" . '$RATE_LIMITING_DATA=' . var_export($RATE_LIMITING_DATA, true) . ';' . "\n", LOCK_EX);
            //sync_file($rate_limiter_path); Not done. Each server should rate limit separately. Synching this data across servers would be too slow and not scalable

            // Save some memory
            unset($RATE_LIMITING_DATA);
        }
    }
}

get_custom_file_base(); // Make sure $CURRENT_SHARE_USER is set if it is a shared site, so we can use CURRENT_SHARE_USER as an indicator of it being one.

// Pass on to next bootstrap level
if (GOOGLE_APPENGINE) {
    require_code('google_appengine');
}
require_code('global2');
