<?php /*

 Composr
 Copyright (c) ocProducts, 2004-2016

 See text/EN/licence.txt for full licencing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

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

/**
 * Standard code module initialisation function.
 *
 * @ignore
 */
function init__failure()
{
    global $DONE_ONE_WEB_SERVICE;
    $DONE_ONE_WEB_SERVICE = false;

    global $THROWING_ERRORS;
    $THROWING_ERRORS = false;

    if (!defined('MAX_STACK_TRACE_VALUE_LENGTH')) {
        define('MAX_STACK_TRACE_VALUE_LENGTH', 300);
    }

    /** Whether we want errors to result in simple text responses. Useful for AJAX scripts.
     *
     * @global boolean $WANT_TEXT_ERRORS
     */
    global $WANT_TEXT_ERRORS;
    $cli = false;
    if (function_exists('php_sapi_name')) {
        $cli = ((php_sapi_name() == 'cli') && (empty($_SERVER['REMOTE_ADDR'])) && (empty($_ENV['REMOTE_ADDR'])));
    }
    $WANT_TEXT_ERRORS = $cli;

    global $RUNNING_TASK;
    $RUNNING_TASK = false;

    global $BLOCK_OCPRODUCTS_ERROR_EMAILS;
    if (!isset($BLOCK_OCPRODUCTS_ERROR_EMAILS)) {
        $BLOCK_OCPRODUCTS_ERROR_EMAILS = false;
    }
}

/**
 * Give the user an option to see a stack trace by adding in a link, but only if they have permission
 */
function suggest_fatalistic()
{
    if ((may_see_stack_dumps()) && (get_param_integer('keep_fatalistic', 0) == 0) && (running_script('index')) && (strpos(cms_srv('SCRIPT_NAME'), '/_tests/') === false)) {
        require_code('urls');
        if (cms_srv('REQUEST_METHOD') != 'POST') {
            $stack_trace_url = build_url(array('page' => '_SELF', 'keep_fatalistic' => 1), '_SELF', null, true);
            $st = do_lang_tempcode('WARN_TO_STACK_TRACE', escape_html($stack_trace_url->evaluate()));
        } elseif (count($_FILES) == 0 || function_exists('is_plupload') && is_plupload()) {
            $stack_trace_url = build_url(array('page' => '_SELF', 'keep_fatalistic' => 1), '_SELF', null, true);
            $p = build_keep_post_fields();
            $p->attach(symbol_tempcode('INSERT_SPAMMER_BLACKHOLE'));
            $st = do_lang_tempcode('WARN_TO_STACK_TRACE_2', escape_html($stack_trace_url->evaluate()), $p->evaluate());
        } else {
            $stack_trace_url = build_url(array('page' => '', 'keep_fatalistic' => 1), '');
            $st = do_lang_tempcode('WARN_TO_STACK_TRACE_3', escape_html($stack_trace_url->evaluate()));
        }
        require_code('site');
        attach_message($st, 'inform');
    }
}

/**
 * Terminate with an error caused by unzipping.
 *
 * @param  integer $errno The zip error number.
 * @param  boolean $mzip Whether mzip was used.
 * @return Tempcode Error message.
 */
function zip_error($errno, $mzip = false)
{
    $zip_file_function_errors = array( // Based on comment from php.net
                                       'ZIPARCHIVE::ER_MULTIDISK' => 'Multi-disk zip archives not supported.',
                                       'ZIPARCHIVE::ER_RENAME' => 'Renaming temporary file failed.',
                                       'ZIPARCHIVE::ER_CLOSE' => 'Closing zip archive failed',
                                       'ZIPARCHIVE::ER_SEEK' => 'Seek error',
                                       'ZIPARCHIVE::ER_READ' => 'Read error',
                                       'ZIPARCHIVE::ER_WRITE' => 'Write error',
                                       'ZIPARCHIVE::ER_CRC' => 'CRC error',
                                       'ZIPARCHIVE::ER_ZIPCLOSED' => 'Containing zip archive was closed',
                                       'ZIPARCHIVE::ER_NOENT' => 'No such file.',
                                       'ZIPARCHIVE::ER_EXISTS' => 'File already exists',
                                       'ZIPARCHIVE::ER_OPEN' => 'Can\'t open file',
                                       'ZIPARCHIVE::ER_TMPOPEN' => 'Failure to create temporary file.',
                                       'ZIPARCHIVE::ER_ZLIB' => 'Zlib error',
                                       'ZIPARCHIVE::ER_MEMORY' => 'Memory allocation failure',
                                       'ZIPARCHIVE::ER_CHANGED' => 'Entry has been changed',
                                       'ZIPARCHIVE::ER_COMPNOTSUPP' => 'Compression method not supported.',
                                       'ZIPARCHIVE::ER_EOF' => 'Premature EOF',
                                       'ZIPARCHIVE::ER_INVAL' => 'Invalid argument',
                                       'ZIPARCHIVE::ER_NOZIP' => 'Not a zip archive',
                                       'ZIPARCHIVE::ER_INTERNAL' => 'Internal error',
                                       'ZIPARCHIVE::ER_INCONS' => 'Zip archive inconsistent',
                                       'ZIPARCHIVE::ER_REMOVE' => 'Can\'t remove file',
                                       'ZIPARCHIVE::ER_DELETED' => 'Entry has been deleted',
    );
    $errmsg = 'unknown';
    foreach ($zip_file_function_errors as $const_name => $error_message) {
        if ((defined($const_name)) && (@constant($const_name)) == $errno) {
            $errmsg = $error_message;
        }
    }
    return do_lang_tempcode($mzip ? 'ZIP_ERROR_MZIP' : 'ZIP_ERROR', $errmsg);
}

/**
 * Handle invalid parameter values.
 *
 * @param  string $name The parameter deemed to have an invalid value somehow
 * @param  ?string $ret The value of the parameter deemed invalid (null: we known we can't recover)
 * @param  boolean $posted Whether the parameter is a POST parameter
 * @return string Fixed parameter (usually the function won't return [instead will give an error], but in special cases, it can filter an invalid return)
 * @ignore
 */
function _param_invalid($name, $ret, $posted)
{
    // Invalid params can happen for many reasons:
    //  [/url] getting onto the end of URLs by bad URL extractors getting URLs out of Comcode
    //  Spiders trying to ascend directory trees, and forcing index.php into the integer position of URL Schemes
    //  Spiders that don't understand entity decoding
    //  People copying and pasting text shown after URLs as part of the URL itself
    //  New line characters getting pasted in (weird, but it's happened-- think might be some kind of screen reader browser)
    //  People typing the wrong URLs for many reasons
    // Therefore we can't really treat it as a hack-attack, even though that would be preferable.

    static $param_invalid_looping = false;
    if ($param_invalid_looping) {
        return '0'; // stop loop, e.g. with keep_fatalistic=<corruptvalue>
    }
    $param_invalid_looping = true;

    if (!is_null($ret)) {
        // Try and recover by stripping junk off...
        $test = preg_replace('#[^\d]+$#', '', $ret);
        if (is_numeric($test)) {
            return $test;
        }
    }

    require_code('global3');
    set_http_status_code('400');

    require_code('lang');
    require_code('tempcode');

    if (function_exists('url_monikers_enabled') && !url_monikers_enabled() && $name == 'id') {
        warn_exit(do_lang_tempcode('javascript:NOT_INTEGER_URL_MONIKERS')); // Complaining about non-integers is just confusing
    }

    warn_exit(do_lang_tempcode('javascript:NOT_INTEGER'));

    return '';
}

/**
 * Complain about a field being missing.
 *
 * @param  string $name The name of the parameter
 * @param  ?boolean $posted Whether the parameter is a POST parameter (null: undetermined)
 * @param  array $array The array we're extracting parameters from
 */
