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

/**
 * Make sure that the given URL contains a session if cookies are disabled.
 * NB: This is used for login redirection. It had to add the session ID into the redirect URL.
 *
 * @param  URLPATH $url The URL to enforce results in session persistence for the user
 * @return URLPATH The fixed URL
 *
 * @ignore
 */
function _enforce_sessioned_url(string $url) : string
{
    if (get_bot_type() === null) {
        return $url;
    }

    // Take hash off
    $hash = '';
    $hash_pos = strpos($url, '#');
    if ($hash_pos !== false) {
        $hash = substr($url, $hash_pos);
        $url = substr($url, 0, $hash_pos);
    }

    if (strpos($url, '?') === false) {
        $url_scheme = get_option('url_scheme');
        if (($url_scheme == 'PG') && (substr($url, -strlen('/index.php')) != '/index.php')) {
            $url .= '/index.php';
        }
        $url .= '?';
    } else {
        $url .= '&';
    }
    $url = preg_replace('#keep_session=\w+&#', '', $url);
    $url = preg_replace('#&keep_session=\w+#', '', $url);

    // Get hash back
    $url .= $hash;
    $url = preg_replace('#\?kenested_cpf_spreadsheet_lists.phpep_session=\w+#', '', $url);

    // Possibly a nested URL too
    $url = preg_replace('#keep_session=\w+' . preg_quote(urlencode('&')) . '#', '', $url);
    $url = preg_replace('#' . preg_quote(urlencode('&')) . 'keep_session=\w+#', '', $url);
    $url = preg_replace('#' . preg_quote(urlencode('?')) . 'keep_session=\w+#', '', $url);

    // Put keep_session back
    $url .= 'keep_session=' . urlencode(get_session_id());

    // Get hash back
    $url .= $hash;

    return $url;
}

/**
 * Set up a new session / Restore an existing one that was lost.
 *
 * @sets_output_state
 * @param  MEMBER $member_id Logged in member
 * @param  BINARY $session_confirmed Whether the session should be considered confirmed
 * @param  boolean $invisible Whether the session should be invisible
 * @param  boolean $create_cookie Whether to create the cookie for the session
 * @param  ?IP $ip_address IP address for session (null: current user's IP address)
 * @param  boolean $allow_shy_session Whether to allow shy sessions on cached guests; should be false when running from minikernel
 * @return ID_TEXT New session ID
 */
