<?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: fileowner|php_sapi_name*/

/*
    Optional general library code that must be manually loaded
*/

/**
 * Standard code module initialisation function.
 *
 * @ignore
 */
function init__global4()
{
    global $ADMIN_ACTIONLOG_ON;
    $ADMIN_ACTIONLOG_ON = true;

    global $RELATED_WARNING_ID;
    $RELATED_WARNING_ID = null;
}

/**
 * Find whether a page is included on the sitemap / indexable.
 *
 * @param  ID_TEXT $zone Zone name
 * @param  ID_TEXT $codename Page name
 * @param  ?array $row The database row (null: lookup directly from the database)
 * @return boolean Whether it is indexable
 */
function comcode_page_include_on_sitemap(string $zone, string $codename, ?array $row = null) : bool
{
    if ($row === null) {
        $include_on_sitemap = $GLOBALS['SITE_DB']->query_select_value_if_there('comcode_pages', 'p_include_on_sitemap', ['the_zone' => $zone, 'the_page' => $codename]);
    } else {
        $include_on_sitemap = $row['p_include_on_sitemap'];
    }
    if ($include_on_sitemap !== null) {
        return ($include_on_sitemap == 1);
    }

    return _comcode_page_include_on_sitemap_default($zone, $codename);
}

/**
 * Find whether a page is included on the sitemap / indexable - by default.
 *
 * @param  ID_TEXT $zone Zone name
 * @param  ID_TEXT $codename Page name
 * @return boolean Whether it is indexable
 */
function _comcode_page_include_on_sitemap_default(string $zone, string $codename) : bool
{
    // These are the defaults...

    $noindex_comcode_pages = [
        ':404',
        ':help',
        ':keymap',
        ':popup_blockers',
        ':recommend_help',
        ':rules',
        ':userguide_chatcode',
        ':userguide_comcode',
        'forum:rules',
        'site:help',
        'site:popup_blockers',
        'site:userguide_chatcode',
        'site:userguide_comcode',
        'site:rules',
    ];

    if (
        (substr($codename, 0, 6) == 'panel_') ||
        (substr($codename, 0, 1) == '_') ||
        ($zone . ':' . $codename == ':404') ||
        (in_array($zone . ':' . $codename, $noindex_comcode_pages))
    ) {
        return false;
    }

    return true;
}

/**
 * Attach a message mentioning how the site is closed.
 *
 * @param  Tempcode $messages_bottom Where to place the message
 */
function attach_message_site_closed(object &$messages_bottom)
{
    if ((!in_array(get_page_name(), ['login', 'join'])) && (get_param_integer('wide_high', 0) == 0) && (($GLOBALS['IS_ACTUALLY_ADMIN']) || (has_privilege(get_member(), 'access_closed_site')))) {
        $messages_bottom->attach(do_template('MESSAGE', [
            '_GUID' => '03a41a91606b3ad05330e7d6f3e741c1',
            'TYPE' => 'notice',
            'MESSAGE' => do_lang_tempcode(has_privilege(get_member(), 'access_closed_site') ? 'SITE_SPECIAL_ACCESS' : 'SITE_SPECIAL_ACCESS_SU'),
        ]));
    }
}

/**
 * Attach a message mentioning SU is active.
 *
 * @param  Tempcode $messages_bottom Where to place the message
 */
function attach_message_su(object &$messages_bottom)
{
    $unsu_url = get_self_url(true, true, ['keep_su' => null]);
    $su_username = $GLOBALS['FORUM_DRIVER']->get_username(get_member());
    $messages_bottom->attach(do_template('MESSAGE', [
        '_GUID' => '13a41a91606b3ad05330e7d6f3e741c1',
        'TYPE' => 'notice',
        'MESSAGE' => do_lang_tempcode('USING_SU', escape_html($unsu_url), escape_html($su_username)),
    ]));
}

/**
 * Take a Tempcode object and run some hackerish code to make it XHTML-strict.
 *
 * @param  object $global Tempcode object
 * @return object Tempcode object (no longer cache safe)
 */