function improperly_filled_in($name, $posted, $array)
{
    require_code('tempcode');

    require_code('global3');
    set_http_status_code('400');

    if ($posted !== false) {
        improperly_filled_in_post($name);
    }

    if ($name == 'login_username') {
        warn_exit(do_lang_tempcode('NO_PARAMETER_SENT_SPECIAL', escape_html($name)));
    }

    if ((!isset($array[$name])) && (($name == 'id') || ($name == 'type')) && (!headers_sent())) {
        set_http_status_code('404');
    }
    warn_exit(do_lang_tempcode('NO_PARAMETER_SENT', escape_html(post_param_string('label_for__' . $name, $name))));
}

/**
 * Complain about a POST field being missing.
 *
 * @param  string $name The name of the parameter
 */
function improperly_filled_in_post($name)
{
    require_code('global3');
    set_http_status_code('400');

    if ((count($_POST) == 0) && (get_option('user_postsize_errors') == '1')) {
        require_code('files');
        $upload_max_filesize = (ini_get('upload_max_filesize') == '0') ? do_lang('NA') : clean_file_size(php_return_bytes(ini_get('upload_max_filesize')));
        $post_max_size = (ini_get('post_max_size') == '0') ? do_lang('NA') : clean_file_size(php_return_bytes(ini_get('post_max_size')));
        warn_exit(do_lang_tempcode((get_param_integer('uploading', 0) == 1) ? 'SHOULD_HAVE_BEEN_POSTED_FILE_ERROR' : 'SHOULD_HAVE_BEEN_POSTED', escape_html($name), escape_html($post_max_size), escape_html($upload_max_filesize)));
    }

    // We didn't give some required input
    warn_exit(do_lang_tempcode('IMPROPERLY_FILLED_IN'));
}

/**
 * Called by 'composr_error_handler'. Composr error handler (hooked into PHP error system).
 *
 * @param  ID_TEXT $type Error type indicator (tiny human-readable text string)
 * @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
 * @param  integer $syslog_type The syslog type (used by GAE logging)
 * @ignore
 */
function _composr_error_handler($type, $errno, $errstr, $errfile, $errline, $syslog_type)
{
    if (!$GLOBALS['SUPPRESS_ERROR_DEATH']) {
        // Turn off MSN, as this increases stability
        if ((array_key_exists('MSN_DB', $GLOBALS)) && (!is_null($GLOBALS['MSN_DB']))) {
            $GLOBALS['FORUM_DB'] = $GLOBALS['MSN_DB'];
            $GLOBALS['MSN_DB'] = null;
        }
    }

    $errstr = _sanitise_error_msg($errstr);

    // Generate error message
    $outx = '<strong>' . strtoupper($type) . '</strong> [' . strval($errno) . '] ' . $errstr . ' in ' . $errfile . ' on line ' . strval($errline) . '<br />' . "\n";
    if (class_exists('Tempcode')) {
        if ($GLOBALS['SUPPRESS_ERROR_DEATH']) {
            $trace = new Tempcode();
        } else {
            $trace = get_html_trace();
        }
        $out = $outx . $trace->evaluate();
    } else {
        $out = $outx;
    }

    // Put into error log
    if ((get_param_integer('keep_fatalistic', 0) == 0) && (!throwing_errors())) {
        require_code('urls');
        $php_error_label = $errstr . ' in ' . $errfile . ' on line ' . strval($errline) . ' @ ' . get_self_url_easy(true);
        /*$log.="\n";
        ob_start();
        debug_print_backtrace(); Does not work consistently, sometimes just kills PHP
        $log.=ob_get_clean();*/
        if ((function_exists('syslog')) && (GOOGLE_APPENGINE)) {
            syslog($syslog_type, $php_error_label);
        }
        if (php_function_allowed('error_log')) {
            @error_log('PHP ' . ucwords($type) . ': ' . $php_error_label, 0);
        }
    }

    if (!$GLOBALS['SUPPRESS_ERROR_DEATH']) { // Don't display - die as normal
        $error_str = 'PHP ' . strtoupper($type) . ' [' . strval($errno) . '] ' . $errstr . ' in ' . $errfile . ' on line ' . strval($errline);

        if (throwing_errors()) {
            throw new CMSException($error_str);
        }

        if ($type == 'error') {
            critical_error('EMERGENCY', escape_html($error_str));
        }

        safe_ini_set('display_errors', '0');
        fatal_exit($error_str);
    } else {
        require_code('site');
        attach_message(protect_from_escaping($out), 'warn'); // Display
    }
}

/**
 * Get the Tempcode for a warn page.
 *
 * @param  Tempcode $title The title of the warn page
 * @param  mixed $text The text to put on the warn page (either Tempcode or string)
 * @param  boolean $provide_back Whether to provide a back button
 * @param  boolean $support_match_key_messages Whether match key messages / redirects should be supported
 * @return Tempcode The warn page
 *
 * @ignore
 */
function _warn_screen($title, $text, $provide_back = true, $support_match_key_messages = false)
{
    $tmp = _look_for_match_key_message(is_object($text) ? $text->evaluate() : $text, !$support_match_key_messages);
    if (!is_null($tmp)) {
        $text = $tmp;
    }

    $text_eval = is_object($text) ? $text->evaluate() : $text;

    if (strpos($text_eval, do_lang('MISSING_RESOURCE_SUBSTRING')) !== false) {
        require_code('global3');
        set_http_status_code('404');
        if (cms_srv('HTTP_REFERER') != '') {
            relay_error_notification($text_eval . ' ' . do_lang('REFERRER', cms_srv('HTTP_REFERER'), substr(get_browser_string(), 0, 255)), false, 'error_occurred_missing_resource');
        }
    }

    if (get_param_integer('keep_fatalistic', 0) == 1) {
        fatal_exit($text);
    }

    return do_template('WARN_SCREEN', array('_GUID' => 'a762a7ac8cd08623a0ed6413d9250d97', 'TITLE' => $title, 'WEBSERVICE_RESULT' => get_webservice_result($text), 'TEXT' => $text, 'PROVIDE_BACK' => $provide_back));
}

/**
 * Do a terminal execution on a defined page type
 *
 * @param  string $text The error message
 * @return string Sanitised error message
 *
 * @ignore
 */
function _sanitise_error_msg($text)
{
    // Strip paths, for security reasons
    return str_replace(array(get_custom_file_base() . '/', get_file_base() . '/'), array('', ''), $text);
}

/**
 * Do a terminal execution on a defined page type
 *
 * @param  mixed $text The error message (string or Tempcode)
 * @param  ID_TEXT $template Name of the terminal page template
 * @param  ?boolean $support_match_key_messages ?Whether match key messages / redirects should be supported (null: detect)
 * @return mixed Never returns (i.e. exits)
 * @ignore
 */
