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

/**
 * Standard code module initialisation function.
 *
 * @ignore
 */
function init__lookup()
{
    require_code('submit'); // For the wrap_probe_ip function
}

/**
 * Get information about the specified user.
 *
 * @param  mixed $param The user identifier for whom we are getting the page
 * @param  ?string $username The member's username (by reference) (null: unknown)
 * @param  ?AUTO_LINK $member_id The member's ID (by reference) (null: unknown)
 * @param  ?string $ip The member's IP from the forum (by reference) (null: unknown)
 * @param  ?string $email_address The member's e-mail address (by reference) (null: unknown)
 * @return array The member's IP addresses from the stats table (IP address and most recent time of hit)
 */
function lookup_user($param, ?string &$username, ?int &$member_id, ?string &$ip, ?string &$email_address) : array
{
    require_code('type_sanitisation');
    require_lang('submitban');

    if (is_numeric($param)) {
        // From member ID
        $username = $GLOBALS['FORUM_DRIVER']->get_username(intval($param), false, USERNAME_DEFAULT_NULL);
        if ($username === null) {
            return [];
        }
        $member_id = intval($param);
        $ip = $GLOBALS['FORUM_DRIVER']->get_member_ip($member_id);
        if ($ip === null) {
            $ip = '127.0.0.1';
        }
        $email_address = $GLOBALS['FORUM_DRIVER']->get_member_email_address($member_id);
    } elseif (is_valid_email_address($param)) {
        // From e-mail address
        $member_id = $GLOBALS['FORUM_DRIVER']->get_member_from_email_address($param);
        if ($member_id === null) {
            return [];
        }
        $username = $GLOBALS['FORUM_DRIVER']->get_username($member_id, false, USERNAME_DEFAULT_NULL);
        $ip = $GLOBALS['FORUM_DRIVER']->get_member_ip($member_id);
        if ($ip === null) {
            $ip = '127.0.0.1';
        }
        $email_address = $param;
    } elseif ((strpos($param, '.') !== false) || (strpos($param, ':') !== false)) {
        // From IP
        $member_ids = wrap_probe_ip($param);
        $ip = $param;
        if ($ip === null) {
            $ip = '127.0.0.1';
        }
        if (empty($member_ids)) {
            return [];
        } else {
            $member_id = $member_ids[0];
        }
        if (count($member_ids) != 1) {
            $also = new Tempcode();
            foreach ($member_ids as $t => $_id) {
                if ($t != 0) {
                    if (!$also->is_empty()) {
                        $also->attach(do_lang('LIST_SEP'));
                    }
                    $also->attach($GLOBALS['FORUM_DRIVER']->member_profile_hyperlink($_id, '', false));
                }
            }
            attach_message(do_lang_tempcode('MEMBERS_ALSO_ON_IP', $also), 'inform');
        }
        $username = $GLOBALS['FORUM_DRIVER']->get_username($member_id);
        $email_address = $GLOBALS['FORUM_DRIVER']->get_member_email_address($member_id);
    } elseif ($param != '') {
        // From name
        $member_id = $GLOBALS['FORUM_DRIVER']->get_member_from_username($param);
        $username = $param;
        if ($member_id === null) {
            return [];
        }
        $ip = $GLOBALS['FORUM_DRIVER']->get_member_ip($member_id);
        if ($ip === null) {
            $ip = '127.0.0.1';
        }
        $email_address = $GLOBALS['FORUM_DRIVER']->get_member_email_address($member_id);
    }

    if (!addon_installed('stats')) {
        return [];
    }

    return $GLOBALS['SITE_DB']->query_select('stats', ['ip', 'MAX(date_and_time) AS date_and_time'], ['member_id' => $member_id], 'GROUP BY ip ORDER BY date_and_time DESC', 100);
}

/**
 * Get a results table showing info about the member's travels around the site.
 *
 * @param  MEMBER $member_id The member we are getting travel stats for
 * @param  IP $ip The IP address of the member
 * @param  integer $start The current position in the browser
 * @param  integer $max The maximum number of rows to show per browser page
 * @param  ?ID_TEXT $sortable The current sortable (null: none)
 * @param  ?ID_TEXT $sort_order The order we are sorting in (null: none)
 * @set ASC DESC
 * @return Tempcode The results table
 */