function make_xhtml_strict(object $global) : object
{
    $_global = $global->evaluate();
    $_global = str_replace(
        '<!DOCTYPE html>',
        '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
        $_global
    );
    $_global = preg_replace('#(<a\s[^>]*)onclick="([^"]*)"(\s[^>]*)target="_blank"#', '${1}onclick="this.target=\'_blank\'; ${2}"${3}', $_global);
    $_global = preg_replace('#(<a\s[^>]*)target="_blank"(\s[^>]*)onclick="([^"]*)"#', '${1}onclick="this.target=\'_blank\'; ${3}"${2}', $_global);
    $_global = preg_replace('#(<a\s[^>]*)target="_blank"#', '${1}onclick="this.target=\'_blank\';"', $_global);
    $_global = preg_replace('#(<form\s[^>]*)onsubmit="([^"]*)"(\s[^>]*)target="_blank"#', '${1}onsubmit="this.target=\'_blank\'; ${2}"${3}', $_global);
    $_global = preg_replace('#(<form\s[^>]*)target="_blank"(\s[^>]*)onsubmit="([^"]*)"#', '${1}onsubmit="this.target=\'_blank\'; ${3}"${2}', $_global);
    $_global = preg_replace('#(<form\s[^>]*)target="_blank"#', '${1}onsubmit="this.target=\'_blank\';"', $_global);
    $_global = preg_replace('#(<(a|form)\s[^>]*)target="[^"]*"#', '${1}', $_global);
    return make_string_tempcode($_global);
}

/**
 * Get links and details related to a member.
 * Note that this is:
 * 1) Available under all forum drivers
 * 2) Mainly about producing UI elements, not raw data
 * See the cns_read_in_member_profile function for a CNS-specific raw-data retriever.
 *
 * @param  MEMBER $member_id A member ID
 * @return array A tuple: links (Tempcode), eCommerce links (Tempcode), details (Tempcode), number of unread inline personal posts or private topics
 */