function _generic_exit($text, $template, $support_match_key_messages = false)
{
    if (throwing_errors()) {
        throw new CMSException($text);
    }

    cms_ob_end_clean(); // Emergency output, potentially, so kill off any active buffer

    if (is_object($text)) {
        $text = $text->evaluate();
        $text = _sanitise_error_msg($text);
        $text = protect_from_escaping($text);
    } else {
        $text = _sanitise_error_msg($text);
    }
    $text_eval = is_object($text) ? $text->evaluate() : $text;

    global $RUNNING_TASK;
    if ($RUNNING_TASK) {
        require_code('notifications');
        require_lang('tasks');
        $n_subject = do_lang('_TASK_FAILED_SUBJECT');
        $n_message = do_notification_lang('TASK_FAILED_BODY', '[semihtml]' . $text_eval . '[/semihtml]');
        dispatch_notification('task_completed', null, $n_subject, $n_message, array(get_member()), A_FROM_SYSTEM_PRIVILEGED, 2);
    }

    if (is_null($support_match_key_messages)) {
        $support_match_key_messages = in_array($text_eval, array(do_lang('NO_ENTRIES'), do_lang('NO_CATEGORIES')));
    }
    $tmp = _look_for_match_key_message($text_eval, false, !$support_match_key_messages);
    if (!is_null($tmp)) {
        $text = $tmp;
    }

    require_code('global3');

    global $WANT_TEXT_ERRORS, $HTTP_STATUS_CODE;
    if ($WANT_TEXT_ERRORS) {
        @header('Content-type: text/plain; charset=' . get_charset());
        if ($HTTP_STATUS_CODE == '200') {
            set_http_status_code('500');
        }
        safe_ini_set('ocproducts.xss_detect', '0');
        @debug_print_backtrace();
        exit((is_object($text) ? strip_html($text->evaluate()) : $text) . "\n");
    }

    if (get_param_integer('keep_fatalistic', 0) == 1) {
        fatal_exit($text);
    }

    @header('Content-type: text/html; charset=' . get_charset());
    @header('Content-Disposition: inline');

    if (($GLOBALS['HTTP_STATUS_CODE'] == '200') && (function_exists('do_lang'))) {
        if (($text_eval == do_lang('cns:NO_MARKERS_SELECTED')) || ($text_eval == do_lang('NOTHING_SELECTED'))) { // HACKHACK
            if (!headers_sent()) {
                set_http_status_code('400');
            }
        } elseif ((strpos($text_eval, do_lang('MISSING_RESOURCE_SUBSTRING')) !== false) || ($text_eval == do_lang('MEMBER_NO_EXIST'))) {
            if (!headers_sent()) {
                set_http_status_code('404');
            }
            if (cms_srv('HTTP_REFERER') != '') {
                relay_error_notification($text_eval . ' ' . do_lang('REFERRER', cms_srv('HTTP_REFERER'), substr(get_browser_string(), 0, 255)), false, 'error_occurred_missing_resource');
            }
        } elseif ($template == 'WARN_SCREEN') {
            if (!headers_sent()) {
                set_http_status_code('500');
            }
        }
    }

    if ((array_key_exists('MSN_DB', $GLOBALS)) && (!is_null($GLOBALS['MSN_DB']))) {
        $GLOBALS['FORUM_DB'] = $GLOBALS['MSN_DB'];
        $GLOBALS['MSN_DB'] = null;
    }

    global $EXITING, $MICRO_BOOTUP, $BOOTSTRAPPING;
    if ((running_script('upgrader')) || ($MICRO_BOOTUP) || ($BOOTSTRAPPING)) {
        critical_error('PASSON', is_object($text) ? $text->evaluate() : escape_html($text));
    }

    if (($EXITING >= 1) || (!function_exists('get_member')) || (!function_exists('get_screen_title')) || (!function_exists('do_lang'))) {
        critical_error('EMERGENCY', is_object($text) ? $text->evaluate() : escape_html($text));
    }
    $EXITING++;

    require_code('site');

    if ((get_forum_type() == 'cns') && (get_db_type() != 'xml') && (isset($GLOBALS['FORUM_DRIVER']))) {
        require_code('cns_groups');
        $restrict_answer = cns_get_best_group_property($GLOBALS['FORUM_DRIVER']->get_members_groups(get_member()), 'flood_control_submit_secs');
        $GLOBALS['FORUM_DB']->query_update('f_members', array('m_last_submit_time' => time() - $restrict_answer - 1), array('id' => get_member()), '', 1);
    }

    if (($template == 'INFORM_SCREEN') && (is_object($GLOBALS['DISPLAYED_TITLE']))) {
        $title = get_screen_title($GLOBALS['DISPLAYED_TITLE'], false);
    } else {
        $title = get_screen_title(($template == 'INFORM_SCREEN') ? 'MESSAGE' : 'ERROR_OCCURRED');
    }

    $middle = do_template($template, array('TITLE' => $title, 'TEXT' => $text, 'PROVIDE_BACK' => true));
    $echo = globalise($middle, null, '', true);
    $echo->evaluate_echo(null, true);
    exit();
}

/**
 * Normalise an IPv6 address.
 *
 * @param  IP $ip IP address
 * @return IP Normalised address
 *
 * @ignore
 */
function _inet_pton($ip)
{
    $_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;
}

/**
 * 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($ip, $cidr)
{
    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);
}

/**
 * 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 ID
 * @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  boolean $silent Whether to silently log the hack rather than also exiting
 * @param  boolean $instant_ban Whether a ban should be immediate
 * @return mixed Never returns (i.e. exits)
 * @ignore
 */
