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');
442 $invite_id = !empty($data['invite_id']) ? Strings::escapeTags(trim($data['invite_id'])) : '';
443 $username = !empty($data['username']) ? Strings::escapeTags(trim($data['username'])) : '';
444 $nickname = !empty($data['nickname']) ? Strings::escapeTags(trim($data['nickname'])) : '';
445 $email = !empty($data['email']) ? Strings::escapeTags(trim($data['email'])) : '';
446 $openid_url = !empty($data['openid_url']) ? Strings::escapeTags(trim($data['openid_url'])) : '';
447 $photo = !empty($data['photo']) ? Strings::escapeTags(trim($data['photo'])) : '';
448 $password = !empty($data['password']) ? trim($data['password']) : '';
449 $password1 = !empty($data['password1']) ? trim($data['password1']) : '';
450 $confirm = !empty($data['confirm']) ? trim($data['confirm']) : '';
451 $blocked = !empty($data['blocked']);
452 $verified = !empty($data['verified']);
453 $language = !empty($data['language']) ? Strings::escapeTags(trim($data['language'])) : 'en';
455 $publish = !empty($data['profile_publish_reg']);
456 $netpublish = $publish && Config::get('system', 'directory');
458 if ($password1 != $confirm) {
459 throw new Exception(L10n::t('Passwords do not match. Password unchanged.'));
460 } elseif ($password1 != '') {
461 $password = $password1;
464 if ($using_invites) {
466 throw new Exception(L10n::t('An invitation is required.'));
469 if (!Register::existsByHash($invite_id)) {
470 throw new Exception(L10n::t('Invitation could not be verified.'));
474 if (empty($username) || empty($email) || empty($nickname)) {
476 if (!Network::isUrlValid($openid_url)) {
477 throw new Exception(L10n::t('Invalid OpenID url'));
479 $_SESSION['register'] = 1;
480 $_SESSION['openid'] = $openid_url;
482 $openid = new LightOpenID($a->getHostName());
483 $openid->identity = $openid_url;
484 $openid->returnUrl = System::baseUrl() . '/openid';
485 $openid->required = ['namePerson/friendly', 'contact/email', 'namePerson'];
486 $openid->optional = ['namePerson/first', 'media/image/aspect11', 'media/image/default'];
488 $authurl = $openid->authUrl();
489 } catch (Exception $e) {
490 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);
492 System::externalRedirect($authurl);
496 throw new Exception(L10n::t('Please enter the required information.'));
499 if (!Network::isUrlValid($openid_url)) {
503 // collapse multiple spaces in name
504 $username = preg_replace('/ +/', ' ', $username);
506 $username_min_length = max(1, min(64, intval(Config::get('system', 'username_min_length', 3))));
507 $username_max_length = max(1, min(64, intval(Config::get('system', 'username_max_length', 48))));
509 if ($username_min_length > $username_max_length) {
510 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);
511 $tmp = $username_min_length;
512 $username_min_length = $username_max_length;
513 $username_max_length = $tmp;
516 if (mb_strlen($username) < $username_min_length) {
517 throw new Exception(L10n::tt('Username should be at least %s character.', 'Username should be at least %s characters.', $username_min_length));
520 if (mb_strlen($username) > $username_max_length) {
521 throw new Exception(L10n::tt('Username should be at most %s character.', 'Username should be at most %s characters.', $username_max_length));
524 // So now we are just looking for a space in the full name.
525 $loose_reg = Config::get('system', 'no_regfullname');
527 $username = mb_convert_case($username, MB_CASE_TITLE, 'UTF-8');
528 if (strpos($username, ' ') === false) {
529 throw new Exception(L10n::t("That doesn't appear to be your full (First Last) name."));
533 if (!Network::isEmailDomainAllowed($email)) {
534 throw new Exception(L10n::t('Your email domain is not among those allowed on this site.'));
537 if (!filter_var($email, FILTER_VALIDATE_EMAIL) || !Network::isEmailDomainValid($email)) {
538 throw new Exception(L10n::t('Not a valid email address.'));
540 if (self::isNicknameBlocked($nickname)) {
541 throw new Exception(L10n::t('The nickname was blocked from registration by the nodes admin.'));
544 if (Config::get('system', 'block_extended_register', false) && DBA::exists('user', ['email' => $email])) {
545 throw new Exception(L10n::t('Cannot use that email.'));
548 // Disallow somebody creating an account using openid that uses the admin email address,
549 // since openid bypasses email verification. We'll allow it if there is not yet an admin account.
550 if (Config::get('config', 'admin_email') && strlen($openid_url)) {
551 $adminlist = explode(',', str_replace(' ', '', strtolower(Config::get('config', 'admin_email'))));
552 if (in_array(strtolower($email), $adminlist)) {
553 throw new Exception(L10n::t('Cannot use that email.'));
557 $nickname = $data['nickname'] = strtolower($nickname);
559 if (!preg_match('/^[a-z0-9][a-z0-9\_]*$/', $nickname)) {
560 throw new Exception(L10n::t('Your nickname can only contain a-z, 0-9 and _.'));
563 // Check existing and deleted accounts for this nickname.
564 if (DBA::exists('user', ['nickname' => $nickname])
565 || DBA::exists('userd', ['username' => $nickname])
567 throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
570 $new_password = strlen($password) ? $password : User::generateNewPassword();
571 $new_password_encoded = self::hashPassword($new_password);
573 $return['password'] = $new_password;
575 $keys = Crypto::newKeypair(4096);
576 if ($keys === false) {
577 throw new Exception(L10n::t('SERIOUS ERROR: Generation of security keys failed.'));
580 $prvkey = $keys['prvkey'];
581 $pubkey = $keys['pubkey'];
583 // Create another keypair for signing/verifying salmon protocol messages.
584 $sres = Crypto::newKeypair(512);
585 $sprvkey = $sres['prvkey'];
586 $spubkey = $sres['pubkey'];
588 $insert_result = DBA::insert('user', [
589 'guid' => System::createUUID(),
590 'username' => $username,
591 'password' => $new_password_encoded,
593 'openid' => $openid_url,
594 'nickname' => $nickname,
597 'spubkey' => $spubkey,
598 'sprvkey' => $sprvkey,
599 'verified' => $verified,
600 'blocked' => $blocked,
601 'language' => $language,
603 'register_date' => DateTimeFormat::utcNow(),
604 'default-location' => ''
607 if ($insert_result) {
608 $uid = DBA::lastInsertId();
609 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
611 throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
615 throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
618 // if somebody clicked submit twice very quickly, they could end up with two accounts
619 // due to race condition. Remove this one.
620 $user_count = DBA::count('user', ['nickname' => $nickname]);
621 if ($user_count > 1) {
622 DBA::delete('user', ['uid' => $uid]);
624 throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
627 $insert_result = DBA::insert('profile', [
630 'photo' => System::baseUrl() . "/photo/profile/{$uid}.jpg",
631 'thumb' => System::baseUrl() . "/photo/avatar/{$uid}.jpg",
632 'publish' => $publish,
634 'net-publish' => $netpublish,
635 'profile-name' => L10n::t('default')
637 if (!$insert_result) {
638 DBA::delete('user', ['uid' => $uid]);
640 throw new Exception(L10n::t('An error occurred creating your default profile. Please try again.'));
643 // Create the self contact
644 if (!Contact::createSelfFromUserId($uid)) {
645 DBA::delete('user', ['uid' => $uid]);
647 throw new Exception(L10n::t('An error occurred creating your self contact. Please try again.'));
650 // Create a group with no members. This allows somebody to use it
651 // right away as a default group for new contacts.
652 $def_gid = Group::create($uid, L10n::t('Friends'));
654 DBA::delete('user', ['uid' => $uid]);
656 throw new Exception(L10n::t('An error occurred creating your default contact group. Please try again.'));
659 $fields = ['def_gid' => $def_gid];
660 if (Config::get('system', 'newuser_private') && $def_gid) {
661 $fields['allow_gid'] = '<' . $def_gid . '>';
664 DBA::update('user', $fields, ['uid' => $uid]);
666 // if we have no OpenID photo try to look up an avatar
667 if (!strlen($photo)) {
668 $photo = Network::lookupAvatarByEmail($email);
671 // unless there is no avatar-addon loaded
672 if (strlen($photo)) {
673 $photo_failure = false;
675 $filename = basename($photo);
676 $img_str = Network::fetchUrl($photo, true);
677 // guess mimetype from headers or filename
678 $type = Image::guessType($photo, true);
680 $Image = new Image($img_str, $type);
681 if ($Image->isValid()) {
682 $Image->scaleToSquare(300);
684 $hash = Photo::newResource();
686 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 4);
689 $photo_failure = true;
692 $Image->scaleDown(80);
694 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 5);
697 $photo_failure = true;
700 $Image->scaleDown(48);
702 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 6);
705 $photo_failure = true;
708 if (!$photo_failure) {
709 Photo::update(['profile' => 1], ['resource-id' => $hash]);
714 Hook::callAll('register_account', $uid);
716 $return['user'] = $user;
721 * @brief Sends pending registration confirmation email
723 * @param array $user User record array
724 * @param string $sitename
725 * @param string $siteurl
726 * @param string $password Plaintext password
727 * @return NULL|boolean from notification() and email() inherited
728 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
730 public static function sendRegisterPendingEmail($user, $sitename, $siteurl, $password)
732 $body = Strings::deindent(L10n::t('
734 Thank you for registering at %2$s. Your account is pending for approval by the administrator.
736 Your login details are as follows:
742 $user['username'], $sitename, $siteurl, $user['nickname'], $password
745 return notification([
746 'type' => SYSTEM_EMAIL,
747 'uid' => $user['uid'],
748 'to_email' => $user['email'],
749 'subject' => L10n::t('Registration at %s', $sitename),
755 * @brief Sends registration confirmation
757 * It's here as a function because the mail is sent from different parts
759 * @param array $user User record array
760 * @param string $sitename
761 * @param string $siteurl
762 * @param string $password Plaintext password
763 * @return NULL|boolean from notification() and email() inherited
764 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
766 public static function sendRegisterOpenEmail($user, $sitename, $siteurl, $password)
768 $preamble = Strings::deindent(L10n::t('
770 Thank you for registering at %2$s. Your account has been created.
772 $user['username'], $sitename
774 $body = Strings::deindent(L10n::t('
775 The login details are as follows:
781 You may change your password from your account "Settings" page after logging
784 Please take a few moments to review the other account settings on that page.
786 You may also wish to add some basic information to your default profile
787 ' . "\x28" . 'on the "Profiles" page' . "\x29" . ' so that other people can easily find you.
789 We recommend setting your full name, adding a profile photo,
790 adding some profile "keywords" ' . "\x28" . 'very useful in making new friends' . "\x29" . ' - and
791 perhaps what country you live in; if you do not wish to be more specific
794 We fully respect your right to privacy, and none of these items are necessary.
795 If you are new and do not know anybody here, they may help
796 you to make some new and interesting friends.
798 If you ever want to delete your account, you can do so at %3$s/removeme
800 Thank you and welcome to %2$s.',
801 $user['nickname'], $sitename, $siteurl, $user['username'], $password
804 return notification([
805 'uid' => $user['uid'],
806 'language' => $user['language'],
807 'type' => SYSTEM_EMAIL,
808 'to_email' => $user['email'],
809 'subject' => L10n::t('Registration details for %s', $sitename),
810 'preamble' => $preamble,
816 * @param object $uid user to remove
818 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
820 public static function remove($uid)
826 Logger::log('Removing user: ' . $uid);
828 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
830 Hook::callAll('remove_user', $user);
832 // save username (actually the nickname as it is guaranteed
833 // unique), so it cannot be re-registered in the future.
834 DBA::insert('userd', ['username' => $user['nickname']]);
836 // The user and related data will be deleted in "cron_expire_and_remove_users" (cronjobs.php)
837 DBA::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utc('now + 7 day')], ['uid' => $uid]);
838 Worker::add(PRIORITY_HIGH, 'Notifier', 'removeme', $uid);
840 // Send an update to the directory
841 $self = DBA::selectFirst('contact', ['url'], ['uid' => $uid, 'self' => true]);
842 Worker::add(PRIORITY_LOW, 'Directory', $self['url']);
844 // Remove the user relevant data
845 Worker::add(PRIORITY_LOW, 'RemoveUser', $uid);
851 * Return all identities to a user
853 * @param int $uid The user id
854 * @return array All identities for this user
856 * Example for a return:
860 * 'username' => 'maxmuster',
861 * 'nickname' => 'Max Mustermann'
865 * 'username' => 'johndoe',
866 * 'nickname' => 'John Doe'
871 public static function identities($uid)
875 $user = DBA::selectFirst('user', ['uid', 'nickname', 'username', 'parent-uid'], ['uid' => $uid]);
876 if (!DBA::isResult($user)) {
880 if ($user['parent-uid'] == 0) {
881 // First add our own entry
882 $identities = [['uid' => $user['uid'],
883 'username' => $user['username'],
884 'nickname' => $user['nickname']]];
886 // Then add all the children
887 $r = DBA::select('user', ['uid', 'username', 'nickname'],
888 ['parent-uid' => $user['uid'], 'account_removed' => false]);
889 if (DBA::isResult($r)) {
890 $identities = array_merge($identities, DBA::toArray($r));
893 // First entry is our parent
894 $r = DBA::select('user', ['uid', 'username', 'nickname'],
895 ['uid' => $user['parent-uid'], 'account_removed' => false]);
896 if (DBA::isResult($r)) {
897 $identities = DBA::toArray($r);
900 // Then add all siblings
901 $r = DBA::select('user', ['uid', 'username', 'nickname'],
902 ['parent-uid' => $user['parent-uid'], 'account_removed' => false]);
903 if (DBA::isResult($r)) {
904 $identities = array_merge($identities, DBA::toArray($r));
908 $r = DBA::p("SELECT `user`.`uid`, `user`.`username`, `user`.`nickname`
910 INNER JOIN `user` ON `manage`.`mid` = `user`.`uid`
911 WHERE `user`.`account_removed` = 0 AND `manage`.`uid` = ?",
914 if (DBA::isResult($r)) {
915 $identities = array_merge($identities, DBA::toArray($r));