<?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: get_php_file_api|test_fail_php_type_check|get_resources|get_resource_type|stream_get_meta_data*/

/*
Some basic developer tools for software PHP development.

Also see:
 firephp
 profiler
 php
*/

/**
 * Standard code module initialisation function.
 *
 * @ignore
 */
function init__developer_tools()
{
    global $MEMORY_PROFILING_POINTS;
    $MEMORY_PROFILING_POINTS = [];

    global $PREVIOUS_XSS_STATE;
    $PREVIOUS_XSS_STATE = ['1'];
}

/**
 * Run some routines needed for semi-dev-mode, during startup.
 */
function semi_dev_mode_startup()
{
    global $SEMI_DEV_MODE, $DEV_MODE;
    if ($SEMI_DEV_MODE) {
        /*if ((mt_rand(0,2)==1) && ($DEV_MODE) && (running_script('index')))  We know this works now, so let's stop messing up our development speed
        {
            require_code('caches3');
            erase_cached_templates(true); // Stop anything trying to read a template cache item (E.g. CSS, JS) that might not exist!
        }*/

        if (
            (strpos($_SERVER['HTTP_REFERER'], get_request_hostname()) !== false) &&
            (strpos($_SERVER['HTTP_REFERER'], 'keep_devtest') !== false) &&
            (!running_script('attachment')) &&
            (!running_script('external_url_proxy')) &&
            (!running_script('hybridauth')) &&
            (!running_script('upgrader')) &&
            (!running_script('csp_logging')) &&
            (strpos($_SERVER['HTTP_REFERER'], 'login') === false) &&
            (!currently_logging_in()) &&
            (get_page_name() != '404') &&
            (get_param_string('keep_devtest', null) === null)
        ) {
            $_GET['keep_devtest'] = '1';
            require_code('site');
            attach_message('URL not constructed properly: development mode in use but keep_devtest was not specified. This indicates that links have been made without build_url (in PHP) or $cms.keep() (in JavaScript). While not fatal this time, failure to use these functions can cause problems when your site goes live. See the codebook for more details.', 'warn', false, true);
        } else {
            $_GET['keep_devtest'] = '1';
        }
    }

    global $_CREATED_FILES;
    if (isset($_CREATED_FILES)) { // Comes from ocProducts custom PHP version
        cms_register_shutdown_function_if_available(function () {
            // Nested so it will run last
            cms_register_shutdown_function_if_available(function () {
                global $_CREATED_FILES, $_MODIFIED_FILES;

                // Use the info from ocProduct's custom PHP version to make sure that all files that were created/modified got synched as they should have been.
                foreach ($_CREATED_FILES as $file) {
                    if ((substr($file, 0, strlen(get_file_base())) == get_file_base()) && (is_file($file)) && (strpos($file, 'log') === false) && (strpos($file, 'tmp') === false) && (strpos($file, 'temp') === false) && (strpos($file, 'cache') === false) && (strpos($file, 'backup') === false) && (strpos($file, 'incoming') === false)) {
                        @exit(escape_html('File not permission-synched: ' . $file));
                    }
                }
                foreach ($_MODIFIED_FILES as $file) {
                    if ((substr($file, 0, strlen(get_file_base())) == get_file_base()) && (is_file($file)) && (strpos($file, 'log') === false) && (strpos($file, 'tmp') === false) && (strpos($file, 'temp') === false) && (strpos($file, 'cache') === false) && (strpos($file, 'backup') === false) && (strpos($file, 'builds') === false) && (strpos($file, 'incoming') === false)) {
                        if ((strpos($file, '_config.php') === false) && (strpos($file, 'failover_rewritemap') === false) && (basename($file) != 'rate_limiter.php')) {
                            @exit(escape_html('File not change-synched: ' . $file));
                        }
                    }
                }

                global $TITLE_CALLED, $SCREEN_TEMPLATE_CALLED, $EXITING;
                if (($SCREEN_TEMPLATE_CALLED === null) && ($EXITING == 0) && (running_script('index'))) {
                    @exit(escape_html('No screen template called.'));
                }
                if ((!$TITLE_CALLED) && (($SCREEN_TEMPLATE_CALLED === null) || ($SCREEN_TEMPLATE_CALLED != '')) && ($EXITING == 0) && (strpos($_SERVER['SCRIPT_NAME'], 'index.php') !== false)) {
                    @exit(escape_html('No title used on screen.'));
                }

                if (function_exists('get_resources') && function_exists('get_resource_type') && function_exists('stream_get_meta_data')) {
                    if (function_exists('_cms_profiler_script_end')) {
                        _cms_profiler_script_end();
                    }

                    $rs = get_resources();
                    foreach ($rs as $r) {
                        $type = get_resource_type($r);
                        if (!in_array($type, ['Unknown', 'stream-context', 'pspell', 'pspell config'])) {
                            if ($type == 'stream') {
                                $stream_meta = stream_get_meta_data($r);
                            } else {
                                $stream_meta = null;
                            }

                            if (($stream_meta === null) || (!in_array($stream_meta['stream_type'], ['TEMP', 'MEMORY', 'STDIO']))) {
                                @exit(escape_html('Unexpected resource left open of type, ' . $type . (($type == 'stream') ? ('; ' . var_export($stream_meta, true)) : '')));
                            }
                        }
                    }
                }
            });
        });
    }

    if (($_SERVER['SCRIPT_NAME'] != '') && (empty($GLOBALS['EXTERNAL_CALL'])) && ($DEV_MODE) && (strpos($_SERVER['SCRIPT_NAME'], 'data_custom') === false)) {
        if (is_file($_SERVER['SCRIPT_NAME'])) {
            $file = cms_file_get_contents_safe($_SERVER['SCRIPT_NAME'], FILE_READ_LOCK);
            if (($file !== false) && strlen($file) > 4500) {
                fatal_exit('Entry scripts (front controllers) should not be shoved full of code.');
            }
        }
    }
}

