3 * @file src/Model/User.php
4 * @brief This file includes the User class with user related database functions
6 namespace Friendica\Model;
8 use DivineOmega\PasswordExposed;
10 use Friendica\Core\Config;
11 use Friendica\Core\Hook;
12 use Friendica\Core\L10n;
13 use Friendica\Core\Logger;
14 use Friendica\Core\PConfig;
15 use Friendica\Core\Protocol;
16 use Friendica\Core\System;
17 use Friendica\Core\Worker;
18 use Friendica\Database\DBA;
19 use Friendica\Model\Photo;
20 use Friendica\Object\Image;
21 use Friendica\Util\Crypto;
22 use Friendica\Util\DateTimeFormat;
23 use Friendica\Util\Network;
24 use Friendica\Util\Strings;
28 * @brief This class handles User related functions
33 * Returns true if a user record exists with the provided id
39 public static function exists($uid)
41 return DBA::exists('user', ['uid' => $uid]);
46 * @return array|boolean User record if it exists, false otherwise
49 public static function getById($uid)
51 return DBA::selectFirst('user', [], ['uid' => $uid]);
55 * @brief Returns the user id of a given profile URL
59 * @return integer user id
62 public static function getIdForURL($url)
64 $self = DBA::selectFirst('contact', ['uid'], ['nurl' => Strings::normaliseLink($url), 'self' => true]);
65 if (!DBA::isResult($self)) {
73 * @brief Get owner data by user id
76 * @return boolean|array
79 public static function getOwnerDataById($uid) {
80 $r = DBA::fetchFirst("SELECT
82 `user`.`prvkey` AS `uprvkey`,
88 `user`.`account-type`,
92 ON `user`.`uid` = `contact`.`uid`
93 WHERE `contact`.`uid` = ?
98 if (!DBA::isResult($r)) {
102 if (empty($r['nickname'])) {
106 // Check if the returned data is valid, otherwise fix it. See issue #6122
107 $url = System::baseUrl() . '/profile/' . $r['nickname'];
108 $addr = $r['nickname'] . '@' . substr(System::baseUrl(), strpos(System::baseUrl(), '://') + 3);
110 if (($addr != $r['addr']) || ($r['url'] != $url) || ($r['nurl'] != Strings::normaliseLink($r['url']))) {
111 Contact::updateSelfFromUserID($uid);
118 * @brief Get owner data by nick name
121 * @return boolean|array
124 public static function getOwnerDataByNick($nick)
126 $user = DBA::selectFirst('user', ['uid'], ['nickname' => $nick]);
128 if (!DBA::isResult($user)) {
132 return self::getOwnerDataById($user['uid']);
136 * @brief Returns the default group for a given user and network
138 * @param int $uid User id
139 * @param string $network network name
141 * @return int group id
142 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
144 public static function getDefaultGroup($uid, $network = '')
148 if ($network == Protocol::OSTATUS) {
149 $default_group = PConfig::get($uid, "ostatus", "default_group");
152 if ($default_group != 0) {
153 return $default_group;
156 $user = DBA::selectFirst('user', ['def_gid'], ['uid' => $uid]);
158 if (DBA::isResult($user)) {
159 $default_group = $user["def_gid"];
162 return $default_group;
167 * Authenticate a user with a clear text password
169 * @brief Authenticate a user with a clear text password
170 * @param mixed $user_info
171 * @param string $password
172 * @return int|boolean
173 * @deprecated since version 3.6
174 * @see User::getIdFromPasswordAuthentication()
176 public static function authenticate($user_info, $password)
179 return self::getIdFromPasswordAuthentication($user_info, $password);
180 } catch (Exception $ex) {
186 * Returns the user id associated with a successful password authentication
188 * @brief Authenticate a user with a clear text password
189 * @param mixed $user_info
190 * @param string $password
191 * @return int User Id if authentication is successful
194 public static function getIdFromPasswordAuthentication($user_info, $password)
196 $user = self::getAuthenticationInfo($user_info);
198 if (strpos($user['password'], '$') === false) {
199 //Legacy hash that has not been replaced by a new hash yet
200 if (self::hashPasswordLegacy($password) === $user['password']) {
201 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
205 } elseif (!empty($user['legacy_password'])) {
206 //Legacy hash that has been double-hashed and not replaced by a new hash yet
207 //Warning: `legacy_password` is not necessary in sync with the content of `password`
208 if (password_verify(self::hashPasswordLegacy($password), $user['password'])) {
209 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
213 } elseif (password_verify($password, $user['password'])) {
215 if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
216 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
222 throw new Exception(L10n::t('Login failed'));
226 * Returns authentication info from various parameters types
228 * User info can be any of the following:
231 * - User email or username or nickname
232 * - User array with at least the uid and the hashed password
234 * @param mixed $user_info
238 private static function getAuthenticationInfo($user_info)
242 if (is_object($user_info) || is_array($user_info)) {
243 if (is_object($user_info)) {
244 $user = (array) $user_info;
249 if (!isset($user['uid'])
250 || !isset($user['password'])
251 || !isset($user['legacy_password'])
253 throw new Exception(L10n::t('Not enough information to authenticate'));
255 } elseif (is_int($user_info) || is_string($user_info)) {
256 if (is_int($user_info)) {
257 $user = DBA::selectFirst('user', ['uid', 'password', 'legacy_password'],
261 'account_expired' => 0,
262 'account_removed' => 0,
267 $fields = ['uid', 'password', 'legacy_password'];
268 $condition = ["(`email` = ? OR `username` = ? OR `nickname` = ?)
269 AND NOT `blocked` AND NOT `account_expired` AND NOT `account_removed` AND `verified`",
270 $user_info, $user_info, $user_info];
271 $user = DBA::selectFirst('user', $fields, $condition);
274 if (!DBA::isResult($user)) {
275 throw new Exception(L10n::t('User not found'));
283 * Generates a human-readable random password
287 public static function generateNewPassword()
289 return ucfirst(Strings::getRandomName(8)) . mt_rand(1000, 9999);
293 * Checks if the provided plaintext password has been exposed or not
295 * @param string $password
298 public static function isPasswordExposed($password)
300 $cache = new \DivineOmega\DOFileCachePSR6\CacheItemPool();
301 $cache->changeConfig([
302 'cacheDirectory' => get_temppath() . '/password-exposed-cache/',
305 $PasswordExposedCHecker = new PasswordExposed\PasswordExposedChecker(null, $cache);
307 return $PasswordExposedCHecker->passwordExposed($password) === PasswordExposed\PasswordStatus::EXPOSED;
311 * Legacy hashing function, kept for password migration purposes
313 * @param string $password
316 private static function hashPasswordLegacy($password)
318 return hash('whirlpool', $password);
322 * Global user password hashing function
324 * @param string $password
328 public static function hashPassword($password)
330 if (!trim($password)) {
331 throw new Exception(L10n::t('Password can\'t be empty'));
334 return password_hash($password, PASSWORD_DEFAULT);
338 * Updates a user row with a new plaintext password
341 * @param string $password
345 public static function updatePassword($uid, $password)
347 $password = trim($password);
349 if (empty($password)) {
350 throw new Exception(L10n::t('Empty passwords are not allowed.'));
353 if (!Config::get('system', 'disable_password_exposed', false) && self::isPasswordExposed($password)) {
354 throw new Exception(L10n::t('The new password has been exposed in a public data dump, please choose another.'));
357 $allowed_characters = '!"#$%&\'()*+,-./;<=>?@[\]^_`{|}~';
359 if (!preg_match('/^[a-z0-9' . preg_quote($allowed_characters, '/') . ']+$/i', $password)) {
360 throw new Exception(L10n::t('The password can\'t contain accentuated letters, white spaces or colons (:)'));
363 return self::updatePasswordHashed($uid, self::hashPassword($password));
367 * Updates a user row with a new hashed password.
368 * Empties the password reset token field just in case.
371 * @param string $pasword_hashed
375 private static function updatePasswordHashed($uid, $pasword_hashed)
378 'password' => $pasword_hashed,
380 'pwdreset_time' => null,
381 'legacy_password' => false
383 return DBA::update('user', $fields, ['uid' => $uid]);
387 * @brief Checks if a nickname is in the list of the forbidden nicknames
389 * Check if a nickname is forbidden from registration on the node by the
390 * admin. Forbidden nicknames (e.g. role namess) can be configured in the
393 * @param string $nickname The nickname that should be checked
394 * @return boolean True is the nickname is blocked on the node
395 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
397 public static function isNicknameBlocked($nickname)
399 $forbidden_nicknames = Config::get('system', 'forbidden_nicknames', '');
401 // if the config variable is empty return false
402 if (empty($forbidden_nicknames)) {
406 // check if the nickname is in the list of blocked nicknames
407 $forbidden = explode(',', $forbidden_nicknames);
408 $forbidden = array_map('trim', $forbidden);
409 if (in_array(strtolower($nickname), $forbidden)) {
418 * @brief Catch-all user creation function
420 * Creates a user from the provided data array, either form fields or OpenID.
421 * Required: { username, nickname, email } or { openid_url }
423 * Performs the following:
424 * - Sends to the OpenId auth URL (if relevant)
425 * - Creates new key pairs for crypto
426 * - Create self-contact
427 * - Create profile image
431 * @throws \ErrorException
432 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
433 * @throws \ImagickException
436 public static function create(array $data)
439 $return = ['user' => null, 'password' => ''];
441 $using_invites = Config::get('system', 'invitation_only');
442 $num_invites = Config::get('system', 'number_invites');
444 $invite_id = !empty($data['invite_id']) ? Strings::escapeTags(trim($data['invite_id'])) : '';
445 $username = !empty($data['username']) ? Strings::escapeTags(trim($data['username'])) : '';
446 $nickname = !empty($data['nickname']) ? Strings::escapeTags(trim($data['nickname'])) : '';
447 $email = !empty($data['email']) ? Strings::escapeTags(trim($data['email'])) : '';
448 $openid_url = !empty($data['openid_url']) ? Strings::escapeTags(trim($data['openid_url'])) : '';
449 $photo = !empty($data['photo']) ? Strings::escapeTags(trim($data['photo'])) : '';
450 $password = !empty($data['password']) ? trim($data['password']) : '';
451 $password1 = !empty($data['password1']) ? trim($data['password1']) : '';
452 $confirm = !empty($data['confirm']) ? trim($data['confirm']) : '';
453 $blocked = !empty($data['blocked']);
454 $verified = !empty($data['verified']);
455 $language = !empty($data['language']) ? Strings::escapeTags(trim($data['language'])) : 'en';
457 $publish = !empty($data['profile_publish_reg']);
458 $netpublish = $publish && Config::get('system', 'directory');
460 if ($password1 != $confirm) {
461 throw new Exception(L10n::t('Passwords do not match. Password unchanged.'));
462 } elseif ($password1 != '') {
463 $password = $password1;
466 if ($using_invites) {
468 throw new Exception(L10n::t('An invitation is required.'));
471 if (!Register::existsByHash($invite_id)) {
472 throw new Exception(L10n::t('Invitation could not be verified.'));
476 if (empty($username) || empty($email) || empty($nickname)) {
478 if (!Network::isUrlValid($openid_url)) {
479 throw new Exception(L10n::t('Invalid OpenID url'));
481 $_SESSION['register'] = 1;
482 $_SESSION['openid'] = $openid_url;
484 $openid = new LightOpenID($a->getHostName());
485 $openid->identity = $openid_url;
486 $openid->returnUrl = System::baseUrl() . '/openid';
487 $openid->required = ['namePerson/friendly', 'contact/email', 'namePerson'];
488 $openid->optional = ['namePerson/first', 'media/image/aspect11', 'media/image/default'];
490 $authurl = $openid->authUrl();
491 } catch (Exception $e) {
492 throw new Exception(L10n::t('We encountered a problem while logging in with the OpenID you provided. Please check the correct spelling of the ID.') . EOL . EOL . L10n::t('The error message was:') . $e->getMessage(), 0, $e);
494 System::externalRedirect($authurl);
498 throw new Exception(L10n::t('Please enter the required information.'));
501 if (!Network::isUrlValid($openid_url)) {
507 // collapse multiple spaces in name
508 $username = preg_replace('/ +/', ' ', $username);
510 $username_min_length = max(1, min(64, intval(Config::get('system', 'username_min_length', 3))));
511 $username_max_length = max(1, min(64, intval(Config::get('system', 'username_max_length', 48))));
513 if ($username_min_length > $username_max_length) {
514 Logger::log(L10n::t('system.username_min_length (%s) and system.username_max_length (%s) are excluding each other, swapping values.', $username_min_length, $username_max_length), Logger::WARNING);
515 $tmp = $username_min_length;
516 $username_min_length = $username_max_length;
517 $username_max_length = $tmp;
520 if (mb_strlen($username) < $username_min_length) {
521 throw new Exception(L10n::tt('Username should be at least %s character.', 'Username should be at least %s characters.', $username_min_length));
524 if (mb_strlen($username) > $username_max_length) {
525 throw new Exception(L10n::tt('Username should be at most %s character.', 'Username should be at most %s characters.', $username_max_length));
528 // So now we are just looking for a space in the full name.
529 $loose_reg = Config::get('system', 'no_regfullname');
531 $username = mb_convert_case($username, MB_CASE_TITLE, 'UTF-8');
532 if (strpos($username, ' ') === false) {
533 throw new Exception(L10n::t("That doesn't appear to be your full (First Last) name."));
537 if (!Network::isEmailDomainAllowed($email)) {
538 throw new Exception(L10n::t('Your email domain is not among those allowed on this site.'));
541 if (!filter_var($email, FILTER_VALIDATE_EMAIL) || !Network::isEmailDomainValid($email)) {
542 throw new Exception(L10n::t('Not a valid email address.'));
544 if (self::isNicknameBlocked($nickname)) {
545 throw new Exception(L10n::t('The nickname was blocked from registration by the nodes admin.'));
548 if (Config::get('system', 'block_extended_register', false) && DBA::exists('user', ['email' => $email])) {
549 throw new Exception(L10n::t('Cannot use that email.'));
552 // Disallow somebody creating an account using openid that uses the admin email address,
553 // since openid bypasses email verification. We'll allow it if there is not yet an admin account.
554 if (Config::get('config', 'admin_email') && strlen($openid_url)) {
555 $adminlist = explode(',', str_replace(' ', '', strtolower(Config::get('config', 'admin_email'))));
556 if (in_array(strtolower($email), $adminlist)) {
557 throw new Exception(L10n::t('Cannot use that email.'));
561 $nickname = $data['nickname'] = strtolower($nickname);
563 if (!preg_match('/^[a-z0-9][a-z0-9\_]*$/', $nickname)) {
564 throw new Exception(L10n::t('Your nickname can only contain a-z, 0-9 and _.'));
567 // Check existing and deleted accounts for this nickname.
568 if (DBA::exists('user', ['nickname' => $nickname])
569 || DBA::exists('userd', ['username' => $nickname])
571 throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
574 $new_password = strlen($password) ? $password : User::generateNewPassword();
575 $new_password_encoded = self::hashPassword($new_password);
577 $return['password'] = $new_password;
579 $keys = Crypto::newKeypair(4096);
580 if ($keys === false) {
581 throw new Exception(L10n::t('SERIOUS ERROR: Generation of security keys failed.'));
584 $prvkey = $keys['prvkey'];
585 $pubkey = $keys['pubkey'];
587 // Create another keypair for signing/verifying salmon protocol messages.
588 $sres = Crypto::newKeypair(512);
589 $sprvkey = $sres['prvkey'];
590 $spubkey = $sres['pubkey'];
592 $insert_result = DBA::insert('user', [
593 'guid' => System::createUUID(),
594 'username' => $username,
595 'password' => $new_password_encoded,
597 'openid' => $openid_url,
598 'nickname' => $nickname,
601 'spubkey' => $spubkey,
602 'sprvkey' => $sprvkey,
603 'verified' => $verified,
604 'blocked' => $blocked,
605 'language' => $language,
607 'register_date' => DateTimeFormat::utcNow(),
608 'default-location' => ''
611 if ($insert_result) {
612 $uid = DBA::lastInsertId();
613 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
615 throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
619 throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
622 // if somebody clicked submit twice very quickly, they could end up with two accounts
623 // due to race condition. Remove this one.
624 $user_count = DBA::count('user', ['nickname' => $nickname]);
625 if ($user_count > 1) {
626 DBA::delete('user', ['uid' => $uid]);
628 throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
631 $insert_result = DBA::insert('profile', [
634 'photo' => System::baseUrl() . "/photo/profile/{$uid}.jpg",
635 'thumb' => System::baseUrl() . "/photo/avatar/{$uid}.jpg",
636 'publish' => $publish,
638 'net-publish' => $netpublish,
639 'profile-name' => L10n::t('default')
641 if (!$insert_result) {
642 DBA::delete('user', ['uid' => $uid]);
644 throw new Exception(L10n::t('An error occurred creating your default profile. Please try again.'));
647 // Create the self contact
648 if (!Contact::createSelfFromUserId($uid)) {
649 DBA::delete('user', ['uid' => $uid]);
651 throw new Exception(L10n::t('An error occurred creating your self contact. Please try again.'));
654 // Create a group with no members. This allows somebody to use it
655 // right away as a default group for new contacts.
656 $def_gid = Group::create($uid, L10n::t('Friends'));
658 DBA::delete('user', ['uid' => $uid]);
660 throw new Exception(L10n::t('An error occurred creating your default contact group. Please try again.'));
663 $fields = ['def_gid' => $def_gid];
664 if (Config::get('system', 'newuser_private') && $def_gid) {
665 $fields['allow_gid'] = '<' . $def_gid . '>';
668 DBA::update('user', $fields, ['uid' => $uid]);
670 // if we have no OpenID photo try to look up an avatar
671 if (!strlen($photo)) {
672 $photo = Network::lookupAvatarByEmail($email);
675 // unless there is no avatar-addon loaded
676 if (strlen($photo)) {
677 $photo_failure = false;
679 $filename = basename($photo);
680 $img_str = Network::fetchUrl($photo, true);
681 // guess mimetype from headers or filename
682 $type = Image::guessType($photo, true);
684 $Image = new Image($img_str, $type);
685 if ($Image->isValid()) {
686 $Image->scaleToSquare(300);
688 $hash = Photo::newResource();
690 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 4);
693 $photo_failure = true;
696 $Image->scaleDown(80);
698 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 5);
701 $photo_failure = true;
704 $Image->scaleDown(48);
706 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 6);
709 $photo_failure = true;
712 if (!$photo_failure) {
713 Photo::update(['profile' => 1], ['resource-id' => $hash]);
718 Hook::callAll('register_account', $uid);
720 $return['user'] = $user;
725 * @brief Sends pending registration confirmation email
727 * @param array $user User record array
728 * @param string $sitename
729 * @param string $siteurl
730 * @param string $password Plaintext password
731 * @return NULL|boolean from notification() and email() inherited
732 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
734 public static function sendRegisterPendingEmail($user, $sitename, $siteurl, $password)
736 $body = Strings::deindent(L10n::t('
738 Thank you for registering at %2$s. Your account is pending for approval by the administrator.
740 Your login details are as follows:
746 $user['username'], $sitename, $siteurl, $user['nickname'], $password
749 return notification([
750 'type' => SYSTEM_EMAIL,
751 'uid' => $user['uid'],
752 'to_email' => $user['email'],
753 'subject' => L10n::t('Registration at %s', $sitename),
759 * @brief Sends registration confirmation
761 * It's here as a function because the mail is sent from different parts
763 * @param array $user User record array
764 * @param string $sitename
765 * @param string $siteurl
766 * @param string $password Plaintext password
767 * @return NULL|boolean from notification() and email() inherited
768 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
770 public static function sendRegisterOpenEmail($user, $sitename, $siteurl, $password)
772 $preamble = Strings::deindent(L10n::t('
774 Thank you for registering at %2$s. Your account has been created.
776 $user['username'], $sitename
778 $body = Strings::deindent(L10n::t('
779 The login details are as follows:
785 You may change your password from your account "Settings" page after logging
788 Please take a few moments to review the other account settings on that page.
790 You may also wish to add some basic information to your default profile
791 ' . "\x28" . 'on the "Profiles" page' . "\x29" . ' so that other people can easily find you.
793 We recommend setting your full name, adding a profile photo,
794 adding some profile "keywords" ' . "\x28" . 'very useful in making new friends' . "\x29" . ' - and
795 perhaps what country you live in; if you do not wish to be more specific
798 We fully respect your right to privacy, and none of these items are necessary.
799 If you are new and do not know anybody here, they may help
800 you to make some new and interesting friends.
802 If you ever want to delete your account, you can do so at %3$s/removeme
804 Thank you and welcome to %2$s.',
805 $user['nickname'], $sitename, $siteurl, $user['username'], $password
808 return notification([
809 'uid' => $user['uid'],
810 'language' => $user['language'],
811 'type' => SYSTEM_EMAIL,
812 'to_email' => $user['email'],
813 'subject' => L10n::t('Registration details for %s', $sitename),
814 'preamble' => $preamble,
820 * @param object $uid user to remove
822 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
824 public static function remove($uid)
832 Logger::log('Removing user: ' . $uid);
834 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
836 Hook::callAll('remove_user', $user);
838 // save username (actually the nickname as it is guaranteed
839 // unique), so it cannot be re-registered in the future.
840 DBA::insert('userd', ['username' => $user['nickname']]);
842 // The user and related data will be deleted in "cron_expire_and_remove_users" (cronjobs.php)
843 DBA::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utc('now + 7 day')], ['uid' => $uid]);
844 Worker::add(PRIORITY_HIGH, 'Notifier', 'removeme', $uid);
846 // Send an update to the directory
847 $self = DBA::selectFirst('contact', ['url'], ['uid' => $uid, 'self' => true]);
848 Worker::add(PRIORITY_LOW, 'Directory', $self['url']);
850 // Remove the user relevant data
851 Worker::add(PRIORITY_LOW, 'RemoveUser', $uid);
857 * Return all identities to a user
859 * @param int $uid The user id
860 * @return array All identities for this user
862 * Example for a return:
866 * 'username' => 'maxmuster',
867 * 'nickname' => 'Max Mustermann'
871 * 'username' => 'johndoe',
872 * 'nickname' => 'John Doe'
877 public static function identities($uid)
881 $user = DBA::selectFirst('user', ['uid', 'nickname', 'username', 'parent-uid'], ['uid' => $uid]);
882 if (!DBA::isResult($user)) {
886 if ($user['parent-uid'] == 0) {
887 // First add our own entry
888 $identities = [['uid' => $user['uid'],
889 'username' => $user['username'],
890 'nickname' => $user['nickname']]];
892 // Then add all the children
893 $r = DBA::select('user', ['uid', 'username', 'nickname'],
894 ['parent-uid' => $user['uid'], 'account_removed' => false]);
895 if (DBA::isResult($r)) {
896 $identities = array_merge($identities, DBA::toArray($r));
899 // First entry is our parent
900 $r = DBA::select('user', ['uid', 'username', 'nickname'],
901 ['uid' => $user['parent-uid'], 'account_removed' => false]);
902 if (DBA::isResult($r)) {
903 $identities = DBA::toArray($r);
906 // Then add all siblings
907 $r = DBA::select('user', ['uid', 'username', 'nickname'],
908 ['parent-uid' => $user['parent-uid'], 'account_removed' => false]);
909 if (DBA::isResult($r)) {
910 $identities = array_merge($identities, DBA::toArray($r));
914 $r = DBA::p("SELECT `user`.`uid`, `user`.`username`, `user`.`nickname`
916 INNER JOIN `user` ON `manage`.`mid` = `user`.`uid`
917 WHERE `user`.`account_removed` = 0 AND `manage`.`uid` = ?",
920 if (DBA::isResult($r)) {
921 $identities = array_merge($identities, DBA::toArray($r));