function _log_hack_attack_and_exit($reason, $reason_param_a = '', $reason_param_b = '', $silent = false, $instant_ban = false)
{
    if (!$GLOBALS['BOOTSTRAPPING']) {
        require_code('site');
        attach_to_screen_header('<meta name="robots" content="noindex" />'); // XHTMLXHTML
    }

    if (!$silent) {
        require_code('global3');
        set_http_status_code('403'); // Stop spiders ever storing the URL that caused this
    }

    if (!addon_installed('securitylogging')) {
        if ($silent) {
            return;
        }
        warn_exit(do_lang_tempcode('HACK_ATTACK_USER'));
    }

    $ip = get_ip_address();
    $ip2 = cms_srv('REMOTE_ADDR');
    if (!is_valid_ip($ip2)) {
        $ip2 = '';
    }
    if (($ip2 == $ip) || ($ip2 == '') || (cms_srv('SERVER_ADDR') == $ip2)) {
        $ip2 = null;
    }
    if ((function_exists('get_member')) && (!$GLOBALS['BOOTSTRAPPING'])) {
        $id = get_member();
        $username = $GLOBALS['FORUM_DRIVER']->get_username($id);
        if (is_null($username)) {
            $username = do_lang('UNKNOWN');
        }
    } else {
        $id = db_get_first_id();
        $username = ((function_exists('do_lang')) && (!$GLOBALS['BOOTSTRAPPING'])) ? do_lang('UNKNOWN') : 'Unknown';
    }

    $url = cms_srv('REQUEST_URI');
    $post = '';
    foreach ($_POST as $key => $val) {
        if (!is_string($val)) {
            continue;
        }
        $post .= $key . ' => ' . $val . "\n\n";
    }

    $count = $GLOBALS['SITE_DB']->query_select_value('hackattack', 'COUNT(*)', array('ip' => $ip));
    $alt_ip = false;
    if (!is_null($ip2)) {
        $count2 = $GLOBALS['SITE_DB']->query_select_value('hackattack', 'COUNT(*)', array('ip' => $ip2));
        if ($count2 > $count) {
            $count = $count2;
            $alt_ip = true;
        }
    }
    $hack_threshold = intval(get_option('hack_ban_threshold'));
    if ((array_key_exists('FORUM_DRIVER', $GLOBALS)) && (function_exists('get_member')) && ($GLOBALS['FORUM_DRIVER']->is_super_admin(get_member()))) {
        $count = 0;
    }
    $new_row = array(
        'user_agent' => cms_mb_substr(get_browser_string(), 0, 255),
        'referer' => cms_mb_substr(cms_srv('HTTP_REFERER'), 0, 255),
        'user_os' => cms_mb_substr(get_os_string(), 0, 255),
        'reason' => $reason,
        'reason_param_a' => cms_mb_substr($reason_param_a, 0, 255),
        'reason_param_b' => cms_mb_substr($reason_param_b, 0, 255),
        'url' => cms_mb_substr($url, 0, 255),
        'data_post' => $post,
        'member_id' => $id,
        'date_and_time' => time(),
        'ip' => $ip,
    );
    $ip_ban_todo = null;
    if ((($count >= $hack_threshold) || ($instant_ban)) && (get_option('autoban') != '0') && (is_null($GLOBALS['SITE_DB']->query_select_value_if_there('unbannable_ip', 'ip', array('ip' => $alt_ip ? $ip2 : $ip))))) {
        // Test we're not banning a good bot
        $se_ip_lists = array();
        $se_ip_lists[get_base_url() . '/data/no_banning.txt'] = false;
        $se_ip_lists[get_base_url() . '/data_custom/no_banning.txt'] = false;
        $ip_stack = array();
        $ip_bits = explode((strpos($alt_ip ? $ip2 : $ip, '.') !== false) ? '.' : ':', $alt_ip ? $ip2 : $ip);
        foreach ($ip_bits as $i => $ip_bit) {
            $buildup = '';
            for ($j = 0; $j <= $i; $j++) {
                if ($buildup != '') {
                    $buildup .= (strpos($alt_ip ? $ip2 : $ip, '.') !== false) ? '.' : ':';
                }
                $buildup .= $ip_bits[$j];
            }
            $ip_stack[] = $buildup;
        }
        $is_se = false;
        foreach ($se_ip_lists as $ip_list => $is_proxy) {
            $ip_list_file = http_download_file($ip_list, null, false);
            if (is_string($ip_list_file)) {
                $ip_list_array = explode("\n", $ip_list_file);
                foreach ($ip_stack as $ip_s) {
                    foreach ($ip_list_array as $_ip_list_array) {
                        if (strpos($ip_s, '/') === false) {
                            if ($ip_s == $_ip_list_array) {
                                $is_se = true;
                            }
                        } else {
                            if (ip_cidr_check($ip_s, $_ip_list_array)) {
                                $is_se = true;
                            }
                        }
                    }
                }
                if ($is_se) {
                    break;
                }
            }
        }
        $dns = cms_gethostbyaddr($alt_ip ? $ip2 : $ip);
        $resolved = cms_gethostbyname($dns);
        if ($resolved === ($alt_ip ? $ip2 : $ip) || $resolved == $dns) { // Verify it's not faking the DNS
            $se_domain_names = array('googlebot.com', 'google.com', 'msn.com', 'yahoo.com', 'ask.com', 'aol.com');
            foreach ($se_domain_names as $domain_name) {
                if ((substr($dns, -strlen($domain_name) - 1) == '.' . $domain_name) || (substr($dns, -strlen($domain_name) - 2) == '.' . $domain_name . '.')) {
                    $is_se = true;
                    break;
                }
            }
        }
        if ((!$is_se) && (($alt_ip ? $ip2 : $ip) != '127.0.0.1')) {
            $rows = $GLOBALS['SITE_DB']->query_select('hackattack', array('*'), array('ip' => $alt_ip ? $ip2 : $ip), 'ORDER BY date_and_time');
            $rows[] = $new_row;
            $summary = '[list]';
            $is_spammer = false;
            foreach ($rows as $row) {
                if ($row['reason'] == 'LAME_SPAM_HACK') {
                    $is_spammer = true;
                }
                $full_reason = do_lang($row['reason'], '[tt]' . comcode_escape($row['reason_param_a']) . '[/tt]', '[tt]' . comcode_escape($row['reason_param_b']) . '[/tt]', null, get_site_default_lang());
                $summary .= "\n" . '[*]' . $full_reason . "\n[tt]" . comcode_escape($row['url']) . "[/tt]\n" . get_timezoned_date($row['date_and_time']);
            }
            $summary .= "\n" . '[/list]';
            if ($is_spammer) {
                require_code('failure_spammers');
                syndicate_spammer_report($alt_ip ? $ip2 : $ip, is_guest() ? '' : $GLOBALS['FORUM_DRIVER']->get_username(get_member()), $GLOBALS['FORUM_DRIVER']->get_member_email_address(get_member()), do_lang('SPAM_REPORT_TRIGGERED_SPAM_HEURISTICS'));
            }
            $ban_happened = add_ip_ban($alt_ip ? $ip2 : $ip, $full_reason);
            $_ip_ban_url = build_url(array('page' => 'admin_ip_ban', 'type' => 'browse'), get_module_zone('admin_ip_ban'), null, false, false, true);
            $ip_ban_url = $_ip_ban_url->evaluate();
            if ($ban_happened) {
                $ip_ban_todo = do_lang('AUTO_BAN_HACK_MESSAGE', $alt_ip ? $ip2 : $ip, integer_format($hack_threshold), array($summary, $ip_ban_url), get_site_default_lang());
            }
        }
    }
    $GLOBALS['SITE_DB']->query_insert('hackattack', $new_row);
    if (!is_null($ip2)) {
        $new_row['ip'] = $ip2;
        $GLOBALS['SITE_DB']->query_insert('hackattack', $new_row);
    }

    if ((function_exists('do_lang')) && (!$GLOBALS['BOOTSTRAPPING'])) {
        require_code('notifications');

        $reason_full = do_lang($reason, $reason_param_a, $reason_param_b, null, get_site_default_lang());
        $_stack_trace = get_html_trace();
        $stack_trace = str_replace('html', '&#104;tml', $_stack_trace->evaluate());
        $time = get_timezoned_date(time(), true, true, true);
        $message = do_notification_template(
            'HACK_ATTEMPT_MAIL',
            array(
                '_GUID' => '6253b3c42c5e6c70d20afa9d1f5b40bd',
                'STACK_TRACE' => $stack_trace,
                'USER_AGENT' => get_browser_string(),
                'REFERER' => cms_srv('HTTP_REFERER'),
                'USER_OS' => get_os_string(),
                'REASON' => $reason_full,
                'IP' => $ip,
                'ID' => strval($id),
                'USERNAME' => $username,
                'TIME_RAW' => strval(time()),
                'TIME' => $time,
                'URL' => $url,
                'POST' => $post,
            ),
            get_site_default_lang(),
            false,
            null,
            '.txt',
            'text'
        );

        if (($reason != 'CAPTCHAFAIL_HACK') && ($reason != 'LAME_SPAM_HACK')) {
            $subject = do_lang('HACK_ATTACK_SUBJECT', $ip, null, null, get_site_default_lang());
            dispatch_notification('hack_attack', null, $subject, $message->evaluate(get_site_default_lang()), null, A_FROM_SYSTEM_PRIVILEGED);
        }

        if (!is_null($ip_ban_todo)) {
            $subject = do_lang('AUTO_BAN_SUBJECT', $ip, null, null, get_site_default_lang());
            dispatch_notification('auto_ban', null, $subject, $ip_ban_todo, null, A_FROM_SYSTEM_PRIVILEGED);
        }
    }

    if ($silent) {
        return;
    }

    if ((function_exists('do_lang')) && (!$GLOBALS['BOOTSTRAPPING'])) {
        if ($GLOBALS['DEV_MODE']) {
            fatal_exit(do_lang('HACK_ATTACK'));
        }
        warn_exit(do_lang_tempcode('HACK_ATTACK_USER'));
    }

    require_code('critical_errors');
    critical_error('EMERGENCY', 'Suspected hack attempt averted');
}

/**
 * Add an IP-ban.
 *
 * @param  IP $ip The IP address to ban (potentially encoded with *'s)
 * @param  LONG_TEXT $descrip Explanation for ban
 * @param  ?TIME $ban_until When to ban until (null: no limit)
 * @param  boolean $ban_positive Whether this is a positive ban (as opposed to a cached negative)
 * @param  boolean $check_caching Whether to check internal run-time caching (disable if doing automated tests)
 * @return boolean Whether a change actually happened
 */
function add_ip_ban($ip, $descrip = '', $ban_until = null, $ban_positive = true, $check_caching = true)
{
    // Edge case: No securitylogging addon
    if (!addon_installed('securitylogging')) {
        return false;
    }

    // Edge case: Invalid IP
    require_code('type_sanitisation');
    if (!is_valid_ip($ip, true)) {
        return false;
    }

    // Some reasons we cannot ban it?
    require_code('global4');
    $is_unbannable_existing = null;
    $ban_until_existing = null;
    $already_banned = ip_banned($ip, true, false, $is_unbannable_existing, $ban_until_existing, $check_caching);
    if ($is_unbannable_existing) {
        return false; // Don't allow automatically banning of what is marked as unbannable
    }
    if (($already_banned) && ($ban_until !== null) && (($ban_until_existing === null) || ($ban_until < $ban_until_existing))) {
        return false; // Don't allow automatically shortening of an existing ban period
    }

    // Ban it
    $GLOBALS['SITE_DB']->query_delete('banned_ip', array('ip' => $ip), '', 1);
    $GLOBALS['SITE_DB']->query_insert('banned_ip', array('ip' => $ip, 'i_descrip' => $descrip, 'i_ban_until' => $ban_until, 'i_ban_positive' => $ban_positive ? 1 : 0), false, true); // To stop weird race-like conditions
    persistent_cache_delete('IP_BANS');
    if ((is_writable_wrap(get_file_base() . '/.htaccess')) && (is_null($ban_until)) && ($ban_positive)) {
        $contents = unixify_line_format(cms_file_get_contents_safe(get_file_base() . '/.htaccess'));
        $ip_cleaned = ip_wild_to_apache($ip);
        if (($ip_cleaned != '') && (stripos($contents, "\n" . 'deny from ' . $ip_cleaned) === false) && (stripos($contents, "\n" . 'require not ip ' . $ip_cleaned) === false)) {
            require_code('files');

            // < Apache 2.4
            $contents = str_ireplace('# deny from xxx.xx.x.x (leave this comment here!)', '# deny from xxx.xx.x.x (leave this comment here!)' . "\n" . 'deny from ' . $ip_cleaned, $contents);

            // >= Apache 2.4
            $contents = str_ireplace('# require not ip xxx.xx.x.x (leave this comment here!)', '# require not ip xxx.xx.x.x (leave this comment here!)' . "\n" . 'require not ip ' . $ip_cleaned, $contents);

            cms_file_put_contents_safe(get_file_base() . '/.htaccess', $contents, FILE_WRITE_FIX_PERMISSIONS | FILE_WRITE_SYNC_FILE);
        }
    }

    return true;
}

