event) ) { $this->event = new kEvent('u:OnLogin'); } if ( !$password && !$remember_login_cookie ) { return LoginResult::INVALID_PASSWORD; } $object =& $this->getUserObject(); // process "Save Username" checkbox if ( $this->Application->isAdmin ) { $save_username = $this->Application->GetVar('cb_save_username') ? $username : ''; $this->Application->Session->SetCookie('save_username', $save_username, strtotime('+1 year')); // cookie will be set on next refresh, but refresh won't occur if // login error present, so duplicate cookie in kHTTPQuery $this->Application->SetVar('save_username', $save_username); } // logging in "root" (admin only) $super_admin = ($username == 'super-root') && $this->verifySuperAdmin(); if ( $this->Application->isAdmin && ($username == 'root') || ($super_admin && $username == 'super-root') ) { $password_formatter = $this->Application->recallObject('kPasswordFormatter'); /* @var $password_formatter kPasswordFormatter */ if ( !$password_formatter->checkPasswordFromSetting('RootPass', $password) ) { return LoginResult::INVALID_PASSWORD; } $user_id = USER_ROOT; $object->Clear($user_id); $object->SetDBField('Username', 'root'); if ( !$dry_run ) { $this->loginUserById($user_id, $remember_login_cookie); if ( $super_admin ) { $this->Application->StoreVar('super_admin', 1); } // reset counters $this->Application->resetCounters('UserSessions'); $this->_processLoginRedirect('root', $password); $this->_processInterfaceLanguage(); $this->_fixNextTemplate(); } return LoginResult::OK; } $user_id = $this->getUserId($username, $password, $remember_login_cookie); if ( $user_id ) { $object->Load($user_id); if ( !$this->checkBanRules($object) ) { return LoginResult::BANNED; } if ( $object->GetDBField('Status') == STATUS_ACTIVE ) { if ( !$this->checkLoginPermission() ) { return LoginResult::NO_PERMISSION; } if ( !$dry_run ) { $this->loginUserById($user_id, $remember_login_cookie); if ( $remember_login ) { // remember username & password when "Remember Login" checkbox us checked (when user is using login form on Front-End) $sql = 'SELECT MD5(Password) FROM ' . TABLE_PREFIX . 'Users WHERE PortalUserId = ' . $user_id; $remember_login_hash = $this->Conn->GetOne($sql); $this->Application->Session->SetCookie('remember_login', $username . '|' . $remember_login_hash, strtotime('+1 month')); } if ( !$remember_login_cookie ) { // reset counters $this->Application->resetCounters('UserSessions'); $this->_processLoginRedirect($username, $password); $this->_processInterfaceLanguage(); $this->_fixNextTemplate(); } } return LoginResult::OK; } else { $pending_template = $this->Application->GetVar('pending_disabled_template'); if ( $pending_template !== false && !$dry_run ) { // when user found, but it's not yet approved redirect hit to notification template $this->event->redirect = $pending_template; return LoginResult::OK; } else { // when no notification template given return an error return LoginResult::INVALID_PASSWORD; } } } if ( !$dry_run ) { $this->event->SetRedirectParam('pass', 'all'); // $this->event->SetRedirectParam('pass_category', 1); // to test } return LoginResult::INVALID_PASSWORD; } /** * Login user by it's id * * @param int $user_id * @param bool $remember_login_cookie */ function loginUserById($user_id, $remember_login_cookie = false) { $object =& $this->getUserObject(); $this->Application->removeObject($object->getPrefixSpecial()); $this->Application->StoreVar('user_id', $user_id); $this->Application->SetVar('u.current_id', $user_id); $this->Application->Session->SetField('PortalUserId', $user_id); if ($user_id != USER_ROOT) { $groups = $this->Application->RecallVar('UserGroups'); list ($first_group, ) = explode(',', $groups); $this->Application->Session->SetField('GroupId', $first_group); $this->Application->Session->SetField('GroupList', $groups); $this->Application->Session->SetField('TimeZone', $object->GetDBField('TimeZone')); } $this->Application->LoadPersistentVars(); if (!$remember_login_cookie) { // don't change last login time when auto-login is used $this_login = (int)$this->Application->RecallPersistentVar('ThisLogin'); $this->Application->StorePersistentVar('LastLogin', $this_login); $this->Application->StorePersistentVar('ThisLogin', time()); } $hook_event = new kEvent('u:OnAfterLogin'); $hook_event->MasterEvent = $this->event; $this->Application->HandleEvent($hook_event); } /** * Checks login permission * * @return bool */ function checkLoginPermission() { $object =& $this->getUserObject(); $ip_restrictions = $object->GetDBField('IPRestrictions'); if ( $ip_restrictions && !$this->Application->isDebugMode() && !kUtil::ipMatch($ip_restrictions, "\n") ) { return false; } $groups = $object->getMembershipGroups(true); if ( !$groups ) { $groups = Array (); } $default_group = $this->getUserTypeGroup(); if ( $default_group !== false ) { array_push($groups, $default_group); } // store groups, because kApplication::CheckPermission will use them! array_push($groups, $this->Application->ConfigValue('User_LoggedInGroup')); $groups = array_unique($groups); $this->Application->StoreVar('UserGroups', implode(',', $groups), true); // true for optional return $this->Application->CheckPermission($this->Application->isAdmin ? 'ADMIN' : 'LOGIN', 1); } /** * Returns default user group for it's type * * @return bool|string * @access protected */ protected function getUserTypeGroup() { $group_id = false; $object =& $this->getUserObject(); if ( $object->GetDBField('UserType') == UserType::USER ) { $group_id = $this->Application->ConfigValue('User_NewGroup'); } elseif ( $object->GetDBField('UserType') == UserType::ADMIN ) { $group_id = $this->Application->ConfigValue('User_AdminGroup'); } $ip_restrictions = $this->getGroupsWithIPRestrictions(); if ( !isset($ip_restrictions[$group_id]) || kUtil::ipMatch($ip_restrictions[$group_id], "\n") ) { return $group_id; } return false; } /** * Returns groups with IP restrictions * * @return Array * @access public */ public function getGroupsWithIPRestrictions() { static $cache = null; if ( $this->Application->isDebugMode() ) { return Array (); } if ( !isset($cache) ) { $sql = 'SELECT IPRestrictions, GroupId FROM ' . TABLE_PREFIX . 'UserGroups WHERE IPRestrictions IS NOT NULL'; $cache = $this->Conn->GetCol($sql, 'GroupId'); } return $cache; } /** * Performs user logout * */ function logoutUser() { if (!isset($this->event)) { $this->event = new kEvent('u:OnLogout'); } $hook_event = new kEvent('u:OnBeforeLogout'); $hook_event->MasterEvent = $this->event; $this->Application->HandleEvent($hook_event); $this->_processLoginRedirect(); $user_id = USER_GUEST; $this->Application->SetVar('u.current_id', $user_id); $object = $this->Application->recallObject('u.current', null, Array('skip_autoload' => true)); /* @var $object UsersItem */ $object->Load($user_id); $this->Application->DestroySession(); $this->Application->StoreVar('user_id', $user_id, true); $this->Application->Session->SetField('PortalUserId', $user_id); $group_list = $this->Application->ConfigValue('User_GuestGroup') . ',' . $this->Application->ConfigValue('User_LoggedInGroup'); $this->Application->StoreVar('UserGroups', $group_list, true); $this->Application->Session->SetField('GroupList', $group_list); if ($this->Application->ConfigValue('UseJSRedirect')) { $this->event->SetRedirectParam('js_redirect', 1); } $this->Application->resetCounters('UserSessions'); $this->Application->Session->SetCookie('remember_login', '', strtotime('-1 hour')); // don't pass user prefix on logout, since resulting url will have broken "env" $this->event->SetRedirectParam('pass', MOD_REWRITE ? 'm' : 'all'); $this->_fixNextTemplate(); } /** * Returns user id based on given criteria * * @param string $username * @param string $password * @param string $remember_login_cookie * @return int */ function getUserId($username, $password, $remember_login_cookie) { if ( $remember_login_cookie ) { list ($username, $password) = explode('|', $remember_login_cookie); // 0 - username, 1 - md5(password_hash) } $sql = 'SELECT PortalUserId, Password, PasswordHashingMethod FROM ' . TABLE_PREFIX . 'Users WHERE Email = %1$s OR Username = %1$s'; $user_info = $this->Conn->GetRow(sprintf($sql, $this->Conn->qstr($username))); if ( $user_info ) { if ( $remember_login_cookie ) { return md5($user_info['Password']) == $password; } else { $password_formatter = $this->Application->recallObject('kPasswordFormatter'); /* @var $password_formatter kPasswordFormatter */ $hashing_method = $user_info['PasswordHashingMethod']; if ( $password_formatter->checkPassword($password, $user_info['Password'], $hashing_method) ) { if ( $hashing_method != PasswordHashingMethod::PHPPASS ) { $this->_fixUserPassword($user_info['PortalUserId'], $password); } return $user_info['PortalUserId']; } } } return false; } /** * Apply new password hashing to given user's password * * @param int $user_id * @param string $password * @return void * @access protected */ protected function _fixUserPassword($user_id, $password) { $password_formatter = $this->Application->recallObject('kPasswordFormatter'); /* @var $password_formatter kPasswordFormatter */ $fields_hash = Array ( 'Password' => $password_formatter->hashPassword($password), 'PasswordHashingMethod' => PasswordHashingMethod::PHPPASS, ); $this->Conn->doUpdate($fields_hash, TABLE_PREFIX . 'Users', 'PortalUserId = ' . $user_id); } /** * Process all required data and redirect logged-in user * * @param string $username * @param string $password * @return void */ protected function _processLoginRedirect($username = null, $password = null) { // set next template $next_template = $this->Application->GetVar('next_template'); if ( $next_template ) { $this->event->redirect = $next_template; } // process IIS redirect if ( $this->Application->ConfigValue('UseJSRedirect') ) { $this->event->SetRedirectParam('js_redirect', 1); } // synchronize login $sync_manager = $this->Application->recallObject('UsersSyncronizeManager', null, Array (), Array ('InPortalSyncronize')); /* @var $sync_manager UsersSyncronizeManager */ if ( isset($username) && isset($password) ) { $sync_manager->performAction('LoginUser', $username, $password); } else { $sync_manager->performAction('LogoutUser'); } } /** * Sets correct interface language after successful login, based on user settings * * @return void * @access protected */ protected function _processInterfaceLanguage() { if ( defined('IS_INSTALL') && IS_INSTALL ) { $this->event->SetRedirectParam('m_lang', 1); // data $this->Application->Session->SetField('Language', 1); // interface return; } $language_field = $this->Application->isAdmin ? 'AdminLanguage' : 'FrontLanguage'; $primary_language_field = $this->Application->isAdmin ? 'AdminInterfaceLang' : 'PrimaryLang'; $is_root = $this->Application->RecallVar('user_id') == USER_ROOT; $object =& $this->getUserObject(); $user_language_id = $is_root ? $this->Application->RecallPersistentVar($language_field) : $object->GetDBField($language_field); $sql = 'SELECT LanguageId, IF(LanguageId = ' . (int)$user_language_id . ', 2, ' . $primary_language_field . ') AS SortKey FROM ' . TABLE_PREFIX . 'Languages WHERE Enabled = 1 HAVING SortKey <> 0 ORDER BY SortKey DESC'; $language_info = $this->Conn->GetRow($sql); $language_id = $language_info && $language_info['LanguageId'] ? $language_info['LanguageId'] : $user_language_id; if ( $user_language_id != $language_id ) { // first login OR language was deleted or disabled if ( $is_root ) { $this->Application->StorePersistentVar($language_field, $language_id); } else { $object->SetDBField($language_field, $language_id); $object->Update(); } } // set language for Admin Console & Front-End with disabled Mod-Rewrite $this->event->SetRedirectParam('m_lang', $language_id); // data $this->Application->Session->SetField('Language', $language_id); // interface } /** * Injects redirect params into next template, which doesn't happen if next template starts with "external:" * * @return void * @access protected */ protected function _fixNextTemplate() { if ( !MOD_REWRITE || !is_object($this->event) ) { return; } // solve problem, when template is "true" instead of actual template name $template = is_string($this->event->redirect) ? $this->event->redirect : ''; $url = $this->Application->HREF($template, '', $this->event->getRedirectParams(), $this->event->redirectScript); $vars = $this->Application->parseRewriteUrl($url, 'pass'); unset($vars['login'], $vars['logout']); // merge back url params, because they were ignored if this was "external:" url $vars = array_merge($vars, $this->getRedirectParams($vars['pass'], 'pass')); $template = $vars['t']; unset($vars['is_virtual'], $vars['t']); $this->event->redirect = $template; $this->event->setRedirectParams($vars, false); } /** * Returns current event redirect params with given $prefixes injected into 'pass'. * * @param array $prefixes List of prefixes to inject. * @param string $pass_name Name of array key in redirect params, containing comma-separated prefixes list. * * @return string * @access protected */ protected function getRedirectParams($prefixes, $pass_name = 'passed') { $redirect_params = $this->event->getRedirectParams(); if ( isset($redirect_params[$pass_name]) ) { $redirect_prefixes = explode(',', $redirect_params[$pass_name]); $prefixes = array_unique(array_merge($prefixes, $redirect_prefixes)); } $redirect_params[$pass_name] = implode(',', $prefixes); return $redirect_params; } /** * Checks that user is allowed to use super admin mode * * @return bool */ function verifySuperAdmin() { $sa_mode = kUtil::ipMatch(defined('SA_IP') ? SA_IP : ''); return $sa_mode || $this->Application->isDebugMode(); } /** * Returns user object, used during login processing * * @return UsersItem * @access public */ public function &getUserObject() { $prefix_special = $this->Application->isAdmin ? 'u.current' : 'u'; // "u" used on front not to change theme $object = $this->Application->recallObject($prefix_special, null, Array('skip_autoload' => true)); /* @var $object UsersItem */ return $object; } /** * Checks, if given user fields matches at least one of defined ban rules * * @param kDBItem $object * @return bool */ function checkBanRules(&$object) { $table = $this->Application->getUnitConfig('ban-rule')->getTableName(); if (!$this->Conn->TableFound($table)) { // when ban table not found -> assume user is ok by default return true; } $sql = 'SELECT * FROM ' . $table . ' WHERE ItemType = 6 AND Status = ' . STATUS_ACTIVE . ' ORDER BY Priority DESC'; $rules = $this->Conn->Query($sql); $found = false; foreach ($rules as $rule) { $field = $rule['ItemField']; $this_value = mb_strtolower( $object->GetDBField($field) ); $test_value = mb_strtolower( $rule['ItemValue'] ); switch ( $rule['ItemVerb'] ) { case 1: // is if ($this_value == $test_value) { $found = true; } break; case 2: // is not if ($this_value != $test_value) { $found = true; } break; case 3: // contains if ( strstr($this_value, $test_value) ) { $found = true; } break; case 4: // not contains if ( !strstr($this_value, $test_value) ) { $found = true; } break; case 7: // exists if ( strlen($this_value) > 0 ) { $found = true; } break; case 8: // unique if ( $this->_checkValueExist($field, $this_value) ) { $found = true; } break; } if ( $found ) { // check ban rules, until one of them matches if ( $rule['RuleType'] ) { // invert rule type $found = false; } break; } } return !$found; } /** * Checks if value is unique in Users table against the specified field * * @param string $field * @param string $value * @return string */ function _checkValueExist($field, $value) { $sql = 'SELECT * FROM ' . $this->Application->getUnitConfig('u')->getTableName() . ' WHERE '. $field .' = ' . $this->Conn->qstr($value); return $this->Conn->GetOne($sql); } public function validateUserCode($user_code, $code_type, $expiration_timeout = null) { $expiration_timeouts = Array ( 'forgot_password' => 'config:Users_AllowReset', 'activation' => 'config:UserEmailActivationTimeout', 'verify_email' => 'config:Users_AllowReset', 'custom' => '', ); if ( !$user_code ) { return 'code_is_not_valid'; } $sql = 'SELECT PwRequestTime, PortalUserId FROM ' . TABLE_PREFIX . 'Users WHERE PwResetConfirm = ' . $this->Conn->qstr( trim($user_code) ); $user_info = $this->Conn->GetRow($sql); if ( $user_info === false ) { return 'code_is_not_valid'; } $expiration_timeout = isset($expiration_timeout) ? $expiration_timeout : $expiration_timeouts[$code_type]; if ( preg_match('/^config:(.*)$/', $expiration_timeout, $regs) ) { $expiration_timeout = $this->Application->ConfigValue( $regs[1] ); } if ( $expiration_timeout && $user_info['PwRequestTime'] < strtotime('-' . $expiration_timeout . ' minutes') ) { return 'code_expired'; } return $user_info['PortalUserId']; } /** * Restores user's email, returns error label, if error occurred * * @param string $hash * @return string * @access public */ public function restoreEmail($hash) { if ( !preg_match('/^[a-f0-9]{32}$/', $hash) ) { return 'invalid_hash'; } $sql = 'SELECT PortalUserId, PrevEmails FROM ' . TABLE_PREFIX . 'Users WHERE PrevEmails LIKE ' . $this->Conn->qstr('%' . $hash . '%'); $user_info = $this->Conn->GetRow($sql); if ( $user_info === false ) { return 'invalid_hash'; } $prev_emails = $user_info['PrevEmails']; $prev_emails = $prev_emails ? unserialize($prev_emails) : Array (); if ( !isset($prev_emails[$hash]) ) { return 'invalid_hash'; } $email_to_restore = $prev_emails[$hash]; unset($prev_emails[$hash]); $object = $this->Application->recallObject('u.email-restore', null, Array ('skip_autoload' => true)); /* @var $object UsersItem */ $object->Load($user_info['PortalUserId']); $object->SetDBField('PrevEmails', serialize($prev_emails)); $object->SetDBField('Email', $email_to_restore); $object->SetDBField('EmailVerified', 1); return $object->Update() ? '' : 'restore_impossible'; } /** * Generates random string * * @param int $length * @param bool $special_chars * @param bool $extra_special_chars * @return string * @access public */ public function generateRandomString($length = 12, $special_chars = true, $extra_special_chars = false) { $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; if ( $special_chars ) { $chars .= '!@#$%^&*()'; } if ( $extra_special_chars ) { $chars .= '-_ []{}<>~`+=,.;:/?|'; } $password = ''; for ($i = 0; $i < $length; $i++) { $password .= substr($chars, $this->_generateRandomNumber(0, strlen($chars) - 1), 1); } return $password; } /** * Generates a random number * * @param int $min Lower limit for the generated number (optional, default is 0) * @param int $max Upper limit for the generated number (optional, default is 4294967295) * @return int A random number between min and max * @access protected */ protected function _generateRandomNumber($min = 0, $max = 0) { static $rnd_value = ''; // Reset $rnd_value after 14 uses // 32(md5) + 40(sha1) + 40(sha1) / 8 = 14 random numbers from $rnd_value if ( strlen($rnd_value) < 8 ) { $random_seed = $this->Application->getDBCache('random_seed'); $rnd_value = md5(uniqid(microtime() . mt_rand(), true) . $random_seed); $rnd_value .= sha1($rnd_value); $rnd_value .= sha1($rnd_value . $random_seed); $random_seed = md5($random_seed . $rnd_value); $this->Application->setDBCache('random_seed', $random_seed); } // Take the first 8 digits for our value $value = substr($rnd_value, 0, 8); // Strip the first eight, leaving the remainder for the next call to wp_rand(). $rnd_value = substr($rnd_value, 8); $value = abs(hexdec($value)); // Reduce the value to be within the min - max range // 4294967295 = 0xffffffff = max random number if ( $max != 0 ) { $value = $min + (($max - $min + 1) * ($value / (4294967295 + 1))); } return abs(intval($value)); } }