function find_page_stats_for(int $member_id, string $ip, int $start = 0, int $max = 50, ?string $sortable = 'date_and_time', ?string $sort_order = 'DESC') : object
{
    if (!addon_installed('stats')) {
        return new Tempcode();
    }

    require_lang('zones');

    $sortables = ['date_and_time' => do_lang_tempcode('DATE'), 'page_link' => do_lang_tempcode('PAGE_LINK')];
    if (((cms_strtoupper_ascii($sort_order) != 'ASC') && (cms_strtoupper_ascii($sort_order) != 'DESC')) || (!array_key_exists($sortable, $sortables))) {
        log_hack_attack_and_exit('ORDERBY_HACK');
    }

    $query = '';
    if (!is_guest($member_id)) {
        $query .= 'member_id=' . strval($member_id);
    }
    if ($ip != '') {
        if (!is_guest($member_id)) {
            $query .= ' OR ';
        }

        if (strpos($ip, '*') === false) {
            $query .= db_string_equal_to('ip', $ip);
        } else {
            $query .= 'ip LIKE \'' . db_encode_like(str_replace('*', '%', $ip)) . '\'';
        }
    }
    if ($query == '') {
        $query = '1=1';
    }
    $max_rows = $GLOBALS['SITE_DB']->query_value_if_there('SELECT COUNT(*) FROM ' . get_table_prefix() . 'stats WHERE ' . $query, false, true);
    $rows = $GLOBALS['SITE_DB']->query('SELECT * FROM ' . get_table_prefix() . 'stats WHERE ' . $query . ' ORDER BY ' . $sortable . ' ' . $sort_order, $max, $start, false, true);

    $out = new Tempcode();
    require_code('templates_results_table');
    $header_row_fields = [
        do_lang_tempcode('DATE'),
        do_lang_tempcode('PAGE_LINK'),
        do_lang_tempcode('IP_ADDRESS'),
        do_lang_tempcode('USERNAME'),
        do_lang_tempcode('TRACKING_CODE'),
    ];
    $header_row = results_header_row($header_row_fields, $sortables, 'sort', $sortable . ' ' . $sort_order);
    foreach ($rows as $myrow) {
        $details_url = build_url(['page' => 'admin_lookup', 'type' => 'view', 'id' => $myrow['id'], 'param' => get_param_string('param', null)], get_module_zone('admin_lookup'));
        $ip_url = build_url(['page' => 'admin_lookup', 'type' => 'results', 'param' => $myrow['ip']], get_module_zone('admin_lookup'));
        $member_url = build_url(['page' => 'admin_lookup', 'type' => 'results', 'param' => $myrow['member_id']], get_module_zone('admin_lookup'));

        $results_row = [
            hyperlink($details_url, get_timezoned_date_time($myrow['date_and_time']), false, true, do_lang_tempcode('VIEW_REQUEST')),
            protect_from_escaping(str_replace(':', ':&#8203;', escape_html($myrow['page_link']))),
            hyperlink($ip_url, $myrow['ip'], false, true),
            hyperlink($member_url, $GLOBALS['FORUM_DRIVER']->get_username($myrow['member_id']), false, true),
            $myrow['tracking_code'],
        ];
        $out->attach(results_entry($results_row, false));
    }
    return results_table(do_lang_tempcode('RESULTS'), $start, 'start', $max, 'max', $max_rows, $header_row, $out, $sortables, $sortable, $sort_order, 'sort');
}

/**
 * Get a results table showing security alerts matching WHERE constraints.
 *
 * @param  array $where WHERE constraints
 * @return array A pair: The results table, The number
 */