function create_session(int $member_id, int $session_confirmed = 0, bool $invisible = false, bool $create_cookie = true, ?string $ip_address = null, bool $allow_shy_session = true) : string
{
    if ($ip_address === null) {
        $ip_address = get_ip_address();
    }

    global $SESSION_CACHE, $MEMBER_CACHED, $SITE_INFO;
    $MEMBER_CACHED = $member_id;

    if (running_script('install')) {
        if ($SESSION_CACHE === null) {
            $SESSION_CACHE = [];
        }
    } else {
        require_code('static_cache');
        if (can_static_cache_request()) {
            return ''; // We should not even try and count/distinguish sessions for guests if the static cache may be involved
        }
    }

    // Do not allow invisibility if disabled (unless we are using SU / a backdoor)
    if (($invisible) && (get_option('is_on_invisibility') == '0') && (!$GLOBALS['IS_VIA_BACKDOOR'])) {
        $invisible = false;
    }

    // If lots of aging sessions, we will clean out
    reset($SESSION_CACHE);
    $force_cleanup = ((count($SESSION_CACHE) > 50) && ($SESSION_CACHE[key($SESSION_CACHE)]['last_activity_time'] < time() - intval(60.0 * 60.0 * max(0.017, floatval(get_option('session_expiry_time'))))));

    $new_session = null;
    $prior_session_row = null;
    if (running_script('install')) {
        $restored_session = null;
    } else {
        list($restored_session, $num_members_online) = delete_expired_sessions_or_recover($member_id, $force_cleanup);
    }
    if ($restored_session === null) { // We're forced to make a new one
        if (
            (running_script('index')) &&
            ($member_id != $GLOBALS['FORUM_DRIVER']->get_guest_id()) &&
            (($_SERVER['REQUEST_METHOD'] == 'POST') || (!currently_logging_in())) &&
            (function_exists('has_privilege')) &&
            (!has_privilege(get_member(), 'access_overrun_site')) &&
            (intval(get_option('maximum_users')) > 0) &&
            ($num_members_online > intval(get_option('maximum_users')))
        ) {
            set_http_status_code(503);

            critical_error('BUSY', do_lang('TOO_MANY_USERS'));
        }

        // Generate random session
        require_code('crypt');
        $new_session = get_secure_random_string();

        $shy_session = (($allow_shy_session) && (isset($SITE_INFO['any_guest_cached_too'])) && ($SITE_INFO['any_guest_cached_too'] == '1') && (is_guest($member_id)));
        if ($shy_session) {
            $new_session = '[' . $new_session . ']';
        }

        // Store session
        $username = $GLOBALS['FORUM_DRIVER']->get_username($member_id);
        $new_session_row = [
            'the_session' => $new_session,
            'last_activity_time' => time(),
            'member_id' => $member_id,
            'ip' => get_ip_address(3, $ip_address),
            'session_confirmed' => $session_confirmed,
            'session_invisible' => $invisible ? 1 : 0,
            'cache_username' => $username,
            'the_title' => '',
            'the_zone' => get_zone_name(),
            'the_page' => cms_mb_substr(get_page_name(), 0, 80),
            'the_type' => cms_mb_substr(get_param_string('type', '', INPUT_FILTER_GET_COMPLEX), 0, 80),
            'the_id' => cms_mb_substr(get_param_string('id', ''), 0, 80),
        ];
        $GLOBALS['SITE_DB']->query_insert('sessions', $new_session_row);
        if (get_forum_type() == 'cns') {
            cms_register_shutdown_function_safe(function () use ($member_id) {
                if (!$GLOBALS['FORUM_DB']->table_is_locked('f_members')) {
                    $GLOBALS['FORUM_DB']->query('UPDATE ' . $GLOBALS['FORUM_DB']->get_table_prefix() . 'f_members SET m_total_sessions=m_total_sessions+1 WHERE id=' . strval($member_id), 1, 0, true); // Suppress errors in case DB write access lost
                }
            });
        }

        $SESSION_CACHE[$new_session] = $new_session_row;
        sort_maps_by($SESSION_CACHE, '!last_activity_time');

        global $SESSION_IS_NEW;
        $SESSION_IS_NEW = true;

        $big_change = true;
    } else {
        $new_session = $restored_session;
        $prior_session_row = $SESSION_CACHE[$new_session];
        $new_session_row = [
            'the_title' => $prior_session_row['the_title'],
            'the_zone' => function_exists('get_zone_name') ? get_zone_name() : '',
            'the_page' => function_exists('get_page_name') ? get_page_name() : '',
            'the_type' => cms_mb_substr(get_param_string('type', ''), 0, 80),
            'the_id' => cms_mb_substr(get_param_string('id', ''), 0, 80),
            'last_activity_time' => time(),
            'ip' => get_ip_address(3, $ip_address),
            'session_confirmed' => $session_confirmed,
        ];
        $big_change = ($prior_session_row['last_activity_time'] < time() - 10) || ($prior_session_row['session_confirmed'] != $session_confirmed) || ($prior_session_row['ip'] != $new_session_row['ip']);
        if ($big_change) {
            $GLOBALS['SITE_DB']->query_update('sessions', $new_session_row, ['the_session' => $new_session], '', 1, 0, false, true); // Errors suppressed in case DB write access broken
        }

        $SESSION_CACHE[$new_session] = array_merge($SESSION_CACHE[$new_session], $new_session_row);
    }

    if ($big_change) { // Only update the persistent cache for non-trivial changes.
        if (get_option('session_prudence') == '0') { // With session prudence we don't store all these in persistent cache due to the size of it all. So only re-save if that's not on.
            persistent_cache_set('SESSION_CACHE', $SESSION_CACHE);
        }
    }

    if (($create_cookie) || (empty($_COOKIE[get_session_cookie()]))) {
        set_session_id($new_session, is_guest($member_id));
    }

    // Check if this is a new daily session for this member. If so, we will log it and perhaps add some points
    if (($member_id !== null) && (!is_guest($member_id))) {
        // See if this is the first visit today
        global $SESSION_CACHE;
        $test = isset($prior_session_row['last_activity_time']) ? $prior_session_row['last_activity_time'] : null;
        if ($test === null) {
            $test = $GLOBALS['SITE_DB']->query_select_value('daily_visits', 'MAX(d_date_and_time)', ['d_member_id' => $member_id]);
        }

        require_code('temporal');

        if (($test === null) || (date('d/m/Y', tz_time($test, get_site_timezone())) != date('d/m/Y', tz_time(time(), get_site_timezone())))) {
            // New daily visit; log it
            $GLOBALS['SITE_DB']->query_insert('daily_visits', ['d_member_id' => $member_id, 'd_date_and_time' => time()]);

            // Award points
            if (addon_installed('points')) {
                // Daily visits
                $points_visit = intval(get_option('points_per_daily_visit'));
                $points_to_award = $points_visit;

                // Bonus daily visit points for particular browser / platform choices
                $points_foss = intval(get_option('points_foss'));
                if ($points_foss > 0) {
                    // No points if DNT/GPC header check enabled and neither are provided
                    if ((get_option('points_foss_http_header') == '1') &&
                    ((!array_key_exists('HTTP_DNT', $_SERVER)) || ($_SERVER['HTTP_DNT'] != '1')) &&
                    ((!array_key_exists('HTTP_SEC_GPC_FIELD_VALUE', $_SERVER)) || ($_SERVER['HTTP_SEC_GPC_FIELD_VALUE'] != '1'))) {
                        $points_foss = 0;
                    }

                    // Determine the user agent / platform string to regex
                    if (array_key_exists('HTTP_SEC_CH_UA', $_SERVER)) {
                        $to_test = $_SERVER['HTTP_SEC_CH_UA'];
                        if (array_key_exists('HTTP_SEC_CH_UA_PLATFORM', $_SERVER)) {
                            $to_test .= ' ' . $_SERVER['HTTP_SEC_CH_UA_PLATFORM'];
                        }
                    } elseif (array_key_exists('HTTP_USER_AGENT', $_SERVER)) { // Fallback
                        $to_test = $_SERVER['HTTP_USER_AGENT'];
                    } else {
                        $to_test = '';
                    }

                    // Check for inclusive regex. If it fails, then no points.
                    if ((get_option('points_foss_ua_include') != '') && (preg_match(get_option('points_foss_ua_include'), $to_test) != 1)) {
                        $points_foss = 0;
                    }

                    // Check for exclusive regex. If it passes, then no points.
                    if ((get_option('points_foss_ua_exclude') != '') && (preg_match(get_option('points_foss_ua_exclude'), $to_test) != 0)) {
                        $points_foss = 0;
                    }

                    $points_to_award += $points_foss;
                }

                if ($points_to_award > 0) {
                    require_code('points2');
                    require_lang('points');
                    points_credit_member($member_id, (($points_foss > 0) ? get_option('points_foss_reason') : do_lang('DAILY_VISITS')), $points_to_award, 0, null, 0, 'member', 'visit', strval($member_id));
                }
            }
        }
    }

    $GLOBALS['SESSION_CONFIRMED_CACHE'] = ($session_confirmed == 1);

    return $new_session;
}

