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; } }