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

/**
 * Find if a member is online.
 *
 * @param  MEMBER $member_id The member to check
 * @return boolean Whether they are online
 */
function member_is_online(int $member_id) : bool
{
    if (get_value('disable_member_tracking') === '1') {
        return false;
    }

    if (is_guest($member_id)) {
        return false;
    }

    if (get_option('session_prudence') == '0') {
        // If we have all sessions loaded, use that...

        $count = 0;
        $online = get_users_online(false, $member_id, $count);
        foreach ($online as $m) {
            if ($m['member_id'] == $member_id) {
                return true;
            }
        }
        return false;
    }

    // Otherwise a manual query...

    static $cache = [];
    if (isset($cache[$member_id])) {
        return $cache[$member_id];
    }

    $users_online_time_seconds = intval(get_option('users_online_time')) * 60;
    $sql = 'SELECT last_activity_time FROM ' . get_table_prefix() . 'sessions WHERE session_invisible=0 AND last_activity_time>' . strval(time() - $users_online_time_seconds) . ' AND member_id=' . strval($member_id);
    $result = $GLOBALS['SITE_DB']->query_value_if_there($sql);
    $ret = $result !== null;

    $cache[$member_id] = $ret;

    return $ret;
}

/**
 * Get database rows of all the online members.
 *
 * @param  boolean $longer_time Whether to use a longer online-time -- the session expiry-time
 * @param  ?MEMBER $filter We really only need to make sure we get the status for this user, although at this functions discretion more may be returned and the row won't be there if the user is not online (null: no filter). May not be the guest ID
 * @param  integer $count The total online members, returned by reference
 * @return ?array Database rows (null: too many)
 */
function get_users_online(bool $longer_time, ?int $filter, int &$count) : ?array
{
    if (get_value('disable_member_tracking') === '1') {
        return [];
    }

    $max_to_show = 200; // FUDGE - Hard-coded value

    $users_online_time_seconds = intval($longer_time ? (60.0 * 60.0 * floatval(get_option('session_expiry_time'))) : (60.0 * floatval(get_option('users_online_time'))));
    $cutoff = time() - $users_online_time_seconds;

    global $SESSION_CACHE;

    if (get_option('session_prudence') == '1') {
        // If we have multiple servers this many not be accurate as we probably turned replication off for the sessions table. The site design should be updated to not show this kind of info
        $count = $GLOBALS['SITE_DB']->query_value_if_there('SELECT COUNT(*) FROM ' . get_table_prefix() . 'sessions WHERE session_invisible=0 AND last_activity_time>' . strval($cutoff)); // Written in by reference
        if ($filter !== null) {
            return $GLOBALS['SITE_DB']->query('SELECT * FROM ' . get_table_prefix() . 'sessions WHERE session_invisible=0 AND last_activity_time>' . strval($cutoff) . ' AND member_id=' . strval($filter), 1);
        }
        if (count($SESSION_CACHE) > $max_to_show) {
            return null;
        }
    }

    $sessions = $SESSION_CACHE;

    $members = [];
    $guests = [];
    $guest_id = $GLOBALS['FORUM_DRIVER']->get_guest_id();
    $guests_online = 0;
    $members_online = 0;
    foreach ($sessions as $row) {
        if (($row['last_activity_time'] > $cutoff) && ($row['session_invisible'] == 0)) {
            if ($row['member_id'] == $guest_id) {
                if ($count < $max_to_show) {
                    $guests[] = $row;
                }

                $count++;
                $guests_online++;
            } elseif ((!member_blocked(get_member(), $row['member_id'])) && (!isset($members[$row['member_id']]))) {
                if ($count >= $max_to_show) { // Guests show with lower priority
                    if (!empty($guests)) {
                        array_pop($guests);
                        $members[$row['member_id']] = $max_to_show;
                    }
                } else {
                    $members[$row['member_id']] = $row;
                }

                $count++;
                $members_online++;
            }
        }

        if (($count >= $max_to_show) && ($filter !== null)) {
            return $GLOBALS['SITE_DB']->query('SELECT * FROM ' . get_table_prefix() . 'sessions WHERE session_invisible=0 AND last_activity_time>' . strval($cutoff) . ' AND member_id=' . strval($filter), 1);
        }
    }

    return array_merge($members, $guests);
}

/**
 * Find if a member is blocked by a member.
 *
 * @param  MEMBER $member_id The member being checked
 * @param  ?MEMBER $member_blocker The member who may be blocking (null: current member)
 * @return boolean Whether the member is blocked
 */
function member_blocked(int $member_id, ?int $member_blocker = null) : bool
{
    if (!addon_installed('chat')) {
        return false;
    }
    if ($member_blocker === null) {
        $member_blocker = get_member();
    }

    if ($member_blocker == $member_id) {
        return false;
    }
    if (is_guest($member_id)) {
        return false;
    }
    if (is_guest($member_blocker)) {
        return false;
    }

    if (!addon_installed('chat')) {
        return false;
    }

    if ($member_id == get_member()) {
        static $members_blocking_us_cache = [];
        if ($members_blocking_us_cache === null) {
            $rows = $GLOBALS['SITE_DB']->query_select('chat_blocking', ['member_blocker'], ['member_blocked' => get_member()]);
            $members_blocking_us_cache = collapse_1d_complexity('member_blocker', $rows);
        }
        return (in_array($member_blocker, $members_blocking_us_cache));
    }

    static $members_blocked_cache = [];
    if ($members_blocked_cache === null) {
        $rows = $GLOBALS['SITE_DB']->query_select('chat_blocking', ['member_blocked'], ['member_blocker' => get_member()]);
        $members_blocked_cache = collapse_1d_complexity('member_blocked', $rows);
    }
    return (in_array($member_id, $members_blocked_cache));
}

