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\Object\Image;
20 use Friendica\Util\Crypto;
21 use Friendica\Util\DateTimeFormat;
22 use Friendica\Util\Network;
23 use Friendica\Util\Strings;
27 * @brief This class handles User related functions
32 * Returns true if a user record exists with the provided id
38 public static function exists($uid)
40 return DBA::exists('user', ['uid' => $uid]);
45 * @return array|boolean User record if it exists, false otherwise
48 public static function getById($uid)
50 return DBA::selectFirst('user', [], ['uid' => $uid]);
54 * @brief Returns the user id of a given profile URL
58 * @return integer user id
61 public static function getIdForURL($url)
63 $self = DBA::selectFirst('contact', ['uid'], ['nurl' => Strings::normaliseLink($url), 'self' => true]);
64 if (!DBA::isResult($self)) {
72 * @brief Get owner data by user id
75 * @return boolean|array
78 public static function getOwnerDataById($uid) {
79 $r = DBA::fetchFirst("SELECT
81 `user`.`prvkey` AS `uprvkey`,
87 `user`.`account-type`,
91 ON `user`.`uid` = `contact`.`uid`
92 WHERE `contact`.`uid` = ?
97 if (!DBA::isResult($r)) {
101 if (empty($r['nickname'])) {
105 // Check if the returned data is valid, otherwise fix it. See issue #6122
106 $url = System::baseUrl() . '/profile/' . $r['nickname'];
107 $addr = $r['nickname'] . '@' . substr(System::baseUrl(), strpos(System::baseUrl(), '://') + 3);
109 if (($addr != $r['addr']) || ($r['url'] != $url) || ($r['nurl'] != Strings::normaliseLink($r['url']))) {
110 Contact::updateSelfFromUserID($uid);
117 * @brief Get owner data by nick name
120 * @return boolean|array
123 public static function getOwnerDataByNick($nick)
125 $user = DBA::selectFirst('user', ['uid'], ['nickname' => $nick]);
127 if (!DBA::isResult($user)) {
131 return self::getOwnerDataById($user['uid']);
135 * @brief Returns the default group for a given user and network
137 * @param int $uid User id
138 * @param string $network network name
140 * @return int group id
141 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
143 public static function getDefaultGroup($uid, $network = '')
147 if ($network == Protocol::OSTATUS) {
148 $default_group = PConfig::get($uid, "ostatus", "default_group");
151 if ($default_group != 0) {
152 return $default_group;
155 $user = DBA::selectFirst('user', ['def_gid'], ['uid' => $uid]);
157 if (DBA::isResult($user)) {
158 $default_group = $user["def_gid"];
161 return $default_group;
166 * Authenticate a user with a clear text password
168 * @brief Authenticate a user with a clear text password
169 * @param mixed $user_info
170 * @param string $password
171 * @return int|boolean
172 * @deprecated since version 3.6
173 * @see User::getIdFromPasswordAuthentication()
175 public static function authenticate($user_info, $password)
178 return self::getIdFromPasswordAuthentication($user_info, $password);
179 } catch (Exception $ex) {
185 * Returns the user id associated with a successful password authentication
187 * @brief Authenticate a user with a clear text password
188 * @param mixed $user_info
189 * @param string $password
190 * @return int User Id if authentication is successful
193 public static function getIdFromPasswordAuthentication($user_info, $password)
195 $user = self::getAuthenticationInfo($user_info);
197 if (strpos($user['password'], '$') === false) {
198 //Legacy hash that has not been replaced by a new hash yet
199 if (self::hashPasswordLegacy($password) === $user['password']) {
200 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
204 } elseif (!empty($user['legacy_password'])) {
205 //Legacy hash that has been double-hashed and not replaced by a new hash yet
206 //Warning: `legacy_password` is not necessary in sync with the content of `password`
207 if (password_verify(self::hashPasswordLegacy($password), $user['password'])) {
208 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
212 } elseif (password_verify($password, $user['password'])) {
214 if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
215 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
221 throw new Exception(L10n::t('Login failed'));
225 * Returns authentication info from various parameters types
227 * User info can be any of the following:
230 * - User email or username or nickname
231 * - User array with at least the uid and the hashed password
233 * @param mixed $user_info
237 private static function getAuthenticationInfo($user_info)
241 if (is_object($user_info) || is_array($user_info)) {
242 if (is_object($user_info)) {
243 $user = (array) $user_info;
248 if (!isset($user['uid'])
249 || !isset($user['password'])
250 || !isset($user['legacy_password'])
252 throw new Exception(L10n::t('Not enough information to authenticate'));
254 } elseif (is_int($user_info) || is_string($user_info)) {
255 if (is_int($user_info)) {
256 $user = DBA::selectFirst('user', ['uid', 'password', 'legacy_password'],
260 'account_expired' => 0,
261 'account_removed' => 0,
266 $fields = ['uid', 'password', 'legacy_password'];
267 $condition = ["(`email` = ? OR `username` = ? OR `nickname` = ?)
268 AND NOT `blocked` AND NOT `account_expired` AND NOT `account_removed` AND `verified`",
269 $user_info, $user_info, $user_info];
270 $user = DBA::selectFirst('user', $fields, $condition);
273 if (!DBA::isResult($user)) {
274 throw new Exception(L10n::t('User not found'));
282 * Generates a human-readable random password
286 public static function generateNewPassword()
288 return ucfirst(Strings::getRandomName(8)) . mt_rand(1000, 9999);
292 * Checks if the provided plaintext password has been exposed or not
294 * @param string $password
297 public static function isPasswordExposed($password)
299 $cache = new \DivineOmega\DOFileCachePSR6\CacheItemPool();
300 $cache->changeConfig([
301 'cacheDirectory' => get_temppath() . '/password-exposed-cache/',
304 $PasswordExposedCHecker = new PasswordExposed\PasswordExposedChecker(null, $cache);
306 return $PasswordExposedCHecker->passwordExposed($password) === PasswordExposed\PasswordStatus::EXPOSED;
310 * Legacy hashing function, kept for password migration purposes
312 * @param string $password
315 private static function hashPasswordLegacy($password)
317 return hash('whirlpool', $password);
321 * Global user password hashing function
323 * @param string $password
327 public static function hashPassword($password)
329 if (!trim($password)) {
330 throw new Exception(L10n::t('Password can\'t be empty'));
333 return password_hash($password, PASSWORD_DEFAULT);
337 * Updates a user row with a new plaintext password
340 * @param string $password
344 public static function updatePassword($uid, $password)
346 $password = trim($password);
348 if (empty($password)) {
349 throw new Exception(L10n::t('Empty passwords are not allowed.'));
352 if (!Config::get('system', 'disable_password_exposed', false) && self::isPasswordExposed($password)) {
353 throw new Exception(L10n::t('The new password has been exposed in a public data dump, please choose another.'));
356 $allowed_characters = '!"#$%&\'()*+,-./;<=>?@[\]^_`{|}~';
358 if (!preg_match('/^[a-z0-9' . preg_quote($allowed_characters, '/') . ']+$/i', $password)) {
359 throw new Exception(L10n::t('The password can\'t contain accentuated letters, white spaces or colons (:)'));
362 return self::updatePasswordHashed($uid, self::hashPassword($password));
366 * Updates a user row with a new hashed password.
367 * Empties the password reset token field just in case.
370 * @param string $pasword_hashed
374 private static function updatePasswordHashed($uid, $pasword_hashed)
377 'password' => $pasword_hashed,
379 'pwdreset_time' => null,
380 'legacy_password' => false
382 return DBA::update('user', $fields, ['uid' => $uid]);
386 * @brief Checks if a nickname is in the list of the forbidden nicknames
388 * Check if a nickname is forbidden from registration on the node by the
389 * admin. Forbidden nicknames (e.g. role namess) can be configured in the
392 * @param string $nickname The nickname that should be checked
393 * @return boolean True is the nickname is blocked on the node
394 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
396 public static function isNicknameBlocked($nickname)
398 $forbidden_nicknames = Config::get('system', 'forbidden_nicknames', '');
400 // if the config variable is empty return false
401 if (empty($forbidden_nicknames)) {
405 // check if the nickname is in the list of blocked nicknames
406 $forbidden = explode(',', $forbidden_nicknames);
407 $forbidden = array_map('trim', $forbidden);
408 if (in_array(strtolower($nickname), $forbidden)) {
417 * @brief Catch-all user creation function
419 * Creates a user from the provided data array, either form fields or OpenID.
420 * Required: { username, nickname, email } or { openid_url }
422 * Performs the following:
423 * - Sends to the OpenId auth URL (if relevant)
424 * - Creates new key pairs for crypto
425 * - Create self-contact
426 * - Create profile image
430 * @throws \ErrorException
431 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
432 * @throws \ImagickException
435 public static function create(array $data)
438 $return = ['user' => null, 'password' => ''];
440 $using_invites = Config::get('system', 'invitation_only');
441 $num_invites = Config::get('system', 'number_invites');
443 $invite_id = !empty($data['invite_id']) ? Strings::escapeTags(trim($data['invite_id'])) : '';
444 $username = !empty($data['username']) ? Strings::escapeTags(trim($data['username'])) : '';
445 $nickname = !empty($data['nickname']) ? Strings::escapeTags(trim($data['nickname'])) : '';
446 $email = !empty($data['email']) ? Strings::escapeTags(trim($data['email'])) : '';
447 $openid_url = !empty($data['openid_url']) ? Strings::escapeTags(trim($data['openid_url'])) : '';
448 $photo = !empty($data['photo']) ? Strings::escapeTags(trim($data['photo'])) : '';
449 $password = !empty($data['password']) ? trim($data['password']) : '';
450 $password1 = !empty($data['password1']) ? trim($data['password1']) : '';
451 $confirm = !empty($data['confirm']) ? trim($data['confirm']) : '';
452 $blocked = !empty($data['blocked']);
453 $verified = !empty($data['verified']);
454 $language = !empty($data['language']) ? Strings::escapeTags(trim($data['language'])) : 'en';
456 $publish = !empty($data['profile_publish_reg']);
457 $netpublish = $publish && Config::get('system', 'directory');
459 if ($password1 != $confirm) {
460 throw new Exception(L10n::t('Passwords do not match. Password unchanged.'));
461 } elseif ($password1 != '') {
462 $password = $password1;
465 if ($using_invites) {
467 throw new Exception(L10n::t('An invitation is required.'));
470 if (!Register::existsByHash($invite_id)) {
471 throw new Exception(L10n::t('Invitation could not be verified.'));
475 if (empty($username) || empty($email) || empty($nickname)) {
477 if (!Network::isUrlValid($openid_url)) {
478 throw new Exception(L10n::t('Invalid OpenID url'));
480 $_SESSION['register'] = 1;
481 $_SESSION['openid'] = $openid_url;
483 $openid = new LightOpenID($a->getHostName());
484 $openid->identity = $openid_url;
485 $openid->returnUrl = System::baseUrl() . '/openid';
486 $openid->required = ['namePerson/friendly', 'contact/email', 'namePerson'];
487 $openid->optional = ['namePerson/first', 'media/image/aspect11', 'media/image/default'];
489 $authurl = $openid->authUrl();
490 } catch (Exception $e) {
491 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);
493 System::externalRedirect($authurl);
497 throw new Exception(L10n::t('Please enter the required information.'));
500 if (!Network::isUrlValid($openid_url)) {
506 // collapse multiple spaces in name
507 $username = preg_replace('/ +/', ' ', $username);
509 $username_min_length = max(1, min(64, intval(Config::get('system', 'username_min_length', 3))));
510 $username_max_length = max(1, min(64, intval(Config::get('system', 'username_max_length', 48))));
512 if ($username_min_length > $username_max_length) {
513 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);
514 $tmp = $username_min_length;
515 $username_min_length = $username_max_length;
516 $username_max_length = $tmp;
519 if (mb_strlen($username) < $username_min_length) {
520 throw new Exception(L10n::tt('Username should be at least %s character.', 'Username should be at least %s characters.', $username_min_length));
523 if (mb_strlen($username) > $username_max_length) {
524 throw new Exception(L10n::tt('Username should be at most %s character.', 'Username should be at most %s characters.', $username_max_length));
527 // So now we are just looking for a space in the full name.
528 $loose_reg = Config::get('system', 'no_regfullname');
530 $username = mb_convert_case($username, MB_CASE_TITLE, 'UTF-8');
531 if (strpos($username, ' ') === false) {
532 throw new Exception(L10n::t("That doesn't appear to be your full (First Last) name."));
536 if (!Network::isEmailDomainAllowed($email)) {
537 throw new Exception(L10n::t('Your email domain is not among those allowed on this site.'));
540 if (!filter_var($email, FILTER_VALIDATE_EMAIL) || !Network::isEmailDomainValid($email)) {
541 throw new Exception(L10n::t('Not a valid email address.'));
543 if (self::isNicknameBlocked($nickname)) {
544 throw new Exception(L10n::t('The nickname was blocked from registration by the nodes admin.'));
547 if (Config::get('system', 'block_extended_register', false) && DBA::exists('user', ['email' => $email])) {
548 throw new Exception(L10n::t('Cannot use that email.'));
551 // Disallow somebody creating an account using openid that uses the admin email address,
552 // since openid bypasses email verification. We'll allow it if there is not yet an admin account.
553 if (Config::get('config', 'admin_email') && strlen($openid_url)) {
554 $adminlist = explode(',', str_replace(' ', '', strtolower(Config::get('config', 'admin_email'))));
555 if (in_array(strtolower($email), $adminlist)) {
556 throw new Exception(L10n::t('Cannot use that email.'));
560 $nickname = $data['nickname'] = strtolower($nickname);
562 if (!preg_match('/^[a-z0-9][a-z0-9\_]*$/', $nickname)) {
563 throw new Exception(L10n::t('Your nickname can only contain a-z, 0-9 and _.'));
566 // Check existing and deleted accounts for this nickname.
567 if (DBA::exists('user', ['nickname' => $nickname])
568 || DBA::exists('userd', ['username' => $nickname])
570 throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
573 $new_password = strlen($password) ? $password : User::generateNewPassword();
574 $new_password_encoded = self::hashPassword($new_password);
576 $return['password'] = $new_password;
578 $keys = Crypto::newKeypair(4096);
579 if ($keys === false) {
580 throw new Exception(L10n::t('SERIOUS ERROR: Generation of security keys failed.'));
583 $prvkey = $keys['prvkey'];
584 $pubkey = $keys['pubkey'];
586 // Create another keypair for signing/verifying salmon protocol messages.
587 $sres = Crypto::newKeypair(512);
588 $sprvkey = $sres['prvkey'];
589 $spubkey = $sres['pubkey'];
591 $insert_result = DBA::insert('user', [
592 'guid' => System::createUUID(),
593 'username' => $username,
594 'password' => $new_password_encoded,
596 'openid' => $openid_url,
597 'nickname' => $nickname,
600 'spubkey' => $spubkey,
601 'sprvkey' => $sprvkey,
602 'verified' => $verified,
603 'blocked' => $blocked,
604 'language' => $language,
606 'register_date' => DateTimeFormat::utcNow(),
607 'default-location' => ''
610 if ($insert_result) {
611 $uid = DBA::lastInsertId();
612 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
614 throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
618 throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
621 // if somebody clicked submit twice very quickly, they could end up with two accounts
622 // due to race condition. Remove this one.
623 $user_count = DBA::count('user', ['nickname' => $nickname]);
624 if ($user_count > 1) {
625 DBA::delete('user', ['uid' => $uid]);
627 throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
630 $insert_result = DBA::insert('profile', [
633 'photo' => System::baseUrl() . "/photo/profile/{$uid}.jpg",
634 'thumb' => System::baseUrl() . "/photo/avatar/{$uid}.jpg",
635 'publish' => $publish,
637 'net-publish' => $netpublish,
638 'profile-name' => L10n::t('default')
640 if (!$insert_result) {
641 DBA::delete('user', ['uid' => $uid]);
643 throw new Exception(L10n::t('An error occurred creating your default profile. Please try again.'));
646 // Create the self contact
647 if (!Contact::createSelfFromUserId($uid)) {
648 DBA::delete('user', ['uid' => $uid]);
650 throw new Exception(L10n::t('An error occurred creating your self contact. Please try again.'));
653 // Create a group with no members. This allows somebody to use it
654 // right away as a default group for new contacts.
655 $def_gid = Group::create($uid, L10n::t('Friends'));
657 DBA::delete('user', ['uid' => $uid]);
659 throw new Exception(L10n::t('An error occurred creating your default contact group. Please try again.'));
662 $fields = ['def_gid' => $def_gid];
663 if (Config::get('system', 'newuser_private') && $def_gid) {
664 $fields['allow_gid'] = '<' . $def_gid . '>';
667 DBA::update('user', $fields, ['uid' => $uid]);
669 // if we have no OpenID photo try to look up an avatar
670 if (!strlen($photo)) {
671 $photo = Network::lookupAvatarByEmail($email);
674 // unless there is no avatar-addon loaded
675 if (strlen($photo)) {
676 $photo_failure = false;
678 $filename = basename($photo);
679 $img_str = Network::fetchUrl($photo, true);
680 // guess mimetype from headers or filename
681 $type = Image::guessType($photo, true);
683 $Image = new Image($img_str, $type);
684 if ($Image->isValid()) {
685 $Image->scaleToSquare(300);
687 $hash = Photo::newResource();
689 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 4);
692 $photo_failure = true;
695 $Image->scaleDown(80);
697 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 5);
700 $photo_failure = true;
703 $Image->scaleDown(48);
705 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 6);
708 $photo_failure = true;
711 if (!$photo_failure) {
712 Photo::update(['profile' => 1], ['resource-id' => $hash]);
717 Hook::callAll('register_account', $uid);
719 $return['user'] = $user;
724 * @brief Sends pending registration confirmation email
726 * @param array $user User record array
727 * @param string $sitename
728 * @param string $siteurl
729 * @param string $password Plaintext password
730 * @return NULL|boolean from notification() and email() inherited
731 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
733 public static function sendRegisterPendingEmail($user, $sitename, $siteurl, $password)
735 $body = Strings::deindent(L10n::t('
737 Thank you for registering at %2$s. Your account is pending for approval by the administrator.
739 Your login details are as follows:
745 $user['username'], $sitename, $siteurl, $user['nickname'], $password
748 return notification([
749 'type' => SYSTEM_EMAIL,
750 'uid' => $user['uid'],
751 'to_email' => $user['email'],
752 'subject' => L10n::t('Registration at %s', $sitename),
758 * @brief Sends registration confirmation
760 * It's here as a function because the mail is sent from different parts
762 * @param array $user User record array
763 * @param string $sitename
764 * @param string $siteurl
765 * @param string $password Plaintext password
766 * @return NULL|boolean from notification() and email() inherited
767 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
769 public static function sendRegisterOpenEmail($user, $sitename, $siteurl, $password)
771 $preamble = Strings::deindent(L10n::t('
773 Thank you for registering at %2$s. Your account has been created.
775 $user['username'], $sitename
777 $body = Strings::deindent(L10n::t('
778 The login details are as follows:
784 You may change your password from your account "Settings" page after logging
787 Please take a few moments to review the other account settings on that page.
789 You may also wish to add some basic information to your default profile
790 ' . "\x28" . 'on the "Profiles" page' . "\x29" . ' so that other people can easily find you.
792 We recommend setting your full name, adding a profile photo,
793 adding some profile "keywords" ' . "\x28" . 'very useful in making new friends' . "\x29" . ' - and
794 perhaps what country you live in; if you do not wish to be more specific
797 We fully respect your right to privacy, and none of these items are necessary.
798 If you are new and do not know anybody here, they may help
799 you to make some new and interesting friends.
801 If you ever want to delete your account, you can do so at %3$s/removeme
803 Thank you and welcome to %2$s.',
804 $user['nickname'], $sitename, $siteurl, $user['username'], $password
807 return notification([
808 'uid' => $user['uid'],
809 'language' => $user['language'],
810 'type' => SYSTEM_EMAIL,
811 'to_email' => $user['email'],
812 'subject' => L10n::t('Registration details for %s', $sitename),
813 'preamble' => $preamble,
819 * @param object $uid user to remove
821 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
823 public static function remove($uid)
831 Logger::log('Removing user: ' . $uid);
833 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
835 Hook::callAll('remove_user', $user);
837 // save username (actually the nickname as it is guaranteed
838 // unique), so it cannot be re-registered in the future.
839 DBA::insert('userd', ['username' => $user['nickname']]);
841 // The user and related data will be deleted in "cron_expire_and_remove_users" (cronjobs.php)
842 DBA::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utc('now + 7 day')], ['uid' => $uid]);
843 Worker::add(PRIORITY_HIGH, 'Notifier', 'removeme', $uid);
845 // Send an update to the directory
846 $self = DBA::selectFirst('contact', ['url'], ['uid' => $uid, 'self' => true]);
847 Worker::add(PRIORITY_LOW, 'Directory', $self['url']);
849 // Remove the user relevant data
850 Worker::add(PRIORITY_LOW, 'RemoveUser', $uid);
856 * Return all identities to a user
858 * @param int $uid The user id
859 * @return array All identities for this user
861 * Example for a return:
865 * 'username' => 'maxmuster',
866 * 'nickname' => 'Max Mustermann'
870 * 'username' => 'johndoe',
871 * 'nickname' => 'John Doe'
876 public static function identities($uid)
880 $user = DBA::selectFirst('user', ['uid', 'nickname', 'username', 'parent-uid'], ['uid' => $uid]);
881 if (!DBA::isResult($user)) {
885 if ($user['parent-uid'] == 0) {
886 // First add our own entry
887 $identities = [['uid' => $user['uid'],
888 'username' => $user['username'],
889 'nickname' => $user['nickname']]];
891 // Then add all the children
892 $r = DBA::select('user', ['uid', 'username', 'nickname'],
893 ['parent-uid' => $user['uid'], 'account_removed' => false]);
894 if (DBA::isResult($r)) {
895 $identities = array_merge($identities, DBA::toArray($r));
898 // First entry is our parent
899 $r = DBA::select('user', ['uid', 'username', 'nickname'],
900 ['uid' => $user['parent-uid'], 'account_removed' => false]);
901 if (DBA::isResult($r)) {
902 $identities = DBA::toArray($r);
905 // Then add all siblings
906 $r = DBA::select('user', ['uid', 'username', 'nickname'],
907 ['parent-uid' => $user['parent-uid'], 'account_removed' => false]);
908 if (DBA::isResult($r)) {
909 $identities = array_merge($identities, DBA::toArray($r));
913 $r = DBA::p("SELECT `user`.`uid`, `user`.`username`, `user`.`nickname`
915 INNER JOIN `user` ON `manage`.`mid` = `user`.`uid`
916 WHERE `user`.`account_removed` = 0 AND `manage`.`uid` = ?",
919 if (DBA::isResult($r)) {
920 $identities = array_merge($identities, DBA::toArray($r));