/**
 * Convert simple Composr wildcard syntax in IP addresses to Apache netmask syntax.
 *
 * @param  IP $ip The IP address (potentially encoded with *'s)
 * @return string The Apache-style IP
 */
function ip_wild_to_apache($ip)
{
    $ip = normalise_ip_address($ip, 4);
    if ($ip == '') {
        return '';
    }

    if (strpos($ip, '*') === false) {
        return $ip;
    }

    $ipv6 = (strpos($ip, ':') !== false);
    if ($ipv6) {
        $delimiter = ':';
        $bits_per_part = 16;
        $expected_blank_part = '0000';
    } else {
        $delimiter = '.';
        $bits_per_part = 8;
        $expected_blank_part = '0';
    }
    $parts = explode($delimiter, $ip);
    $ip_section = '';
    $range_bits = 0;
    foreach ($parts as $i => $part) {
        if ($i > 0) {
            $ip_section .= $delimiter;
        }
        if ($part == '*') {
            $ip_section .= $expected_blank_part;
        } else {
            $ip_section .= $part;
            $range_bits += $bits_per_part;
        }
    }
    return $ip_section . '/' . strval($range_bits);
}

/**
 * Remove an IP-ban.
 *
 * @param  IP $ip The IP address to unban (potentially encoded with *'s, although this will only unban an exact matching wildcard ban)
 */
function remove_ip_ban($ip)
{
    if (!addon_installed('securitylogging')) {
        return;
    }

    $GLOBALS['SITE_DB']->query_delete('banned_ip', array('ip' => $ip), '', 1);
    persistent_cache_delete('IP_BANS');
    if (is_writable_wrap(get_file_base() . '/.htaccess')) {
        $contents = unixify_line_format(cms_file_get_contents_safe(get_file_base() . '/.htaccess'));
        $ip_cleaned = ip_wild_to_apache($ip);
        if ($ip_cleaned != '') {
            require_code('files');

            // < Apache 2.4
            $contents = str_ireplace("\n" . 'deny from ' . $ip_cleaned . "\n", "\n", $contents);

            // >= Apache 2.4
            $contents = str_ireplace("\n" . 'require not ip ' . $ip_cleaned . "\n", "\n", $contents);

            cms_file_put_contents_safe(get_file_base() . '/.htaccess', $contents, FILE_WRITE_FIX_PERMISSIONS | FILE_WRITE_SYNC_FILE);
        }
    }
    $GLOBALS['SITE_DB']->query_delete('hackattack', array('ip' => $ip));
}

/**
 * Lookup error on compo.sr, to see if there is more information.
 *
 * @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)
{
    if (get_domain() == 'compo.sr') {
        return null;
    }
    if (get_domain() == 'ocproducts.com') {
        return null;
    }
    if (get_domain() == 'localhost') {
        return null; // In case of no Internet connection
    }

    if ((!function_exists('has_zone_access')) || (!has_zone_access(get_member(), 'adminzone'))) {
        return null;
    }

    require_code('files');
    require_code('files2');
    global $DONE_ONE_WEB_SERVICE;
    if (($GLOBALS['DOWNLOAD_LEVEL'] > 0) || ($DONE_ONE_WEB_SERVICE)) {
        return null;
    }
    $DONE_ONE_WEB_SERVICE = true;

    if (is_object($error_message)) {
        $error_message = $error_message->evaluate();
    }

    if ($GLOBALS['HTTP_STATUS_CODE'] == '401') {
        return null;
    }

    // Get message IN ENGLISH
    if (user_lang() != fallback_lang()) {
        global $LANGUAGE_STRINGS_CACHE;
        foreach ($LANGUAGE_STRINGS_CACHE as $_) {
            foreach ($_ as $key => $val) {
                $regexp = preg_replace('#\\\{\d+\\\}#', '.*', preg_quote($val, '#'));
                if ($regexp != '.*') {
                    if (preg_match('#' . $regexp . '#', $error_message) != 0) {
                        $_error_message = do_lang($key, '', '', '', fallback_lang(), false);
                        if (!is_null($_error_message)) {
                            $error_message = $_error_message;
                        }
                        break;
                    }
                }
            }
        }
    }

    // Certain thing(s) are too common and should not result in queries
    if (strpos($error_message, 'was referenced') !== false) {
        return null;
    }

    // Talk to web service
    $brand = get_value('rebrand_name');
    if (is_null($brand)) {
        $brand = 'Composr';
    }

    require_code('version2');
    $url = 'http://compo.sr/uploads/website_specific/compo.sr/scripts/errorservice.php?version=' . urlencode(get_version_dotted()) . '&error_message=' . urlencode($error_message) . '&product=' . urlencode($brand);
    list($result) = cache_and_carry('http_download_file', array($url, null, false), 60 * 24 * 31/*once a month*/);
    if ($GLOBALS['HTTP_DOWNLOAD_MIME_TYPE'] != 'text/plain') {
        return null;
    }

    if ($result == '') {
        return null;
    }
    if (function_exists('ocp_mark_as_escaped')) {
        ocp_mark_as_escaped($result);
    }
    return $result;
}

/**
 * Do a fatal exit, echo the header (if possible) and an error message, followed by a debugging back-trace.
 * It also adds an entry to the error log, for reference.
 *
 * @param  mixed $text The error message (string or Tempcode)
 * @param  boolean $return Whether to return
 * @return mixed Never returns (i.e. exits)
 * @ignore
 */