function member_personal_links_and_details(int $member_id) : array
{
    static $cache = [];
    if (isset($cache[$member_id])) {
        return $cache[$member_id];
    }

    $details = new Tempcode();
    $links = new Tempcode();
    $links_ecommerce = new Tempcode();

    if (get_forum_type() != 'none') {
        // Post count
        if ((!has_no_forum()) && (get_option('forum_show_personal_stats_posts') == '1')) {
            $post_count = $GLOBALS['FORUM_DRIVER']->get_post_count($member_id);
            $details->attach(do_template('BLOCK_SIDE_PERSONAL_STATS_LINE', [
                '_GUID' => '371dfee46e8c40b1b109e0350055f8cc',
                'KEY' => do_lang_tempcode('COUNT_POSTSCOUNT'),
                'RAW_VALUE' => strval($post_count),
                'VALUE' => integer_format($post_count, 0),
            ]));
        }
        // Topic count
        if ((!has_no_forum()) && (get_option('forum_show_personal_stats_topics') == '1')) {
            $topic_count = $GLOBALS['FORUM_DRIVER']->get_topic_count($member_id);
            $details->attach(do_template('BLOCK_SIDE_PERSONAL_STATS_LINE', [
                '_GUID' => '2dd2a2d30c4ea7144c74ab058239fb23',
                'KEY' => do_lang_tempcode('COUNT_TOPICSCOUNT'),
                'RAW_VALUE' => strval($topic_count),
                'VALUE' => integer_format($topic_count, 0),
            ]));
        }

        // Member profile view link
        if (get_option('cns_show_profile_link') == '1') {
            $url = $GLOBALS['FORUM_DRIVER']->member_profile_url($member_id, true);
            $links->attach(do_template('BLOCK_SIDE_PERSONAL_STATS_LINK', [
                '_GUID' => '2c8648c953c802a9de41c3adeef0e97f',
                'NAME' => do_lang_tempcode('MY_PROFILE'),
                'URL' => $url,
                'REL' => 'me',
            ]));
        }
    }

    // Point count
    if (addon_installed('points')) {
        require_lang('points');
        require_code('points');
        if (get_option('points_show_personal_stats_points_balance') == '1') {
            $points_balance = points_balance($member_id);
            $details->attach(do_template('BLOCK_SIDE_PERSONAL_STATS_LINE', [
                '_GUID' => '6241e58e30457576735f3a2618fd7fff',
                'KEY' => do_lang_tempcode('COUNT_POINTS_BALANCE'),
                'RAW_VALUE' => strval($points_balance),
                'VALUE' => integer_format($points_balance, 0),
            ]));
        }
        if (get_option('points_show_personal_stats_points_spent') == '1') {
            $points_spent = points_used($member_id);
            $details->attach(do_template('BLOCK_SIDE_PERSONAL_STATS_LINE', [
                '_GUID' => '6241e58edfdsf735f3a2618fd7fff',
                'KEY' => do_lang_tempcode('COUNT_POINTS_SPENT'),
                'RAW_VALUE' => strval($points_spent),
                'VALUE' => integer_format($points_spent, 0),
            ]));
        }
        if (get_option('points_show_personal_stats_points_rank') == '1') {
            $points_rank = points_rank($member_id);
            $details->attach(do_template('BLOCK_SIDE_PERSONAL_STATS_LINE', [
                '_GUID' => '3e6183abf9054574c0cd292d25a4fe5c',
                'KEY' => do_lang_tempcode('COUNT_RANK_POINTS'),
                'RAW_VALUE' => strval($points_rank),
                'VALUE' => integer_format($points_rank, 0),
            ]));
        }
        if (get_option('enable_gift_points') == '1') {
            if (get_option('points_show_personal_stats_gift_points_balance') == '1') {
                $gift_points_balance = gift_points_balance($member_id);
                $details->attach(do_template('BLOCK_SIDE_PERSONAL_STATS_LINE', [
                    '_GUID' => '6241e5ssd45ddsdsdsa2618fd7fff',
                    'KEY' => do_lang_tempcode('COUNT_GIFT_POINTS_BALANCE'),
                    'RAW_VALUE' => strval($gift_points_balance),
                    'VALUE' => integer_format($gift_points_balance, 0),
                ]));
            }
            if (get_option('points_show_personal_stats_gift_points_sent') == '1') {
                $gift_points_sent = gift_points_sent($member_id);
                $details->attach(do_template('BLOCK_SIDE_PERSONAL_STATS_LINE', [
                    '_GUID' => '6241eddsd4sdddssdsa2618fd7fff',
                    'KEY' => do_lang_tempcode('COUNT_GIFT_POINTS_SENT'),
                    'RAW_VALUE' => strval($gift_points_sent),
                    'VALUE' => integer_format($gift_points_sent, 0),
                ]));
            }
        }
    }

    // Links to usergroups
    if (get_option('show_personal_usergroup') == '1') {
        $group_id = $GLOBALS['FORUM_DRIVER']->mrow_primary_group($GLOBALS['FORUM_DRIVER']->get_member_row($member_id));
        $usergroups = $GLOBALS['FORUM_DRIVER']->get_usergroup_list();
        if (array_key_exists($group_id, $usergroups)) {
            if (get_forum_type() == 'cns') {
                $group_url = build_url(['page' => 'groups', 'type' => 'view', 'id' => $group_id], get_module_zone('groups'));
                $hyperlink = hyperlink($group_url, $usergroups[$group_id], false, true);
                $details->attach(do_template('BLOCK_SIDE_PERSONAL_STATS_LINE_COMPLEX', [
                    '_GUID' => 'sas41eddsd4sdddssdsa2618fd7fff',
                    'KEY' => do_lang_tempcode('USERGROUP'),
                    'RAW_VALUE' => $group_url,
                    'VALUE' => $hyperlink,
                ]));
            } else {
                $details->attach(do_template('BLOCK_SIDE_PERSONAL_STATS_LINE', [
                    '_GUID' => '65180134fbc4cf7e227011463d466677',
                    'KEY' => do_lang_tempcode('USERGROUP'),
                    'RAW_VALUE' => $usergroups[$group_id],
                    'VALUE' => $usergroups[$group_id],
                ]));
            }
        }
    }

    // Last visit time
    if (get_option('show_personal_last_visit') == '1') {
        $row = $GLOBALS['FORUM_DRIVER']->get_member_row($member_id);
        $last_visit = $GLOBALS['FORUM_DRIVER']->mrow_lastvisit($row);
        if (get_forum_type() == 'cns') {
            $last_visit = intval(cms_admirecookie('last_visit', strval($GLOBALS['FORUM_DRIVER']->mrow_lastvisit($row))));
        } else {
            $last_visit = $GLOBALS['FORUM_DRIVER']->mrow_lastvisit($row);
        }
        $details->attach(do_template('BLOCK_SIDE_PERSONAL_STATS_LINE', [
            '_GUID' => 'sas41eddsdsdsdsdsa2618fd7fff',
            'KEY' => do_lang_tempcode('LAST_HERE'),
            'RAW_VALUE' => strval($last_visit),
            'VALUE' => get_timezoned_date_time($last_visit),
        ]));
    }

    // Subscription expiry date
    if (addon_installed('ecommerce')) {
        if (get_option('manual_subscription_expiry_notice') != '') {
            $manual_subscription_expiry_notice = intval(get_option('manual_subscription_expiry_notice'));

            require_code('ecommerce_subscriptions');
            $subscriptions = find_member_subscriptions($member_id);
            foreach ($subscriptions as $subscription) {
                $expiry_time = $subscription['expiry_time'];
                if (($expiry_time !== null) && (($expiry_time - time()) < ($manual_subscription_expiry_notice * 24 * 60 * 60)) && ($expiry_time >= time())) {
                    require_lang('ecommerce');
                    $expiry_date = ($expiry_time === null) ? do_lang('INTERNAL_ERROR') : get_timezoned_date($expiry_time, false);
                    $details->attach(do_template('BLOCK_SIDE_PERSONAL_STATS_LINE', [
                        '_GUID' => '2675d56aa278616aa9f00b051ca084fc',
                        'KEY' => do_lang_tempcode('SUBSCRIPTION_EXPIRY_MESSAGE', escape_html($subscription['item_name'])),
                        'RAW_VALUE' => ($expiry_time === null) ? '' : strval($expiry_time),
                        'VALUE' => do_lang_tempcode('SUBSCRIPTION_EXPIRY_DATE', escape_html($expiry_date)),
                    ]));
                }
            }
        }
    }

    // Subscription links
    if ((get_forum_type() == 'cns') && (addon_installed('ecommerce')) && (get_option('show_personal_sub_links') == '1') && (!has_zone_access($member_id, 'adminzone')) && (has_actual_page_access($member_id, 'purchase'))) {
        require_lang('ecommerce');

        $usergroup_subs = $GLOBALS['FORUM_DB']->query_select('f_usergroup_subs', ['id', 's_title', 's_group_id', 's_price'], ['s_enabled' => 1]);
        $in_one = false;
        $members_groups = $GLOBALS['FORUM_DRIVER']->get_members_groups($member_id);
        foreach ($usergroup_subs as $i => $sub) {
            $usergroup_subs[$i]['s_price'] = floatval($sub['s_price']);
            if (in_array($sub['s_group_id'], $members_groups)) {
                $in_one = true;
                break;
            }
        }
        if (!$in_one) {
            sort_maps_by($usergroup_subs, 's_price');
            foreach ($usergroup_subs as $sub) {
                $db = get_db_for('f_usergroup_subs');
                $url = build_url(['page' => 'purchase', 'type' => 'message', 'type_code' => 'USERGROUP' . strval($sub['id'])], get_module_zone('purchase'));
                $links_ecommerce->attach(do_template('BLOCK_SIDE_PERSONAL_STATS_LINK', [
                    '_GUID' => '5c4a1f300b37722e587fe2f608f1ee3a',
                    'NAME' => do_lang_tempcode('UPGRADE_TO', escape_html(get_translated_text($sub['s_title'], $db))),
                    'URL' => $url,
                ]));
            }
        }
    }

    // Admin Zone link
    if (get_option('show_personal_adminzone_link') == '1') {
        if (has_zone_access($member_id, 'adminzone')) {
            $url = build_url(['page' => '', 'keep_theme' => null], 'adminzone');
            $links->attach(do_template('BLOCK_SIDE_PERSONAL_STATS_LINK', [
                '_GUID' => 'ae243058f780f9528016f7854763a5fa',
                'TARGET' => '_blank',
                'TITLE' => do_lang_tempcode('LINK_NEW_WINDOW'),
                'NAME' => do_lang_tempcode('ADMIN_ZONE'),
                'URL' => $url,
            ]));
        } elseif (has_zone_access($member_id, 'cms')) {
            $url = build_url(['page' => '', 'keep_theme' => null], 'cms');
            $links->attach(do_template('BLOCK_SIDE_PERSONAL_STATS_LINK', [
                '_GUID' => '3f63dad2645b6c39f68dcfebe7d7a0ab',
                'TARGET' => '_blank',
                'TITLE' => do_lang_tempcode('LINK_NEW_WINDOW'),
                'NAME' => do_lang_tempcode('CMS'),
                'URL' => $url,
            ]));
        }
    }

    // Conceded mode link
    if (($GLOBALS['SESSION_CONFIRMED_CACHE']) && (get_option('show_conceded_mode_link') == '1')) {
        $url = build_url(['page' => 'login', 'type' => 'concede', 'redirect' => protect_url_parameter(currently_logging_in() ? null : SELF_REDIRECT_RIP)], get_module_zone('login'));
        $links->attach(do_template('BLOCK_SIDE_PERSONAL_STATS_LINK_2', [
            '_GUID' => '81fa81cfd3130e42996bf72b0e03d8aa',
            'POST' => true,
            'NAME' => do_lang_tempcode('CONCEDED_MODE'),
            'DESCRIPTION' => do_lang_tempcode('DESCRIPTION_CONCEDED_MODE'),
            'URL' => $url,
        ]));
    }

    // Becomes-invisible link
    if ((get_forum_type() == 'cns') && (get_option('is_on_invisibility') == '1')) {
        if ((array_key_exists(get_session_id(), $GLOBALS['SESSION_CACHE'])) && ($GLOBALS['SESSION_CACHE'][get_session_id()]['session_invisible'] == 0)) {
            $visible = (array_key_exists(get_session_id(), $GLOBALS['SESSION_CACHE'])) && ($GLOBALS['SESSION_CACHE'][get_session_id()]['session_invisible'] == 0);
            $url = build_url(['page' => 'login', 'type' => 'invisible', 'redirect' => protect_url_parameter(currently_logging_in() ? null : SELF_REDIRECT_RIP)], get_module_zone('login'));
            $links->attach(do_template('BLOCK_SIDE_PERSONAL_STATS_LINK_2', [
                '_GUID' => '2af618fe39444861c21cf0caec216227',
                'NAME' => do_lang_tempcode($visible ? 'INVISIBLE' : 'BE_VISIBLE'),
                'DESCRIPTION' => '',
                'URL' => $url,
                'POST' => false,
            ]));
        }
    }

    // Logout link
    $url = build_url(['page' => 'login', 'type' => 'logout'], get_module_zone('login'));
    if (!is_httpauth_login()) {
        $links->attach(do_template('BLOCK_SIDE_PERSONAL_STATS_LOGOUT', [
            '_GUID' => 'd1caacba272a7ee3bf5b2a758e4e54ee',
            'NAME' => do_lang_tempcode('LOGOUT'),
            'URL' => $url,
        ]));
    }

    if (get_forum_type() == 'cns') {
        require_code('cns_notifications');
        $num_unread_pps = generate_notifications_count($member_id);
    } else {
        $num_unread_pps = 0;
    }

    $cache[$member_id] = [$links, $links_ecommerce, $details, $num_unread_pps];
    return $cache[$member_id];
}

