to('extra')
->text('keyword', _te('users', 'Keyword'), '', false)
->onValidate(function ($param) use ($form) {
$opts = OptionGroup::find(OptionGroup::keywordToID('users-user-contacts'))->options->toArray();
foreach ($opts as $v) {
if ($v['extra']['keyword'] == $param['value'] && $form->recordID() != $v['id']) {
$this->errors->set(_t('users', 'The keyword must be unique'));
return false;
}
}
return true;
})
->required(_t('users', 'Specify Keyword'))
->text('icon', _te('users', 'Icon class'), '', false)
->select(
'regexp',
_te('users', 'Check'),
false,
function () {
return [
['id' => '/[^\.\s\[\]\:\-\_a-zA-Z0-9]/i', 'title' => _te('users', 'Letters, numbers, dash')],
['id' => '/[^\.\-\s\_0-9]/', 'title' => _te('users', 'Numbers')],
['id' => '/[^\s\+\-0-9]/', 'title' => _te('users', 'Phone Number')],
];
},
function () use (&$form) {
return _te('users', 'Without checking');
}
)
->number('maxlength', _te('users', 'Characters number'), 1, 2048)
->text('view', _te('users', 'View'), '', false)
->tip(_te('users', 'Use the {value} macros for the value specified by user'));
});
# UserServices
Svc::registerServiceManager(UserServices::KEY, UserServices::class);
}
/**
* Системные настройки модуля
* @param array $options @ref
* @return string
*/
public function settingsSystem(array &$options = []): string
{
return $this->template('admin/settings.sys', ['options' => &$options]);
}
/**
* Настройки полей контактных данных
* @param array|bool $data данные требующие проверки на соответствие разрешенным полям
* @return array
*/
public function contactsFields($data = false): array
{
$list = [];
$fields = Options::getOptions('users-user-contacts', null, true);
foreach ($fields as $v) {
$extra = $v['extra'];
$list[$extra['keyword']] = [
'title' => $v['title'],
'icon' => $extra['icon'],
'maxlength' => $extra['maxlength'],
'key' => $extra['keyword'],
];
if (!empty($extra['view'])) {
$list[$extra['keyword']]['view'] = $extra['view'];
}
if (!empty($extra['regexp'])) {
$list[$extra['keyword']]['regexp'] = $extra['regexp'];
}
}
foreach ($list as $k => $v) {
if (in_array($k, ['phones','name','has','contacts'])) {
unset($list[$k]);
continue;
}
if (!isset($v['maxlength'])) {
$list[$k]['maxlength'] = 1000;
}
$list[$k]['key'] = $k;
}
if ($data !== false) {
if (is_array($data)) {
foreach ($data as $k => $v) {
if (! isset($list[$k])) {
unset($data[$k]);
continue;
}
$list[$k]['value'] = $v;
$data[$k] = $list[$k];
}
return $data;
} elseif ($data === true) {
return array_keys($list);
}
return [];
}
return $list;
}
/**
* Clean contacts data
* @param string|array $contacts
* @return array cleaned contacts
*/
public function contactsToArray($contacts): array
{
if (is_string($contacts) && !empty($contacts)) {
$contacts = json_decode($contacts, true);
}
if (! is_array($contacts)) {
$contacts = [];
}
# only allowed
$allowed = $this->contactsFields();
foreach ($contacts as $k => $v) {
if (
!isset($allowed[$k]) || # only allowed
in_array($k, ['phones', 'name', 'has', 'contacts']) || # forbidden key
empty($v) # not empty
) {
unset($contacts[$k]);
}
}
return $contacts;
}
/**
* Clean contacts data
* @param array $contacts
* @return array cleaned contacts
*/
public function contactsCleanData(array $contacts): array
{
$result = [];
foreach ($this->contactsFields($contacts) as $contact) {
$value = $contact['value'];
if (isset($contact['regexp'])) {
$value = preg_replace($contact['regexp'], '', $value);
}
$value = mb_strcut($value, 0, $contact['maxlength']);
if (!empty($value)) {
$result[$contact['key']] = $value;
}
}
return $result;
}
/**
* Список шаблонов уведомлений
* @return array
*/
public function sendmailTemplates(): array
{
$templates = [
'users_register' => [
'title' => _t('users', 'Users: Registration Notification'),
'description' => _t('users', 'Notification sent to the user after registration, with instructions to activate the account'),
'vars' => [
'{email}' => _t('', 'Email'),
'{password}' => _t('users', 'Password'),
'{activate_link}' => _t('users', 'Account Activation Link'),
],
'priority' => 1,
'enotify' => 0, # всегда
'group' => 'users',
'force_verified' => true,
Sendmail::CHANNEL_SMS => true,
],
'users_register_phone' => [
'title' => _t('users', 'Users: Registration Notification (with phone number input)'),
'description' => _t('users', 'Email template sent to the user after successful registration with confirmation of the phone number'),
'vars' => [
'{email}' => _t('', 'Email'),
'{password}' => _t('users', 'Password'),
'{phone}' => _t('users', 'Phone Number'),
],
'priority' => 1.5,
'enotify' => 0, # всегда
'group' => 'users',
],
'users_register_auto' => [
'title' => _t('users', 'Users: Notification of Successful Automatic Registration'),
'description' => _t('users', 'Notification sent to the user in case of automatic registration.
Activating the listing / clicking on the link "continue chat"'),
'vars' => [
'{name}' => _t('users', 'Name'),
'{email}' => _t('', 'Email'),
'{password}' => _t('users', 'Password'),
],
'priority' => 2,
'enotify' => 0, # всегда
'group' => 'users',
],
'users_forgot_start' => [
'title' => _t('users', 'Users: Password Recovery'),
'description' => _t('users', 'Notification sent to the user in the case of a password recovery request'),
'vars' => [
'{name}' => _t('users', 'Name'),
'{email}' => _t('users', 'User Email'),
'{link}' => _t('users', 'Password Recovery Link'),
],
'priority' => 3,
'enotify' => 0, # всегда
'force_verified' => true,
'group' => 'users',
],
'users_blocked' => [
'title' => _t('users', 'Users: Account Blocking Notification'),
'description' => _t('users', 'Notification sent to the user in case of account blocking'),
'vars' => [
'{name}' => _t('users', 'Name'),
'{email}' => _t('', 'Email'),
'{blocked_reason}' => _t('users', 'Blocking Reason'),
],
'priority' => 4,
'enotify' => 0, # всегда
'group' => 'users',
],
'users_unblocked' => [
'title' => _t('users', 'Users: Account Unlock Notification'),
'description' => _t('users', 'Notification sent to the user in case of unlocking the account'),
'vars' => [
'{name}' => _t('users', 'Name'),
'{email}' => _t('', 'Email'),
],
'priority' => 5,
'enotify' => 0, # всегда
'group' => 'users',
],
'users_email_change' => [
'title' => _t('users', 'Users: Change Email Address'),
'description' => _t('users', 'Notification sent to the user when changing the email address'),
'vars' => [
'{name}' => _t('users', 'Name'),
'{email}' => _t('', 'Email'),
'{activate_link}' => _t('users', 'Activation Link'),
],
'priority' => 6,
'enotify' => 0, # всегда
'group' => 'users',
'force_verified' => true,
],
];
if ($this->registerPhone(static::REGISTER_TYPE_EMAIL)) {
unset($templates['users_register_phone']);
}
Sendmail::addTemplateGroup('users', _t('users', 'Users'), 3);
return $templates;
}
/**
* Формирование URL
* @param string $key ключ
* @param array $params параметры
* @param bool $dynamic динамическая ссылка
* @return string
*/
public function url(string $key, array $params = [], $dynamic = false): string
{
return $this->router->url('users-' . $key, $params, ['dynamic' => $dynamic, 'module' => 'users']);
}
/**
* Страница просмотра профиля пользователя
* @param string $login логин пользователя
* @param string $tab ключ подраздела
* @param array $query доп.параметры
* @param bool $dynamic динамическая ссылка
* @return string
*/
public function urlProfile(string $login, string $tab = '', array $query = [], $dynamic = false): string
{
if ($tab == 'items') {
$tab = '';
}
return $this->url('user.profile', ['login' => $login, 'tab' => $tab] + $query, $dynamic);
}
/**
* Send email notification to user to confirm email & finish registration (onlyEmail flow)
* @param int $userId
* @param string $email to send to
* @param string $password (open)
* @param string $activationUrl
* @param array $data
* @return bool
*/
public function sendEmailRegistrationConfirmation($userId, $email, $password, $activationUrl, array $data = [])
{
$data['user_id'] = $userId;
$data['email'] = $email;
$data['password'] = $password;
$data['activate_link'] = $activationUrl;
return $this->app->sendMailTemplate($data, 'users_register', $email);
}
/**
* Send email notification to user about successfully finished automatic registration
* @param int $userId
* @param string $name
* @param string $email to send to
* @param string $password (open)
* @param array $data
* @return bool
*/
public function sendAutoRegistrationNotification($userId, $name, $email, $password, array $data = [])
{
$data['name'] = $name;
$data['email'] = $email;
$data['password'] = $password;
$data['user_id'] = $userId;
return $this->app->sendMailTemplate($data, 'users_register_auto', $email);
}
/**
* Send email notification to user about successfully finished registration using phone
* @param int $userId
* @param string $email to send to
* @param string $phone used to register
* @param string $password (open)
* @param array $data
* @return bool
*/
public function sendPhoneRegistrationNotification($userId, $email, $phone, $password, array $data = [])
{
$data['email'] = $email;
$data['phone'] = $phone;
$data['password'] = $password;
$data['user_id'] = $userId;
return $this->app->sendMailTemplate($data, 'users_register_phone', $email);
}
/**
* Send notification to confirm email change
* @param int $userID
* @param string $email
* @return bool
*/
public function sendEmailChangeConfirmation($userID, string $email): bool
{
$user = $this->model->userData($userID, ['extra', 'name']);
$activation = $this->getActivationInfo([], func::generator(32));
$user['extra']['email_change'] = [
'email' => $email,
'key' => $activation['key'],
'expire' => $activation['expire'],
];
$activationLink = $this->url('register', ['step' => 'emailchange', 'key' => $activation['key'] . '.' . $userID]);
$success = $this->model->userSave($userID, ['extra' => serialize($user['extra'])]); # todo: json
if ($success) {
# отправляем уведомление
$mailData = [
'id' => $userID,
'user_id' => $userID,
'name' => $user['name'],
'email' => $email,
'activate_link' => $activationLink,
];
$this->app->sendMailTemplate($mailData, 'users_email_change', $email);
return true;
}
return false;
}
/**
* Валидация данных пользователя
* @param array $data @ref данные
* @param array $keys список ключей требующих валидации данных или [] - все
* @param array $opts дополнительные параметры валидации
* @return void
*/
public function cleanUserData(array &$data, array $keys = [], array $opts = [])
{
$opts = $this->defaults($opts, [
'name_length' => 50,
'phones_limit' => $this->profilePhonesLimit,
'about_limit' => 2500,
]);
if (empty($keys)) {
$keys = array_keys($data);
}
foreach ($keys as $key) {
if (! isset($data[$key])) {
continue;
}
if ($this->app->hooksAdded('users.clean.data.' . $key)) {
$data[$key] = $this->app->filter('users.clean.data.' . $key, $data[$key], [
'data' => &$data,
'extraSettings' => $opts,
]);
continue;
}
switch ($key) {
case 'name': # Контактное лицо
case 'firstname': # Имя
case 'surname': # Фамилия
# допустимые символы:
# латиница, кирилица, тире, пробелы
$data[$key] = preg_replace('/[^\.\s\[\]\-\_\p{L}0-9\w\’\']+/iu', '', $data[$key]);
$data[$key] = trim(mb_substr($data[$key], 0, $opts['name_length']), '- ');
break;
case 'birthdate': # дата рождения
if (!$this->profileBirthdate) {
break;
}
if (empty($data[$key]) || !checkdate($data[$key]['month'], $data[$key]['day'], $data[$key]['year'])) {
$this->errors->set(_t('users', 'Incorrect date of birth'));
} else {
$data[$key] = "{$data[$key]['year']}-{$data[$key]['month']}-{$data[$key]['day']}";
}
break;
case 'site': # сайт
if (mb_strlen($data[$key]) > 3) {
$data[$key] = mb_substr(strip_tags($data[$key]), 0, 255);
if (stripos($data[$key], 'http') !== 0) {
$data[$key] = 'http://' . $data[$key];
}
} else {
$data[$key] = '';
}
break;
case 'about': # о себе
$data[$key] = mb_substr($data[$key], 0, $opts['about_limit']);
break;
case 'phones': # телефоны
# в случае если телефоны в serialized виде => пропускаем
if (is_string($data[$key]) && mb_stripos($data[$key], 'a:') === 0) {
break;
}
$phones = $this->validatePhones($data[$key], $opts['phones_limit']);
$data[$key] = serialize($phones);
# сохраняем первый телефон в отдельное поле
if (!empty($phones)) {
$phoneFirst = reset($phones);
$data['phone'] = $phoneFirst['v'];
} else {
$data['phone'] = '';
}
break;
case 'skype': # skype
$data[$key] = preg_replace('/[^\.\s\[\]\:\-\_a-zA-Z0-9]/', '', $data[$key]);
$data[$key] = trim(mb_substr($data[$key], 0, 32), ' -');
break;
case 'icq': # icq
$data[$key] = preg_replace('/[^\.\-\s\_0-9]/', '', $data[$key]);
$data[$key] = trim(mb_substr($data[$key], 0, 20), ' .-');
break;
case 'contacts':
if (is_array($data[$key])) {
foreach ($data[$key] as $k => $v) {
if (empty($v)) {
unset($data[$key][$k]);
}
}
$data[$key] = json_encode($this->contactsCleanData($data[$key]));
}
break;
case 'geo_city':
if ($data[$key] > 0) {
# проверяем корректность указанного города
if (!Geo::isCity($data[$key])) {
$data[$key] = 0;
$deep = Geo::maxDeep();
for ($i = 1; $i < $deep; $i++) {
$data['geo_region' . $i] = 0;
}
} else {
# разворачиваем данные о регионе: geo_city => geo_region1, geo_region2, geo_region3, geo_region4
$regions = Geo::regionParents($data[$key]);
$data = array_merge($data, $regions['db']);
}
} else {
$deep = Geo::maxDeep();
for ($i = 1; $i < $deep; $i++) {
$data['geo_region' . $i] = 0;
}
}
break;
}
}
}
/**
* Иницилизация компонента работы с соц. аккаунтами
* @return Social
*/
public function social()
{
return Social::i();
}
/**
* SMS шлюз
* @param bool $userErrors фиксировать ошибки для пользователей
* @return Sms
*/
public function sms(bool $userErrors = true)
{
$sms = Sms::i();
$sms->userErrorsEnabled($userErrors);
return $sms;
}
/**
* Проверка доступности указанного способа регистрации пользователей
* @param mixed $compare способ для проверки, array - соответствует как минимум одному из перечисленных
* @return bool
*/
public function registerPhone($compare = false): bool
{
$type = $this->config('users.register.type', 0, TYPE_UINT);
if ($type === 0) {
$type = ($this->config('users.register.phone', false, TYPE_BOOL) ?
static::REGISTER_TYPE_BOTH :
static::REGISTER_TYPE_EMAIL
);
}
if ($compare === false) {
$compare = [static::REGISTER_TYPE_BOTH, static::REGISTER_TYPE_PHONE];
}
if (is_array($compare)) {
return in_array($type, $compare);
}
return ($compare == $type);
}
/**
* Отображать номер телефона указанный при регистрации в контактах профиля
* @return bool
*/
public function registerPhoneContacts(): bool
{
return $this->config('users.register.phone.contacts', false, TYPE_BOOL)
&& $this->registerPhone([static::REGISTER_TYPE_BOTH, static::REGISTER_TYPE_PHONE]);
}
/**
* Поле ввода номера телефона
* @param array $fieldAttr аттрибуты поля
* @param array $opts доп. параметры
* @return string HTML
*/
public function registerPhoneInput(array $fieldAttr = [], array $opts = []): string
{
$opts = $this->defaults($opts, [
'country' => Geo::defaultCountry(), # ID страны по умолчанию
]);
$fieldAttr = array_merge(['name' => 'phone_number'], $fieldAttr);
$countryList = Geo::countriesList();
$countrySelected = $opts['country'];
if (!$countrySelected) {
$filter = Geo::filter();
if (!empty($filter['country'])) {
$countrySelected = $filter['country'];
}
}
if (! isset($countryList[$countrySelected])) {
$countrySelected = key($countryList);
}
if (empty($fieldAttr['value'])) {
$fieldAttr['value'] = '+' . intval($countryList[$countrySelected]['extra']['phone_code'] ?? 0);
}
return $this->template('phone.input', [
'attr' => $fieldAttr,
'options' => $opts,
'countryList' => $countryList,
'countrySelected' => $countryList[$countrySelected],
'countrySelectedID' => $countrySelected,
'itemForm' => !empty($opts['item-form']),
]);
}
/**
* Register new user account (not activated)
* @param array $data user data to store in database
* @param array $opts
* @return array|bool
* false - failed
* array - success & user data: [user_id, password, activate_link]
*/
public function userRegister(array $data, array $opts = [])
{
$opts = $this->defaults($opts, [
'auth' => false, # authenticate in case of success
]);
# login is required
if (empty($data['login'])) {
# generate login based on email@ (first part)
if (isset($data['email'])) {
$login = mb_substr($data['email'], 0, mb_strpos($data['email'], '@'));
$login = preg_replace('/[^a-z0-9\_]/ui', '', $login);
$login = mb_strtolower(trim($login, '_ '));
if (mb_strlen($login) >= $this->loginMinLength) {
if (mb_strlen($login) > $this->loginMaxLength) {
$login = mb_substr($login, 0, $this->loginMaxLength);
}
$data['login'] = $this->model->userLoginGenerate($login, true);
} else {
$data['login'] = $this->model->userLoginGenerate();
}
} else {
$data['login'] = $this->model->userLoginGenerate();
}
}
# generate new password
$password = $data['password'] ?? $this->security->generatePassword();
$data['password_salt'] = $this->security->generatePasswordSalt();
$data['password'] = $this->security->passwordHash($password, $data['password_salt']);
# filter data
$this->cleanUserData($data, ['name','firstname','surname','birthdate','site','about','phones','region_id']);
# generate activation key
$activation = $this->getActivationInfo();
$data['activated'] = 0;
$data['activate_key'] = $activation['key'];
$data['activate_expire'] = $activation['expire'];
# subscribe on all notification channels (email, sms)
$channels = Sendmail::channels();
foreach ($channels as $k => $v) {
$field = 'enotify' . ($k == Sendmail::CHANNEL_EMAIL ? '' : '_' . $k);
$data[$field] = $this->getEnotifyTypes(0, true, $k);
}
# create user account
$userId = $this->model->userCreate($data);
if (! $userId) {
return false;
}
# gift money for registration
$gift = $this->config('users.register.money.gift', 0, TYPE_UINT);
if ($gift > 0) {
if (Bills::updateUserBalance($userId, $gift, true)) {
Bills::createBill_InGift($userId, $gift, $gift, _t('users', 'Gift for registration'));
}
}
# authenticate
if ($opts['auth']) {
$this->authById($userId);
}
return [
'user_id' => $userId,
'password' => $password,
'activate_key' => $activation['key'],
'activate_link' => $activation['link'],
'password_salt' => $data['password_salt'],
];
}
/**
* Формируем ключ активации
* @param array $query дополнительные параметры ссылки активации
* @param string $key ключ активации (если был сгенерирован ранее)
* @return array [
* 'key' => ключ активации,
* 'link' => ссылка для активации,
* 'expire' => дата истечения срока действия ключа,
* ]
*/
public function getActivationInfo(array $query = [], string $key = ''): array
{
$data = [];
if (empty($key)) {
$shortCode = $this->registerPhone([static::REGISTER_TYPE_PHONE, static::REGISTER_TYPE_BOTH]);
if ($shortCode) {
# В случае регистрации через телефон генерируем короткий ключ активации
$len = $this->config('users.sms.code.length', 5, TYPE_UINT);
if ($this->config('users.sms.code.type', 'alphanum') === 'num') {
$key = func::generator($len, true);
} else {
$key = func::generatorLetters($len, $this->config('users.sms.code.letters', '', TYPE_STR));
}
} else {
$salt = config::sys('users.activation.salt', '^*RD%S&()%$#', TYPE_STR);
$key = md5(substr(md5(uniqid(mt_rand() . SITEHOST . $salt, true)), 0, 10) . microtime());
}
}
$data['key'] = $query['key'] = $key;
$data['link'] = $this->url('register', $query + ['step' => 'activate']);
$data['expire'] = date('Y-m-d H:i:s', '+' . strtotime($this->config('users.activation.expire', '7 days', TYPE_STR)));
return $data;
}
/**
* Обновляем ключ активации пользователя
* @param int $userID ID пользователя
* @param string $currentKey ключ активации (если был сгенерирован ранее)
* @return array|bool
*/
public function updateActivationKey($userID, string $currentKey = '')
{
if (! $userID) {
return false;
}
$activationData = $this->getActivationInfo([], $currentKey);
$saved = $this->model->userSave($userID, [
'activate_key' => $activationData['key'],
'activate_expire' => $activationData['expire'],
]);
if (! $saved) {
$this->log(_t('users', 'Error saving information about the user #[id]', ['id' => $userID]), ['method' => __FUNCTION__]);
return false;
}
return $activationData;
}
/**
* Получаем доступные варианты email-уведомлений
* @param int $settings текущие активные настройки пользователя (битовое поле)
* @param bool $allCheckedSettings получить битовое поле всех активированных настроек
* @param string $channel
* @return array|int
*/
public function getEnotifyTypes(int $settings = 0, bool $allCheckedSettings = false, string $channel = Sendmail::CHANNEL_EMAIL)
{
$types = [
static::ENOTIFY_NEWS => [
'title' => _t('users', 'Newsletter from [site_title]', [
'site_title' => Site::title('users.enotify.news'),
]),
'a' => 0,
],
static::ENOTIFY_INTERNALMAIL => [
'title' => _t('users', 'New Messages'),
'a' => 0,
],
];
if ($channel == Sendmail::CHANNEL_SMS) {
unset($types[static::ENOTIFY_NEWS]);
}
if (Listings::commentsEnabled()) {
$types[static::ENOTIFY_LISTINGS_COMMENTS] = [
'title' => _t('users', 'New Comments on Listings'),
'a' => 0,
];
}
$types = $this->app->filter('users.enotify.types', $types, $settings, $allCheckedSettings, $channel);
foreach ($types as $k => &$v) {
if (! is_array($v)) {
$v = ['title' => $v];
}
} unset($v);
func::sortByPriority($types, 'priority', 2);
if ($allCheckedSettings) {
return ( ! empty($types) ? array_sum(array_keys($types)) : 0);
}
if (! empty($settings)) {
foreach ($types as $k => $v) {
if ($settings & $k) {
$types[$k]['a'] = 1;
}
}
}
return $types;
}
/**
* Заблокировать / разблокировать пользователя
* @param int $id ID пользователя
* @param array $opts @ref параметры
* @return bool
*/
public function userBlock($id, array &$opts = []): bool
{
$opts = $this->defaults($opts, [
'blocked_reason' => '',
'blocked' => true,
'mailSend' => true,
]);
do {
if (! $id) {
$this->errors->unknownRecord();
break;
}
$data = $this->model->userData($id, ['blocked', 'activated', 'email', 'name', 'lang']);
if (empty($data)) {
$this->errors->unknownRecord();
break;
}
$update = [];
if (! $opts['blocked']) {
$update['blocked'] = 0;
} else {
$update['blocked'] = 1;
$update['blocked_reason'] = $opts['blocked_reason'];
}
$saved = $this->model->userSave($id, $update);
if (! $saved) {
$this->errors->impossible();
break;
}
if ($update['blocked'] != $data['blocked']) {
if ($update['blocked'] === 1) {
BlockedEvent::dispatch($id);
$this->app->callModules('onUserBlocked', [$id, $update['blocked']]);
# аккаунт заблокирован
if ($opts['mailSend']) {
$this->app->sendMailTemplate(
[
'name' => $data['name'],
'email' => $data['email'],
'user_id' => $id,
'blocked_reason' => $opts['blocked_reason'],
],
'users_blocked',
$data['email'],
false,
'',
'',
$data['lang']
);
}
# завершаем сессию пользователя (если авторизован)
$this->userSessionDestroy($id, false);
} else {
# аккаунт разблокирован
UnblockedEvent::dispatch($id);
$this->app->callModules('onUserBlocked', [$id, $update['blocked']]);
if ($opts['mailSend']) {
$this->app->sendMailTemplate(
[
'name' => $data['name'],
'email' => $data['email'],
'user_id' => $id,
],
'users_unblocked',
$data['email'],
false,
'',
'',
$data['lang']
);
}
}
}
return true;
} while (false);
return false;
}
/**
* Конвертировать сгенерированного пользователя в обычного
* @param int $id ID пользователя
* @param array $opts @ref параметры
* @return bool
*/
public function userUnfake($id, array &$opts = []): bool
{
$opts = $this->defaults($opts, [
//
]);
do {
if (! $id) {
$this->errors->unknownRecord();
break;
}
$data = $this->model->userData($id, ['fake']);
if (empty($data)) {
$this->errors->unknownRecord();
break;
}
if ($data['fake']) {
$this->model->userSave($id, [
'fake' => 0,
]);
}
return true;
} while (false);
return false;
}
/**
* Формирование контакта пользователя в виде изображения
* @param string|array $text текст контакта
* @param bool|array $imageTag обворачивать в тег
* @param array $opts
* @return string base64
*/
public function contactAsImage($text, $imageTag = false, array $opts = []): string
{
func::array_defaults($opts, [
'width' => 0,
'height' => 0,
]);
$width = $opts['width'];
$height = $opts['height'];
if (is_array($text) && sizeof($text) == 1) {
$text = reset($text);
}
# Указываем шрифт
$font = $this->app->corePath('fonts/ubuntu-b.ttf');
$fontSize = 11;
$fontAngle = 0;
$calc = false;
# Определяем необходимые размера изображения
if (! $width || ! $height) {
$calc = true;
$textDimm = false;
if (function_exists('imagettfbbox')) {
if (is_array($text)) {
$textMulti = join("\n", $text);
$textDimm = imagettfbbox($fontSize, $fontAngle, $font, $textMulti);
} else {
$textDimm = imagettfbbox($fontSize, $fontAngle, $font, $text);
}
}
if ($textDimm === false) {
return (is_array($text) ? join('
', $text) : $text);
}
$width = $textDimm[4] - $textDimm[6] + 8;
$height = $textDimm[1] - $textDimm[7] + 4;
}
# Создаем холст
$image = imagecreatetruecolor($width, $height);
# Формируем прозрачный фон
imagealphablending($image, false);
$transparentColor = imagecolorallocatealpha($image, 0, 0, 0, 127);
imagefill($image, 0, 0, $transparentColor);
imagesavealpha($image, true);
# Пишем текст
$textColor = imagecolorallocate($image, 0x33, 0x33, 0x33); # цвет текста
$w = 0;
$h = 0;
if (is_array($text)) {
$i = 0;
foreach ($text as $v) {
$y = ($i++ * $fontSize) + (5 * $i) + 8;
$boundary = imagettftext($image, $fontSize, $fontAngle, 0, $y, $textColor, $font, $v);
if ($w < $boundary[2]) {
$w = $boundary[2];
}
if ($h < $boundary[3]) {
$h = $boundary[3];
}
}
} else {
$boundary = imagettftext($image, $fontSize, $fontAngle, 2, $height - 2, $textColor, $font, $text);
if ($w < $boundary[2]) {
$w = $boundary[2];
}
if ($h < $boundary[3]) {
$h = $boundary[3];
}
}
if ($calc && ($width < $w || $height < $h)) {
imagedestroy($image);
return $this->contactAsImage($text, $imageTag, [
'width' => $w + 8,
'height' => $h + 4,
]);
}
# Формируем base64 версию изображения
ob_start();
imagepng($image);
imagedestroy($image);
$data = ob_get_clean();
$data = 'data:image/png;base64,' . base64_encode($data);
if (! empty($imageTag)) {
if (! is_array($imageTag)) {
$imageTag = [];
}
$imageTag['src'] = $data;
$imageTag['alt'] = $imageTag['alt'] ?? '';
return '
';
}
return $data;
}
/**
* Отображение номеров телефонов
* @param array $phones номера телефонов в формате [[v => '123','m' => XXX], ...]
* @param bool $wrap
* @return string HTML
*/
public function phonesView(array $phones, bool $wrap = true): string
{
if (empty($phones) || !is_array($phones)) {
return '';
}
foreach ($phones as $k => &$v) {
if (is_array($v)) {
if (isset($v['v'])) {
$v = $v['v'];
} else {
unset($phones[$k]);
}
}
} unset($v);
if (! Request::isMobile()) {
$phones = $this->contactAsImage($phones, true);
} else {
$view = [];
foreach ($phones as $v) {
$phone = HTML::obfuscate($v);
$view[] = ' 'tel:' . $phone]) . '>' . $phone . '';
}
$phones = join(', ', $view);
}
return ($wrap ? '' . $phones . '' : $phones);
}
/**
* Формирование маски (скрытого вида) номера телефона
* @param string $phoneNumber номера телефона
* @return string
*/
public function phoneMask(string $phoneNumber): string
{
if ($this->app->hooksAdded('users.phone.mask.filter')) {
return $this->app->filter('users.phone.mask.filter', $phoneNumber);
}
return mb_substr(trim(strval($phoneNumber), ' -+'), 0, 2)
. $this->app->filter('users.phone.mask', 'x xxx xxxx');
}
/**
* Валидация номеров телефонов
* @param array $phones номера телефонов
* @param int $limit лимит
* @return array
*/
public function validatePhones(array $phones = [], int $limit = 0): array
{
$result = [];
foreach ($phones as $v) {
if (is_array($v)) {
$v = $v['v'] ?? '';
}
$v = preg_replace('/[^\s\+\-0-9]/', '', $v);
$v = preg_replace('/\s+/', ' ', $v);
$v = trim($v, '- ');
if (strlen($v) > 4) {
$v = mb_substr($v, 0, 20);
$v = trim($v, '- ');
$v = (strpos($v, '+') === 0 ? '+' : '') . str_replace('+', '', $v);
$result[] = [
'v' => $v,
'm' => $this->phoneMask($v),
];
}
}
if ($limit > 0 && sizeof($result) > $limit) {
$result = array_slice($result, 0, $limit);
}
return $result;
}
/**
* Формирование ключа для авторизации от имени пользователя (из админ-панели)
* @param int $userID ID пользователя
* @param string $userLastLogin дата последней авторизации
* @param string $userEmail E-mail пользователя
* @param string|null $encrypt сравнить со строкой
* @return string | bool
*/
public function adminAuthURL($userID, string $userLastLogin, string $userEmail, $encrypt = null)
{
$hash = join('|', [
$userID,
strtotime($userLastLogin),
$userEmail,
]);
if ($encrypt) {
return Crypt::decryptString($encrypt) === $hash;
}
return $this->url('login.admin', [
'encrypted' => Crypt::encryptString($hash),
'uid' => $userID,
]);
}
/**
* Инициируем событие удаления пользователя
* @param int $userID ID пользователя
* @param array $options доп. параметры
* @return void
*/
public function fireUserDeleted($userID, array $options = [])
{
DeletedEvent::dispatch($userID, $options);
$this->app->callModules('onUserDeleted', [$userID, $options]);
}
/**
* Проверка на временный e-mail
* @param string $email
* @return bool
*/
public function isEmailTemporary(string $email): bool
{
if ($this->config('users.email.temporary.check', false, TYPE_BOOL)) {
return $this->input->isEmailTemporary($email, $this->config('users.email.temporary.extra', '', TYPE_NOTAGS));
}
return false;
}
/**
* Формирование хеша для рассылки
* @param int $userID ID пользователя
* @param int $massendID ID рассылки
* @return string
*/
public function userHashGenerate($userID, int $massendID): string
{
return md5(
$userID . '!' . $massendID . 'X2/$T' . mb_substr(md5($massendID . $userID), 4, 9) . $massendID .
substr(md5('3eGH!Cm6X' . $massendID . 'C3*TB' . $userID . 'IG3[B' . $massendID . 'v@-1w' . $userID . 'm5q,E'), 2, 5)
) . '.' . $userID . '.' . $massendID;
}
/**
* Разбор хеша рассылки
* @param string $hash hash
* @return array|bool [user_id, massend_id]
*/
public function userHashValidate(string $hash)
{
if (empty($hash) || mb_strpos($hash, '.') === false) {
return false;
}
$data = explode('.', $hash, 3);
if (empty($data) || empty($data[0]) || empty($data[1]) || ! isset($data[2])) {
return false;
}
$userID = intval($data[1]);
$massendID = intval($data[2]);
$userData = $this->model->userData($userID, ['user_id']);
if (empty($userData) || empty($userData['user_id'])) {
return false;
}
if ($hash !== $this->userHashGenerate($userID, $massendID)) {
return false;
}
return [
'user_id' => $userID,
'massend_id' => $massendID,
];
}
/**
* Формирование E-mail адреса для сгенерированных пользователей
* @param string $name имя пользователя
* @return string E-mail адрес
*/
public function fakeEmailGenerate(string $name): string
{
if (empty($name)) {
return '';
}
$template = $this->config('users.fake.email.template', '{name}-fake@{host}', TYPE_STR);
return strtr($template, [
'{name}' => $name,
'{host}' => SITEHOST,
]);
}
/**
* Выполнение массовой операции над пользователями, вызывается из админ панели или крона
* @param array $params
* @return array|mixed
*/
public function cronAdminMassAction(array $params): array
{
if (! $this->isCron() && ! $this->isAdminPanel()) {
$this->errors->impossible();
return [];
}
if (empty($params['action']) || ! is_string($params['action'])) {
$this->errors->impossible();
return [];
}
if (empty($params['sql'])) {
$this->errors->impossible();
return [];
}
if ($this->isCron() && empty($params['count'])) {
$this->log(__FUNCTION__ . ' params[count] is empty');
return [];
}
$method = 'cronAdminMassAction_' . $params['action'];
if (! method_exists($this, $method)) {
$this->errors->impossible();
return [];
}
return call_user_func([$this, $method], $params);
}
/**
* Выполнение массовой блокировки пользователей
* @param array $params
* @return array
*/
protected function cronAdminMassAction_block(array $params): array
{
if (! isset($params['blocked_reason'])) {
$this->errors->impossible();
return [];
}
$last = 0;
$cnt = 0;
$count = isset($params['count']) ? $params['count'] : 100;
do {
$sql = $params['sql'];
$sql[':last_id'] = ['user_id > :last', ':last' => $last];
$data = $this->model->usersList($sql, ['user_id'], false, $this->db->prepareLimit(false, $count > 100 ? 100 : $count), 'user_id');
if (empty($data)) {
break;
}
foreach ($data as $v) {
$last = $v['user_id'];
$count--;
$o = [
'blocked_reason' => $params['blocked_reason'],
'blocked' => true,
];
if ($this->userBlock($v['user_id'], $o)) {
$cnt++;
}
}
} while (! empty($data) && $count > 0);
return [
'message' => _t('users', '[users] blocked', [
'users' => tpl::declension($cnt, _t('users', 'user;users;users')),
]),
];
}
/**
* Выполнение массовой разблокировки пользователей
* @param array $params
* @return array
*/
protected function cronAdminMassAction_unblock(array $params): array
{
$last = 0;
$cnt = 0;
$count = isset($params['count']) ? $params['count'] : 100;
do {
$sql = $params['sql'];
$sql[':last_id'] = ['user_id > :last', ':last' => $last];
$data = $this->model->usersList($sql, ['user_id'], false, $this->db->prepareLimit(false, $count > 100 ? 100 : $count), 'user_id');
if (empty($data)) {
break;
}
foreach ($data as $v) {
$last = $v['user_id'];
$count--;
$o = [
'blocked' => false,
];
if ($this->userBlock($v['user_id'], $o)) {
$cnt++;
}
}
} while (! empty($data) && $count > 0);
return [
'message' => _t('users', '[users] unblocked', [
'users' => tpl::declension($cnt, _t('users', 'user;users;users')),
]),
];
}
/**
* Выполнение массового конвертирования сгенерированных пользователей в обычных
* @param array $params
* @return array
*/
protected function cronAdminMassAction_unfake(array $params): array
{
$last = 0;
$cnt = 0;
$count = isset($params['count']) ? $params['count'] : 100;
do {
$sql = $params['sql'];
$sql[':last_id'] = ['user_id > :last', ':last' => $last];
$data = $this->model->usersList($sql, ['user_id'], false, $this->db->prepareLimit(false, $count > 100 ? 100 : $count), 'user_id');
if (empty($data)) {
break;
}
foreach ($data as $v) {
$last = $v['user_id'];
$count--;
if ($this->userUnfake($v['user_id'])) {
$cnt++;
}
}
} while (! empty($data) && $count > 0);
return [
'message' => _t('users', '[users] converted', [
'users' => tpl::declension($cnt, _t('users', 'user;users;users')),
]),
];
}
/**
* Выполнение массового удаления пользователей
* @param array $params
* @return array
*/
protected function cronAdminMassAction_delete(array $params): array
{
$onlyEmpty = ! empty($params['onlyEmpty']);
$last = 0;
$cnt = 0;
$count = isset($params['count']) ? $params['count'] : 100;
do {
$sql = $params['sql'];
$sql[':last_id'] = ['user_id > :last', ':last' => $last];
$data = $this->model->usersList($sql, ['user_id'], false, $this->db->prepareLimit(false, $count > 100 ? 100 : $count), 'user_id');
if (empty($data)) {
break;
}
$ids = [];
foreach ($data as $v) {
$last = $v['user_id'];
$count--;
if ($onlyEmpty) {
$items = (int)Listings::model()->itemsCount(['user_id' => $v['user_id']]);
if ($items > 0) {
continue;
}
}
$ids[] = $v['user_id'];
$cnt++;
}
if (! empty($ids)) {
$this->db->update(static::TABLE_USERS, [
'deleted' => static::DESTROY_BY_ADMIN,
'blocked' => 1,
'blocked_reason' => _t('users', 'The account will be deleted within 24 hours.'),
], ['user_id' => $ids]);
}
} while (! empty($data) && $count > 0);
if ($cnt) {
$this->app->cronManager()->executeOnce('users', 'cronDeleteUsers');
}
return [
'message' => _t('users', '[users] selected for deletion', [
'users' => tpl::declension($cnt, _t('users', 'user;users;users')),
]),
];
}
/**
* Выполнение массового удаления пустых аккаунтов
* @param array $params
* @return array
*/
protected function cronAdminMassAction_del_empty(array $params): array
{
$params['onlyEmpty'] = 1;
return $this->cronAdminMassAction_delete($params);
}
/**
* User services
* @param string|null $serviceKey
* @return \modules\users\UserServices | \bff\modules\svc\Service | \bff\modules\svc\ServiceManager | null
*/
public function userServices(?string $serviceKey = null)
{
$manager = Svc::getServiceManager(UserServices::KEY);
if (! $manager) {
return null;
}
if ($serviceKey) {
$service = $manager->getService($serviceKey);
if (! $service || ! $service->isEnabled()) {
return null;
}
return $service;
}
return $manager;
}
/**
* Detect if user has active service
* @param array $user
* @param string $key
* @return bool
*/
public function isUserService(array $user, string $key)
{
$manager = $this->userServices();
if (! $manager) {
return false;
}
return $manager->isUserService($user, $key);
}
/**
* Определение по строке запроса для автокомплитера вводится ли номер телефона
* @param string $query
* @return bool
*/
public function isForcePhone($query)
{
$isEmail = filter_var($query, FILTER_VALIDATE_EMAIL) !== false;
$forcePhone = false;
if (! $isEmail) {
if (preg_match('/[0-9]+/', $query) && ! preg_match('/[a-zA-Z@]+/', $query)) {
$forcePhone = true;
}
}
return $forcePhone;
}
/**
* Формирование названия для вывода результатов в ajax ответе для автокомплитера
* @param array $user данные о пользователе
* @param bool $forcePhone форсировать выдачу телефона
* @return string
*/
public function autocompleteTitleEmail($user, $forcePhone = false)
{
$title = '';
if (isset($user['email'])) {
$title = $user['email'];
}
if (empty($title) && ! empty($user['phone_number'])) {
$title = $user['phone_number'];
}
if ($forcePhone && ! empty($user['phone_number'])) {
$title = $user['phone_number'];
}
return $title;
}
}