/**
 * Remove strictness, to help integration of third-party code.
 *
 * @param  boolean $db_too Whether to destrictify database commands over the software's database driver
 */
function destrictify(bool $db_too = false)
{
    // Turn off strictness
    $GLOBALS['SCREEN_TEMPLATE_CALLED'] = '';
    $GLOBALS['TITLE_CALLED'] = true;
    error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT & ~E_NOTICE);
    cms_extend_time_limit(TIME_LIMIT_EXTEND__MODEST);
    if (($db_too) && (is_object($GLOBALS['SITE_DB']->connection_read))) {
        $smq = $GLOBALS['SITE_DB']->strict_mode_query(false);
        if ($smq !== null) {
            $GLOBALS['SITE_DB']->query($smq, null, 0, true); // Suppress errors in case access denied
        }
    }
    global $PREVIOUS_XSS_STATE;
    @array_push($PREVIOUS_XSS_STATE, ini_get('ocproducts.xss_detect'));
    cms_ini_set('ocproducts.type_strictness', '0');
    cms_ini_set('ocproducts.xss_detect', '0');
    $include_path = ini_get('include_path');
    $include_path .= PATH_SEPARATOR . './';
    $include_path .= PATH_SEPARATOR . get_file_base() . '/';
    $include_path .= PATH_SEPARATOR . get_file_base() . '/sources_custom/';
    $include_path .= PATH_SEPARATOR . get_file_base() . '/uploads/website_specific/';
    if (function_exists('get_zone_name')) {
        if (get_zone_name() != '') {
            $include_path .= PATH_SEPARATOR . get_file_base() . '/' . get_zone_name() . '/';
        }
        cms_ini_set('include_path', $include_path);
    }
    //disable_php_memory_limit();   Don't do this, recipe for disaster
    cms_ini_set('suhosin.executor.disable_emodifier', '0');
    cms_ini_set('suhosin.executor.multiheader', '0');
    pop_db_scope_check();
    push_db_scope_check(false);
    pop_query_limiting();
    push_query_limiting(false);
}

/**
 * Add strictness, after finishing with third-party code. To be run optionally at some point after destrictify().
 */
function restrictify()
{
    global $_CREATED_FILES, $_MODIFIED_FILES, $SITE_INFO;

    // Reset functions
    if (isset($_CREATED_FILES)) {
        $_CREATED_FILES = [];
    }
    if (isset($_MODIFIED_FILES)) {
        $_MODIFIED_FILES = [];
    }

    // Put back strictness
    error_reporting(E_ALL);
    if (is_object($GLOBALS['SITE_DB']->connection_read)) {
        $smq = $GLOBALS['SITE_DB']->strict_mode_query(true);
        if ($smq !== null) {
            $GLOBALS['SITE_DB']->query($smq, null, 0, true); // Suppress errors in case access denied
        }
    }
    if (($GLOBALS['DEV_MODE']) && (strpos($_SERVER['SCRIPT_NAME'], '_tests') === false)) {
        if (get_param_integer('keep_type_strictness', null) !== 0) {
            cms_ini_set('ocproducts.type_strictness', '1');
        }

        if (get_param_integer('keep_xss_detect', null) !== 0) {
            global $PREVIOUS_XSS_STATE;
            $prev = array_pop($PREVIOUS_XSS_STATE);
            if ($prev !== false) {
                cms_ini_set('ocproducts.xss_detect', $prev);
            }
        }
    }
    if (!GOOGLE_APPENGINE) {
        cms_ini_set('include_path', '');
    }
    cms_ini_set('suhosin.executor.disable_emodifier', '1');
    cms_ini_set('suhosin.executor.multiheader', '1');
    pop_db_scope_check();
    push_db_scope_check(true);
    //push_query_limiting(false);   Leave off, may have been set elsewhere than destrictify();
}