/**
 * Isolate the words in the input string, with utf-8 awareness where possible/required.
 *
 * @param  string $input String to count words in
 * @param  integer $format The format
 * @set 0 1 2
 * @return mixed Typically a list - the words of the input string
 */
function cms_mb_str_word_count(string $input, int $format = 0)
{
    $matches = [];
    if ($format == 1) {
        $num_matches = cms_preg_match_all_safe("#[\w']+#", $input, $matches);
        $words = [];
        for ($i = 0; $i < $num_matches; $i++) {
            $words[] = $matches[0][$i];
        }
        return $words;
    }

    if ($format == 2) {
        $num_matches = cms_preg_match_all_safe("#[\w']+#", $input, $matches, PREG_OFFSET_CAPTURE);
        $words = [];
        for ($i = 0; $i < $num_matches; $i++) {
            $words[$matches[0][$i][1]] = $matches[0][$i][0];
        }
        return $words;
    }

    // 0...

    $num_matches = cms_preg_match_all_safe("#[\w']+#", $input, $matches);
    return $num_matches;
}

/**
 * Convert a string to an array, with utf-8 awareness where possible/required.
 *
 * @param  string $str Input
 * @param  boolean $force Whether to force unicode as on
 * @return array Output
 */
function cms_mb_str_split(string $str, bool $force = false) : array
{
    $len = cms_mb_strlen($str, $force);
    $array = [];
    for ($i = 0; $i < $len; $i++) {
        $array[] = cms_mb_substr($str, $i, 1, $force);
    }
    return $array;
}