function _fatal_exit($text, $return = false)
{
    if (is_object($text)) {
        $text = $text->evaluate();
        $text = _sanitise_error_msg($text);
        $text = protect_from_escaping($text);
    } else {
        $text = _sanitise_error_msg($text);
    }

    if (throwing_errors()) {
        throw new CMSException($text);
    }

    if (!headers_sent()) {
        require_code('firephp');
        if (function_exists('fb')) {
            fb('Error: ' . (is_object($text) ? $text->evaluate() : $text));
        }
    }

    global $WANT_TEXT_ERRORS, $HTTP_STATUS_CODE;
    if ($WANT_TEXT_ERRORS && !headers_sent()) {
        header('Content-type: text/plain; charset=' . get_charset());
        require_code('global3');
        if ($HTTP_STATUS_CODE == '200') {
            set_http_status_code('500');
        }
        safe_ini_set('ocproducts.xss_detect', '0');
        @debug_print_backtrace();
        exit(is_object($text) ? strip_html($text->evaluate()) : $text);
    }

    if ($HTTP_STATUS_CODE == '200') {
        set_http_status_code('500');
    }
    if (!headers_sent()) {
        header('Content-type: text/html; charset=' . get_charset());
        header('Content-Disposition: inline');
    }

    if ((array_key_exists('MSN_DB', $GLOBALS)) && (!is_null($GLOBALS['MSN_DB']))) {
        $GLOBALS['FORUM_DB'] = $GLOBALS['MSN_DB'];
        $GLOBALS['MSN_DB'] = null;
    }

    // Supplement error message with some useful info
    if ((function_exists('cms_version_pretty')) && (function_exists('cms_srv'))) {
        $sup = ' (version: ' . cms_version_pretty() . ', PHP version: ' . PHP_VERSION . ', URL: ' . cms_srv('REQUEST_URI') . ')';
    } else {
        $sup = '';
    }
    if (is_object($text)) {
        if ($text->pure_lang) {
            $sup = escape_html($sup);
        }
        $text->attach($sup);
    } else {
        $text .= $sup;
    }

    // To break any looping of errors
    global $EXITING;
    $EXITING++;
    if (($EXITING > 1) || (running_script('upgrader')) || (!class_exists('Tempcode'))) {
        if (($EXITING == 2) && (function_exists('may_see_stack_dumps')) && (may_see_stack_dumps()) && ($GLOBALS['HAS_SET_ERROR_HANDLER'])) {
            die_html_trace(is_object($text) ? $text->evaluate() : escape_html($text));
        } else { // Failed even in die_html_trace
            critical_error('EMERGENCY', is_object($text) ? $text->evaluate() : escape_html($text));
        }
    }

    $may_see_trace = may_see_stack_dumps();
    if ($may_see_trace) {
        $trace = get_html_trace();
    } else {
        $trace = new Tempcode();
    }

    $title = get_screen_title('ERROR_OCCURRED');

    if (get_param_integer('keep_fatalistic', 0) == 0) {
        require_code('urls');
        $php_error_label = (is_object($text) ? $text->evaluate() : $text) . ' @ ' . get_self_url_easy(true);
        if ((function_exists('syslog')) && (GOOGLE_APPENGINE)) {
            syslog(LOG_ERR, $php_error_label);
        }
        if (php_function_allowed('error_log')) {
            @error_log('Composr: ' . $php_error_label, 0);
        }
    }

    $error_tpl = do_template('FATAL_SCREEN', array('_GUID' => '9fdc6d093bdb685a0eda6bb56988a8c5', 'TITLE' => $title, 'WEBSERVICE_RESULT' => get_webservice_result($text), 'MESSAGE' => $text, 'TRACE' => $trace, 'MAY_SEE_TRACE' => $may_see_trace));
    $echo = globalise($error_tpl, null, '', true);
    $echo->evaluate_echo(null, true);

    if (get_param_integer('keep_fatalistic', 0) == 0) {
        if (!may_see_stack_dumps()) {
            $trace = get_html_trace();
        }
        relay_error_notification((is_object($text) ? $text->evaluate() : $text) . $trace->evaluate());
    }

    if (!$return) {
        exit();
    }
}

/**
 * Relay an error message, if appropriate, to e-mail listeners (sometimes ocProducts, and site staff).
 *
 * @param  string $text A error message (in HTML)
 * @param  boolean $ocproducts Also send to ocProducts
 * @param  ID_TEXT $notification_type The notification type
 */
function relay_error_notification($text, $ocproducts = true, $notification_type = 'error_occurred')
{
    // Make sure we don't send too many error emails
    if ((function_exists('get_value')) && (!$GLOBALS['BOOTSTRAPPING']) && (array_key_exists('SITE_DB', $GLOBALS)) && (!is_null($GLOBALS['SITE_DB']))) {
        $num = intval(get_value('num_error_mails_' . date('Y-m-d'), null, true)) + 1;
        if ($num == 51) {
            return; // We've sent too many error mails today
        }
        $GLOBALS['SITE_DB']->query('DELETE FROM ' . get_table_prefix() . 'values_elective WHERE the_name LIKE \'' . db_encode_like('num\_error\_mails\_%') . '\'');
        persistent_cache_delete('VALUES');
        set_value('num_error_mails_' . date('Y-m-d'), strval($num), true);
    }

    if (!function_exists('require_lang')) {
        return;
    }

    require_code('urls');
    require_code('tempcode');

    $error_url = get_self_url_easy(true);

    global $BLOCK_OCPRODUCTS_ERROR_EMAILS;

    require_code('notifications');
    require_code('comcode');
    $mail = do_notification_lang('ERROR_MAIL', comcode_escape($error_url), $text, $ocproducts ? '?' : get_ip_address(), get_site_default_lang());
    dispatch_notification($notification_type, null, do_lang('ERROR_OCCURRED_SUBJECT', get_page_or_script_name(), $ocproducts ? '?' : get_ip_address(), null, get_site_default_lang()), $mail, null, A_FROM_SYSTEM_PRIVILEGED);
    if (
        ($ocproducts) &&
        (get_option('send_error_emails_ocproducts') == '1') &&
        (!$BLOCK_OCPRODUCTS_ERROR_EMAILS) &&
        (!running_script('cron_bridge')) &&
        (strpos($text, '_custom/') === false) &&
        (strpos($text, '_custom\\') === false) &&
        (strpos($text, 'FTP server error') === false) && // LDAP error, misconfiguration
        (strpos($text, 'Search: Operations error') === false) && // LDAP error, misconfiguration
        (strpos($text, 'Can\'t contact LDAP server') === false) && // LDAP error, network issue
        (strpos($text, 'Unknown: failed to open stream') === false) && // Comes up on some free webhosts
        (strpos($text, 'failed with: Connection refused') === false) && // Memcache error
        (strpos($text, 'data/commandr.php') === false) &&
        (strpos($text, '.less problem') === false) &&
        (strpos($text, '/mini') === false) &&
        (strpos($text, 'A transaction for the wrong IPN e-mail went through') === false) &&
        (strpos($text, 'XCache var cache was not initialized properly') === false) &&
        (strpos($text, 'has been disabled for security reasons') === false) &&
        (strpos($text, 'max_questions')/*mysql limit*/ === false) &&
        (strpos($text, 'Error at offset') === false) &&
        (strpos($text, 'expects parameter 1 to be a valid path, string given') === false) &&
        (strpos($text, 'gd-png: fatal libpng error') === false) &&
        (strpos($text, 'No word lists can be found for the language &quot;en&quot;') === false) &&
        (strpos($text, 'Unable to allocate memory for pool') === false) &&
        (strpos($text, 'Out of memory') === false) &&
        (strpos($text, 'Can\'t open file') === false) &&
        (strpos($text, 'INSERT command denied to user') === false) &&
        (strpos($text, 'Disk is full writing') === false) &&
        (strpos($text, 'Disk quota exceeded') === false) &&
        (strpos($text, 'Lock wait timeout exceeded') === false) &&
        (strpos($text, 'No space left on device') === false) &&
        (strpos($text, 'from storage engine') === false) &&
        (strpos($text, 'Lost connection to MySQL server') === false) &&
        (strpos($text, 'The SELECT would examine more than MAX_JOIN_SIZE rows') === false) &&
        (strpos($text, 'Unable to save result set') === false) &&
        (strpos($text, 'Deadlock found when trying to get lock; try restarting transaction') === false) &&
        (strpos($text, 'MySQL client ran out of memory') === false) &&
        (strpos($text, 'Server shutdown in progress') === false) &&
        (strpos($text, '.MAI') === false) && // MariaDB
        (strpos($text, '.MAD') === false) && // MariaDB
        (strpos($text, '.MYI') === false) && // MySQL
        (strpos($text, '.MYD') === false) && // MySQL
        (strpos($text, 'syntax error, unexpected') === false) && // MySQL full-text parsing error
        (strpos($text, 'MySQL server has gone away') === false) &&
        (strpos($text, 'Incorrect key file') === false) &&
        (strpos($text, 'Too many connections') === false) &&
        (strpos($text, 'duplicate key in table') === false) &&
        (strpos($text, 'Incorrect string value') === false) &&
        (strpos($text, 'Too many words in a FTS phrase or proximity search') === false) &&
        (strpos($text, 'Can\'t create/write to file') === false) &&  // MySQL
        (strpos($text, 'Error writing file') === false) && // E.g. cannot PHP create a temporary file
        (strpos($text, 'possibly out of free disk space') === false) &&
        (strpos($text, 'Illegal mix of collations') === false) &&
        (strpos($text, 'Query execution was interrupted') === false) &&
        (strpos($text, 'The MySQL server is running with the --read-only option so it cannot execute this statement') === false) &&
        (strpos($text, 'marked as crashed and should be repaired') === false) &&
        (strpos($text, 'connect to') === false) &&
        (strpos($text, 'Access denied for') === false) &&
        (strpos($text, 'command denied for') === false) && // MySQL
        (strpos($text, 'was deadlocked on lock resources with another process') === false) && // SQL Server
        (strpos($text, 'Unknown database') === false) &&
        (strpos($text, 'headers already sent') === false) &&
        (strpos($text, 'Resource temporarily unavailable') === false) &&
        (strpos($text, 'Broken pipe') === false) &&
        (strpos($text, 'Interrupted system call') === false) &&
        (preg_match('#php\.net.*SSL3_GET_SERVER_CERTIFICATE:certificate #', $text) == 0) && // Missing certificates on server
        (preg_match('#Maximum execution time of \d+ seconds#', $text) == 0) &&
        (preg_match('#Out of memory \(allocated (1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24)\d{6}\)#', $text) == 0) &&
        (strpos($text, 'is marked as crashed and last') === false) &&
        (strpos($text, 'failed to open stream: Permission denied') === false) &&
        ((strpos($text, 'Maximum execution time') === false) || ((strpos($text, '/js_') === false) && (strpos($text, '/caches_filesystem.php') === false) && (strpos($text, '/files2.php') === false))) &&
        ((strpos($text, 'doesn\'t exist') === false) || ((strpos($text, 'import') === false))) &&
        ((strpos($text, 'No such file or directory') === false) || ((strpos($text, 'admin_setupwizard') === false))) &&
        (strpos($text, 'File(/tmp/) is not within the allowed path') === false)
    ) {
        require_code('mail');
        mail_wrap(cms_version_pretty() . ': ' . do_lang('ERROR_OCCURRED_SUBJECT', get_page_or_script_name(), null, null, get_site_default_lang()), $mail, array('errors_final' . strval(cms_version()) . '@compo.sr'), '', '', '', 3, null, true, null, true);
    }
    if (($ocproducts) && (!is_null(get_value('agency_email_address')))) {
        require_code('mail');
        $agency_email_address = get_value('agency_email_address');
        mail_wrap(cms_version_pretty() . ': ' . do_lang('ERROR_OCCURRED_SUBJECT', get_page_or_script_name(), null, null, get_site_default_lang()), $mail, array($agency_email_address), '', '', '', 3, null, true, null, true);
    }
}