/**
 * Output whatever arguments are given for debugging. If possible it'll output with plain text, but if output has already started it will attach messages.
 */
function inspect()
{
    $args = func_get_args();

    _inspect($args, false);
}

/**
 * Output whatever arguments are given for debugging as text and exit. If possible it'll output with plain text, but if output has already started it will attach messages.
 */
function inspect_plain()
{
    $args = func_get_args();

    _inspect($args, true);
}

/**
 * Output whatever arguments are given for debugging. If possible it'll output with plain text, but if output has already started it will attach messages.
 *
 * @param  array $args Arguments to output
 * @param  boolean $force_plain Whether to force text output
 *
 * @ignore
 */
function _inspect(array $args, bool $force_plain = false)
{
    $plain = headers_sent() || $force_plain || !running_script('index');

    if ($plain) {
        cms_ini_set('ocproducts.xss_detect', '0');

        $GLOBALS['SCREEN_TEMPLATE_CALLED'] = '';

        if (!headers_sent()) {
            header('Content-Type: text/plain; charset=' . get_charset());
            header('Content-Disposition: inline'); // Override what might have been set
        }

        echo 'DEBUGGING. INSPECTING VARIABLES...' . "\n";
    } else {
        header('Content-Type: text/html; charset=' . get_charset());
        header('Content-Disposition: inline'); // Override what might have been set
    }

    foreach ($args as $arg_name => $arg_value) {
        if (!is_string($arg_name)) {
            $arg_name = strval($arg_name + 1);
        }

        if ($plain) {
            echo "\n\n" . $arg_name . ' is...' . "\n";
            if ((is_object($arg_value) && (is_a($arg_value, 'Tempcode')))) {
                echo 'Tempcode: ' . $arg_value->evaluate() . ' (';
                var_dump($arg_value);
                echo ')';
            } else {
                var_dump($arg_value);
            }
        } else {
            if ((is_object($arg_value) && (is_a($arg_value, 'Tempcode')))) {
                attach_message($arg_name . ' is...' . "\n" . 'Tempcode: ' . $arg_value->evaluate());
            } else {
                attach_message($arg_name . ' is...' . "\n" . var_export($arg_value, true));
            }
        }
    }

    if ($plain) {
        echo "\n\n" . '--------------------' . "\n\n" . 'STACK TRACE FOLLOWS...' . "\n\n";

        debug_print_backtrace();
        exit();
    }
}

/**
 * Record the memory usage at this point.
 *
 * @param  ?string $name The name of the memory point (null: use a simple counter)
 */
function memory_trace_point(?string $name = null)
{
    global $MEMORY_PROFILING_POINTS;
    if ($name === null) {
        $name = '#' . integer_format(count($MEMORY_PROFILING_POINTS) + 1);
    }
    $MEMORY_PROFILING_POINTS[] = [memory_get_usage(), $name];
}

/**
 * Output whatever memory points we collected up.
 */
function show_memory_points()
{
    @header('Content-Type: text/plain; charset=' . get_charset());

    cms_ini_set('ocproducts.xss_detect', '0');

    $GLOBALS['SCREEN_TEMPLATE_CALLED'] = '';

    global $MEMORY_PROFILING_POINTS;
    $before = null;
    foreach ($MEMORY_PROFILING_POINTS as $point) {
        list($memory, $name) = $point;
        echo 'Memory at ' . $name . ' is' . "\t" . integer_format($memory) . ' (growth of ' . (($before === null) ? 'N/A' : integer_format($memory - $before)) . ')' . "\n";
        $before = $memory;
    }
    exit();
}

/*!*
 * Finds if a function is being run underneath another function, and exit if there is a death message to output. This function should only be used when coding.
 *
 * @param  string $function he function to check running underneath
 * @param  ?string $death_message The message to exit with (null: return, do not exit)
 * @return boolean Whether we are
 */