/**
 * Split a string into smaller chunks, with utf-8 awareness where possible/required. Can be used to split a string into smaller chunks which is useful for e.g. converting base64_encode output to match RFC 2045 semantics. It inserts end (defaults to "\r\n") every chunklen characters.
 *
 * @param  string $str The input string
 * @param  integer $len The maximum chunking length
 * @param  string $glue Split character
 * @param  boolean $force Whether to force unicode as on
 * @return string The chunked version of the input string
 */
function cms_mb_chunk_split(string $str, int $len = 76, string $glue = "\r\n", bool $force = false) : string
{
    if ($str == '') {
        return '';
    }
    $array = cms_mb_str_split($str, $force);
    $n = -1;
    $new = '';
    foreach ($array as $char) {
        $n++;
        if ($n < $len) {
            $new .= $char;
        } elseif ($n == $len) {
            $new .= $glue . $char;
            $n = 0;
        }
    }
    return $new . $glue;
}

/**
 * Prevent double submission, by reference to recent matching admin log entries by the current member.
 *
 * @param  ID_TEXT $type The type of activity just carried out (a language string codename)
 * @param  ?SHORT_TEXT $a The most important parameter of the activity (e.g. ID) (null: none / cannot match against)
 * @param  ?SHORT_TEXT $b A secondary (perhaps, human readable) parameter of the activity (e.g. caption) (null: none / cannot match against)
 */