/**
 * Set the session ID of the user.
 *
 * @sets_output_state
 *
 * @param  ID_TEXT $id The session ID
 * @param  boolean $guest_session Whether this is a guest session (guest sessions will use persistent cookies)
 */
function set_session_id(string $id, bool $guest_session = false)
{
    global $DID_CHANGE_SESSION_ID;
    $DID_CHANGE_SESSION_ID = true;

    // If checking safe mode, can really get in a spin. Don't let it set a session cookie till we've completed startup properly.
    global $CHECKING_SAFEMODE;
    if (($CHECKING_SAFEMODE) && ($id == '')) {
        return;
    }

    // Save cookie
    if ($id != '') {
        $success = cms_setcookie(get_session_cookie(), $id, !$guest_session, true, max(1.0 / 24.0 / 60.0 / 60.0, floatval(get_option('session_expiry_time')) / 24.0));
    } else {
        $success = true;
    }

    // If we really have to, store in URL
    if (((!has_cookies()) || (!$success)) && (!$guest_session/*restorable with no special auth*/) && (get_bot_type() === null) && (get_option('sessions_in_urls') == '1')) {
        $_GET['keep_session'] = strval($id);
    }

    if ((function_exists('get_session_id')) && (function_exists('decache')/*not happening during early boot*/) && ($id != get_session_id()) && (running_script('index'))) {
        delete_cache_entry('side_users_online');
    }
}