/*function debug_running_underneath($function, $death_message = null)
{
    $stack = debug_backtrace();
    foreach ($stack as $level) {
        if (in_array($function, $level)) {
            if ($death_message !== null) {
                fatal_exit($death_message);
            }
            return true;
        }
    }
    return false;
}*/

/**
 * Verify the parameters passed into the *calling* function match the PHPDoc specification for that function.
 * Useful when testing robustness of APIs where the CQC and ocProducts PHP are not suitable.
 * For example, when web APIs are plumbed into the software's APIs and you need to ensure the types are coming in correctly.
 *
 * @param  boolean $dev_only Whether to only run the checks in dev-mode
 */
function cms_verify_parameters_phpdoc(bool $dev_only = false)
{
    if ($dev_only) {
        if (!$GLOBALS['DEV_MODE']) {
            return;
        }
    }

    if (!addon_installed('testing_platform')) {
        return;
    }

    $trace = debug_backtrace();

    $filename = $trace[1]['file'];
    if (substr($filename, 0, strlen(get_file_base() . '/')) == get_file_base() . '/') {
        $filename = substr($filename, strlen(get_file_base() . '/'));
    }
    $class = isset($trace[1]['class']) ? $trace[1]['class'] : '__global';
    $function = $trace[1]['function'];

    static $api = [];
    if (!isset($api[$filename])) {
        require_code('phpdoc');
        $api[$filename] = get_php_file_api($filename);
    }

    if (isset($api[$filename][$class]['functions'][$function]['parameters'])) {
        foreach ($api[$filename][$class]['functions'][$function]['parameters'] as $i => $param) {
            $name = $param['name'];
            $type_expected = $param['type'];

            if (isset($trace[1]['args'][$i])) {
                $value = $trace[1]['args'][$i];

                test_fail_php_type_check($type_expected, (isset($trace[1]['class']) ? ($class . '::') : '') . $function, $name, $value); // TODO: is this correct?
            }
        }
    }
}

/**
 * Generate a row of random dummy data into the database.
 * Ideally, you should instead run the stress test loader, but this is useful for quick tests utilising specific tables in the database.
 * You should immediately delete the rows after use.
 *
 * @param  ID_TEXT $table The name of the table to generate dummy data
 * @param  boolean $allow_null Whether to randomly save null values in nullable columns (false: a value is always saved even if nullable unless explicitly defined in $forced_values)
 * @param  array $forced_values A map of fields => values that should be used instead of randomness
 * @return array Map of primary fields associated with the row created
 */
