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
34 * PAGE_FLAGS_NORMAL is a typical personal profile account
35 * PAGE_FLAGS_SOAPBOX automatically approves all friend requests as Contact::SHARING, (readonly)
36 * PAGE_FLAGS_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_FLAGS_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;
55 * ACCOUNT_TYPE_PERSON - the account belongs to a person
56 * Associated page types: PAGE_FLAGS_NORMAL, PAGE_FLAGS_SOAPBOX, PAGE_FLAGS_FREELOVE
58 * ACCOUNT_TYPE_ORGANISATION - the account belongs to an organisation
59 * Associated page type: PAGE_FLAGS_SOAPBOX
61 * ACCOUNT_TYPE_NEWS - the account is a news reflector
62 * Associated page type: PAGE_FLAGS_SOAPBOX
64 * ACCOUNT_TYPE_COMMUNITY - the account is community forum
65 * Associated page types: PAGE_COMMUNITY, PAGE_FLAGS_PRVGROUP
67 * ACCOUNT_TYPE_RELAY - the account is a relay
68 * This will only be assigned to contacts, not to user accounts
71 const ACCOUNT_TYPE_PERSON = 0;
72 const ACCOUNT_TYPE_ORGANISATION = 1;
73 const ACCOUNT_TYPE_NEWS = 2;
74 const ACCOUNT_TYPE_COMMUNITY = 3;
75 const ACCOUNT_TYPE_RELAY = 4;
81 * Returns true if a user record exists with the provided id
87 public static function exists($uid)
89 return DBA::exists('user', ['uid' => $uid]);
94 * @param array $fields
95 * @return array|boolean User record if it exists, false otherwise
98 public static function getById($uid, array $fields = [])
100 return DBA::selectFirst('user', $fields, ['uid' => $uid]);
104 * @param string $nickname
105 * @param array $fields
106 * @return array|boolean User record if it exists, false otherwise
109 public static function getByNickname($nickname, array $fields = [])
111 return DBA::selectFirst('user', $fields, ['nickname' => $nickname]);
115 * @brief Returns the user id of a given profile URL
119 * @return integer user id
122 public static function getIdForURL($url)
124 $self = DBA::selectFirst('contact', ['uid'], ['nurl' => Strings::normaliseLink($url), 'self' => true]);
125 if (!DBA::isResult($self)) {
133 * Get a user based on its email
135 * @param string $email
136 * @param array $fields
138 * @return array|boolean User record if it exists, false otherwise
142 public static function getByEmail($email, array $fields = [])
144 return DBA::selectFirst('user', $fields, ['email' => $email]);
148 * @brief Get owner data by user id
151 * @return boolean|array
154 public static function getOwnerDataById($uid) {
155 $r = DBA::fetchFirst("SELECT
157 `user`.`prvkey` AS `uprvkey`,
163 `user`.`account-type`,
165 `user`.`account_removed`
168 ON `user`.`uid` = `contact`.`uid`
169 WHERE `contact`.`uid` = ?
174 if (!DBA::isResult($r)) {
178 if (empty($r['nickname'])) {
182 // Check if the returned data is valid, otherwise fix it. See issue #6122
183 $url = System::baseUrl() . '/profile/' . $r['nickname'];
184 $addr = $r['nickname'] . '@' . substr(System::baseUrl(), strpos(System::baseUrl(), '://') + 3);
186 if (($addr != $r['addr']) || ($r['url'] != $url) || ($r['nurl'] != Strings::normaliseLink($r['url']))) {
187 Contact::updateSelfFromUserID($uid);
194 * @brief Get owner data by nick name
197 * @return boolean|array
200 public static function getOwnerDataByNick($nick)
202 $user = DBA::selectFirst('user', ['uid'], ['nickname' => $nick]);
204 if (!DBA::isResult($user)) {
208 return self::getOwnerDataById($user['uid']);
212 * @brief Returns the default group for a given user and network
214 * @param int $uid User id
215 * @param string $network network name
217 * @return int group id
218 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
220 public static function getDefaultGroup($uid, $network = '')
224 if ($network == Protocol::OSTATUS) {
225 $default_group = PConfig::get($uid, "ostatus", "default_group");
228 if ($default_group != 0) {
229 return $default_group;
232 $user = DBA::selectFirst('user', ['def_gid'], ['uid' => $uid]);
234 if (DBA::isResult($user)) {
235 $default_group = $user["def_gid"];
238 return $default_group;
243 * Authenticate a user with a clear text password
245 * @brief Authenticate a user with a clear text password
246 * @param mixed $user_info
247 * @param string $password
248 * @return int|boolean
249 * @deprecated since version 3.6
250 * @see User::getIdFromPasswordAuthentication()
252 public static function authenticate($user_info, $password)
255 return self::getIdFromPasswordAuthentication($user_info, $password);
256 } catch (Exception $ex) {
262 * Returns the user id associated with a successful password authentication
264 * @brief Authenticate a user with a clear text password
265 * @param mixed $user_info
266 * @param string $password
267 * @return int User Id if authentication is successful
270 public static function getIdFromPasswordAuthentication($user_info, $password)
272 $user = self::getAuthenticationInfo($user_info);
274 if (strpos($user['password'], '$') === false) {
275 //Legacy hash that has not been replaced by a new hash yet
276 if (self::hashPasswordLegacy($password) === $user['password']) {
277 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
281 } elseif (!empty($user['legacy_password'])) {
282 //Legacy hash that has been double-hashed and not replaced by a new hash yet
283 //Warning: `legacy_password` is not necessary in sync with the content of `password`
284 if (password_verify(self::hashPasswordLegacy($password), $user['password'])) {
285 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
289 } elseif (password_verify($password, $user['password'])) {
291 if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
292 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
298 throw new Exception(L10n::t('Login failed'));
302 * Returns authentication info from various parameters types
304 * User info can be any of the following:
307 * - User email or username or nickname
308 * - User array with at least the uid and the hashed password
310 * @param mixed $user_info
314 private static function getAuthenticationInfo($user_info)
318 if (is_object($user_info) || is_array($user_info)) {
319 if (is_object($user_info)) {
320 $user = (array) $user_info;
325 if (!isset($user['uid'])
326 || !isset($user['password'])
327 || !isset($user['legacy_password'])
329 throw new Exception(L10n::t('Not enough information to authenticate'));
331 } elseif (is_int($user_info) || is_string($user_info)) {
332 if (is_int($user_info)) {
333 $user = DBA::selectFirst('user', ['uid', 'password', 'legacy_password'],
337 'account_expired' => 0,
338 'account_removed' => 0,
343 $fields = ['uid', 'password', 'legacy_password'];
344 $condition = ["(`email` = ? OR `username` = ? OR `nickname` = ?)
345 AND NOT `blocked` AND NOT `account_expired` AND NOT `account_removed` AND `verified`",
346 $user_info, $user_info, $user_info];
347 $user = DBA::selectFirst('user', $fields, $condition);
350 if (!DBA::isResult($user)) {
351 throw new Exception(L10n::t('User not found'));
359 * Generates a human-readable random password
363 public static function generateNewPassword()
365 return ucfirst(Strings::getRandomName(8)) . mt_rand(1000, 9999);
369 * Checks if the provided plaintext password has been exposed or not
371 * @param string $password
374 public static function isPasswordExposed($password)
376 $cache = new \DivineOmega\DOFileCachePSR6\CacheItemPool();
377 $cache->changeConfig([
378 'cacheDirectory' => get_temppath() . '/password-exposed-cache/',
381 $PasswordExposedCHecker = new PasswordExposed\PasswordExposedChecker(null, $cache);
383 return $PasswordExposedCHecker->passwordExposed($password) === PasswordExposed\PasswordStatus::EXPOSED;
387 * Legacy hashing function, kept for password migration purposes
389 * @param string $password
392 private static function hashPasswordLegacy($password)
394 return hash('whirlpool', $password);
398 * Global user password hashing function
400 * @param string $password
404 public static function hashPassword($password)
406 if (!trim($password)) {
407 throw new Exception(L10n::t('Password can\'t be empty'));
410 return password_hash($password, PASSWORD_DEFAULT);
414 * Updates a user row with a new plaintext password
417 * @param string $password
421 public static function updatePassword($uid, $password)
423 $password = trim($password);
425 if (empty($password)) {
426 throw new Exception(L10n::t('Empty passwords are not allowed.'));
429 if (!Config::get('system', 'disable_password_exposed', false) && self::isPasswordExposed($password)) {
430 throw new Exception(L10n::t('The new password has been exposed in a public data dump, please choose another.'));
433 $allowed_characters = '!"#$%&\'()*+,-./;<=>?@[\]^_`{|}~';
435 if (!preg_match('/^[a-z0-9' . preg_quote($allowed_characters, '/') . ']+$/i', $password)) {
436 throw new Exception(L10n::t('The password can\'t contain accentuated letters, white spaces or colons (:)'));
439 return self::updatePasswordHashed($uid, self::hashPassword($password));
443 * Updates a user row with a new hashed password.
444 * Empties the password reset token field just in case.
447 * @param string $pasword_hashed
451 private static function updatePasswordHashed($uid, $pasword_hashed)
454 'password' => $pasword_hashed,
456 'pwdreset_time' => null,
457 'legacy_password' => false
459 return DBA::update('user', $fields, ['uid' => $uid]);
463 * @brief Checks if a nickname is in the list of the forbidden nicknames
465 * Check if a nickname is forbidden from registration on the node by the
466 * admin. Forbidden nicknames (e.g. role namess) can be configured in the
469 * @param string $nickname The nickname that should be checked
470 * @return boolean True is the nickname is blocked on the node
471 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
473 public static function isNicknameBlocked($nickname)
475 $forbidden_nicknames = Config::get('system', 'forbidden_nicknames', '');
477 // if the config variable is empty return false
478 if (empty($forbidden_nicknames)) {
482 // check if the nickname is in the list of blocked nicknames
483 $forbidden = explode(',', $forbidden_nicknames);
484 $forbidden = array_map('trim', $forbidden);
485 if (in_array(strtolower($nickname), $forbidden)) {
494 * @brief Catch-all user creation function
496 * Creates a user from the provided data array, either form fields or OpenID.
497 * Required: { username, nickname, email } or { openid_url }
499 * Performs the following:
500 * - Sends to the OpenId auth URL (if relevant)
501 * - Creates new key pairs for crypto
502 * - Create self-contact
503 * - Create profile image
507 * @throws \ErrorException
508 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
509 * @throws \ImagickException
512 public static function create(array $data)
515 $return = ['user' => null, 'password' => ''];
517 $using_invites = Config::get('system', 'invitation_only');
519 $invite_id = !empty($data['invite_id']) ? Strings::escapeTags(trim($data['invite_id'])) : '';
520 $username = !empty($data['username']) ? Strings::escapeTags(trim($data['username'])) : '';
521 $nickname = !empty($data['nickname']) ? Strings::escapeTags(trim($data['nickname'])) : '';
522 $email = !empty($data['email']) ? Strings::escapeTags(trim($data['email'])) : '';
523 $openid_url = !empty($data['openid_url']) ? Strings::escapeTags(trim($data['openid_url'])) : '';
524 $photo = !empty($data['photo']) ? Strings::escapeTags(trim($data['photo'])) : '';
525 $password = !empty($data['password']) ? trim($data['password']) : '';
526 $password1 = !empty($data['password1']) ? trim($data['password1']) : '';
527 $confirm = !empty($data['confirm']) ? trim($data['confirm']) : '';
528 $blocked = !empty($data['blocked']);
529 $verified = !empty($data['verified']);
530 $language = !empty($data['language']) ? Strings::escapeTags(trim($data['language'])) : 'en';
532 $publish = !empty($data['profile_publish_reg']);
533 $netpublish = $publish && Config::get('system', 'directory');
535 if ($password1 != $confirm) {
536 throw new Exception(L10n::t('Passwords do not match. Password unchanged.'));
537 } elseif ($password1 != '') {
538 $password = $password1;
541 if ($using_invites) {
543 throw new Exception(L10n::t('An invitation is required.'));
546 if (!Register::existsByHash($invite_id)) {
547 throw new Exception(L10n::t('Invitation could not be verified.'));
551 if (empty($username) || empty($email) || empty($nickname)) {
553 if (!Network::isUrlValid($openid_url)) {
554 throw new Exception(L10n::t('Invalid OpenID url'));
556 $_SESSION['register'] = 1;
557 $_SESSION['openid'] = $openid_url;
559 $openid = new LightOpenID($a->getHostName());
560 $openid->identity = $openid_url;
561 $openid->returnUrl = System::baseUrl() . '/openid';
562 $openid->required = ['namePerson/friendly', 'contact/email', 'namePerson'];
563 $openid->optional = ['namePerson/first', 'media/image/aspect11', 'media/image/default'];
565 $authurl = $openid->authUrl();
566 } catch (Exception $e) {
567 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);
569 System::externalRedirect($authurl);
573 throw new Exception(L10n::t('Please enter the required information.'));
576 if (!Network::isUrlValid($openid_url)) {
580 // collapse multiple spaces in name
581 $username = preg_replace('/ +/', ' ', $username);
583 $username_min_length = max(1, min(64, intval(Config::get('system', 'username_min_length', 3))));
584 $username_max_length = max(1, min(64, intval(Config::get('system', 'username_max_length', 48))));
586 if ($username_min_length > $username_max_length) {
587 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);
588 $tmp = $username_min_length;
589 $username_min_length = $username_max_length;
590 $username_max_length = $tmp;
593 if (mb_strlen($username) < $username_min_length) {
594 throw new Exception(L10n::tt('Username should be at least %s character.', 'Username should be at least %s characters.', $username_min_length));
597 if (mb_strlen($username) > $username_max_length) {
598 throw new Exception(L10n::tt('Username should be at most %s character.', 'Username should be at most %s characters.', $username_max_length));
601 // So now we are just looking for a space in the full name.
602 $loose_reg = Config::get('system', 'no_regfullname');
604 $username = mb_convert_case($username, MB_CASE_TITLE, 'UTF-8');
605 if (strpos($username, ' ') === false) {
606 throw new Exception(L10n::t("That doesn't appear to be your full (First Last) name."));
610 if (!Network::isEmailDomainAllowed($email)) {
611 throw new Exception(L10n::t('Your email domain is not among those allowed on this site.'));
614 if (!filter_var($email, FILTER_VALIDATE_EMAIL) || !Network::isEmailDomainValid($email)) {
615 throw new Exception(L10n::t('Not a valid email address.'));
617 if (self::isNicknameBlocked($nickname)) {
618 throw new Exception(L10n::t('The nickname was blocked from registration by the nodes admin.'));
621 if (Config::get('system', 'block_extended_register', false) && DBA::exists('user', ['email' => $email])) {
622 throw new Exception(L10n::t('Cannot use that email.'));
625 // Disallow somebody creating an account using openid that uses the admin email address,
626 // since openid bypasses email verification. We'll allow it if there is not yet an admin account.
627 if (Config::get('config', 'admin_email') && strlen($openid_url)) {
628 $adminlist = explode(',', str_replace(' ', '', strtolower(Config::get('config', 'admin_email'))));
629 if (in_array(strtolower($email), $adminlist)) {
630 throw new Exception(L10n::t('Cannot use that email.'));
634 $nickname = $data['nickname'] = strtolower($nickname);
636 if (!preg_match('/^[a-z0-9][a-z0-9\_]*$/', $nickname)) {
637 throw new Exception(L10n::t('Your nickname can only contain a-z, 0-9 and _.'));
640 // Check existing and deleted accounts for this nickname.
641 if (DBA::exists('user', ['nickname' => $nickname])
642 || DBA::exists('userd', ['username' => $nickname])
644 throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
647 $new_password = strlen($password) ? $password : User::generateNewPassword();
648 $new_password_encoded = self::hashPassword($new_password);
650 $return['password'] = $new_password;
652 $keys = Crypto::newKeypair(4096);
653 if ($keys === false) {
654 throw new Exception(L10n::t('SERIOUS ERROR: Generation of security keys failed.'));
657 $prvkey = $keys['prvkey'];
658 $pubkey = $keys['pubkey'];
660 // Create another keypair for signing/verifying salmon protocol messages.
661 $sres = Crypto::newKeypair(512);
662 $sprvkey = $sres['prvkey'];
663 $spubkey = $sres['pubkey'];
665 $insert_result = DBA::insert('user', [
666 'guid' => System::createUUID(),
667 'username' => $username,
668 'password' => $new_password_encoded,
670 'openid' => $openid_url,
671 'nickname' => $nickname,
674 'spubkey' => $spubkey,
675 'sprvkey' => $sprvkey,
676 'verified' => $verified,
677 'blocked' => $blocked,
678 'language' => $language,
680 'register_date' => DateTimeFormat::utcNow(),
681 'default-location' => ''
684 if ($insert_result) {
685 $uid = DBA::lastInsertId();
686 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
688 throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
692 throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
695 // if somebody clicked submit twice very quickly, they could end up with two accounts
696 // due to race condition. Remove this one.
697 $user_count = DBA::count('user', ['nickname' => $nickname]);
698 if ($user_count > 1) {
699 DBA::delete('user', ['uid' => $uid]);
701 throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
704 $insert_result = DBA::insert('profile', [
707 'photo' => System::baseUrl() . "/photo/profile/{$uid}.jpg",
708 'thumb' => System::baseUrl() . "/photo/avatar/{$uid}.jpg",
709 'publish' => $publish,
711 'net-publish' => $netpublish,
712 'profile-name' => L10n::t('default')
714 if (!$insert_result) {
715 DBA::delete('user', ['uid' => $uid]);
717 throw new Exception(L10n::t('An error occurred creating your default profile. Please try again.'));
720 // Create the self contact
721 if (!Contact::createSelfFromUserId($uid)) {
722 DBA::delete('user', ['uid' => $uid]);
724 throw new Exception(L10n::t('An error occurred creating your self contact. Please try again.'));
727 // Create a group with no members. This allows somebody to use it
728 // right away as a default group for new contacts.
729 $def_gid = Group::create($uid, L10n::t('Friends'));
731 DBA::delete('user', ['uid' => $uid]);
733 throw new Exception(L10n::t('An error occurred creating your default contact group. Please try again.'));
736 $fields = ['def_gid' => $def_gid];
737 if (Config::get('system', 'newuser_private') && $def_gid) {
738 $fields['allow_gid'] = '<' . $def_gid . '>';
741 DBA::update('user', $fields, ['uid' => $uid]);
743 // if we have no OpenID photo try to look up an avatar
744 if (!strlen($photo)) {
745 $photo = Network::lookupAvatarByEmail($email);
748 // unless there is no avatar-addon loaded
749 if (strlen($photo)) {
750 $photo_failure = false;
752 $filename = basename($photo);
753 $img_str = Network::fetchUrl($photo, true);
754 // guess mimetype from headers or filename
755 $type = Image::guessType($photo, true);
757 $Image = new Image($img_str, $type);
758 if ($Image->isValid()) {
759 $Image->scaleToSquare(300);
761 $hash = Photo::newResource();
763 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 4);
766 $photo_failure = true;
769 $Image->scaleDown(80);
771 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 5);
774 $photo_failure = true;
777 $Image->scaleDown(48);
779 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 6);
782 $photo_failure = true;
785 if (!$photo_failure) {
786 Photo::update(['profile' => 1], ['resource-id' => $hash]);
791 Hook::callAll('register_account', $uid);
793 $return['user'] = $user;
798 * @brief Sends pending registration confirmation email
800 * @param array $user User record array
801 * @param string $sitename
802 * @param string $siteurl
803 * @param string $password Plaintext password
804 * @return NULL|boolean from notification() and email() inherited
805 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
807 public static function sendRegisterPendingEmail($user, $sitename, $siteurl, $password)
809 $body = Strings::deindent(L10n::t('
811 Thank you for registering at %2$s. Your account is pending for approval by the administrator.
813 Your login details are as follows:
819 $user['username'], $sitename, $siteurl, $user['nickname'], $password
822 return notification([
823 'type' => SYSTEM_EMAIL,
824 'uid' => $user['uid'],
825 'to_email' => $user['email'],
826 'subject' => L10n::t('Registration at %s', $sitename),
832 * @brief Sends registration confirmation
834 * It's here as a function because the mail is sent from different parts
836 * @param array $user User record array
837 * @param string $sitename
838 * @param string $siteurl
839 * @param string $password Plaintext password
840 * @return NULL|boolean from notification() and email() inherited
841 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
843 public static function sendRegisterOpenEmail($user, $sitename, $siteurl, $password)
845 $preamble = Strings::deindent(L10n::t('
847 Thank you for registering at %2$s. Your account has been created.
849 $user['username'], $sitename
851 $body = Strings::deindent(L10n::t('
852 The login details are as follows:
858 You may change your password from your account "Settings" page after logging
861 Please take a few moments to review the other account settings on that page.
863 You may also wish to add some basic information to your default profile
864 ' . "\x28" . 'on the "Profiles" page' . "\x29" . ' so that other people can easily find you.
866 We recommend setting your full name, adding a profile photo,
867 adding some profile "keywords" ' . "\x28" . 'very useful in making new friends' . "\x29" . ' - and
868 perhaps what country you live in; if you do not wish to be more specific
871 We fully respect your right to privacy, and none of these items are necessary.
872 If you are new and do not know anybody here, they may help
873 you to make some new and interesting friends.
875 If you ever want to delete your account, you can do so at %3$s/removeme
877 Thank you and welcome to %2$s.',
878 $user['nickname'], $sitename, $siteurl, $user['username'], $password
881 return notification([
882 'uid' => $user['uid'],
883 'language' => $user['language'],
884 'type' => SYSTEM_EMAIL,
885 'to_email' => $user['email'],
886 'subject' => L10n::t('Registration details for %s', $sitename),
887 'preamble' => $preamble,
893 * @param object $uid user to remove
895 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
897 public static function remove($uid)
903 Logger::log('Removing user: ' . $uid);
905 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
907 Hook::callAll('remove_user', $user);
909 // save username (actually the nickname as it is guaranteed
910 // unique), so it cannot be re-registered in the future.
911 DBA::insert('userd', ['username' => $user['nickname']]);
913 // The user and related data will be deleted in "cron_expire_and_remove_users" (cronjobs.php)
914 DBA::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utc('now + 7 day')], ['uid' => $uid]);
915 Worker::add(PRIORITY_HIGH, 'Notifier', 'removeme', $uid);
917 // Send an update to the directory
918 $self = DBA::selectFirst('contact', ['url'], ['uid' => $uid, 'self' => true]);
919 Worker::add(PRIORITY_LOW, 'Directory', $self['url']);
921 // Remove the user relevant data
922 Worker::add(PRIORITY_NEGLIGIBLE, 'RemoveUser', $uid);
928 * Return all identities to a user
930 * @param int $uid The user id
931 * @return array All identities for this user
933 * Example for a return:
937 * 'username' => 'maxmuster',
938 * 'nickname' => 'Max Mustermann'
942 * 'username' => 'johndoe',
943 * 'nickname' => 'John Doe'
948 public static function identities($uid)
952 $user = DBA::selectFirst('user', ['uid', 'nickname', 'username', 'parent-uid'], ['uid' => $uid]);
953 if (!DBA::isResult($user)) {
957 if ($user['parent-uid'] == 0) {
958 // First add our own entry
959 $identities = [['uid' => $user['uid'],
960 'username' => $user['username'],
961 'nickname' => $user['nickname']]];
963 // Then add all the children
964 $r = DBA::select('user', ['uid', 'username', 'nickname'],
965 ['parent-uid' => $user['uid'], 'account_removed' => false]);
966 if (DBA::isResult($r)) {
967 $identities = array_merge($identities, DBA::toArray($r));
970 // First entry is our parent
971 $r = DBA::select('user', ['uid', 'username', 'nickname'],
972 ['uid' => $user['parent-uid'], 'account_removed' => false]);
973 if (DBA::isResult($r)) {
974 $identities = DBA::toArray($r);
977 // Then add all siblings
978 $r = DBA::select('user', ['uid', 'username', 'nickname'],
979 ['parent-uid' => $user['parent-uid'], 'account_removed' => false]);
980 if (DBA::isResult($r)) {
981 $identities = array_merge($identities, DBA::toArray($r));
985 $r = DBA::p("SELECT `user`.`uid`, `user`.`username`, `user`.`nickname`
987 INNER JOIN `user` ON `manage`.`mid` = `user`.`uid`
988 WHERE `user`.`account_removed` = 0 AND `manage`.`uid` = ?",
991 if (DBA::isResult($r)) {
992 $identities = array_merge($identities, DBA::toArray($r));
999 * Returns statistical information about the current users of this node
1005 public static function getStatistics()
1009 'active_users_halfyear' => 0,
1010 'active_users_monthly' => 0,
1013 $userStmt = DBA::p("SELECT `user`.`uid`, `user`.`login_date`, `contact`.`last-item`
1015 INNER JOIN `profile` ON `profile`.`uid` = `user`.`uid` AND `profile`.`is-default`
1016 INNER JOIN `contact` ON `contact`.`uid` = `user`.`uid` AND `contact`.`self`
1017 WHERE (`profile`.`publish` OR `profile`.`net-publish`) AND `user`.`verified`
1018 AND NOT `user`.`blocked` AND NOT `user`.`account_removed`
1019 AND NOT `user`.`account_expired`");
1021 if (!DBA::isResult($userStmt)) {
1025 $halfyear = time() - (180 * 24 * 60 * 60);
1026 $month = time() - (30 * 24 * 60 * 60);
1028 while ($user = DBA::fetch($userStmt)) {
1029 $statistics['total_users']++;
1031 if ((strtotime($user['login_date']) > $halfyear) ||
1032 (strtotime($user['last-item']) > $halfyear)) {
1033 $statistics['active_users_halfyear']++;
1036 if ((strtotime($user['login_date']) > $month) ||
1037 (strtotime($user['last-item']) > $month)) {
1038 $statistics['active_users_monthly']++;