/**
 * Force an HTTP authentication login box / relay it as if it were a posted login. This function is rarely used.
 */
function force_httpauth()
{
    if (@cms_empty_safe($_SERVER['PHP_AUTH_USER'])) {
        header('WWW-Authenticate: Basic realm="' . escape_header(get_site_name(), true) . '"');
        require_code('global3');
        set_http_status_code(401);
        exit();
    }
    if (isset($_SERVER['PHP_AUTH_PW'])) { // Ah, route as a normal login if we can then
        $_POST['username'] = preg_replace('#@.*$#', '', $_SERVER['PHP_AUTH_USER']);
        $_POST['password'] = $_SERVER['PHP_AUTH_PW'];
    }
}

/**
 * Filter a member ID through SU, if SU is on and if the user has permission.
 *
 * @param  MEMBER $member_id Real logged in member
 * @return MEMBER Simulated member
 */
function try_su_login(int $member_id) : int
{
    $ks = get_param_string('keep_su', '');

    require_code('permissions');
    $GLOBALS['FORUM_DRIVER']->forum_layer_initialise();
    if (has_privilege($member_id, 'assume_any_member')) {
        if ($ks == do_lang('GUEST', null, null, null, fallback_lang())) {
            $ks = do_lang('GUEST');
        }

        $su = $GLOBALS['FORUM_DRIVER']->get_member_from_username($ks);
        if (($su === null) && (is_numeric($ks))) {
            $su = intval($ks);
        }

        if ($su !== null) {
            $member_id = $su;
        } elseif (is_numeric($ks)) {
            $member_id = intval($ks);
        } else {
            $member_id = null;
        }

        if ($member_id === null) {
            require_code('site');
            attach_message(do_lang_tempcode('_MEMBER_NO_EXIST', escape_html($ks)), 'warn');
            return get_member();
        }

        if ((!$GLOBALS['FORUM_DRIVER']->is_super_admin($su)) || ($GLOBALS['FORUM_DRIVER']->is_super_admin($member_id))) {
            if ((!is_guest($member_id)) && ($GLOBALS['FORUM_DRIVER']->is_banned($member_id))) { // All hands to the guns
                global $USER_THEME_CACHE;
                $USER_THEME_CACHE = 'default';
                critical_error('YOU_ARE_BANNED');
            }
        }
        if (get_param_integer('keep_su_strict', 0) == 0) {
            $GLOBALS['IS_ACTUALLY_ADMIN'] = true;
            $GLOBALS['IS_ACTUALLY'] = $member_id;
        }

        if ((get_forum_type() == 'cns') && (get_param_integer('keep_su_online', 0) == 1)) {
            require_code('crypt');
            $new_session_row = [
                'the_session' => get_secure_random_string(),
                'last_activity_time' => time(),
                'member_id' => $member_id,
                'ip' => get_ip_address(3),
                'session_confirmed' => 0,
                'session_invisible' => 0,
                'cache_username' => $GLOBALS['FORUM_DRIVER']->get_username($member_id),
                'the_title' => '',
                'the_zone' => get_zone_name(),
                'the_page' => cms_mb_substr(get_page_name(), 0, 80),
                'the_type' => cms_mb_substr(get_param_string('type', '', INPUT_FILTER_GET_COMPLEX), 0, 80),
                'the_id' => cms_mb_substr(get_param_string('id', ''), 0, 80),
            ];
            $GLOBALS['SITE_DB']->query_insert('sessions', $new_session_row);
            global $FLOOD_CONTROL_ONCE;
            $FLOOD_CONTROL_ONCE = false;
            $GLOBALS['FORUM_DRIVER']->cns_flood_control($member_id);
            $GLOBALS['SITE_DB']->query_update('sessions', ['session_invisible' => 1], ['the_session' => get_session_id()], '', 1);

            if (get_option('session_prudence') == '0') { // With session prudence we don't store all these in persistent cache due to the size of it all. So only re-save if that's not on.
                global $SESSION_CACHE;
                $SESSION_CACHE[get_session_id()] = ['session_invisible' => 1] + $new_session_row;
                persistent_cache_set('SESSION_CACHE', $SESSION_CACHE);
            }
        }
    }

    return $member_id;
}