function prevent_double_submit(string $type, ?string $a = null, ?string $b = null)
{
    if (get_mass_import_mode()) {
        return;
    }

    if ($GLOBALS['IN_MINIKERNEL_VERSION']) {
        return;
    }

    if (is_cli()) {
        return;
    }

    if (post_param_integer('confirm_double_post', 0) == 1) {
        return;
    }

    if (strpos($_SERVER['SCRIPT_NAME'], '_tests') !== false) {
        return;
    }

    $where = [
        'the_type' => $type,
        'member_id' => get_member(),
    ];
    if ($a !== null) {
        if ($a == '') {
            return; // Cannot work with this
        }
        $where += [
            'param_a' => cms_mb_substr($a, 0, 80),
        ];
    }
    if ($b !== null) {
        if ($b == '') {
            return; // Cannot work with this
        }
        $where += [
            'param_b' => cms_mb_substr($b, 0, 80),
        ];
    }
    $time_window = 60 * 5; // 5 minutes seems reasonable
    $test = $GLOBALS['SITE_DB']->query_select_value_if_there('actionlogs', 'date_and_time', $where, ' AND date_and_time>' . strval(time() - $time_window));
    if ($test !== null) {
        $title = get_screen_title('ERROR_OCCURRED');
        require_code('templates_confirm_screen');
        $_time_window = display_time_period($time_window);
        $_time_remaining = display_time_period($time_window - (time() - $test));
        $_time_since = display_time_period(time() - $test);
        $preview = do_lang_tempcode('DOUBLE_SUBMISSION_PREVENTED', $_time_window, $_time_remaining, [$_time_since]);
        $output = confirm_screen($title, $preview, get_self_url(), null, ['confirm_double_post' => 1]);
        $echo = globalise($output, null, '', true);
        $echo->handle_symbol_preprocessing();
        $echo->evaluate_echo();
        exit();
    }
}

/**
 * Sets global $RELATED_WARNING_ID for use in referencing warning IDs to action and moderator logs.
 *
 * @param  ?integer $warning_id The ID of the warning (null: do not reference a warning id anymore in future logs)
 */
function set_related_warning_id(?int $warning_id)
{
    global $RELATED_WARNING_ID;
    $RELATED_WARNING_ID = $warning_id;
}

/**
 * Log an action.
 *
 * @param  ID_TEXT $type The type of activity just carried out (a language string codename)
 * @param  ?SHORT_TEXT $a The most important parameter of the activity (e.g. ID) (null: none)
 * @param  ?SHORT_TEXT $b A secondary (perhaps, human readable) parameter of the activity (e.g. caption) (null: none)
 * @param  ?integer $related_warning_id The related warning ID (null: none)
 * @return ?AUTO_LINK Log ID (null: did not save a log)
 * @ignore
 */
function _log_it(string $type, ?string $a = null, ?string $b = null, ?int $related_warning_id = null) : ?int
{
    if (!function_exists('get_member')) {
        return null; // If this is during installation
    }

    // Need to update copyright date?
    if ($GLOBALS['FORUM_DRIVER']->is_staff(get_member()) && $type != 'CONFIGURATION') {
        $matches = [];
        $old_copyright = get_option('copyright');
        if (preg_match('#^(.*\$CURRENT_YEAR=)(\d+)(.*)$#', $old_copyright, $matches) != 0) {
            $new_copyright = $matches[1] . date('Y') . $matches[3];
            if ($old_copyright != $new_copyright) {
                require_code('config2');
                set_option('copyright', $new_copyright);
                require_code('caches3');
                erase_cached_templates(false, ['GLOBAL_HTML_WRAP']);
            }
        }
    }

    // Addon not installed
    if (!addon_installed('actionlog')) {
        return null;
    }

    // No more logging if site closed (possibly)
    if ((get_option('site_closed') != '0') && (get_option('stats_when_closed') == '0')) {
        return null;
    }

    // Cache clearing
    static $logged = 0;
    $logged++;
    if ($logged == 1) {
        delete_cache_entry('side_tag_cloud');
        delete_cache_entry('main_staff_actions');
        delete_cache_entry('main_staff_checklist');
        delete_cache_entry('main_awards');
        delete_cache_entry('main_multi_content');
        delete_cache_entry('menu'); // Due to the content counts in the CMS/Admin Zones, and Sitemap menus

        if (get_value('flush_cache_on_action') === '1') {
            erase_static_cache();
        }
    }

    // Add to log
    $log_id = null;
    global $ADMIN_ACTIONLOG_ON;
    if ($ADMIN_ACTIONLOG_ON) {
        $ip = get_ip_address();
        $log_id = $GLOBALS['SITE_DB']->query_insert('actionlogs', [
            'the_type' => $type,
            'param_a' => ($a === null) ? '' : cms_mb_substr($a, 0, 80),
            'param_b' => ($b === null) ? '' : cms_mb_substr($b, 0, 80),
            'date_and_time' => time(),
            'member_id' => get_member(),
            'warning_id' => $related_warning_id,
            'ip' => $ip,
        ], true, true/*LEGACY*/);

        // LEGACY
        if ($log_id === null) {
            $log_id = $GLOBALS['SITE_DB']->query_insert('actionlogs', [
                'the_type' => $type,
                'param_a' => ($a === null) ? '' : cms_mb_substr($a, 0, 80),
                'param_b' => ($b === null) ? '' : cms_mb_substr($b, 0, 80),
                'date_and_time' => time(),
                'member_id' => get_member(),
                'ip' => $ip,
            ], true);
        }
    }

    // Run hooks, if any exist
    $hooks = find_all_hook_obs('systems', 'upon_action_logging', 'Hook_upon_action_logging_');
    foreach ($hooks as $ob) {
        $ob->run($type, $a, $b);
    }

    // Tidy up auto-save
    require_code('autosave');
    clear_cms_autosave();

    // Notification
    require_code('actionlog');
    if ((!get_mass_import_mode()) && ($ADMIN_ACTIONLOG_ON) && ((get_handler_flags($type) & ACTIONLOG_FLAG__GDPR) == 0)) {
        if ($logged < 10) { // Be extra sure it's not some kind of import, causing spam
            if (addon_installed('actionlog')) {
                if (do_lang($type, null, null, null, null, false) === null) {
                    require_all_lang();
                }
                if ($a === null) {
                    $a = do_lang('NA');
                }
                if ($b === null) {
                    $b = do_lang('NA');
                }
                require_code('notifications');
                require_lang('actionlog');
                $subject = do_lang('ACTIONLOG_NOTIFICATION_MAIL_SUBJECT', get_site_name(), do_lang($type), [($a === null) ? '' : $a, ($b === null) ? '' : $b]);
                $mail = do_notification_lang('ACTIONLOG_NOTIFICATION_MAIL', comcode_escape(get_site_name()), comcode_escape(do_lang($type)), [($a === null) ? '' : comcode_escape($a), ($b === null) ? '' : comcode_escape($b)]);
                dispatch_notification('actionlog', $type, $subject, $mail, null, get_member(), ['use_real_from' => true]);
            }
        }
    }

    return $log_id;
}

