yf_auth_user class

Special methods for user authentification.

Fileplugins/auth/classes/auth/yf_auth_user.class.php
Use_class('auth_user', 'classes/auth/')

Public properties

A site sets them in its config with $PROJECT_CONF['auth_user']['NAME'] = ...;

PropertyDefaultDescription
$ALLOW_PLAINTEXT_PASSWORDS
bool
true Accept a plaintext stored password at login (yf_register stored them that way); previous behaviour
$ALLOW_REMEMBER_ME
bool
true Allow to use 'remember me in cookies' feature @security
$AUTH_ONLY_HTTPS
bool
false Allow to login only by HTTPS protocol, else raise error @security
$BLOCK_BANNED_IPS
bool
false Track banned IPs list @security
$BLOCK_FAILED_LOGINS
bool
false Block failed logins after several attempts (To prevent password bruteforcing, hacking, etc) @security
$BLOCK_FAILED_TTL
bool
3600 Track failed logins TTL @security
$BLOCK_FAILS_BY_IP_COUNT
bool
10 @security
$BLOCK_FAILS_BY_LOGIN_COUNT
bool
5 @security
$CATCH_REF_CODES
bool
false Catch ref codes
$CHECK_MULTI_ACCOUNTS
bool
false Check if user have multiple accounts @security
$DEF_USER_MODULE
string
'signup' Default user object (module) to redirect
$DO_LOG_LOGINS
bool
true Do log into db user login actions
$EXEC_AFTER_INIT
array
[] @conf_skip
$EXEC_AFTER_LOGIN
array
array(array('test_method', array('Working!'))); */ Property [ public $EXEC_AFTER_LOGIN = [] @conf_skip Methods to execute after success login or logout @example $EXEC_AFTER_LOGIN = array(array('test_method', array('Working!')));
$EXEC_AFTER_LOGOUT
array
[] @conf_skip
$FORCE_LOGOUT_INACTIVE
string
true
$INSIDE_LOGIN_FROM_ADMIN false
$LOGIN_ALIAS
string
['email', 'phone'] Alternative login field name to use @conf_skip
$LOGIN_FIELD
string
'login' Login field name to use @conf_skip
$LOG_FAILED_LOGINS
bool
true Save failed logins @security
$LOG_FAILED_PSWD
bool
true Store the submitted password in log_auth_fails (previous behaviour); false keeps secrets out of the log
$ONLINE_AUTO_CLEAN
int
1800 Online TTL if inactive
$ONLINE_MAX_IDS
int
1000 Online TTL if inactive
$ONLINE_SKIP_PAGES
array
['user_profile->compact_info', 'help->show_tip', 'forum->compact_topic_repliers', 'aff', 'task_loader'] Pages where we do not need to track online stats @conf_skip
$PSWD_FIELD
string
'password' Password field name to use @conf_skip
$REMEMBER_FIELD
string
'remember_me' Remeber field name to use @conf_skip
$REMEMBER_ME_TOKENS
bool
false Remember-me as a random server-side token (user_remember_tokens, hashed, rotated on use) instead of the encrypted login+password cookie; old cookies expire once enabled
$REMEMBER_ROTATE_GRACE
int
30 Seconds the previous validator stays valid after a rotation, for parallel requests that still carry it
$SESSION_LOCK_TO_HOST
bool
false Lock session to hostname string (to prevent hacks) @security
$SESSION_LOCK_TO_IP
bool
true Lock session to IP address (to prevent hacks) @security
$SESSION_LOCK_TO_UA
bool
false Lock session to User Agent string (to prevent hacks) @security
$SESSION_REFERER_CHECK
bool
false Check referer in session @security
$SITE_CLOSED_STPL
string
'site_closed' Site closed stpl name
$STORE_ONLINE_USERS
bool
false Online users vars
$TRACK_GEO_LOCATION
bool
false Store cookie with geo info for guests
$URL_ACCOUNT_INACTIVE
string
'./?object=login_form&action=account_inactive' Redirect URL
$URL_AFTER_LOGOUT
string
'./' Redirect URL
$URL_SUCCESS_LOGIN
string
'' Redirect URL
$URL_WRONG_LOGIN
string
'./?object=login_form&action=wrong_login' Redirect URL
$USER_INFO_IN_SESSION
bool
0 Store or not user info in session sub array or not
$USER_PASSWORD_SALT
string
''
$USER_SECURITY_CHECKS
string
false
$VAR_LOCK_HOST
string
'auth_lock_to_host' field name @conf_skip
$VAR_LOCK_IP
string
'auth_lock_to_ip' field name @conf_skip
$VAR_LOCK_UA
string
'auth_lock_to_ua' field name @conf_skip
$VAR_USER_GO_URL
string
'user_go_url' field name @conf_skip
$VAR_USER_GROUP_ID
string
'user_group' field name @conf_skip
$VAR_USER_ID
string
'user_id' field name @conf_skip
$VAR_USER_INFO
string
'user_info' field name @conf_skip
$VAR_USER_LOGIN_TIME
string
'user_login_time' field name @conf_skip
$_encrypted_error null

Public methods 35

auto_login

public function auto_login($user_info = [], $no_redirect_on_success = false)

Alias.

    public function auto_login($user_info = [], $no_redirect_on_success = false)
    {
        return $this->_save_login_in_session($user_info, $no_redirect_on_success);
    }

init

public function init()

Initialize auth.

    public function init()
    {
        // Chained config rule
        if ($this->BLOCK_FAILED_LOGINS) {
            $this->LOG_FAILED_LOGINS = true;
        }
        // Check if we need to skip online stats
        if ($this->STORE_ONLINE_USERS) {
            if (in_array(($_GET['object'] ?? null) . '->' . ($_GET['action'] ?? null), $this->ONLINE_SKIP_PAGES) || in_array(($_GET['object'] ?? null), $this->ONLINE_SKIP_PAGES)) {
                $this->STORE_ONLINE_USERS = false;
            }
            // Check CPU Load
            if (conf('HIGH_CPU_LOAD') == 1) {
                $this->STORE_ONLINE_USERS = false;
            }
        }
        // Delete expired users (expiration time for now 300 seconds == 5 minutes)
        // make this to run randomly every ~10th page call
        // TODO: this needed to be done on global cron rarely (every minute or every hour), not on every request
        if ($this->STORE_ONLINE_USERS && $this->ONLINE_AUTO_CLEAN && rand(1, 10) == 1) {
            db()->_add_shutdown_query('DELETE FROM ' . db('online') . ' WHERE (' . time() . '-time) > ' . (int) ($this->ONLINE_AUTO_CLEAN));
        }
        // Remember last query string to process it after succesful login
        if (empty($_SESSION[$this->VAR_USER_ID]) && isset($_GET['task']) && ($_GET['task'] ?? null) == 'login') {
            if (false === strpos(($_SERVER['HTTP_REFERER'] ?? ''), 'login')) {
                $_SESSION[$this->VAR_USER_GO_URL] = ($_SERVER['HTTP_REFERER'] ?? null);
            }
        }
        // Check for session IP
        if ($this->SESSION_LOCK_TO_IP && ! empty($_SESSION[$this->VAR_USER_ID]) && ($_GET['task'] ?? null) !== 'logout') {
            // User has changed IP, logout immediately
            $ip = common()->get_ip();
            if ( ! isset($_SESSION[$this->VAR_LOCK_IP]) || ($_SESSION[$this->VAR_LOCK_IP] ?? null) !== $ip) {
                trigger_error('AUTH: Attempt to use session with changed IP blocked, auth_ip:' . ($_SESSION[$this->VAR_LOCK_IP] ?? null) . ', new_ip:' . $ip . ', user_id: ' . (int) ($_SESSION[$this->VAR_USER_ID] ?? null), E_USER_WARNING);
                $this->_log_fail(['reason' => 'auth_blocked_by_ip']);
                $_GET['task'] = 'logout';
            }
        }
        // Check for session User Agent
        if ($this->SESSION_LOCK_TO_UA && ! empty($_SESSION[$this->VAR_USER_ID]) && ($_GET['task'] ?? null) !== 'logout') {
            // User has changed User Agent, logout immediately
            $ua = ($_SERVER['HTTP_USER_AGENT'] ?? null);
            if ( ! isset($_SESSION[$this->VAR_LOCK_UA]) || ($_SESSION[$this->VAR_LOCK_UA] ?? null) !== $ua) {
                trigger_error('AUTH: Attempt to use session with changed User Agent blocked, auth_ua:"' . ($_SESSION[$this->VAR_LOCK_UA] ?? null) . '", new_ua:"' . $ua . '", user_id: ' . (int) ($_SESSION[$this->VAR_USER_ID] ?? null), E_USER_WARNING);
                $this->_log_fail(['reason' => 'auth_blocked_by_ua']);
                $_GET['task'] = 'logout';
            }
        }
        // Check for session hostname
        if ($this->SESSION_LOCK_TO_HOST && ! empty($_SESSION[$this->VAR_USER_ID]) && ($_GET['task'] ?? null) !== 'logout') {
            // User has changed Host, logout immediately
            $host = $_SERVER['HTTP_HOST'];
            if ( ! isset($_SESSION[$this->VAR_LOCK_HOST]) || $_SESSION[$this->VAR_LOCK_HOST] !== $host) {
                trigger_error('AUTH: Attempt to use session with changed Host blocked, auth_host:"' . $_SESSION[$this->VAR_LOCK_HOST] . '", new_host:"' . $host . '", user_id: ' . (int) ($_SESSION[$this->VAR_USER_ID]), E_USER_WARNING);
                $this->_log_fail(['reason' => 'auth_blocked_by_host']);
                $_GET['task'] = 'logout';
            }
        }
        // Check referer matched to WEB_PATH
        $referer = ($_SERVER['HTTP_REFERER'] ?? null);
        if ($this->SESSION_REFERER_CHECK && ( ! $referer || substr($referer, 0, strlen(WEB_PATH)) != WEB_PATH) && ($_GET['task'] ?? null) !== 'logout') {
            trigger_error('AUTH: Referer not matched and session blocked, referer:' . $referer, E_USER_WARNING);
            $this->_log_fail(['reason' => 'auth_blocked_by_referer']);
            $_GET['task'] = 'logout';
        }
        // This code needed to auto-logout previously logged-in user, but disabled later
        if ($this->FORCE_LOGOUT_INACTIVE && ($_SESSION[$this->VAR_USER_ID] ?? null) && ($_GET['task'] ?? null) !== 'logout') {
            $main = main();
            $main->_init_cur_user_info($main);
            $user = &$main->_user_info;
            if (isset($user['id']) && ! $user['active']) {
                trigger_error('AUTH: Force logout inactive account', E_USER_WARNING);
                $this->_log_fail(['reason' => 'auth_logout_inactive']);
                $_GET['task'] = 'logout';
            }
        }
        // Switch between login/logout actions
        if (isset($_GET['task']) && ($_GET['task'] ?? null) === 'logout') {
            return $this->_do_logout();
        }
        if ( ! empty($_COOKIE[$this->VAR_COOKIE_NAME]) && empty($_SESSION[$this->VAR_USER_ID])) {
            $this->_process_cookie();
        }
        if (isset($_GET['task']) && ($_GET['task'] ?? null) == 'login') {
            if (($_GET['id'] ?? null) && strlen(($_GET['id'] ?? '')) > 16 && ! is_numeric($_GET['id'] ?? null)) {
                $this->_do_login_with_encrypted();
            }
            if (empty($_SESSION[$this->VAR_USER_ID])) {
                $this->_do_login([
                    'login' => $_POST[$this->LOGIN_FIELD] ?? '',
                    'pswd' => $_POST[$this->PSWD_FIELD] ?? '',
                ]);
            }
        }
        // Check if current user session has expired
        if ($this->STORE_ONLINE_USERS) {
            $online_users = &main()->_online_users;
            if ( ! isset($online_users)) {
                $online_users = [];
                $sql = 'SELECT id, user_id FROM ' . db('online') . (MAIN_TYPE_USER ? ' WHERE type != "admin"' : '') . ($this->ONLINE_MAX_IDS ? ' LIMIT ' . (int) ($this->ONLINE_MAX_IDS) : '');
                foreach ((array) db()->get_2d($sql) as $online_id => $user_id) {
                    $online_users[$online_id] = $user_id;
                }
            }
            // Create default record if not exists one
            if ( ! isset($online_users[session_id()])) {
                $_cur_user_data = $this->_update_online_info();
                $online_users[session_id()] = $_cur_user_data['user_id'];
            }
        }
        if ($this->TRACK_GEO_LOCATION && main()->USE_GEO_IP) {
            $this->_track_geo_location();
        }
        if ($this->CATCH_REF_CODES) {
            $this->_catch_ref_codes();
        }
        // Try to assign first page of the site (if $_GET['object'] is empty)
        if (empty($_GET['object'])) {
            $go = defined('SITE_DEFAULT_PAGE') ? SITE_DEFAULT_PAGE : conf('site_first_page');
            // Check if default url is not empty and then use it
            if ( ! empty($go)) {
                $go = str_replace(['./?', './'], '', $go);
                $tmp_array = [];
                parse_str($go, $tmp_array);
                foreach ((array) $tmp_array as $k => $v) {
                    $_GET[$k] = $v;
                }
            }
        }
        if (empty($_GET['object'])) {
            $_GET['object'] = $this->DEF_USER_MODULE;
        }
        // Store user inside the main module
        if ( ! empty($_SESSION[$this->VAR_USER_ID]) && ! empty($_SESSION[$this->VAR_USER_GROUP_ID])) {
            main()->USER_ID = ($_SESSION[$this->VAR_USER_ID] ?? null);
            main()->USER_GROUP = ($_SESSION[$this->VAR_USER_GROUP_ID] ?? null);
            if ($this->USER_INFO_IN_SESSION && ! empty($_SESSION[$this->VAR_USER_INFO])) {
                main()->USER_INFO = ($_SESSION[$this->VAR_USER_INFO] ?? null);
            }
        }
        $this->_exec_method_on_action('init');
    }

login_as

public function login_as($id)
    public function login_as($id)
    {
        if ( ! $id) {
            return _e('Wrong id');
        }
        $a = db()->from('user')->whereid($id)->get();
        if ( ! $a) {
            return _e('Target user not found');
        }
        $t = time();
        $secret_key = db()->get_one('SELECT MD5(CONCAT(`password`, "' . str_replace(['http://', 'https://'], '//', INCLUDE_PATH) . '")) FROM ' . db('admin') . ' WHERE id=1');
        $to_encode = 'userid-' . $a['id'] . '-' . $t . '-' . md5($a['password']);
        $integrity_hash = md5($to_encode);
        $encrypted = _class('encryption')->_safe_encrypt_with_base64($to_encode . '-' . $integrity_hash, $secret_key);
        if (tpl()->REWRITE_MODE) {
            $url = url_user(['task' => 'login', 'id' => $encrypted], parse_url(WEB_PATH, PHP_URL_HOST));
        } else {
            $url = WEB_PATH . '?task=login&id=' . $encrypted;
        }
        return js_redirect($url, $rewrite = false);
    }

Methods starting with _ hooks and helpers; not routable as page actions

__construct

public function __construct()
    public function __construct()
    {
        $cookie_life_time = conf('cookie_life_time');
        if (isset($cookie_life_time)) {
            $this->VAR_COOKIE_LIFE_TIME = 86400 * $cookie_life_time;
        }
        $online_auto_clean = conf('online_auto_clean');
        if (isset($online_auto_clean)) {
            $this->ONLINE_AUTO_CLEAN = $online_auto_clean;
        }
        if (defined('DEFAULT_USER_MODULE')) {
            $this->DEF_USER_MODULE = DEFAULT_USER_MODULE;
        }
        if (empty($this->COOKIE_PATH)) {
            $url_parts = @parse_url(WEB_PATH);
            $this->COOKIE_PATH = $url_parts['path'];
        }
    }
public function _catch_ref_codes()

Ref codes.

    public function _catch_ref_codes()
    {
        if ( ! $this->CATCH_REF_CODES) {
            return false;
        }
        $COOKIE_VAR_NAME = 'ref_code';
        $where = ($_SERVER['REQUEST_URI'] ?? null);
        $ref_code = '';
        if ( ! empty($where) && preg_match('/\?r=([0-9]{1,8})$/ims', $where, $m)) {
            $ref_code = $m[1];
        }
        if (empty($ref_code)) {
            return false;
        }
        $this->_cookie_set($COOKIE_VAR_NAME, $ref_code, time() + 86400 * 30);
        // Return user to the initial page without ref_code
        $location = ((($_SERVER['HTTPS'] ?? null) || ($_SERVER['SSL_PROTOCOL'] ?? null)) ? 'https://' : 'http://') . ($_SERVER['HTTP_HOST'] ?? null) . substr($where, 0, -strlen('?r=' . $ref_code));
        common()->redirect($location, false, 'http', '', 0);
        exit();
    }
public function _check_multi_accounts()

Multiple accounts checker.

    public function _check_multi_accounts()
    {
        $last_id = null;
        if (empty($_SESSION[$this->VAR_USER_ID]) || empty($_SESSION[$this->VAR_USER_GROUP_ID])) {
            return false;
        }
        $_SPECIAL_NAME = 'accounts';
        if (empty($_COOKIE[$_SPECIAL_NAME])) {
            return $this->_set_special_cookie($_SESSION[$this->VAR_USER_ID] ?? null);
        }
        $last_id = null;
        $cookie_users = [];
        foreach (explode('_', ($_COOKIE[$_SPECIAL_NAME] ?? '')) as $_user_id) {
            $cookie_users[$_user_id] = $_user_id;
        }
        if (($_COOKIE[$_SPECIAL_NAME] ?? null) == ($_SESSION[$this->VAR_USER_ID] ?? null)
            || (count((array) $cookie_users) > 1 && isset($cookie_users[$_SESSION[$this->VAR_USER_ID]]))
        ) {
            return false;
        }
        $data = db()->query_fetch('SELECT * FROM ' . db('check_multi_accounts') . ' WHERE user_id=' . (int) ($_SESSION[$this->VAR_USER_ID] ?? null));
        if (isset($cookie_users[$_SESSION[$this->VAR_USER_ID]])) {
            unset($cookie_users[$_SESSION[$this->VAR_USER_ID]]);
        }
        $matching_users = implode(',', $cookie_users);
        if (empty($data)) {
            db()->insert_safe('check_multi_accounts', [
                'user_id' => (int) ($_SESSION[$this->VAR_USER_ID] ?? null),
                'matching_users' => $matching_users,
                'last_update' => time(),
                'cookie_match' => 1,
            ]);
        } else {
            db()->update_safe('check_multi_accounts', [
                'matching_users' => $matching_users,
                'last_update' => time(),
                'cookie_match' => 1,
            ], 'user_id=' . (int) ($_SESSION[$this->VAR_USER_ID] ?? null));
        }
        $this->_set_special_cookie(($_COOKIE[$_SPECIAL_NAME] ?? null) . '_' . ($_SESSION[$this->VAR_USER_ID] ?? null));
        main()->_HAS_MULTI_ACCOUNTS = true;
        _debug_log('_check_multi_accounts: found possible multi-account, old_id=' . $last_id . ',new_id=' . ($_SESSION[$this->VAR_USER_ID] ?? null) . ',cookie=' . ($_COOKIE[$_SPECIAL_NAME] ?? null), E_NOTICE);
    }
public function _check_password($plain, $stored)

The stored form is detected by yf_password (md5/sha1/sha256 digests of pass . salt, password_hash() strings); the legacy md5(pass . USER_PASSWORD_SALT) and, while ALLOW_PLAINTEXT_PASSWORDS, the plain value stay accepted.

    public function _check_password($plain, $stored)
    {
        return _class('password')->verify($plain, $stored, [
            'salts' => [$this->USER_PASSWORD_SALT],
            'plain' => $this->ALLOW_PLAINTEXT_PASSWORDS,
        ]);
    }

_do_login

public function _do_login($params = [])

Try to log in user.

    public function _do_login($params = [])
    {
        $AUTH_LOGIN = trim($params['login']);
        $AUTH_PSWD = trim($params['pswd']);

        $cur_ip = common()->get_ip();

        if ($this->AUTH_ONLY_HTTPS && ! (($_SERVER['HTTPS'] ?? null) || ($_SERVER['SSL_PROTOCOL'] ?? null))) {
            $redirect_url = '';
            if ($_SERVER['HTTP_REFERER'] ?? null) {
                $redirect_url = str_replace('http://', 'https://', $_SERVER['HTTP_REFERER']);
            }
            if ( ! $redirect_url) {
                $request_uri = getenv('REQUEST_URI');
                $cur_web_path = $request_uri[strlen($request_uri) - 1] == '/' ? substr($request_uri, 0, -1) : dirname($request_uri);
                $redirect_url = 'https://' . getenv('HTTP_HOST') . str_replace(['\\', '//'], ['/', '/'], (MAIN_TYPE_ADMIN ? dirname($cur_web_path) : $cur_web_path) . '/');
            }
            if ($params['no_redirect']) {
                return false;
            }
            return js_redirect($redirect_url);
        }
        $fail_reason = false;
        $user_info = null;
        if ( ! empty($AUTH_LOGIN) && ! empty($AUTH_PSWD)) {
            $NEED_QUERY_DB = true;
            if ($this->BLOCK_BANNED_IPS && common()->_ip_is_banned()) {
                $fail_reason = 'ip_blocked';
                $this->_log_fail([
                    'login' => $AUTH_LOGIN,
                    'pswd' => $AUTH_PSWD,
                    'reason' => $fail_reason,
                ]);
                $msg = 'Attempt to login from banned IP (' . _prepare_html($cur_ip) . ') as "' . _prepare_html($AUTH_LOGIN) . '" blocked';
                common()->message_error($msg);
                trigger_error('AUTH: ' . $msg, E_USER_WARNING);
                return js_redirect($this->URL_WRONG_LOGIN);
            }
            if ($this->BLOCK_FAILED_LOGINS) {
                $min_time = time() - $this->BLOCK_FAILED_TTL;
                $fails_by_login = (int) db()->get_one('SELECT COUNT(*) FROM ' . db('log_auth_fails') . ' WHERE time > ' . $min_time . ' AND login="' . _es($AUTH_LOGIN) . '"');
                if ($fails_by_login >= $this->BLOCK_FAILS_BY_LOGIN_COUNT) {
                    $fail_reason = 'blocked_fails_by_login';
                    $msg = 'Attempt to login as "' . _prepare_html($AUTH_LOGIN) . '" blocked';
                    common()->message_error($msg);
                    trigger_error('AUTH: ' . $msg . ', fails_by_login: ' . $fails_by_login, E_USER_WARNING);
                } else {
                    $fails_by_ip = (int) db()->get_one('SELECT COUNT(*) FROM ' . db('log_auth_fails') . ' WHERE time > ' . $min_time . ' AND ip="' . _es($cur_ip) . '"');
                    if ($fails_by_ip >= $this->BLOCK_FAILS_BY_IP_COUNT) {
                        $fail_reason = 'blocked_fails_by_ip';
                        $msg = 'Attempt to login as "' . _prepare_html($AUTH_LOGIN) . '" blocked';
                        common()->message_error($msg);
                        trigger_error('AUTH: ' . $msg . ', fails_by_ip: ' . $fails_by_ip, E_USER_WARNING);
                    }
                }
            }
            if ( ! $fail_reason) {
                $PSWD_OK = false;
                $user_info = $this->_get_user_info($AUTH_LOGIN);
                $stored = is_array($user_info) ? (string) ($user_info['password'] ?? '') : '';
                if ( ! empty($params['stored_match'])) {
                    // the remember-me cookie carries the stored value itself (REMEMBER_ME_TOKENS replaces it with a server-side token)
                    $PSWD_OK = $stored !== '' && hash_equals($stored, $AUTH_PSWD);
                } else {
                    $check = $this->_check_password($AUTH_PSWD, $stored);
                    $PSWD_OK = $check['ok'];
                    if ($PSWD_OK && $check['rehash'] && ! empty($user_info['id'])) {
                        $new_hash = _class('password')->rehash('user', $user_info['id'], $stored, $AUTH_PSWD);
                        if ($new_hash) {
                            // the remember-me cookie built below must carry the value now stored
                            $user_info['password'] = $new_hash;
                        }
                    }
                }
                if ( ! $PSWD_OK) {
                    $fail_reason = 'wrong_login';
                }
            }
            if ( ! $fail_reason && $this->USER_SECURITY_CHECKS) {
                $fail_reason = $this->_user_security_checks($user_info['id']);
            }
            if ($fail_reason) {
                $user_info = null;
                $this->_log_fail([
                    'login' => $AUTH_LOGIN,
                    'pswd' => $AUTH_PSWD,
                    'reason' => $fail_reason,
                ]);
            }
        }
        return $this->_save_login_in_session($user_info, $params['no_redirect']);
    }
public function _do_login_with_encrypted()

Try to log in user with encrypted string (used to quickly login admin as common user).

    public function _do_login_with_encrypted()
    {
        header('X-Robots-Tag: noindex,nofollow,noarchive,nosnippet');
        if ( ! ($_GET['id'] ?? null) || strlen(($_GET['id'] ?? '')) < 16 || is_numeric($_GET['id'] ?? null)) {
            $this->_encrypted_error = 'GET_id is not like an encrypted string';
            return false;
        }
        $secret_key = db()->get_one('SELECT MD5(CONCAT(`password`, "' . str_replace(['http://', 'https://'], '//', INCLUDE_PATH) . '")) FROM ' . db('admin') . ' WHERE id=1');
        if ( ! $secret_key) {
            $this->_encrypted_error = 'secret key generation failed';
            return false;
        }
        // Should contain this: 'userid-%id-%time-%md5';
        $decrypted = _class('encryption')->_safe_decrypt_with_base64(($_GET['id'] ?? null), $secret_key);
        if ( ! $decrypted || ! strlen($decrypted) || substr($decrypted, 0, strlen('userid-')) != 'userid-') {
            $this->_encrypted_error = 'decryption failed, possibly broken string or hack attempt';
            return false;
        }
        $d = explode('-', $decrypted);
        if (count((array) $d) != 5) {
            $this->_encrypted_error = 'decrypted string is not a valid array';
            return false;
        }
        if (md5($d[0] . '-' . $d[1] . '-' . $d[2] . '-' . $d[3]) != $d[4]) {
            $this->_encrypted_error = 'md5 hash not matches';
            return false;
        }
        $user_id = (int) ($d[1]);
        $time = (int) ($d[2]);
        $pswd_hash = strtolower(trim($d[3]));
        if ( ! $user_id || ! $time) {
            $this->_encrypted_error = 'wrong user_id or time';
            return false;
        }
        if ( ! preg_match('~^[a-z0-9]{32}$~', $pswd_hash)) {
            $this->_encrypted_error = 'wrong pswd_hash';
            return false;
        }
        // Allowing only 6 hours for link to keep alive
        if ($time < (time() - 3600 * 6)) {
            $this->_encrypted_error = 'time elapsed';
            return false;
        }
        $user_info = db()->get('SELECT *, MD5(`password`) AS md5_pswd FROM ' . db('user') . ' WHERE id=' . (int) $user_id);
        if ( ! $user_info || $user_info['id'] != $user_id) {
            $this->_encrypted_error = 'user not found';
            return false;
        }
        if ( ! $user_info['md5_pswd'] || $user_info['md5_pswd'] != $pswd_hash) {
            $this->_encrypted_error = 'user pswd_hash not matched';
            return false;
        }
        $this->INSIDE_LOGIN_FROM_ADMIN = true;
        return $this->_save_login_in_session($user_info);
    }

_do_logout

public function _do_logout($no_redirect = false)

Do log out user.

    public function _do_logout($no_redirect = false)
    {
        if ($this->STORE_ONLINE_USERS) {
            db()->_add_shutdown_query('DELETE FROM ' . db('online') . ' WHERE user_id=' . (int) ($_SESSION[$this->VAR_USER_ID] ?? null));
            $MAIN = &main();
            if (isset($MAIN->_online_users[session_id()])) {
                unset($MAIN->_online_users[session_id()]);
            }
        }
        $this->_exec_method_on_action('logout');
        if ($this->REMEMBER_ME_TOKENS) {
            $this->_remember_revoke(is_string($_COOKIE[$this->VAR_COOKIE_NAME] ?? null) ? $_COOKIE[$this->VAR_COOKIE_NAME] : '');
        }
        $user_session_vars = [
            $this->VAR_USER_ID,
            $this->VAR_USER_GROUP_ID,
            $this->VAR_USER_LOGIN_TIME,
            $this->VAR_LOCK_IP,
            $this->VAR_LOCK_UA,
            $this->VAR_LOCK_HOST,
        ];
        foreach ((array) $_SESSION as $k => $v) {
            if (in_array($k, $user_session_vars)) {
                unset($_SESSION[$k]);
            }
        }
        $main = main();
        $main->_init_cur_user_info($main);
        $main->USER_INFO = &$main->_user_info;
        $main->_LOGGED_IN_USER_INFO = &$main->_user_info;

        $this->_cleanup_cookie();
        session_regenerate_id($destroy = true);

        if ($no_redirect) {
            return false;
        }
        js_redirect($this->URL_AFTER_LOGOUT);
    }
public function _exec_method_on_action($action = 'login')

Execute user method after specified action.

    public function _exec_method_on_action($action = 'login')
    {
        if ($action == 'login') {
            $callbacks = $this->EXEC_AFTER_LOGIN;
        } elseif ($action == 'logout') {
            $callbacks = $this->EXEC_AFTER_LOGOUT;
        } elseif ($action == 'init') {
            $callbacks = $this->EXEC_AFTER_INIT;
        }
        if (empty($callbacks) || ! is_array($callbacks)) {
            return false;
        }
        foreach ((array) $callbacks as $callback) {
            if (is_callable($callback)) {
                call_user_func_array($callback, ['self' => $this]);
            }
        }
    }
public function _get_user_info($login = '')
    public function _get_user_info($login = '')
    {
        if (empty($login)) {
            return false;
        }
        $sql = 'SELECT * FROM ' . db('user') . ' WHERE ' . db()->escape_key($this->LOGIN_FIELD) . '="' . db()->es($login) . '" ';
        $login_aliases = $this->LOGIN_ALIAS;
        if ($login_aliases) {
            if ( ! is_array($login_aliases)) {
                $login_aliases = [$login_aliases];
            }
            $alias_sql = [];
            foreach ((array) $login_aliases as $alias) {
                $alias = trim($alias);
                if (preg_match('/^[a-z0-9_]+$/ims', $alias)) {
                    $alias_sql[] = db()->escape_key($alias) . '="' . db()->es($login) . '"';
                }
            }
            if ($alias_sql) {
                $sql .= ' OR ' . implode(' OR ', $alias_sql);
            }
        }
        return db()->get($sql);
    }
public function _is_user_hidden($user_info = [])
    public function _is_user_hidden($user_info = [])
    {
        if (empty($user_info)) {
            return true;
        }
        if (empty($user_info['hidden'])) {
            return true;
        }
        return false;
    }
public function _is_user_inactive($user_info = [])
    public function _is_user_inactive($user_info = [])
    {
        if (empty($user_info)) {
            return true;
        }
        if (empty($user_info['active'])) {
            return true;
        }
        return false;
    }

_log_fail

public function _log_fail($data = [])
    public function _log_fail($data = [])
    {
        if ( ! $this->LOG_FAILED_LOGINS) {
            return false;
        }
        return db()->insert_safe('log_auth_fails', [
            'time'         => str_replace(',', '.', microtime(true)),
            'ip'           => common()->get_ip(),
            'user_id'      => ($data['user_id'] ?? null) ?: ($_SESSION[$this->VAR_USER_ID] ?? null),
            'login'        => $data['login'],
            'pswd'         => $this->LOG_FAILED_PSWD ? $data['pswd'] : '',
            'reason'       => $data['reason'],
            'host'         => ($_SERVER['HTTP_HOST'] ?? null),
            'user_agent'   => ($_SERVER['HTTP_USER_AGENT'] ?? null),
            'referer'      => ($_SERVER['HTTP_REFERER'] ?? null),
            'query_string' => ($_SERVER['QUERY_STRING'] ?? null),
            'site_id'      => (int) conf('SITE_ID'),
            'server_id'    => (int) conf('SERVER_ID'),
        ]);
    }
public function _remember_consume($cookie)

Log the user in from a valid token cookie and rotate the validator; false (and the cookie dropped by the caller) otherwise.

    public function _remember_consume($cookie)
    {
        $parts = $this->_remember_parts($cookie);
        if ( ! $parts) {
            return false;
        }
        list($selector, $validator) = $parts;
        $row = db()->get('SELECT * FROM ' . db('user_remember_tokens') . ' WHERE selector = "' . _es($selector) . '"', $use_cache = false);
        if (empty($row['id'])) {
            return false;
        }
        if ((int) $row['expires'] < time()) {
            db()->query('DELETE FROM ' . db('user_remember_tokens') . ' WHERE id = ' . (int) $row['id']);
            return false;
        }
        $hash = $this->_remember_hash($validator);
        $current = hash_equals((string) $row['token_hash'], $hash);
        // a parallel request (second tab) may still carry the validator rotated a moment ago
        $recent = (string) $row['prev_token_hash'] !== '' && hash_equals((string) $row['prev_token_hash'], $hash)
            && (int) $row['rotated'] >= time() - $this->REMEMBER_ROTATE_GRACE;
        if ( ! $current && ! $recent) {
            // neither the current nor the just-rotated validator: the cookie was copied, drop the row
            db()->query('DELETE FROM ' . db('user_remember_tokens') . ' WHERE id = ' . (int) $row['id']);
            return false;
        }
        $user_info = db()->get('SELECT * FROM ' . db('user') . ' WHERE id = ' . (int) $row['user_id'], $use_cache = false);
        if (empty($user_info['id'])) {
            return false;
        }
        if ($current) {
            // rotate: same selector, new validator, fresh expiry; the old one stays valid for the grace window
            $new_validator = bin2hex(random_bytes(32));
            $expires = time() + $this->VAR_COOKIE_LIFE_TIME;
            db()->update('user_remember_tokens', db()->es_for_builder([
                'token_hash' => $this->_remember_hash($new_validator),
                'prev_token_hash' => $row['token_hash'],
                'rotated' => time(),
                'expires' => $expires,
            ]), (int) $row['id']);
            $this->_cookie_set($this->VAR_COOKIE_NAME, $this->_remember_cookie_value($selector, $new_validator), $expires);
        }
        $_SESSION[$this->VAR_USER_GO_URL] = null;
        $this->_save_login_in_session($user_info, $no_redirect_on_success = true);
        return true;
    }
public function _remember_hash($validator)
    public function _remember_hash($validator)
    {
        return hash('sha256', (string) $validator);
    }
public function _remember_issue($user_id, $selector = null)

Cookie value 'r1.<selector>.<validator>': the selector finds the row, only sha256(validator) is stored.

    public function _remember_issue($user_id, $selector = null)
    {
        $user_id = (int) $user_id;
        if ( ! $user_id) {
            return false;
        }
        $selector = $selector ?: bin2hex(random_bytes(12));
        $validator = bin2hex(random_bytes(32));
        $expires = time() + $this->VAR_COOKIE_LIFE_TIME;
        // abandoned tokens of everybody go when a new one is issued
        db()->query('DELETE FROM ' . db('user_remember_tokens') . ' WHERE expires < ' . time());
        $row = [
            'user_id' => $user_id,
            'selector' => $selector,
            'token_hash' => $this->_remember_hash($validator),
            'prev_token_hash' => '',
            'expires' => $expires,
            'created' => time(),
            'rotated' => time(),
        ];
        $ok = db()->insert('user_remember_tokens', db()->es_for_builder($row));
        if ( ! $ok) {
            return false;
        }
        $this->_cookie_set($this->VAR_COOKIE_NAME, $this->_remember_cookie_value($selector, $validator), $expires);
        return $selector;
    }
public function _remember_parts($cookie)

[selector, validator] from the cookie value, or false when it is not a token cookie.

    public function _remember_parts($cookie)
    {
        if ( ! is_string($cookie)) {
            return false;
        }
        if (strpos($cookie, $this->REMEMBER_COOKIE_PREFIX) !== 0) {
            return false;
        }
        $parts = explode('.', substr($cookie, strlen($this->REMEMBER_COOKIE_PREFIX)));
        if (count($parts) !== 2 || ! preg_match('~^[0-9a-f]{24}$~', $parts[0]) || ! preg_match('~^[0-9a-f]{64}$~', $parts[1])) {
            return false;
        }
        return $parts;
    }
public function _remember_revoke($cookie)
    public function _remember_revoke($cookie)
    {
        $parts = $this->_remember_parts($cookie);
        if ( ! $parts) {
            return false;
        }
        return (bool) db()->query('DELETE FROM ' . db('user_remember_tokens') . ' WHERE selector = "' . _es($parts[0]) . '"');
    }
public function _remember_revoke_user($user_id)

"Log out everywhere": after a password change or reset every remembered browser of the user has to log in again.

    public function _remember_revoke_user($user_id)
    {
        $user_id = (int) $user_id;
        if ( ! $user_id) {
            return false;
        }
        return (bool) db()->query('DELETE FROM ' . db('user_remember_tokens') . ' WHERE user_id = ' . $user_id);
    }
public function _save_login_in_session($user_info = [], $no_redirect_on_success = false)

Save auth information in session.

    public function _save_login_in_session($user_info = [], $no_redirect_on_success = false)
    {
        if (empty($user_info['id'])) {
            return js_redirect($this->URL_WRONG_LOGIN);
        } elseif ($this->_is_user_inactive($user_info)) {
            return js_redirect($this->URL_ACCOUNT_INACTIVE);
        }

        session_regenerate_id($destroy = true);
        $_SESSION[$this->VAR_USER_ID] = $user_info['id'];
        $_SESSION[$this->VAR_USER_GROUP_ID] = $user_info['group'];
        $_SESSION[$this->VAR_USER_LOGIN_TIME] = time();
        $_SESSION[$this->VAR_LOCK_IP] = common()->get_ip();
        $_SESSION[$this->VAR_LOCK_UA] = ($_SERVER['HTTP_USER_AGENT'] ?? null);
        $_SESSION[$this->VAR_LOCK_HOST] = ($_SERVER['HTTP_HOST'] ?? null);
        $main = main();
        $main->_init_cur_user_info($main);
        $main->USER_INFO = &$main->_user_info;
        $main->_LOGGED_IN_USER_INFO = &$main->_user_info;

        if ($this->DO_LOG_LOGINS) {
            _class('logs')->store_user_auth($user_info);
        }
        if ( ! empty($_POST[$this->REMEMBER_FIELD]) && $this->ALLOW_REMEMBER_ME) {
            if ($this->REMEMBER_ME_TOKENS) {
                $this->_remember_issue($user_info['id']);
            } else {
                $encrypted_string = _class('encryption')->_safe_encrypt_with_base64($user_info['id'] . '-' . $user_info[$this->LOGIN_FIELD] . '-' . $user_info[$this->PSWD_FIELD] . '-' . time());
                $this->_cookie_set($this->VAR_COOKIE_NAME, $encrypted_string, time() + $this->VAR_COOKIE_LIFE_TIME);
            }
        }
        // Set member id cookie expired on session end
        if ($this->SET_MEMBER_ID_COOKIE && preg_match('/^[a-z0-9_\-]+$/ims', $this->SET_MEMBER_ID_COOKIE)) {
            $this->_cookie_set($this->SET_MEMBER_ID_COOKIE, $user_info['id']);
        }
        if ($this->SET_IS_LOGGED_COOKIE && preg_match('/^[a-z0-9_\-]+$/ims', $this->SET_IS_LOGGED_COOKIE)) {
            $this->_cookie_set($this->SET_IS_LOGGED_COOKIE, '1');
        }
        if ($this->CHECK_MULTI_ACCOUNTS) {
            $this->_check_multi_accounts();
        }
        $this->_update_user_info_on_login($user_info);
        $this->_update_online_info();
        $this->_exec_method_on_action('login');
        $group_info = [];
        if ( ! empty($user_info['group'])) {
            $groups = main()->get_data('user_groups_details');
            $group_info = $groups[$user_info['group']];
        }
        if (empty($no_redirect_on_success)) {
            $this->_success_login_redirect($user_info, $group_info);
        }
        if ($this->USER_INFO_IN_SESSION) {
            $_SESSION[$this->VAR_USER_INFO] = $user_info;
        }
        return true;
    }
public function _success_login_redirect($user_info = [], $group_info = [])
    public function _success_login_redirect($user_info = [], $group_info = [])
    {
        $REDIRECT_URL = null;
        if ($this->INSIDE_LOGIN_FROM_ADMIN) {
            return js_redirect('/');
            // Auto-redirect to the page before login form if needed
        } elseif ( ! empty($_SESSION[$this->VAR_USER_GO_URL]) && ! ($this->URL_SUCCESS_LOGIN && ($_POST['skip_auto_url'] ?? null))) {
            $REDIRECT_URL = (substr(($_SESSION[$this->VAR_USER_GO_URL] ?? ''), 0, 2) != './' ? './?' : '') . str_replace(WEB_PATH, '', str_replace(['http:', 'https:'], '', $_SESSION[$this->VAR_USER_GO_URL]));
            $_SESSION[$this->VAR_USER_GO_URL] = '';
        } elseif ( ! empty($user_info['go_after_login'])) {
            $REDIRECT_URL = $user_info['go_after_login'];
        } elseif ( ! empty($group_info['go_after_login'])) {
            $REDIRECT_URL = $group_info['go_after_login'];
            // Force redirect user to the default location
        } elseif ( ! empty($this->URL_SUCCESS_LOGIN)) {
            $REDIRECT_URL = $this->URL_SUCCESS_LOGIN;
        }
        if ($REDIRECT_URL) {
            js_redirect($REDIRECT_URL);
        }
    }
public function _track_geo_location()

Do save geo location for guests.

    public function _track_geo_location()
    {
        if ( ! $this->TRACK_GEO_LOCATION || ! main()->USE_GEO_IP) {
            return false;
        }
        if ( ! ($_SESSION[$this->VAR_USER_ID] ?? null) && main()->SPIDERS_DETECTION) {
            if (conf('SPIDER_NAME')) {
                return false;
            }
        }
        // Cleanup old-style geo location cookie
        $old_cookie_name = 'geo_location';
        if (isset($_COOKIE[$old_cookie_name])) {
            $this->_cookie_del($old_cookie_name);
            unset($_COOKIE[$old_cookie_name]);
        }
        $cur_ip = _es(common()->get_ip());
        // Try to get data from cookie (current user selection)
        $SEL_COOKIE_NAME = 'geo_selected';
        if (isset($_COOKIE[$SEL_COOKIE_NAME]) && ! empty($_COOKIE[$SEL_COOKIE_NAME])) {
            if ($this->GEO_COOKIE_JSON) {
                $geo_data = $this->_geo_cookie_decode($_COOKIE[$SEL_COOKIE_NAME]);
                $geo_data && $geo_data['_source'] = 'sel_cookie';
            } else {
                $geo_data = unserialize($_COOKIE[$SEL_COOKIE_NAME], ['allowed_classes' => false]);
                $geo_data['_source'] = 'sel_cookie';
            }
        }
        // Try to get data from nginx geoip module
        if (empty($geo_data) && ! empty($_SERVER['HTTP_X_GEO_CITY'])) {
            // Nginx currently not support lon/lat here:
            /*
                HTTP_X_GEO_CITY => 'Kiev',
                HTTP_X_GEO_REGION => '13',
                HTTP_X_GEO_COUNTRY
            */
        }
        // Try to get data from 'mod_geoip'
        if (empty($geo_data) && ! empty($_SERVER['GEOIP_LATITUDE'])) {
            $geo_data = [
                'country_code' => ($_SERVER['GEOIP_COUNTRY_CODE'] ?? null),
                'country_name' => ($_SERVER['GEOIP_COUNTRY_NAME'] ?? null),
                'region_code' => ($_SERVER['GEOIP_REGION'] ?? null),
                'city_name' => ($_SERVER['GEOIP_CITY'] ?? null),
                'dma_code' => ($_SERVER['GEOIP_DMA_CODE'] ?? null),
                'area_code' => ($_SERVER['GEOIP_AREA_CODE'] ?? null),
                'longitude' => ($_SERVER['GEOIP_LONGITUDE'] ?? null),
                'latitude' => ($_SERVER['GEOIP_LATITUDE'] ?? null),
            ];
            $geo_data['_source'] = 'mod_geoip';
        }
        // Try to get data from 'geo_city_location' table
        if (empty($geo_data)) {
            $geo_data = common()->_get_geo_data_from_db($cur_ip);
            $geo_data['_source'] = 'db_by_ip';
        }
        // Check data consistency
        if ( ! empty($geo_data['city_name']) && ! strlen($geo_data['region_code'])) {
            $geo_data['city_name'] = '';
        }
        if ( ! empty($geo_data['region_code']) && empty($geo_data['country_code'])) {
            $geo_data['region_code'] = '';
            $geo_data['region_name'] = '';
        }
        // Assign global array for use by all other code
        main()->_USER_GEO_DATA = $geo_data;
    }
public function _update_online_info()

Update online info.

    public function _update_online_info()
    {
        if ( ! $this->STORE_ONLINE_USERS) {
            return false;
        }
        if ( ! ($_SESSION[$this->VAR_USER_ID] ?? null) && main()->SPIDERS_DETECTION) {
            if (conf('SPIDER_NAME')) {
                return false;
            }
        }
        $data = [
            'id' => _es(session_id()),
            'user_id' => (int) ($_SESSION[$this->VAR_USER_ID] ?? null),
            'user_group' => (int) ($_SESSION[$this->VAR_USER_GROUP_ID] ?? null),
            'time' => time(),
            'type' => _es(MAIN_TYPE),
            'ip' => _es(common()->get_ip()),
            'user_agent' => _es($_SERVER['HTTP_USER_AGENT'] ?? null),
            'query_string' => _es($_SERVER['QUERY_STRING'] ?? null),
            'site_id' => (int) conf('SITE_ID'),
        ];
        $sql = db()->REPLACE('online', $data, 1);
        db()->_add_shutdown_query($sql);
        return $data;
    }
public function _update_user_info_on_login($user_info)
    public function _update_user_info_on_login($user_info)
    {
    }
public function _user_security_checks($user_id)
    public function _user_security_checks($user_id)
    {
        $user_id = (int) $user_id;
        if ( ! $this->USER_SECURITY_CHECKS || ! $user_id) {
            return false;
        }
        $fail_reason = false;

        $cur_ip = common()->get_ip();
        $cur_country = strtoupper(($_SERVER['GEOIP_COUNTRY_CODE'] ?? ''));

        $user_settings = db()->from('user_settings')->whereid($user_id, 'user_id')->get_2d('key, value');

        $fields_ip = ['ip_whitelist', 'ip_blacklist'];
        $fields_country = ['country_whitelist', 'country_blacklist'];
        foreach (array_merge($fields_ip, $fields_country) as $k) {
            if ( ! strlen(trim($user_settings[$k] ?? ''))) {
                continue;
            }
            $tmp = [];
            foreach (explode(';', trim($user_settings[$k])) as $v) {
                $v = trim($v);
                $tmp[$v] = $v;
            }
            $user_settings[$k] = $tmp;
        }

        // Whitelists have more priority over blacklists, IP checks have more priority over country checks
        $country_allow = null;
        if ( ! empty($user_settings['country_whitelist'])) {
            if (isset($user_settings['country_whitelist'][$cur_country])) {
                $country_allow = true;
            } else {
                $country_allow = false;
            }
        } elseif ( ! empty($user_settings['country_blacklist']) && $cur_country && isset($user_settings['country_blacklist'][$cur_country])) {
            $country_allow = false;
        }
        $ip_allow = null;
        if ( ! empty($user_settings['ip_whitelist'])) {
            if (isset($user_settings['ip_whitelist'][$cur_ip])) {
                $ip_allow = true;
            } else {
                $ip_allow = false;
            }
        } elseif ( ! empty($user_settings['ip_blacklist']) && $cur_ip && isset($user_settings['ip_blacklist'][$cur_ip])) {
            $ip_allow = false;
        }
        if ($ip_allow === false) {
            $fail_reason = 'block_ip_user_settings';
            $msg = 'Login from your IP (' . $cur_ip . ') is blocked by user security settings';
            common()->message_error($msg);
            trigger_error('AUTH: ' . $msg, E_USER_WARNING);
        } elseif ($country_allow === false && $ip_allow !== true) {
            $fail_reason = 'block_country_user_settings';
            $msg = 'Login from your country (' . $cur_country . ') is blocked by user security settings';
            common()->message_error($msg);
            trigger_error('AUTH: ' . $msg, E_USER_WARNING);
        }
        return $fail_reason;
    }