module_title = _t('users', 'Users'); # кол-во доступных номеров телефон (в профиле) $this->profilePhonesLimit = $this->config('users.profile.phones', 5, TYPE_UINT); } public function onNewRequest($request) { parent::onNewRequest($request); $this->profilePhonesLimit = $this->config('users.profile.phones', 5, TYPE_UINT); } /** * Get user model by id or filter e.g. ['email' => 'user@example.com'] * @param int|array $id * @param array $columns * @param array $with * @return \modules\users\models\User */ public function user($id, $columns = ['*'], array $with = []) { return $this->model->user($id); } /** * Выполняем авторизацию пользователя по ID * @param int|string $id * @param array $opts * @return mixed */ public function authById($id, array $opts = []) { return $this->authByCredentials(['user_id' => $id], null, $opts); } /** * Выполняем авторизацию пользователя * @param array $credentials ['key' => 'value', ...] * @param string|null $password * @param array $opts * @return mixed: * 0 - ошибка в логине/пароле, * 1 - неактивирован, * true - успешная авторизация, * ['reason' => 'причина блокировки'] */ public function authByCredentials(array $credentials, ?string $password = null, array $opts = []) { $remember = $opts['remember'] ?? false; $silent = $opts['silent'] ?? true; $single = $opts['single'] ?? false; # true = allow one open session per user only $admin = $opts['admin'] ?? false; if ($admin) { $credentials['admin'] = 1; } else { $credentials['member'] = 1; } // blocked, blocked_reason, activated, user_id as id, password_salt $data = $this->model->userSessionData($credentials); if ($data && $password !== null) { if (! $this->security->passwordCheck($password, $data['password'], $data['password_salt'])) { $data = false; } } $data = $this->app->filter('users.user.auth.data', $data, [ 'credentials' => $credentials, 'remember' => &$remember, 'silent' => &$silent, ]); if (!$data) { # пользователя с таким логином и/или паролем не существует if (! $silent) { if (array_key_exists('email', $credentials)) { $this->errors->set(_t('users', 'Incorrect email or password')); } else { $this->errors->set(_t('users', 'Incorrect login or password')); } } return 0; } # аккаунт заблокирован if ($data['blocked'] == 1) { if (! $silent) { $this->errors->set(_t('users', 'Account blocked due to: [reason]', [ 'reason' => '
' . nl2br($data['blocked_reason']) ])); } return ['reason' => $data['blocked_reason']]; } # аккаунт неактивирован if ($data['activated'] == 0) { config::temp('__users_preactivate_data', $data); return 1; } if (User::isCurrent($data['id'])) { # текущий пользователь уже авторизован # под аккаунтом под которым необходимо произвести авторизацию return true; } $userID = $data['id']; $user = Auth::loginUsingId($userID, $remember); if (! $user) { return false; } $user->touchOnLogin(); if ($single && $data['session_id']) { Session::destroy($data['session_id']); } return true; } /** * Проверяем корректность логина * @param string $login логин * @param int|null $maxLength максимально допустимая длина логина * @return bool корректный ли логин */ public function isLoginCorrect($login, ?int $maxLength = null) { if (empty($maxLength)) { $maxLength = $this->loginMaxLength; } if ( empty($login) || # пустой (strlen($login) < $this->loginMinLength) || # короткий (strlen($login) > $maxLength) || # длинный preg_match('/[^\_a-z0-9]+/i', $login) # содержит недопустимые символы ) { return false; } return true; } /** * Проверяем является ли поле $value логином или email адресом * @param string $value @ref поле * @param bool $isEmail @ref результат проверки true - email, false - логин * @param bool $validate валидировать поле $value * @return bool корректное ли поле $value */ public function isLoginOrEmail(&$value, &$isEmail, $validate = true) { $isEmail = false; if (mb_strpos($value, '@') !== false) { # указали "email" if ($validate) { if (!$this->input->isEmail($value, false)) { $this->errors->set(_t('users', 'Incorrect email address'), 'email'); return false; } } $isEmail = true; } else { # указали "login" if ($validate) { if (!$this->isLoginCorrect($value)) { $this->errors->set(_t('users', 'Incorrect login'), 'login'); return false; } } } return true; } /** * Формируем выпадающие списки для выбора дня рождения * @param mixed $birthDate дата рождения * @param int $yearMin минимальный год рождения * @param mixed $emptyOptions пустые значения по-умолчанию * @return array */ public function getBirthdateOptions($birthDate = '', $yearMin = 1930, $emptyOptions = false) { if (is_array($birthDate)) { if (isset($birthDate['year'])) { list($year, $month, $day) = [$birthDate['year'], $birthDate['month'], $birthDate['day']]; } else { list($year, $month, $day) = [0, 0, 0]; } } else { list($year, $month, $day) = ( ! empty($birthDate) && $birthDate !== '0000-00-00' && $birthDate !== '1901-01-01' ? explode(',', date('Y,n,j', strtotime($birthDate))) : array(0,0,0) ); } $months = $this->locale->getMonthTitle(); unset($months[0]); $result = [ 'days' => range(1, 31), 'months' => $months, 'years' => range(date('Y') - 5, $yearMin), ]; if (! empty($emptyOptions)) { if (! is_array($emptyOptions)) { $emptyOptions = [_t('', 'day'), _t('', 'month'), _t('', 'year')]; } $result['days'] = [-1 => $emptyOptions[0]] + $result['days']; $result['months'] = [ 0 => $emptyOptions[1]] + $result['months']; $result['years'] = [-1 => $emptyOptions[2]] + $result['years']; } array_walk($result['days'], function (&$value, $key, $selectedValue) { $value = ''; }, $day); array_walk($result['months'], function (&$option, $value, $selectedValue) { $option = ''; }, $month); array_walk($result['years'], function (&$value, $key, $selectedValue) { $value = ''; }, $year); $result['days'] = join('', $result['days']); $result['months'] = join('', $result['months']); $result['years'] = join('', $result['years']); return $result; } /** * Проверяем статус "online" по времени последней активности * @param mixed $lastActivity время последней активности (Users::TABLE_USERS_STAT::last_activity) * @return bool true - online, false - offline */ public function isOnline($lastActivity) { if (empty($lastActivity)) { return false; } if (is_string($lastActivity)) { $lastActivity = strtotime($lastActivity); } return ( (time() - $lastActivity) < (config::sys('users.profile.online.timeout', 5) * 60) ); } /** * Актуализируем "Время последней активности" пользователя * @param int $userID * @return void */ public function updateUserLastActivity($userID) { if (empty($userID)) { return; } $lastActive = (time() - Session::get('last_activity', 0)); if ($lastActive < config::get('users.activity.timeout')) { return; } $this->model->userSave($userID, false, [ 'last_activity' => $this->db->now(), ]); Session::put('last_activity', time()); } /** * Генерация ключа для автоматической авторизации * @param array $data данные пользователя user_id, user_id_ex, last_login * @return string */ public function loginAutoHash(array $data): string { if ( empty($data['user_id']) || empty($data['user_id_ex']) || empty($data['last_login']) ) { return ''; } return $data['user_id'] . '.' . mb_strtolower( hash('sha256', join('', [ $data['user_id'], $data['user_id_ex'], md5($data['last_login']), $data['last_login'], ])) ); } /** * Проверяет забанен ли пользователь по ip или email. Если параметры не переданы, берем из текущей сессии. * @param string|array|bool $userIP строка с одним IP или массив IP-адресов, если true - текущий IP адрес * @param string|bool $userEmail E-mail пользователя или FALSE * @return bool|string */ public function checkBan($userIP = true, $userEmail = false) { if ($userIP === true) { $userIP = $this->request->remoteAddress(); } $banned = false; $queryWhere = []; if ($userEmail === false) { $queryWhere[] = "email = ''"; } if ($userIP === false) { $queryWhere[] = "(ip = '' OR exclude = 1)"; } $queryWhere[] = '(uid = 0 OR exclude = 1)'; $result = $this->db->select('SELECT ip, uid, email, exclude, reason, finished FROM ' . static::TABLE_USERS_BANLIST . ' WHERE ' . (sizeof($queryWhere) ? join(' AND ', $queryWhere) : '') . ' ORDER BY exclude ASC'); $banTriggeredBy = ''; foreach ($result as $ban) { if ($ban['finished'] && $ban['finished'] < time()) { continue; } $ip_banned = false; if (!empty($ban['ip'])) { if (!is_array($userIP)) { $ip_banned = preg_match('#^' . str_replace('\*', '.*?', preg_quote($ban['ip'], '#')) . '$#i', $userIP); } else { foreach ($userIP as $ip) { if (preg_match('#^' . str_replace('\*', '.*?', preg_quote($ban['ip'], '#')) . '$#i', $ip)) { $ip_banned = true; break; } } } if ($ip_banned && !empty($ban['exclude'])) { $ip_banned = false; } } if ( $ip_banned || (!empty($ban['email']) && preg_match('#^' . str_replace('\*', '.*?', preg_quote($ban['email'], '#')) . '$#i', $userEmail)) ) { if (!empty($ban['exclude'])) { $banned = false; break; } else { $banned = true; $banData = $ban; if ($ip_banned) { $banTriggeredBy = 'ip'; } else { $banTriggeredBy = 'email'; } # Не делаем break, т.к. возможно есть exclude правило для этого юзера } } } return ($banned && $banData['reason'] ? $banData['reason'] : $banned); } /** * Иницилизация компонента работы с аватарами * @param int $userID ID пользователя * @return \UsersAvatar component */ public function avatar($userID = 0) { return UsersAvatar::i($userID); } /** * Activate user account * @param int $id user id * @param array $opts * @return bool */ public function userActivate($id, array $opts = []): bool { $opts = $this->defaults($opts, [ 'verifyEmail' => false, 'verifyPhone' => false, 'password' => null, 'passwordSalt' => null, 'registeredEvent' => true, 'activateKeyClear' => true, ]); if (empty($id)) { return false; } $update = [ 'activated' => 1, ]; if ($opts['activateKeyClear']) { $update['activate_key'] = ''; } if ($opts['verifyEmail']) { $update['email_verified'] = 1; } if ($opts['verifyPhone']) { $update['phone_number_verified'] = 1; } if ($opts['password'] !== null) { if ($opts['passwordSalt'] !== false) { # hash password $passwordSalt = $opts['passwordSalt'] ?? $this->model->userData($id, 'password_salt')['password_salt'] ?? ''; $update['password'] = $this->security->passwordHash($opts['password'], $passwordSalt); } else { # password was hashed before $update['password'] = $opts['password']; } } $result = $this->model->userSave($id, $update); if ($result && $opts['registeredEvent']) { RegisteredEvent::dispatch($id, $opts); $this->app->callModules('onUserRegistered', [$id, $opts]); } return $result; } /** * Завершение сеанса пользователя по ID * @param int $userID ID пользователя * @param bool|null $adminPanel * @param string|null $sessionID * @return bool */ public function userSessionDestroy($userID, $adminPanel = null, $sessionID = null) { if (empty($userID)) { return false; } if ($adminPanel && $this->isAdminPanel() && User::isCurrent($userID)) { Auth::logout(); return true; } if (empty($sessionID)) { $data = $this->model->userDataByFilter($userID, ['session_id']); if (empty($data['session_id'])) { return false; } $sessionID = $data['session_id']; } Session::destroy($sessionID); $this->model->userSave($userID, false, ['session_id' => '']); return true; } /** * Коррекция настроек авторизации через соц. сети * @param array $opts * @return mixed */ public function socialConfig($opts = []) { $sysPrefix = 'users.social.config.'; $config = config::file('social'); $config['callback'] = $config['base_url'] = $this->url('login.social', [ 'provider' => (!empty($opts['provider']) ? $opts['provider'] : ''), ]); $ds = DS; foreach ($config['providers'] as $k => &$v) { if (! empty($opts['path']) && ! file_exists($opts['path'] . 'hybridauth' . $ds . 'hybridauth' . $ds . 'Hybrid' . $ds . 'Providers' . $ds . $k . '.php')) { if (! isset($v['wrapper'])) { $v['wrapper'] = [ 'class' => 'Hybrid_Providers_' . $k, 'path' => $opts['path'] . 'hybridauth' . $ds . 'additional-providers' . $ds . 'hybridauth-' . mb_strtolower($k) . $ds . 'Providers' . $ds . $k . '.php', ]; } } $v['enabled'] = $this->config($sysPrefix . $k . '.enabled', !empty($v['enabled']), TYPE_BOOL); foreach ($v['keys'] as $kk => $vv) { $v['keys'][$kk] = $this->config($sysPrefix . $k . '.keys.' . $kk, $vv, ($kk === 'secret' ? TYPE_PASS : TYPE_STR)); } if (isset($v['scope'])) { $v['scope'] = $this->config($sysPrefix . $k . '.scope', $v['scope'], TYPE_STR); } } unset($v); return $config; } /** * Формирование списка директорий/файлов требующих проверки на наличие прав записи * @return array */ public function writableCheck() { return array_merge(parent::writableCheck(), [ $this->app->path('avatars', 'images') => 'dir', # аватары ]); } }