/**
 * Find if an action already logged.
 *
 * @param  ID_TEXT $type The type of activity just carried out (a language string codename)
 * @param  ?SHORT_TEXT $a The most important parameter of the activity (e.g. ID) (null: none)
 * @param  ?SHORT_TEXT $b A secondary (perhaps, human readable) parameter of the activity (e.g. caption) (null: none)
 * @return boolean Whether it is
 * @ignore
 */
function already_in_log(string $type, ?string $a = null, ?string $b = null) : bool
{
    $map = [
        'the_type' => $type,
    ];
    if ($a !== null) {
        $map['param_a'] = $a;
    }
    if ($b !== null) {
        $map['param_b'] = $b;
    }
    return ($GLOBALS['SITE_DB']->query_select_value_if_there('actionlogs', 'id', $map) !== null);
}

/**
 * Generate a v4 GUID.
 * This is not cryptographically secure. For secure GUIDs, see get_secure_random_v4_guid() in crypt.php.
 *
 * @return ID_TEXT A GUID
 */
function generate_guid() : string
{
    // Calculate hash value
    $hash = md5(uniqid('', true));

    // Based on a comment in the PHP manual
    return sprintf(
        '%08s-%04s-%04x-%04x-%12s',

        // 32 bits for "time_low"
        substr($hash, 0, 8),

        // 16 bits for "time_mid"
        substr($hash, 8, 4),

        // 16 bits for "time_hi_and_version",
        // four most significant bits holds version number 5
        (hexdec(substr($hash, 12, 4)) & 0x0fff) | 0x5000,

        // 16 bits, 8 bits for "clk_seq_hi_res",
        // 8 bits for "clk_seq_low",
        // two most significant bits holds zero and one for variant DCE1.1
        (hexdec(substr($hash, 16, 4)) & 0x3fff) | 0x8000,

        // 48 bits for "node"
        substr($hash, 20, 12)
    );
}

/**
 * Calculate a performance score for this server.
 * The resulting number indicates how many md5 uniqid operations can be performed in a second.
 *
 * @return float Performance score; higher is better
 */
function calculate_performance_score() : float
{
    static $score = null;
    if ($score !== null) {
        return $score;
    }

    $operations = 10000;

    $t = microtime(true);
    for ($i = 0; $i < $operations; $i++) {
        md5(uniqid('', false)); // Some fairly heavy crunching
    }
    $score = $operations / (microtime(true) - $t);
    return $score;
}

/**
 * Get time when the site was started.
 *
 * @return TIME The time
 */
function get_site_start_time() : int
{
    $time_a = $GLOBALS['SITE_DB']->query_select_value_if_there('actionlogs', 'MIN(date_and_time)');
    if ($time_a === null) {
        $time_a = time();
    }
    $time_b = filectime(get_file_base() . '/_config.php');
    return min($time_a, $time_b);
}

/**
 * Find if a particular redirect is itself pointing to a login/join page, and thus should not be injected as a nested redirect.
 *
 * @param  string $redirect Redirect to check
 * @return boolean If it is unhelpful
 */