/**
 * Get template-ready details of members viewing the specified software location.
 *
 * @param  ?ID_TEXT $page The page they need to be viewing (null: don't care)
 * @param  ?ID_TEXT $type The page-type they need to be viewing (null: don't care)
 * @param  ?SHORT_TEXT $id The type-id they need to be viewing (null: don't care)
 * @param  boolean $forum_layer Whether this has to be done over the forum driver (multi site network)
 * @return ?array A map of member-IDs to rows about them (null: Too many)
 */
function get_members_viewing_wrap(?string $page = null, ?string $type = null, ?string $id = null, bool $forum_layer = false) : ?array
{
    $members = ($id === null) ? [] : get_members_viewing($page, $type, $id, $forum_layer);
    $num_guests = 0;
    $num_members = 0;
    if ($members === null) {
        $members_viewing = new Tempcode();
    } else {
        $members_viewing = new Tempcode();
        if (!isset($members[get_member()])) {
            if (is_guest()) {
                $members[get_member()] = 1;
            } else {
                $members[get_member()] = ['mt_cache_username' => $GLOBALS['FORUM_DRIVER']->get_username(get_member())];
            }
        }
        foreach ($members as $member_id => $at_details) {
            if (is_guest($member_id)) {
                $num_guests += $at_details;/*is integer for guest*/
            } else {
                $username = $at_details['mt_cache_username'];
                $num_members++;
                $profile_url = $GLOBALS['FORUM_DRIVER']->member_profile_url($member_id, true);
                $map = [
                    'FIRST' => $num_members == 1,
                    'PROFILE_URL' => $profile_url,
                    'USERNAME' => $username,
                    'MEMBER_ID' => strval($member_id),
                ];
                if (isset($at_details['the_title'])) {
                    if ((has_privilege(get_member(), 'show_user_browsing')) || ((in_array($at_details['the_page'], ['topics', 'topicview'])) && ($at_details['the_id'] == $id))) {
                        $map['AT'] = escape_html($at_details['the_title']);
                    }
                }
                $map['COLOUR'] = get_group_colour(cns_get_member_primary_group($member_id));
                $members_viewing->attach(do_template('CNS_USER_MEMBER', $map));
            }
        }
        if ($members_viewing->is_empty()) {
            $members_viewing = do_lang_tempcode('NONE_EM');
        }
    }

    return [$num_guests, $num_members, $members_viewing];
}

/**
 * Get a map of members viewing the specified software location.
 *
 * @param  ?ID_TEXT $page The page they need to be viewing (null: environment current) (blank: don't care)
 * @param  ?ID_TEXT $type The page-type they need to be viewing (null: environment current) (blank: don't care)
 * @param  ?SHORT_TEXT $id The type-id they need to be viewing (null: environment current) (blank: don't care)
 * @param  boolean $forum_layer Whether this has to be done over the forum driver (multi site network)
 * @return ?array A map of member-IDs to rows about them (except for guest, which is a count) (null: Too many / disabled)
 */
function get_members_viewing(?string $page = null, ?string $type = null, ?string $id = null, bool $forum_layer = false) : ?array
{
    if (get_value('disable_member_tracking') === '1') {
        return null;
    }

    if ($page === null) {
        $page = get_page_name();
    }
    if ($type === null) {
        $type = get_param_string('type', '/');
    }
    if ($id === null) {
        $id = get_param_string('id', '/', INPUT_FILTER_GET_COMPLEX);
    }
    if ($type == '/') {
        $type = '';
    }
    if ($id == '/') {
        $id = '';
    }

    // Update the member tracking
    member_tracking_update($page, $type, $id);

    $map = [];
    if (!cms_empty_safe($page)) {
        $map['mt_page'] = $page;
    }
    if (!cms_empty_safe($type)) {
        $map['mt_type'] = $type;
    }
    if (!cms_empty_safe($id)) {
        $map['mt_id'] = $id;
    }
    $map['session_invisible'] = 0;
    $db = ($forum_layer ? $GLOBALS['FORUM_DB'] : $GLOBALS['SITE_DB']);
    $table_clause = 'member_tracking t' . $GLOBALS['SITE_DB']->singular_join('sessions', 's', 't.mt_member_id=s.member_id', 'the_session', 'MAX');
    $results = $db->query_select($table_clause, ['*'], $map, ' AND mt_member_id<>' . strval($GLOBALS['FORUM_DRIVER']->get_guest_id()) . ' AND mt_time>' . strval(time() - 60 * intval(get_option('users_online_time'))) . ' ORDER BY mt_member_id', 200);
    if (count($results) == 200) {
        return null;
    }

    unset($map['session_invisible']);
    $num_guests = $db->query_select_value('member_tracking t', 'COUNT(*)', $map, ' AND mt_member_id=' . strval($GLOBALS['FORUM_DRIVER']->get_guest_id()));

    $out = [
        $GLOBALS['FORUM_DRIVER']->get_guest_id() => $num_guests,
    ];
    foreach ($results as $row) {
        if (!member_blocked(get_member(), $row['mt_member_id'])) {
            $out[$row['mt_member_id']] = $row;
        }
    }
    return $out;
}

/**
 * Find a user to test access against, if we're planning on making presence of something public.
 *
 * @return MEMBER The modal member
 */
function get_modal_user() : int
{
    $modal_user = get_option('modal_user');
    if ($modal_user != '') {
        if ($modal_user == '<self>') {
            return get_member();
        }
        $member_id = $GLOBALS['FORUM_DRIVER']->get_member_from_username($modal_user);
        if ($member_id !== null) {
            return $member_id;
        }
    }
    return $GLOBALS['FORUM_DRIVER']->get_guest_id();
}