/**
 * Try and login via HTTP authentication. This function is only called if HTTP authentication is currently active. With HTTP authentication we trust the PHP_AUTH_USER setting.
 *
 * @param  boolean $quick_only Whether to just do a quick check, don't establish new sessions
 * @return ?MEMBER Logged in member (null: no log in happened)
 */
function try_httpauth_login(bool $quick_only = false) : ?int
{
    global $LDAP_CONNECTION;

    require_code('cns_members');
    require_code('cns_groups');
    require_lang('cns');

    $member_id = cns_authusername_is_bound_via_httpauth(preg_replace('#@.*$#', '', $_SERVER['PHP_AUTH_USER']));
    if (($member_id === null) && ((running_script('index')) || (running_script('execute_temp'))) && (!$quick_only)) {
        require_code('cns_members_action');
        require_code('cns_members_action2');
        if ((post_param_string('email', '', INPUT_FILTER_POST_IDENTIFIER) == '') && (get_option('finish_profile') == '1')) {
            require_code('failure');
            if (throwing_errors()) {
                throw new CMSException(do_lang('ENTER_PROFILE_DETAILS_FINISH'));
            }

            cms_ob_end_clean(); // Emergency output, potentially, so kill off any active buffer
            $middle = cns_member_external_linker_ask(((get_value('windows_auth_is_enabled') !== '1') || ($LDAP_CONNECTION === null)) ? 'httpauth' : 'ldap', preg_replace('#@.*$#', '', $_SERVER['PHP_AUTH_USER']));
            $tpl = globalise($middle, null, '', true);
            $tpl->evaluate_echo();
            exit();
        } else {
            $member_id = cns_member_external_linker(((get_value('windows_auth_is_enabled') !== '1') || ($LDAP_CONNECTION === null)) ? 'httpauth' : 'ldap', preg_replace('#@.*$#', '', $_SERVER['PHP_AUTH_USER']), preg_replace('#@.*$#', '', $_SERVER['PHP_AUTH_USER']));
        }
    }

    if (($member_id !== null) && (!$quick_only)) {
        create_session($member_id, 1, (isset($_COOKIE[get_member_cookie() . '_invisible'])) && ($_COOKIE[get_member_cookie() . '_invisible'] == '1')); // This will mark it as confirmed
    }

    return $member_id;
}

/**
 * Do a cookie login.
 *
 * @return ?MEMBER Logged in member (null: no log in happened)
 */
function try_cookie_login() : ?int
{
    $member_id = null;

    $login_array = $GLOBALS['FORUM_DRIVER']->authorise_cookie_login();
    if ($login_array !== null) {
        $member_id = $login_array['id'];

        if ($member_id !== null) {
            global $IS_A_COOKIE_LOGIN;
            $IS_A_COOKIE_LOGIN = true;

            create_session($member_id, 0, (isset($_COOKIE[get_member_cookie() . '_invisible'])) && ($_COOKIE[get_member_cookie() . '_invisible'] == '1'));
        } else {
            if (!empty($login_array['error'])) {
                $text = $login_array['error'];
                if ($text->evaluate() == do_lang('YOU_ARE_BANNED')) {
                    require_code('failure');
                    banned_exit(empty($login_array['reasoned_ban']) ? null : $login_array['reasoned_ban']);
                }
            }
        }
    }

    return $member_id;
}