function find_security_alerts(array $where = []) : array
{
    require_code('templates_tooltip');
    require_code('templates_results_table');

    require_lang('security');

    $start = get_param_integer('alert_start', 0);
    $max = get_param_integer('alert_max', 25);

    $sortables = ['date_and_time' => do_lang_tempcode('DATE_TIME'), 'risk_score' => do_lang_tempcode('RISK'), 'ip' => do_lang_tempcode('IP_ADDRESS')];
    $test = explode(' ', get_param_string('alert_sort', 'date_and_time DESC', INPUT_FILTER_GET_COMPLEX));
    if (count($test) == 1) {
        $test[1] = 'DESC';
    }
    list($sortable, $sort_order) = $test;
    if (((cms_strtoupper_ascii($sort_order) != 'ASC') && (cms_strtoupper_ascii($sort_order) != 'DESC')) || (!array_key_exists($sortable, $sortables))) {
        log_hack_attack_and_exit('ORDERBY_HACK');
    }

    $_fields = [do_lang_tempcode('FROM'), do_lang_tempcode('DATE_TIME'), do_lang_tempcode('RISK'), do_lang_tempcode('IP_ADDRESS'), do_lang_tempcode('REASON'), new Tempcode()];
    $header_row = results_header_row($_fields, $sortables, 'alert_sort', $sortable . ' ' . $sort_order);

    $max_rows = $GLOBALS['SITE_DB']->query_select_value('hackattack', 'COUNT(*)', $where);

    $rows = $GLOBALS['SITE_DB']->query_select('hackattack', ['*'], $where, 'AND risk_score>=1 AND silent_to_staff_log=0 ORDER BY ' . $sortable . ' ' . $sort_order, $max, $start);

    $result_entries = new Tempcode();
    foreach ($rows as $row) {
        $date = get_timezoned_date_time($row['date_and_time']);

        $lookup_url = build_url(['page' => 'admin_lookup', 'type' => 'results', 'param' => $row['ip']], get_module_zone('admin_lookup'));

        $member_url = build_url(['page' => 'admin_lookup', 'type' => 'results', 'param' => $row['member_id']], get_module_zone('admin_lookup'));

        $full_url = build_url(['page' => 'admin_security', 'type' => 'view', 'id' => $row['id']], get_module_zone('admin_security'));

        $reason = do_lang($row['reason'], $row['reason_param_a'], $row['reason_param_b'], null, null, false);
        if ($reason === null) {
            $reason = $row['reason'];
        }
        $reason = generate_tooltip_by_truncation($reason, 50);

        $username = $GLOBALS['FORUM_DRIVER']->get_username($row['member_id']);

        $_row = [
            hyperlink($member_url, $username, false, true),
            hyperlink($full_url, $date, false, true),
            integer_format($row['risk_score']),
            hyperlink($lookup_url, $row['ip'], false, true),
            $reason
        ];

        $deletion_tick = do_template('RESULTS_TABLE_TICK', ['_GUID' => '9d310a90afa8bd1817452e476385bc57', 'ID' => strval($row['id'])]);
        $_row[] = $deletion_tick;

        $result_entries->attach(results_entry($_row, false));
    }

    return [results_table(do_lang_tempcode('SECURITY_ALERTS'), $start, 'alert_start', $max, 'alert_max', $max_rows, $header_row, $result_entries, $sortables, $sortable, $sort_order, 'alert_sort'), count($rows)];
}

/**
 * Save analytics metadata for the current user's request into a file.
 *
 * @param  boolean $include_referer Whether to include the referer
 * @param  ?MEMBER $member_id Member to lookup (null: current user)
 * @param  ?IP $ip IP address (null: current / member's)
 * @return PATH The path of the file
 */
function save_user_metadata(bool $include_referer = false, ?int $member_id = null, ?string $ip = null) : string
{
    $data = find_user_metadata($include_referer, $member_id, $ip);

    require_code('crypt');
    $path = get_custom_file_base() . '/temp/mail_' . get_secure_random_string() . '.txt';

    require_code('files');
    cms_file_put_contents_safe($path, json_encode($data, JSON_PRETTY_PRINT), FILE_WRITE_FIX_PERMISSIONS | FILE_WRITE_SYNC_FILE | FILE_WRITE_BOM);

    return $path;
}

/**
 * Find analytics metadata for the current user's request.
 *
 * @param  boolean $include_referer Whether to include the referer
 * @param  ?MEMBER $member_id Member to lookup (null: current user)
 * @param  ?IP $ip IP address (null: current / member's)
 * @param  boolean $advanced Whether to include advanced data
 * @return array Data
 */