function make_dummy_db_row(string $table, bool $allow_null = true, array $forced_values = []) : array
{
    $primary_map = [];
    $map = [];

    // Get db metadata
    $db = get_db_for($table);
    $fields = $db->query_select('db_meta', ['m_name', 'm_type'], ['m_table' => $table]);
    $metadata = collapse_2d_complexity('m_name', 'm_type', $fields);

    $has_id_field = false;

    foreach ($metadata as $field => $type) {
        $nullable_field = false;
        $primary_field = false;
        if (strpos($type, '*') !== false) {
            $primary_field = true;
        }
        if (strpos($type, '?') !== false) {
            $nullable_field = true;
        }

        // Set forced values
        if (array_key_exists($field, $forced_values)) {
            $map[$field] = $forced_values[$field];
            if ($primary_field === true) {
                $primary_map[$field] = $forced_values[$field];
            }
            continue;
        }

        // We do not want to populate our own data on id fields
        if ($type == '*AUTO') {
            $has_id_field = true;
            continue;
        }

        // Randomly assign null to nullable fields if we are allowed (not on TRANS though; too complicated to deal with null translations)
        if (($nullable_field === true) && ($allow_null === true) && (mt_rand(0, 100) >= 50) && (strpos($type, '_TRANS') === false)) {
            $map[$field] = null;
            if ($primary_field === true) { // Should never happen but we added it just in case
                $primary_map[$field] = null;
            }
            continue;
        }

        if (strpos($type, '_TRANS__COMCODE') !== false) {
            $field2 = insert_lang_comcode($field, uniqid('', true), 4, $db);
            $map += $field2;
            if ($primary_field === true) {
                $primary_map += $field2;
            }
        } elseif (strpos($type, '_TRANS') !== false) {
            $field3 = insert_lang($field, uniqid('', true), 4, $db);
            $map += $field3;
            if ($primary_field === true) {
                $primary_map += $field3;
            }
        } elseif (strpos($type, 'TIME') !== false) {
            $value2 = mt_rand(time() - (60 * 60 * 24 * 365), time()); // Between 1 year ago and now
            $map[$field] = $value2;
            if ($primary_field === true) {
                $primary_map[$field] = $value2;
            }
        } elseif (strpos($type, 'BINARY') !== false) {
            $value3 = mt_rand(0, 1);
            $map[$field] = $value3;
            if ($primary_field === true) {
                $primary_map[$field] = $value3;
            }
        } elseif (strpos($type, 'MEMBER') !== false) {
            $value4 = mt_rand($GLOBALS['DB_DRIVER']->get_first_id(), $GLOBALS['FORUM_DRIVER']->get_num_members()); // Could generate an invalid / deleted member
            $map[$field] = $value4;
            if ($primary_field === true) {
                $primary_map[$field] = $value4;
            }
        } elseif (strpos($type, 'GROUP') !== false) {
            // We have no easy mechanism in forum drivers to do randomness with groups
            $map[$field] = $GLOBALS['DB_DRIVER']->get_first_id();
            if ($primary_field === true) {
                $primary_map[$field] = $GLOBALS['DB_DRIVER']->get_first_id();
            }
        } elseif (strpos($type, 'AUTO_LINK') !== false) {
            $value5 = mt_rand($GLOBALS['DB_DRIVER']->get_first_id(), $GLOBALS['DB_DRIVER']->get_first_id() + 65535);
            $map[$field] = $value5;
            if ($primary_field === true) {
                $primary_map[$field] = $value5;
            }
        } elseif (strpos($type, 'IP') !== false) {
            $value6 = '192.168.' . strval(mt_rand(0, 255)) . '.' . strval(mt_rand(0, 255)); // Enforce local IP addresses for dummy data
            $map[$field] = $value6;
            if ($primary_field === true) {
                $primary_map[$field] = $value6;
            }
        } elseif (strpos($type, 'URLPATH') !== false) {
            $value7 = 'https://example.com/' . uniqid('', false); // we do not want a valid URL for dummy data
            $map[$field] = $value7;
            if ($primary_field === true) {
                $primary_map[$field] = $value7;
            }
        } elseif (strpos($type, 'SHORT_INTEGER') !== false) {
            $value8 = mt_rand(-128, 127);
            $map[$field] = $value8;
            if ($primary_field === true) {
                $primary_map[$field] = $value8;
            }
        } elseif (strpos($type, 'UINTEGER') !== false) {
            $value9 = mt_rand(0, 1000000);
            $map[$field] = $value9;
            if ($primary_field === true) {
                $primary_map[$field] = $value9;
            }
        } elseif (strpos($type, 'INTEGER') !== false) {
            $value10 = random_int(-1000000, 1000000);
            $map[$field] = $value10;
            if ($primary_field === true) {
                $primary_map[$field] = $value10;
            }
        } elseif (strpos($type, 'REAL') !== false) {
            $value11 = (mt_rand(0, 1000000) / 1000000);
            $map[$field] = $value11;
            if ($primary_field === true) {
                $primary_map[$field] = $value11;
            }
        } elseif (strpos($type, 'LANGUAGE_NAME') !== false) {
            $value12 = substr(str_shuffle('QWERTYUIOPASDFGHJKLZXCVBNM'), 0, 4);
            $map[$field] = $value12;
            if ($primary_field === true) {
                $primary_map[$field] = $value12;
            }
        } elseif (strpos($type, 'SERIAL') !== false) {
            $value13 = serialize([uniqid('', false)]);
            $map[$field] = $value13;
            if ($primary_field === true) {
                $primary_map[$field] = $value13;
            }
        // } elseif (strpos($type, 'BGUID') !== false) { // TODO: Implement
        } else {
            $value_else = uniqid('', false);
            $map[$field] = $value_else;
            if ($primary_field === true) {
                $primary_map[$field] = $value_else;
            }
        }
    }

    // Create the record
    if ($has_id_field) {
        $id = $db->query_insert($table, $map, true);
        if ($id !== null) {
            $primary_map['id'] = $id;
        }
    } else {
        $db->query_insert($table, $map);
    }

    if (count($primary_map) == 0) { // Uh oh! This should never happen.
        warn_exit(do_lang_tempcode('INTERNAL_ERROR', escape_html('ac7955fb28aa5e74bf92c9d27142197e')));
    }

    return $primary_map;
}
