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', # аватары
]);
}
}