function find_user_metadata(bool $include_referer = true, ?int $member_id = null, ?string $ip = null, bool $advanced = false) : array
{
    if ($member_id === null) {
        $member_id = get_member();
        $current_user = true;
    } else {
        $current_user = false;
    }

    if ($ip === null) {
        if ($current_user) {
            $ip = get_ip_address();
        } else {
            $ip = $GLOBALS['FORUM_DRIVER']->get_member_ip($member_id);
        }
    }

    require_lang('lookup');

    $data = [];

    $member_data = [];

    $member_data[do_lang('USERNAME', null, null, null, get_site_default_lang())] = $GLOBALS['FORUM_DRIVER']->get_username($member_id);
    if (!is_guest($member_id)) {
        $member_data[do_lang('MEMBER_ID', null, null, null, get_site_default_lang())] = '#' . strval($member_id);
    }
    if (!is_guest($member_id)) {
        $member_data[do_lang('cns:PROFILE', null, null, null, get_site_default_lang())] = $GLOBALS['FORUM_DRIVER']->member_profile_url($member_id, false);
    }
    $data[do_lang('ACCOUNT', null, null, null, get_site_default_lang())] = $member_data;

    $l_tracking_code = do_lang('TRACKING_CODE', null, null, null, get_site_default_lang());
    $data[$l_tracking_code] = find_session_tracking_codes();

    //$data[do_lang('SESSION_ID', null, null, null, get_site_default_lang())] = get_session_id();   Don't want to pass this out too freely, not useful anyway

    $location_data = [];
    $got_geo_lookup = false;
    if (get_option('ipstack_api_key') != '') {
        // NB: https requires a paid plan
        $ip_stack_url = 'http://api.ipstack.com/' . rawurlencode($ip) . '?access_key=' . urlencode(get_option('ipstack_api_key'));
        $_json = cms_http_request($ip_stack_url, ['ignore_http_status' => true, 'convert_to_internal_encoding' => true, 'trigger_error' => false]);
        $json = @json_decode($_json->data, true);
        if (is_array($json)) {
            if (array_key_exists('error', $json)) {
                require_code('failure');
                cms_error_log('IP Stack: ERROR ' . $json['error']['info'], 'error_occurred_api');
            }

            $useful_fields = [
                'ip' => 'IP address',
                'country_name' => 'Country',
                'city' => 'City',
                'zip' => 'Postal code',
            ];
            foreach ($useful_fields as $desired_key => $lang_str) {
                if (!empty($json[$desired_key])) {
                    $location_data[$lang_str] = $json[$desired_key];
                }
            }
            if (!@cms_empty_safe($json['latitude'])) {
                $location_data['GPS'] = float_format($json['latitude'], 6) . ',' . float_format($json['longitude'], 6);
            }

            $got_geo_lookup = true;
        } else {
            require_code('failure');
            cms_error_log('IP Stack: ERROR ' . $_json->message, 'error_occurred_api');
        }
    }
    if (!$got_geo_lookup) {
        $location_data[do_lang('IP_ADDRESS', null, null, null, get_site_default_lang())] = $ip;

        if ($ip != '') {
            $reverse_dns = cms_gethostbyaddr($ip);
            if (strpos($reverse_dns, 'NXDOMAIN') === false) {
                $location_data[do_lang('REVERSE_DNS_AND_WHOIS', null, null, null, get_site_default_lang())] = $reverse_dns;
            }
        }

        require_code('locations');
        if ($current_user) {
            $region = get_region();
        } else {
            if (!is_guest($member_id)) {
                $region = get_cms_cpf('country');
            }

            if ((empty($region)) && (addon_installed('stats'))) {
                $region = geolocate_ip($ip);
            }
        }
        if (!empty($region)) {
            $location_data['~' . do_lang('LOCATION', null, null, null, get_site_default_lang())] = $region;
        }

        require_code('temporal2');
        $location_data['~' . do_lang('TIMEZONE', null, null, null, get_site_default_lang())] = make_nice_timezone_name(get_users_timezone($member_id));
    }
    $data[do_lang('LOCATION', null, null, null, get_site_default_lang())] = $location_data;

    if ($current_user) {
        $lang = user_lang();
    } else {
        $lang = get_lang_member($member_id);
    }
    if (multi_lang()) {
        require_code('lang2');
        $data[do_lang('LANGUAGE', null, null, null, get_site_default_lang())] = lookup_language_full_name($lang);
    }

    if (($include_referer) && ($current_user)) {
        $referer = $_SERVER['HTTP_REFERER'];
        if ($referer != '') {
            $data[do_lang('REFERER', null, null, null, get_site_default_lang())] = $referer;
        }
    }

    $l_browser = do_lang('USER_AGENT', null, null, null, get_site_default_lang());
    $l_os = do_lang('USER_OS', null, null, null, get_site_default_lang());
    $l_requested_language = do_lang('REQUESTED_LANGUAGE', null, null, null, get_site_default_lang());

    if ($current_user) {
        $ua_data = [];
        $ua_data[$l_browser] = get_browser_string();
        $ua_data[$l_os] = get_os_string();
        $ua_data[$l_requested_language] = substr(preg_replace('#[,;].*$#', '', $_SERVER['HTTP_ACCEPT_LANGUAGE']), 0, 10);
        $data[$l_browser] = $ua_data;
    }

    if (((running_script('index')) || (running_script('form_to_email'))) && ($current_user)) {
        $data[do_lang('URL', null, null, null, get_site_default_lang())] = get_self_url_easy(true);
    }

    if (addon_installed('stats')) {
        $history = [];

        $select = 'a.page_link,a.date_and_time,a.member_id,a.ip,a.session_id,a.referer_url,a.browser,a.operating_system,a.tracking_code,a.requested_language';

        $where = db_string_equal_to('ip', $ip) . ' AND member_id=' . strval($GLOBALS['FORUM_DRIVER']->get_guest_id());
        if (!is_guest($member_id)) {
            $where .= ' OR member_id=' . strval($member_id);
        }
        if ($current_user) {
            if (get_session_id() != '') {
                $where .= ' OR ' . db_string_equal_to('session_id', get_session_id());
            }
        }

        $tp = get_table_prefix();
        if ($advanced) {
            $sql = 'SELECT ' . $select . ' FROM ' . $tp . 'stats a WHERE ' . $where;
        } else { // Filter to one-result-per-page
            $inner_sql = 'SELECT page_link,MAX(date_and_time) AS date_and_time FROM ' . $tp . 'stats WHERE ' . $where . ' GROUP BY page_link';
            $sql = 'SELECT ' . $select . ' FROM ' . $tp . 'stats a JOIN (' . $inner_sql . ') b ON a.page_link=b.page_link AND a.date_and_time=b.date_and_time WHERE ' . $where;
        }

        $pages = $GLOBALS['SITE_DB']->query($sql . ' ORDER BY date_and_time DESC', 50);
        if (count($pages) == 50) { // We have more than 50, so we'll place in the bottom 50 too
            $pages2 = $GLOBALS['SITE_DB']->query($sql . ' ORDER BY date_and_time ASC', 50);
            $pages = array_merge($pages, [null], array_reverse($pages2));
        }

        $l_date_time = do_lang('DATE_TIME', null, null, null, get_site_default_lang());
        $l_url = do_lang('URL', null, null, null, get_site_default_lang());
        $l_member_id = do_lang('MEMBER_ID', null, null, null, get_site_default_lang());
        $l_ip_address = do_lang('IP_ADDRESS', null, null, null, get_site_default_lang());
        //$l_session_id = do_lang('SESSION_ID', null, null, null, get_site_default_lang());
        $l_referer = do_lang('REFERER', null, null, null, get_site_default_lang());
        $l_browsing_environment = do_lang('BROWSING_ENVIRONMENT', null, null, null, get_site_default_lang());

        $first_browser = null;

        foreach ($pages as $myrow) {
            if ($myrow === null) {
                $history[] = ['_' => do_lang('TOO_MANY_PAGES_LOGGED', null, null, null, get_site_default_lang())];
                continue;
            }

            if ($myrow['page_link'] == ':404') {
                continue;
            }

            $h = [];

            $h[$l_date_time] = get_timezoned_date(tz_time($myrow['date_and_time'], get_server_timezone()), false);

            $h[$l_url] = page_link_to_url($myrow['page_link']);

            if (($myrow['member_id'] != $member_id) && (($advanced) || (!is_guest($myrow['member_id'])))) {
                if (is_guest($myrow['member_id'])) {
                    $h[$l_member_id] = do_lang('GUEST', null, null, null, get_site_default_lang());
                } else {
                    $h[$l_member_id] = '#' . strval($myrow['member_id']);
                }
            }

            if ($advanced) {
                if ($myrow['ip'] != $ip) {
                    $h[$l_ip_address] = $myrow['ip'];
                }

                //$h[$l_session_id] = $myrow['session_id'];
            }

            if (($myrow['referer'] != '') && (($advanced) || (/*external referer*/@cms_parse_url_safe($myrow['referer'], PHP_URL_HOST) != get_base_url_hostname()))) {
                $h[$l_referer] = $myrow['referer'];
            }

            $h[$l_tracking_code] = $myrow['tracking_code'];

            $ua_data = [];
            $ua_data[$l_browser] = $myrow['browser'];
            $ua_data[$l_os] = $myrow['operating_system'];
            $ua_data[$l_requested_language] = $myrow['requested_language'];
            $this_browser = preg_replace('#\d#', '', $myrow['browser'] . ' ' . $myrow['operating_system']);
            if ($first_browser === null) {
                $data[$l_browsing_environment] = $ua_data;
            } elseif (($first_browser != $this_browser) && ($advanced)) {
                $h[$l_browsing_environment] = $ua_data;
            }

            $history[] = $h;

            if ($first_browser === null) {
                $first_browser = $this_browser;
            }
        }
        $data[do_lang($advanced ? 'HISTORY' : 'PAGES_SEEN', null, null, null, get_site_default_lang())] = $history;
    }

    //$data['Cookie'] = $_COOKIE;   Don't want to pass this out too freely, not useful anyway

    return $data;
}