/**
 * Find whether the current user may see stack dumps.
 *
 * @return boolean Whether the current user may see stack dumps
 */
function may_see_stack_dumps()
{
    if (!is_null($GLOBALS['CURRENT_SHARE_USER'])) {
        return true; // Demonstratr exception
    }
    if ((function_exists('cms_srv')) && (cms_srv('REQUEST_METHOD') == '')) {
        return true; // Command line
    }
    if ((function_exists('running_script')) && (running_script('upgrader'))) {
        return true;
    }
    if (!function_exists('get_member')) {
        return false;
    }
    if (!function_exists('has_privilege')) {
        return false;
    }
    if ($GLOBALS['IS_ACTUALLY_ADMIN']) {
        return true;
    }

    return ($GLOBALS['DEV_MODE']) || (has_privilege(get_member(), 'see_stack_dump'));
}

/**
 * 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)
{
    $_trace = debug_backtrace();
    $trace = '<div class="box guid_{_GUID}"><div class="box_inner"><h2>Stack trace&hellip;</h2>';
    foreach ($_trace as $i => $stage) {
        if ($i > 20) {
            break;
        }

        $traces = '';
        foreach ($stage as $key => $value) {
            $_value = put_value_in_stack_trace($value);

            global $SITE_INFO;
            if ((isset($SITE_INFO['db_site_password'])) && (strlen($SITE_INFO['db_site_password']) > 4)) {
                $_value = str_replace($SITE_INFO['db_site_password'], '(password removed)', $_value);
            }
            if ((isset($SITE_INFO['db_forums_password'])) && (strlen($SITE_INFO['db_forums_password']) > 4)) {
                $_value = str_replace($SITE_INFO['db_forums_password'], '(password removed)', $_value);
            }

            $traces .= ucfirst($key) . ' -> ' . $_value . '<br />' . "\n";
        }
        $trace .= '<p>' . $traces . '</p>' . "\n";
    }
    $trace .= '</div></div>';

    if ($GLOBALS['XSS_DETECT']) {
        ocp_mark_as_escaped($trace);
    }

    critical_error('EMERGENCY', $message . $trace);
}

/**
 * Prepare a value for display in a stack trace.
 *
 * @param  mixed $value Complex value
 * @return string String version
 */
function put_value_in_stack_trace($value)
{
    try {
        if ((is_null($value)) || (is_array($value) && (strlen(serialize($value)) > MAX_STACK_TRACE_VALUE_LENGTH))) {
            $_value = gettype($value);
        } elseif (is_object($value) && (is_a($value, 'Tempcode'))) {
            if (($value->codename == 'GLOBAL_HTML_WRAP') || (strlen(serialize($value)) > 1000)) { // NB: We can't do an eval on GLOBAL_HTML_WRAP because it may be output streaming, incomplete
                $_value = 'Tempcode -> ...';
            } else {
                $_value = $value->evaluate();
                if (!is_string($_value)) {
                    $_value = 'Tempcode -> ' . gettype($_value);
                } else {
                    $_value = 'Tempcode -> ' . $_value;
                }
            }
        } elseif ((is_array($value)) || (is_object($value))) {
            $_value = serialize($value);
        } elseif (is_string($value)) {
            $_value = '\'' . php_addslashes($value) . '\'';
        } elseif (is_float($value)) {
            $_value = float_to_raw_string($value);
        } elseif (is_integer($value)) {
            $_value = integer_format($value);
        } elseif (is_bool($value)) {
            $_value = $value ? 'true' : 'false';
        } else {
            $_value = strval($value);
        }
    } catch (Exception $e) { // Can happen for SimpleXMLElement or PDO
        $_value = '...';
    }

    global $SITE_INFO;
    if ((isset($SITE_INFO['db_site_password'])) && (strlen($SITE_INFO['db_site_password']) > 4)) {
        $_value = str_replace($SITE_INFO['db_site_password'], '(password removed)', $_value);
    }
    if ((isset($SITE_INFO['db_forums_password'])) && (strlen($SITE_INFO['db_forums_password']) > 4)) {
        $_value = str_replace($SITE_INFO['db_forums_password'], '(password removed)', $_value);
    }

    return escape_html($_value);
}

/**
 * Return a debugging back-trace of the current execution stack. Use this for debugging purposes.
 *
 * @return Tempcode Debugging backtrace
 */
function get_html_trace()
{
    require_code('templates');

    $bak = $GLOBALS['SUPPRESS_ERROR_DEATH'];
    $GLOBALS['SUPPRESS_ERROR_DEATH'] = true;

    $_trace = debug_backtrace();
    $trace = array();
    foreach ($_trace as $i => $stage) {
        $traces = array();
        //if (in_array($stage['function'], array('get_html_trace', 'composr_error_handler', 'fatal_exit'))) continue;  Hinders more than helps
        $file = '';
        $line = '';
        $_value = mixed();
        $__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.
                        $_value->attach(paragraph(put_value_in_stack_trace($param)));
                    }
                }
            } else {
                $_value = put_value_in_stack_trace($__value);
            }

            $traces[] = array('LINE' => $line, 'FILE' => $file, 'KEY' => ucfirst($key), 'VALUE' => $_value);
        }
        $trace[] = array('TRACES' => $traces);
    }
    $GLOBALS['SUPPRESS_ERROR_DEATH'] = $bak;

    $post = array();
    if (count($_POST) < 200) {
        foreach ($_POST as $key => $val) {
            if (stripos($key, 'password') !== false) {
                continue;
            }

            if (@get_magic_quotes_gpc()) {
                $val = stripslashes($val);
            }

            $post[$key] = $val;
        }
    }

    return do_template('STACK_TRACE', array('_GUID' => '9620695fb8c3e411a6a4926432cea64f', 'POST' => $post, 'TRACE' => $trace));
}

