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 * @name page/profile types
34 * PAGE_NORMAL is a typical personal profile account
35 * PAGE_SOAPBOX automatically approves all friend requests as Contact::SHARING, (readonly)
36 * PAGE_COMMUNITY automatically approves all friend requests as Contact::SHARING, but with
37 * write access to wall and comments (no email and not included in page owner's ACL lists)
38 * PAGE_FREELOVE automatically approves all friend requests as full friends (Contact::FRIEND).
42 const PAGE_FLAGS_NORMAL = 0;
43 const PAGE_FLAGS_SOAPBOX = 1;
44 const PAGE_FLAGS_COMMUNITY = 2;
45 const PAGE_FLAGS_FREELOVE = 3;
46 const PAGE_FLAGS_BLOG = 4;
47 const PAGE_FLAGS_PRVGROUP = 5;
52 * Returns true if a user record exists with the provided id
58 public static function exists($uid)
60 return DBA::exists('user', ['uid' => $uid]);
65 * @return array|boolean User record if it exists, false otherwise
68 public static function getById($uid)
70 return DBA::selectFirst('user', [], ['uid' => $uid]);
74 * @brief Returns the user id of a given profile URL
78 * @return integer user id
81 public static function getIdForURL($url)
83 $self = DBA::selectFirst('contact', ['uid'], ['nurl' => Strings::normaliseLink($url), 'self' => true]);
84 if (!DBA::isResult($self)) {
92 * @brief Get owner data by user id
95 * @return boolean|array
98 public static function getOwnerDataById($uid) {
99 $r = DBA::fetchFirst("SELECT
101 `user`.`prvkey` AS `uprvkey`,
107 `user`.`account-type`,
111 ON `user`.`uid` = `contact`.`uid`
112 WHERE `contact`.`uid` = ?
117 if (!DBA::isResult($r)) {
121 if (empty($r['nickname'])) {
125 // Check if the returned data is valid, otherwise fix it. See issue #6122
126 $url = System::baseUrl() . '/profile/' . $r['nickname'];
127 $addr = $r['nickname'] . '@' . substr(System::baseUrl(), strpos(System::baseUrl(), '://') + 3);
129 if (($addr != $r['addr']) || ($r['url'] != $url) || ($r['nurl'] != Strings::normaliseLink($r['url']))) {
130 Contact::updateSelfFromUserID($uid);
137 * @brief Get owner data by nick name
140 * @return boolean|array
143 public static function getOwnerDataByNick($nick)
145 $user = DBA::selectFirst('user', ['uid'], ['nickname' => $nick]);
147 if (!DBA::isResult($user)) {
151 return self::getOwnerDataById($user['uid']);
155 * @brief Returns the default group for a given user and network
157 * @param int $uid User id
158 * @param string $network network name
160 * @return int group id
161 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
163 public static function getDefaultGroup($uid, $network = '')
167 if ($network == Protocol::OSTATUS) {
168 $default_group = PConfig::get($uid, "ostatus", "default_group");
171 if ($default_group != 0) {
172 return $default_group;
175 $user = DBA::selectFirst('user', ['def_gid'], ['uid' => $uid]);
177 if (DBA::isResult($user)) {
178 $default_group = $user["def_gid"];
181 return $default_group;
186 * Authenticate a user with a clear text password
188 * @brief Authenticate a user with a clear text password
189 * @param mixed $user_info
190 * @param string $password
191 * @return int|boolean
192 * @deprecated since version 3.6
193 * @see User::getIdFromPasswordAuthentication()
195 public static function authenticate($user_info, $password)
198 return self::getIdFromPasswordAuthentication($user_info, $password);
199 } catch (Exception $ex) {
205 * Returns the user id associated with a successful password authentication
207 * @brief Authenticate a user with a clear text password
208 * @param mixed $user_info
209 * @param string $password
210 * @return int User Id if authentication is successful
213 public static function getIdFromPasswordAuthentication($user_info, $password)
215 $user = self::getAuthenticationInfo($user_info);
217 if (strpos($user['password'], '$') === false) {
218 //Legacy hash that has not been replaced by a new hash yet
219 if (self::hashPasswordLegacy($password) === $user['password']) {
220 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
224 } elseif (!empty($user['legacy_password'])) {
225 //Legacy hash that has been double-hashed and not replaced by a new hash yet
226 //Warning: `legacy_password` is not necessary in sync with the content of `password`
227 if (password_verify(self::hashPasswordLegacy($password), $user['password'])) {
228 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
232 } elseif (password_verify($password, $user['password'])) {
234 if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
235 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
241 throw new Exception(L10n::t('Login failed'));
245 * Returns authentication info from various parameters types
247 * User info can be any of the following:
250 * - User email or username or nickname
251 * - User array with at least the uid and the hashed password
253 * @param mixed $user_info
257 private static function getAuthenticationInfo($user_info)
261 if (is_object($user_info) || is_array($user_info)) {
262 if (is_object($user_info)) {
263 $user = (array) $user_info;
268 if (!isset($user['uid'])
269 || !isset($user['password'])
270 || !isset($user['legacy_password'])
272 throw new Exception(L10n::t('Not enough information to authenticate'));
274 } elseif (is_int($user_info) || is_string($user_info)) {
275 if (is_int($user_info)) {
276 $user = DBA::selectFirst('user', ['uid', 'password', 'legacy_password'],
280 'account_expired' => 0,
281 'account_removed' => 0,
286 $fields = ['uid', 'password', 'legacy_password'];
287 $condition = ["(`email` = ? OR `username` = ? OR `nickname` = ?)
288 AND NOT `blocked` AND NOT `account_expired` AND NOT `account_removed` AND `verified`",
289 $user_info, $user_info, $user_info];
290 $user = DBA::selectFirst('user', $fields, $condition);
293 if (!DBA::isResult($user)) {
294 throw new Exception(L10n::t('User not found'));
302 * Generates a human-readable random password
306 public static function generateNewPassword()
308 return ucfirst(Strings::getRandomName(8)) . mt_rand(1000, 9999);
312 * Checks if the provided plaintext password has been exposed or not
314 * @param string $password
317 public static function isPasswordExposed($password)
319 $cache = new \DivineOmega\DOFileCachePSR6\CacheItemPool();
320 $cache->changeConfig([
321 'cacheDirectory' => get_temppath() . '/password-exposed-cache/',
324 $PasswordExposedCHecker = new PasswordExposed\PasswordExposedChecker(null, $cache);
326 return $PasswordExposedCHecker->passwordExposed($password) === PasswordExposed\PasswordStatus::EXPOSED;
330 * Legacy hashing function, kept for password migration purposes
332 * @param string $password
335 private static function hashPasswordLegacy($password)
337 return hash('whirlpool', $password);
341 * Global user password hashing function
343 * @param string $password
347 public static function hashPassword($password)
349 if (!trim($password)) {
350 throw new Exception(L10n::t('Password can\'t be empty'));
353 return password_hash($password, PASSWORD_DEFAULT);
357 * Updates a user row with a new plaintext password
360 * @param string $password
364 public static function updatePassword($uid, $password)
366 $password = trim($password);
368 if (empty($password)) {
369 throw new Exception(L10n::t('Empty passwords are not allowed.'));
372 if (!Config::get('system', 'disable_password_exposed', false) && self::isPasswordExposed($password)) {
373 throw new Exception(L10n::t('The new password has been exposed in a public data dump, please choose another.'));
376 $allowed_characters = '!"#$%&\'()*+,-./;<=>?@[\]^_`{|}~';
378 if (!preg_match('/^[a-z0-9' . preg_quote($allowed_characters, '/') . ']+$/i', $password)) {
379 throw new Exception(L10n::t('The password can\'t contain accentuated letters, white spaces or colons (:)'));
382 return self::updatePasswordHashed($uid, self::hashPassword($password));
386 * Updates a user row with a new hashed password.
387 * Empties the password reset token field just in case.
390 * @param string $pasword_hashed
394 private static function updatePasswordHashed($uid, $pasword_hashed)
397 'password' => $pasword_hashed,
399 'pwdreset_time' => null,
400 'legacy_password' => false
402 return DBA::update('user', $fields, ['uid' => $uid]);
406 * @brief Checks if a nickname is in the list of the forbidden nicknames
408 * Check if a nickname is forbidden from registration on the node by the
409 * admin. Forbidden nicknames (e.g. role namess) can be configured in the
412 * @param string $nickname The nickname that should be checked
413 * @return boolean True is the nickname is blocked on the node
414 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
416 public static function isNicknameBlocked($nickname)
418 $forbidden_nicknames = Config::get('system', 'forbidden_nicknames', '');
420 // if the config variable is empty return false
421 if (empty($forbidden_nicknames)) {
425 // check if the nickname is in the list of blocked nicknames
426 $forbidden = explode(',', $forbidden_nicknames);
427 $forbidden = array_map('trim', $forbidden);
428 if (in_array(strtolower($nickname), $forbidden)) {
437 * @brief Catch-all user creation function
439 * Creates a user from the provided data array, either form fields or OpenID.
440 * Required: { username, nickname, email } or { openid_url }
442 * Performs the following:
443 * - Sends to the OpenId auth URL (if relevant)
444 * - Creates new key pairs for crypto
445 * - Create self-contact
446 * - Create profile image
450 * @throws \ErrorException
451 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
452 * @throws \ImagickException
455 public static function create(array $data)
458 $return = ['user' => null, 'password' => ''];
460 $using_invites = Config::get('system', 'invitation_only');
462 $invite_id = !empty($data['invite_id']) ? Strings::escapeTags(trim($data['invite_id'])) : '';
463 $username = !empty($data['username']) ? Strings::escapeTags(trim($data['username'])) : '';
464 $nickname = !empty($data['nickname']) ? Strings::escapeTags(trim($data['nickname'])) : '';
465 $email = !empty($data['email']) ? Strings::escapeTags(trim($data['email'])) : '';
466 $openid_url = !empty($data['openid_url']) ? Strings::escapeTags(trim($data['openid_url'])) : '';
467 $photo = !empty($data['photo']) ? Strings::escapeTags(trim($data['photo'])) : '';
468 $password = !empty($data['password']) ? trim($data['password']) : '';
469 $password1 = !empty($data['password1']) ? trim($data['password1']) : '';
470 $confirm = !empty($data['confirm']) ? trim($data['confirm']) : '';
471 $blocked = !empty($data['blocked']);
472 $verified = !empty($data['verified']);
473 $language = !empty($data['language']) ? Strings::escapeTags(trim($data['language'])) : 'en';
475 $publish = !empty($data['profile_publish_reg']);
476 $netpublish = $publish && Config::get('system', 'directory');
478 if ($password1 != $confirm) {
479 throw new Exception(L10n::t('Passwords do not match. Password unchanged.'));
480 } elseif ($password1 != '') {
481 $password = $password1;
484 if ($using_invites) {
486 throw new Exception(L10n::t('An invitation is required.'));
489 if (!Register::existsByHash($invite_id)) {
490 throw new Exception(L10n::t('Invitation could not be verified.'));
494 if (empty($username) || empty($email) || empty($nickname)) {
496 if (!Network::isUrlValid($openid_url)) {
497 throw new Exception(L10n::t('Invalid OpenID url'));
499 $_SESSION['register'] = 1;
500 $_SESSION['openid'] = $openid_url;
502 $openid = new LightOpenID($a->getHostName());
503 $openid->identity = $openid_url;
504 $openid->returnUrl = System::baseUrl() . '/openid';
505 $openid->required = ['namePerson/friendly', 'contact/email', 'namePerson'];
506 $openid->optional = ['namePerson/first', 'media/image/aspect11', 'media/image/default'];
508 $authurl = $openid->authUrl();
509 } catch (Exception $e) {
510 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);
512 System::externalRedirect($authurl);
516 throw new Exception(L10n::t('Please enter the required information.'));
519 if (!Network::isUrlValid($openid_url)) {
523 // collapse multiple spaces in name
524 $username = preg_replace('/ +/', ' ', $username);
526 $username_min_length = max(1, min(64, intval(Config::get('system', 'username_min_length', 3))));
527 $username_max_length = max(1, min(64, intval(Config::get('system', 'username_max_length', 48))));
529 if ($username_min_length > $username_max_length) {
530 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);
531 $tmp = $username_min_length;
532 $username_min_length = $username_max_length;
533 $username_max_length = $tmp;
536 if (mb_strlen($username) < $username_min_length) {
537 throw new Exception(L10n::tt('Username should be at least %s character.', 'Username should be at least %s characters.', $username_min_length));
540 if (mb_strlen($username) > $username_max_length) {
541 throw new Exception(L10n::tt('Username should be at most %s character.', 'Username should be at most %s characters.', $username_max_length));
544 // So now we are just looking for a space in the full name.
545 $loose_reg = Config::get('system', 'no_regfullname');
547 $username = mb_convert_case($username, MB_CASE_TITLE, 'UTF-8');
548 if (strpos($username, ' ') === false) {
549 throw new Exception(L10n::t("That doesn't appear to be your full (First Last) name."));
553 if (!Network::isEmailDomainAllowed($email)) {
554 throw new Exception(L10n::t('Your email domain is not among those allowed on this site.'));
557 if (!filter_var($email, FILTER_VALIDATE_EMAIL) || !Network::isEmailDomainValid($email)) {
558 throw new Exception(L10n::t('Not a valid email address.'));
560 if (self::isNicknameBlocked($nickname)) {
561 throw new Exception(L10n::t('The nickname was blocked from registration by the nodes admin.'));
564 if (Config::get('system', 'block_extended_register', false) && DBA::exists('user', ['email' => $email])) {
565 throw new Exception(L10n::t('Cannot use that email.'));
568 // Disallow somebody creating an account using openid that uses the admin email address,
569 // since openid bypasses email verification. We'll allow it if there is not yet an admin account.
570 if (Config::get('config', 'admin_email') && strlen($openid_url)) {
571 $adminlist = explode(',', str_replace(' ', '', strtolower(Config::get('config', 'admin_email'))));
572 if (in_array(strtolower($email), $adminlist)) {
573 throw new Exception(L10n::t('Cannot use that email.'));
577 $nickname = $data['nickname'] = strtolower($nickname);
579 if (!preg_match('/^[a-z0-9][a-z0-9\_]*$/', $nickname)) {
580 throw new Exception(L10n::t('Your nickname can only contain a-z, 0-9 and _.'));
583 // Check existing and deleted accounts for this nickname.
584 if (DBA::exists('user', ['nickname' => $nickname])
585 || DBA::exists('userd', ['username' => $nickname])
587 throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
590 $new_password = strlen($password) ? $password : User::generateNewPassword();
591 $new_password_encoded = self::hashPassword($new_password);
593 $return['password'] = $new_password;
595 $keys = Crypto::newKeypair(4096);
596 if ($keys === false) {
597 throw new Exception(L10n::t('SERIOUS ERROR: Generation of security keys failed.'));
600 $prvkey = $keys['prvkey'];
601 $pubkey = $keys['pubkey'];
603 // Create another keypair for signing/verifying salmon protocol messages.
604 $sres = Crypto::newKeypair(512);
605 $sprvkey = $sres['prvkey'];
606 $spubkey = $sres['pubkey'];
608 $insert_result = DBA::insert('user', [
609 'guid' => System::createUUID(),
610 'username' => $username,
611 'password' => $new_password_encoded,
613 'openid' => $openid_url,
614 'nickname' => $nickname,
617 'spubkey' => $spubkey,
618 'sprvkey' => $sprvkey,
619 'verified' => $verified,
620 'blocked' => $blocked,
621 'language' => $language,
623 'register_date' => DateTimeFormat::utcNow(),
624 'default-location' => ''
627 if ($insert_result) {
628 $uid = DBA::lastInsertId();
629 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
631 throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
635 throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
638 // if somebody clicked submit twice very quickly, they could end up with two accounts
639 // due to race condition. Remove this one.
640 $user_count = DBA::count('user', ['nickname' => $nickname]);
641 if ($user_count > 1) {
642 DBA::delete('user', ['uid' => $uid]);
644 throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
647 $insert_result = DBA::insert('profile', [
650 'photo' => System::baseUrl() . "/photo/profile/{$uid}.jpg",
651 'thumb' => System::baseUrl() . "/photo/avatar/{$uid}.jpg",
652 'publish' => $publish,
654 'net-publish' => $netpublish,
655 'profile-name' => L10n::t('default')
657 if (!$insert_result) {
658 DBA::delete('user', ['uid' => $uid]);
660 throw new Exception(L10n::t('An error occurred creating your default profile. Please try again.'));
663 // Create the self contact
664 if (!Contact::createSelfFromUserId($uid)) {
665 DBA::delete('user', ['uid' => $uid]);
667 throw new Exception(L10n::t('An error occurred creating your self contact. Please try again.'));
670 // Create a group with no members. This allows somebody to use it
671 // right away as a default group for new contacts.
672 $def_gid = Group::create($uid, L10n::t('Friends'));
674 DBA::delete('user', ['uid' => $uid]);
676 throw new Exception(L10n::t('An error occurred creating your default contact group. Please try again.'));
679 $fields = ['def_gid' => $def_gid];
680 if (Config::get('system', 'newuser_private') && $def_gid) {
681 $fields['allow_gid'] = '<' . $def_gid . '>';
684 DBA::update('user', $fields, ['uid' => $uid]);
686 // if we have no OpenID photo try to look up an avatar
687 if (!strlen($photo)) {
688 $photo = Network::lookupAvatarByEmail($email);
691 // unless there is no avatar-addon loaded
692 if (strlen($photo)) {
693 $photo_failure = false;
695 $filename = basename($photo);
696 $img_str = Network::fetchUrl($photo, true);
697 // guess mimetype from headers or filename
698 $type = Image::guessType($photo, true);
700 $Image = new Image($img_str, $type);
701 if ($Image->isValid()) {
702 $Image->scaleToSquare(300);
704 $hash = Photo::newResource();
706 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 4);
709 $photo_failure = true;
712 $Image->scaleDown(80);
714 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 5);
717 $photo_failure = true;
720 $Image->scaleDown(48);
722 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 6);
725 $photo_failure = true;
728 if (!$photo_failure) {
729 Photo::update(['profile' => 1], ['resource-id' => $hash]);
734 Hook::callAll('register_account', $uid);
736 $return['user'] = $user;
741 * @brief Sends pending registration confirmation email
743 * @param array $user User record array
744 * @param string $sitename
745 * @param string $siteurl
746 * @param string $password Plaintext password
747 * @return NULL|boolean from notification() and email() inherited
748 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
750 public static function sendRegisterPendingEmail($user, $sitename, $siteurl, $password)
752 $body = Strings::deindent(L10n::t('
754 Thank you for registering at %2$s. Your account is pending for approval by the administrator.
756 Your login details are as follows:
762 $user['username'], $sitename, $siteurl, $user['nickname'], $password
765 return notification([
766 'type' => SYSTEM_EMAIL,
767 'uid' => $user['uid'],
768 'to_email' => $user['email'],
769 'subject' => L10n::t('Registration at %s', $sitename),
775 * @brief Sends registration confirmation
777 * It's here as a function because the mail is sent from different parts
779 * @param array $user User record array
780 * @param string $sitename
781 * @param string $siteurl
782 * @param string $password Plaintext password
783 * @return NULL|boolean from notification() and email() inherited
784 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
786 public static function sendRegisterOpenEmail($user, $sitename, $siteurl, $password)
788 $preamble = Strings::deindent(L10n::t('
790 Thank you for registering at %2$s. Your account has been created.
792 $user['username'], $sitename
794 $body = Strings::deindent(L10n::t('
795 The login details are as follows:
801 You may change your password from your account "Settings" page after logging
804 Please take a few moments to review the other account settings on that page.
806 You may also wish to add some basic information to your default profile
807 ' . "\x28" . 'on the "Profiles" page' . "\x29" . ' so that other people can easily find you.
809 We recommend setting your full name, adding a profile photo,
810 adding some profile "keywords" ' . "\x28" . 'very useful in making new friends' . "\x29" . ' - and
811 perhaps what country you live in; if you do not wish to be more specific
814 We fully respect your right to privacy, and none of these items are necessary.
815 If you are new and do not know anybody here, they may help
816 you to make some new and interesting friends.
818 If you ever want to delete your account, you can do so at %3$s/removeme
820 Thank you and welcome to %2$s.',
821 $user['nickname'], $sitename, $siteurl, $user['username'], $password
824 return notification([
825 'uid' => $user['uid'],
826 'language' => $user['language'],
827 'type' => SYSTEM_EMAIL,
828 'to_email' => $user['email'],
829 'subject' => L10n::t('Registration details for %s', $sitename),
830 'preamble' => $preamble,
836 * @param object $uid user to remove
838 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
840 public static function remove($uid)
846 Logger::log('Removing user: ' . $uid);
848 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
850 Hook::callAll('remove_user', $user);
852 // save username (actually the nickname as it is guaranteed
853 // unique), so it cannot be re-registered in the future.
854 DBA::insert('userd', ['username' => $user['nickname']]);
856 // The user and related data will be deleted in "cron_expire_and_remove_users" (cronjobs.php)
857 DBA::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utc('now + 7 day')], ['uid' => $uid]);
858 Worker::add(PRIORITY_HIGH, 'Notifier', 'removeme', $uid);
860 // Send an update to the directory
861 $self = DBA::selectFirst('contact', ['url'], ['uid' => $uid, 'self' => true]);
862 Worker::add(PRIORITY_LOW, 'Directory', $self['url']);
864 // Remove the user relevant data
865 Worker::add(PRIORITY_LOW, 'RemoveUser', $uid);
871 * Return all identities to a user
873 * @param int $uid The user id
874 * @return array All identities for this user
876 * Example for a return:
880 * 'username' => 'maxmuster',
881 * 'nickname' => 'Max Mustermann'
885 * 'username' => 'johndoe',
886 * 'nickname' => 'John Doe'
891 public static function identities($uid)
895 $user = DBA::selectFirst('user', ['uid', 'nickname', 'username', 'parent-uid'], ['uid' => $uid]);
896 if (!DBA::isResult($user)) {
900 if ($user['parent-uid'] == 0) {
901 // First add our own entry
902 $identities = [['uid' => $user['uid'],
903 'username' => $user['username'],
904 'nickname' => $user['nickname']]];
906 // Then add all the children
907 $r = DBA::select('user', ['uid', 'username', 'nickname'],
908 ['parent-uid' => $user['uid'], 'account_removed' => false]);
909 if (DBA::isResult($r)) {
910 $identities = array_merge($identities, DBA::toArray($r));
913 // First entry is our parent
914 $r = DBA::select('user', ['uid', 'username', 'nickname'],
915 ['uid' => $user['parent-uid'], 'account_removed' => false]);
916 if (DBA::isResult($r)) {
917 $identities = DBA::toArray($r);
920 // Then add all siblings
921 $r = DBA::select('user', ['uid', 'username', 'nickname'],
922 ['parent-uid' => $user['parent-uid'], 'account_removed' => false]);
923 if (DBA::isResult($r)) {
924 $identities = array_merge($identities, DBA::toArray($r));
928 $r = DBA::p("SELECT `user`.`uid`, `user`.`username`, `user`.`nickname`
930 INNER JOIN `user` ON `manage`.`mid` = `user`.`uid`
931 WHERE `user`.`account_removed` = 0 AND `manage`.`uid` = ?",
934 if (DBA::isResult($r)) {
935 $identities = array_merge($identities, DBA::toArray($r));