function is_unhelpful_redirect(string $redirect) : bool
{
    $unhelpful_url_stubs = [
        static_evaluate_tempcode(build_url(['page' => 'login'], '', [], false, false, true)),
        static_evaluate_tempcode(build_url(['page' => 'login', 'type' => 'browse'], '', [], false, false, true)),
        static_evaluate_tempcode(build_url(['page' => 'login', 'type' => 'login'], '', [], false, false, true)),
        static_evaluate_tempcode(build_url(['page' => 'login', 'type' => 'logout'], '', [], false, false, true)),
        static_evaluate_tempcode(build_url(['page' => 'login'], '_SELF', [], false, false, true)),
        static_evaluate_tempcode(build_url(['page' => 'login', 'type' => 'browse'], '_SELF', [], false, false, true)),
        static_evaluate_tempcode(build_url(['page' => 'login', 'type' => 'login'], '_SELF', [], false, false, true)),
        static_evaluate_tempcode(build_url(['page' => 'login', 'type' => 'logout'], '_SELF', [], false, false, true)),
        static_evaluate_tempcode(build_url(['page' => 'join'], '', [], false, false, true)),
        static_evaluate_tempcode(build_url(['page' => 'join', 'type' => 'browse'], '', [], false, false, true)),
        static_evaluate_tempcode(build_url(['page' => 'join', 'type' => 'step2'], '', [], false, false, true)),
        static_evaluate_tempcode(build_url(['page' => 'join', 'type' => 'step3'], '', [], false, false, true)),
        static_evaluate_tempcode(build_url(['page' => 'join', 'type' => 'step4'], '', [], false, false, true)),
    ];
    foreach ($unhelpful_url_stubs as $unhelpful_url_stub) {
        if (substr($redirect, 0, strlen($unhelpful_url_stub)) == $unhelpful_url_stub) {
            return true;
        }
    }

    return false;
}

/**
 * Do whatever complex magic we can to try and find out the username we are running as, and suEXEC status.
 * This function should be assumed to be a little slow, don't put it into the critical path.
 *
 * @return array A pair: The username as best can be found, the suEXEC status
 */
function get_exact_usernames_and_suexec() : array
{
    $user = null;
    $suexec = null;
    $username = null;

    if (running_script('install')) {
        $path_to_check = get_file_base() . '/install.php';
    } else {
        $path_to_check = get_file_base() . '/sources/global.php';
    }

    if ((php_function_allowed('posix_getuid')) && (php_function_allowed('posix_getpwuid'))) {
        // Linux or Mac OS...

        $user = posix_getuid();
        $suexec = ($user == fileowner($path_to_check));
        $dets = posix_getpwuid($user);
        if ($dets !== false) {
            $username = $dets['name'];
            return [$username, $suexec];
        } elseif (($suexec) && (php_function_allowed('get_current_user'))) {
            $username = get_current_user();
            return [$username, $suexec];
        }
    }

    if ((php_function_allowed('shell_exec')) && (strpos(PHP_OS, 'WIN') === false)) {
        // Linux or Mac OS but crippled with missing or broken POSIX...

        $username = @shell_exec('whoami');
        if (!empty($username)) {
            $username = trim($username);
            if (php_function_allowed('get_current_user')) {
                $suexec = ($username == get_current_user());
            }
            return [$username, $suexec];
        }
    }

    // Windows, or very crippled Linux or Mac OS...

    if ($user === null) {
        $tmp = cms_tempnam();
        $user = @fileowner($tmp);
        @unlink($tmp);
    }
    $suexec = ($user == fileowner($path_to_check));
    if ((($suexec) || (strpos(PHP_OS, 'WIN') !== false)) && (php_function_allowed('get_current_user'))) {
        $username = get_current_user(); // On Windows this returns the user PHP is running as, counter to documentation
    }

    if ($username === null) {
        $username = '#' . strval($user);
    }

    return [$username, $suexec];
}

/**
 * Check whether we might be running Apache.
 * This should not be used to definitively detect Apache; only use this for code which would probably not break the site if we really are not running Apache.
 *
 * @return boolean Whether we might be running Apache
 */
function is_possibly_apache() : bool
{
    // Check server variable
    $server_software = $_SERVER['SERVER_SOFTWARE'];
    if ((stripos($server_software, 'Apache') !== false) || (is_maintained('platform_litespeed') && stripos($server_software, 'LiteSpeed') !== false)) {
        return true;
    }

    // Check PHP SAPI
    $sapi = php_sapi_name();
    if ((stripos($sapi, 'apache') !== false) || (is_maintained('platform_litespeed') && stripos($sapi, 'LiteSpeed') !== false) || (stripos($sapi, 'cgi') !== false)) {
        return true;
    }

    return false;
}