/**
 * See if a match-key message affects the error context we are in. May also internally trigger a redirect.
 *
 * @param  string $natural_text Message screen text that is about to be displayed
 * @param  boolean $only_if_zone Only if it is a zone-level match-key
 * @param  boolean $only_text_match Whether to only consider text matches, not match-key matches
 * @return ?Tempcode The message (null: no change)
 * @ignore
 */
function _look_for_match_key_message($natural_text, $only_if_zone = false, $only_text_match = false)
{
    if (!isset($GLOBALS['SITE_DB'])) {
        return null;
    }
    $match_keys = $GLOBALS['SITE_DB']->query_select('match_key_messages', array('*'));
    sort_maps_by__strlen($match_keys, 'k_match_key');
    $match_keys = array_reverse($match_keys);
    foreach ($match_keys as $match_key) {
        if ($only_if_zone) {
            if ((substr($match_key['k_match_key'], -6) != ':_WILD') && (substr($match_key['k_match_key'], -2) != ':*')) {
                continue;
            }
        }

        $pass = false;

        $matches = array();
        if (preg_match('#^((.*) )?"(.*)"$#', $match_key['k_match_key'], $matches) != 0) {
            if (strpos($natural_text, $matches[3]) !== false) {
                if ($matches[1] == '') {
                    $pass = true;
                } else {
                    $pass = match_key_match($matches[2]); // An AND condition essentially
                }
            }
        } else {
            if (!$only_text_match) {
                if (match_key_match($match_key['k_match_key'])) {
                    $pass = true;
                }
            }
        }

        if ($pass) {
            $message_raw = get_translated_text($match_key['k_message']);
            $message = get_translated_tempcode('match_key_messages', $match_key, 'k_message');

            // Maybe it is actually a redirect
            if ((strpos($message_raw, "\n") === false) && (strpos($message_raw, ' ') === false)) {
                if (preg_match('#^https?://#', $message_raw) != 0) { // Looks like a URL
                    $url = $message_raw;
                    require_code('site2');
                    assign_refresh($url, 0.0);
                    $message = do_lang_tempcode('_REDIRECTING');
                } elseif (preg_match('#^[' . URL_CONTENT_REGEXP . ']*:[' . URL_CONTENT_REGEXP . ']*#', $message_raw) != 0) { // Looks like a page-link
                    list($zone, $map, $hash) = page_link_decode($message_raw);
                    if ((isset($map['error_message'])) && ($map['error_message'] == '')) {
                        $map['error_message'] = $natural_text;
                    }
                    $url = static_evaluate_tempcode(build_url($map, $zone, array(), false, false, false, $hash));
                    require_code('site2');
                    assign_refresh($url, 0.0);
                    $message = do_lang_tempcode('_REDIRECTING');
                }
            }

            return $message;
        }
    }
    return null;
}

/**
 * Show a helpful access-denied page. Has a login ability if it senses that logging in could curtail the error.
 *
 * @param  ID_TEXT $class The class of error (e.g. PRIVILEGE)
 * @param  string $param The parameter given to the error message
 * @param  boolean $force_login Force the user to login (even if perhaps they are logged in already)
 * @ignore
 */
function _access_denied($class, $param, $force_login)
{
    require_code('global3');
    set_http_status_code('401'); // Stop spiders ever storing the URL that caused this

    if ((running_script('messages')) && (get_param_string('action', 'new') == 'new') && (addon_installed('chat'))) { // FUDGE: Architecturally hackerish chat erroring. We do this as a session may have expired while the background message checker is running (e.g. after a computer unsuspend) and we don't want to leave it doing relatively intensive access-denied pages responses
        require_code('chat_poller');
        chat_null_exit();
    }

    require_lang('permissions');
    require_lang('cns_config');

    if (strpos($class, ' ') !== false) {
        $message = make_string_tempcode($class);
    } else {
        if ($class == 'PRIVILEGE') {
            $param = do_lang('PRIVILEGE_' . $param);
        }
        $message = do_lang_tempcode('ACCESS_DENIED__' . $class, escape_html($GLOBALS['FORUM_DRIVER']->get_username(get_member())), escape_html($param));
    }

    $_message = _look_for_match_key_message($message->evaluate(), strpos($class, 'ZONE') !== false);
    if (!is_null($_message)) {
        $message = $_message;
    }

    // Run hooks, if any exist
    $hooks = find_all_hooks('systems', 'upon_access_denied');
    foreach (array_keys($hooks) as $hook) {
        require_code('hooks/systems/upon_access_denied/' . filter_naughty_harsh($hook));
        $ob = object_factory('Hook_upon_access_denied_' . filter_naughty_harsh($hook), true);
        if (is_null($ob)) {
            continue;
        }
        $ob->run($class, $param, $force_login);
    }

    if (throwing_errors()) {
        throw new CMSException($message);
    }

    require_code('site');
    log_stats('/access_denied', 0);

    if (($GLOBALS['IS_ACTUALLY_ADMIN']) && (get_param_integer('keep_fatalistic', 0) == 1)) {
        fatal_exit($message);
    }

    if (((is_guest()) && ((running_script('attachment')) || (running_script('dload')) || (running_script('index')))) || ($force_login)) {// Show login screen if appropriate
        // We do want to supply a nice login screen for attachment/dload scripts because they are sometimes externally linked to (e.g. in emails or hotlinks)
        // Otherwise we want flat access denied due to a flat request/response model
        // NB: Also see similar running_script lines in globalise function
        if (get_param_integer('save_and_stay', 0) == 1) {
            $middle = inform_screen(get_screen_title('ERROR_OCCURRED'), protect_from_escaping('
                    <script type="text/javascript">// <![CDATA[
                            window.fauxmodal_alert(\'' . addslashes(strip_html($message->evaluate())) . '\');
                    //]]></script>
            '));

            $echo = globalise($middle, null, '', true);
            $echo->evaluate_echo(null, true);
            exit();
        }

        cms_ob_end_clean(); // Emergency output, potentially, so kill off any active buffer

        $real_page = get_page_name();

        $redirect = get_self_url(true, false, array('page' => $real_page)); // We have to pass in 'page' because an access-denied situation tells get_page_name() (which get_self_url() relies on) that we are on page ''.
        set_extra_request_metadata(array(
            'real_page' => $real_page,
        ));
        ecv_CANONICAL_URL(user_lang(), array(), array()); // Cause this to be pre-cached with the correct value
        $_GET['redirect'] = $redirect;
        $_GET['page'] = 'login';
        $_GET['type'] = 'browse';
        global $PAGE_NAME_CACHE;
        $PAGE_NAME_CACHE = 'login';

        $middle = load_module_page(_get_module_path('', 'login'), 'login');
        require_code('site');
        if (get_value('no_tech_login_messages') !== '1') {
            attach_message($message, 'warn');
        }
        $echo = globalise($middle, null, '', true);
        $echo->evaluate_echo(null, true);
        exit();
    }

    warn_exit($message); // Or if no login screen, just show normal error screen
}

/**
 * Specify if errors should be thrown, rather than resulting in HTML exit screens.
 *
 * @param  boolean  $_throwing_errors Whether we should throw errors
 */
function set_throw_errors($_throwing_errors = true)
{
    global $THROWING_ERRORS;
    $THROWING_ERRORS = $_throwing_errors;
}

/**
 * Find whether we should throw errors, rather than create HTML exit screens with the error messages / correction screens.
 *
 * @return boolean        Whether to are throwing errors
 */
function throwing_errors()
{
    global $THROWING_ERRORS;
    return $THROWING_ERRORS;
}

/**
 * A Composr  exception.
 *
 * @package        core
 */
class CMSException extends Exception
{
    /**
     * Constructor.
     *
     * @param mixed $msg Error message (Tempcode containing HTML, or string containing non-HTML)
     */
    public function __construct($msg)
    {
        if (is_object($msg)) {
            $msg = strip_html($msg->evaluate());
        }

        parent::__construct($msg);
    }
}
