3 * @file src/Model/User.php
4 * @brief This file includes the User class with user related database functions
6 namespace Friendica\Model;
8 use DivineOmega\PasswordExposed;
10 use Friendica\Core\Config;
11 use Friendica\Core\Hook;
12 use Friendica\Core\L10n;
13 use Friendica\Core\Logger;
14 use Friendica\Core\PConfig;
15 use Friendica\Core\Protocol;
16 use Friendica\Core\System;
17 use Friendica\Core\Worker;
18 use Friendica\Database\DBA;
19 use Friendica\Model\Photo;
20 use Friendica\Model\TwoFactor\AppSpecificPassword;
21 use Friendica\Object\Image;
22 use Friendica\Util\Crypto;
23 use Friendica\Util\DateTimeFormat;
24 use Friendica\Util\Network;
25 use Friendica\Util\Strings;
26 use Friendica\Worker\Delivery;
30 * @brief This class handles User related functions
37 * PAGE_FLAGS_NORMAL is a typical personal profile account
38 * PAGE_FLAGS_SOAPBOX automatically approves all friend requests as Contact::SHARING, (readonly)
39 * PAGE_FLAGS_COMMUNITY automatically approves all friend requests as Contact::SHARING, but with
40 * write access to wall and comments (no email and not included in page owner's ACL lists)
41 * PAGE_FLAGS_FREELOVE automatically approves all friend requests as full friends (Contact::FRIEND).
45 const PAGE_FLAGS_NORMAL = 0;
46 const PAGE_FLAGS_SOAPBOX = 1;
47 const PAGE_FLAGS_COMMUNITY = 2;
48 const PAGE_FLAGS_FREELOVE = 3;
49 const PAGE_FLAGS_BLOG = 4;
50 const PAGE_FLAGS_PRVGROUP = 5;
58 * ACCOUNT_TYPE_PERSON - the account belongs to a person
59 * Associated page types: PAGE_FLAGS_NORMAL, PAGE_FLAGS_SOAPBOX, PAGE_FLAGS_FREELOVE
61 * ACCOUNT_TYPE_ORGANISATION - the account belongs to an organisation
62 * Associated page type: PAGE_FLAGS_SOAPBOX
64 * ACCOUNT_TYPE_NEWS - the account is a news reflector
65 * Associated page type: PAGE_FLAGS_SOAPBOX
67 * ACCOUNT_TYPE_COMMUNITY - the account is community forum
68 * Associated page types: PAGE_COMMUNITY, PAGE_FLAGS_PRVGROUP
70 * ACCOUNT_TYPE_RELAY - the account is a relay
71 * This will only be assigned to contacts, not to user accounts
74 const ACCOUNT_TYPE_PERSON = 0;
75 const ACCOUNT_TYPE_ORGANISATION = 1;
76 const ACCOUNT_TYPE_NEWS = 2;
77 const ACCOUNT_TYPE_COMMUNITY = 3;
78 const ACCOUNT_TYPE_RELAY = 4;
84 * Returns true if a user record exists with the provided id
90 public static function exists($uid)
92 return DBA::exists('user', ['uid' => $uid]);
97 * @param array $fields
98 * @return array|boolean User record if it exists, false otherwise
101 public static function getById($uid, array $fields = [])
103 return DBA::selectFirst('user', $fields, ['uid' => $uid]);
107 * @param string $nickname
108 * @param array $fields
109 * @return array|boolean User record if it exists, false otherwise
112 public static function getByNickname($nickname, array $fields = [])
114 return DBA::selectFirst('user', $fields, ['nickname' => $nickname]);
118 * @brief Returns the user id of a given profile URL
122 * @return integer user id
125 public static function getIdForURL($url)
127 $self = DBA::selectFirst('contact', ['uid'], ['nurl' => Strings::normaliseLink($url), 'self' => true]);
128 if (!DBA::isResult($self)) {
136 * Get a user based on its email
138 * @param string $email
139 * @param array $fields
141 * @return array|boolean User record if it exists, false otherwise
145 public static function getByEmail($email, array $fields = [])
147 return DBA::selectFirst('user', $fields, ['email' => $email]);
151 * @brief Get owner data by user id
154 * @param boolean $check_valid Test if data is invalid and correct it
155 * @return boolean|array
158 public static function getOwnerDataById($uid, $check_valid = true) {
159 $r = DBA::fetchFirst("SELECT
161 `user`.`prvkey` AS `uprvkey`,
167 `user`.`account-type`,
169 `user`.`account_removed`
172 ON `user`.`uid` = `contact`.`uid`
173 WHERE `contact`.`uid` = ?
178 if (!DBA::isResult($r)) {
182 if (empty($r['nickname'])) {
190 // Check if the returned data is valid, otherwise fix it. See issue #6122
192 // Check for correct url and normalised nurl
193 $url = System::baseUrl() . '/profile/' . $r['nickname'];
194 $repair = ($r['url'] != $url) || ($r['nurl'] != Strings::normaliseLink($r['url']));
197 // Check if "addr" is present and correct
198 $addr = $r['nickname'] . '@' . substr(System::baseUrl(), strpos(System::baseUrl(), '://') + 3);
199 $repair = ($addr != $r['addr']);
203 // Check if the avatar field is filled and the photo directs to the correct path
204 $avatar = Photo::selectFirst(['resource-id'], ['uid' => $uid, 'profile' => true]);
205 if (DBA::isResult($avatar)) {
206 $repair = empty($r['avatar']) || !strpos($r['photo'], $avatar['resource-id']);
211 Contact::updateSelfFromUserID($uid);
212 // Return the corrected data and avoid a loop
213 $r = self::getOwnerDataById($uid, false);
220 * @brief Get owner data by nick name
223 * @return boolean|array
226 public static function getOwnerDataByNick($nick)
228 $user = DBA::selectFirst('user', ['uid'], ['nickname' => $nick]);
230 if (!DBA::isResult($user)) {
234 return self::getOwnerDataById($user['uid']);
238 * @brief Returns the default group for a given user and network
240 * @param int $uid User id
241 * @param string $network network name
243 * @return int group id
244 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
246 public static function getDefaultGroup($uid, $network = '')
250 if ($network == Protocol::OSTATUS) {
251 $default_group = PConfig::get($uid, "ostatus", "default_group");
254 if ($default_group != 0) {
255 return $default_group;
258 $user = DBA::selectFirst('user', ['def_gid'], ['uid' => $uid]);
260 if (DBA::isResult($user)) {
261 $default_group = $user["def_gid"];
264 return $default_group;
269 * Authenticate a user with a clear text password
271 * @brief Authenticate a user with a clear text password
272 * @param mixed $user_info
273 * @param string $password
274 * @param bool $third_party
275 * @return int|boolean
276 * @deprecated since version 3.6
277 * @see User::getIdFromPasswordAuthentication()
279 public static function authenticate($user_info, $password, $third_party = false)
282 return self::getIdFromPasswordAuthentication($user_info, $password, $third_party);
283 } catch (Exception $ex) {
289 * Returns the user id associated with a successful password authentication
291 * @brief Authenticate a user with a clear text password
292 * @param mixed $user_info
293 * @param string $password
294 * @param bool $third_party
295 * @return int User Id if authentication is successful
298 public static function getIdFromPasswordAuthentication($user_info, $password, $third_party = false)
300 $user = self::getAuthenticationInfo($user_info);
302 if ($third_party && PConfig::get($user['uid'], '2fa', 'verified')) {
303 // Third-party apps can't verify two-factor authentication, we use app-specific passwords instead
304 if (AppSpecificPassword::authenticateUser($user['uid'], $password)) {
307 } elseif (strpos($user['password'], '$') === false) {
308 //Legacy hash that has not been replaced by a new hash yet
309 if (self::hashPasswordLegacy($password) === $user['password']) {
310 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
314 } elseif (!empty($user['legacy_password'])) {
315 //Legacy hash that has been double-hashed and not replaced by a new hash yet
316 //Warning: `legacy_password` is not necessary in sync with the content of `password`
317 if (password_verify(self::hashPasswordLegacy($password), $user['password'])) {
318 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
322 } elseif (password_verify($password, $user['password'])) {
324 if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
325 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
331 throw new Exception(L10n::t('Login failed'));
335 * Returns authentication info from various parameters types
337 * User info can be any of the following:
340 * - User email or username or nickname
341 * - User array with at least the uid and the hashed password
343 * @param mixed $user_info
347 private static function getAuthenticationInfo($user_info)
351 if (is_object($user_info) || is_array($user_info)) {
352 if (is_object($user_info)) {
353 $user = (array) $user_info;
358 if (!isset($user['uid'])
359 || !isset($user['password'])
360 || !isset($user['legacy_password'])
362 throw new Exception(L10n::t('Not enough information to authenticate'));
364 } elseif (is_int($user_info) || is_string($user_info)) {
365 if (is_int($user_info)) {
366 $user = DBA::selectFirst('user', ['uid', 'password', 'legacy_password'],
370 'account_expired' => 0,
371 'account_removed' => 0,
376 $fields = ['uid', 'password', 'legacy_password'];
377 $condition = ["(`email` = ? OR `username` = ? OR `nickname` = ?)
378 AND NOT `blocked` AND NOT `account_expired` AND NOT `account_removed` AND `verified`",
379 $user_info, $user_info, $user_info];
380 $user = DBA::selectFirst('user', $fields, $condition);
383 if (!DBA::isResult($user)) {
384 throw new Exception(L10n::t('User not found'));
392 * Generates a human-readable random password
396 public static function generateNewPassword()
398 return ucfirst(Strings::getRandomName(8)) . mt_rand(1000, 9999);
402 * Checks if the provided plaintext password has been exposed or not
404 * @param string $password
407 public static function isPasswordExposed($password)
409 $cache = new \DivineOmega\DOFileCachePSR6\CacheItemPool();
410 $cache->changeConfig([
411 'cacheDirectory' => get_temppath() . '/password-exposed-cache/',
414 $PasswordExposedCHecker = new PasswordExposed\PasswordExposedChecker(null, $cache);
416 return $PasswordExposedCHecker->passwordExposed($password) === PasswordExposed\PasswordStatus::EXPOSED;
420 * Legacy hashing function, kept for password migration purposes
422 * @param string $password
425 private static function hashPasswordLegacy($password)
427 return hash('whirlpool', $password);
431 * Global user password hashing function
433 * @param string $password
437 public static function hashPassword($password)
439 if (!trim($password)) {
440 throw new Exception(L10n::t('Password can\'t be empty'));
443 return password_hash($password, PASSWORD_DEFAULT);
447 * Updates a user row with a new plaintext password
450 * @param string $password
454 public static function updatePassword($uid, $password)
456 $password = trim($password);
458 if (empty($password)) {
459 throw new Exception(L10n::t('Empty passwords are not allowed.'));
462 if (!Config::get('system', 'disable_password_exposed', false) && self::isPasswordExposed($password)) {
463 throw new Exception(L10n::t('The new password has been exposed in a public data dump, please choose another.'));
466 $allowed_characters = '!"#$%&\'()*+,-./;<=>?@[\]^_`{|}~';
468 if (!preg_match('/^[a-z0-9' . preg_quote($allowed_characters, '/') . ']+$/i', $password)) {
469 throw new Exception(L10n::t('The password can\'t contain accentuated letters, white spaces or colons (:)'));
472 return self::updatePasswordHashed($uid, self::hashPassword($password));
476 * Updates a user row with a new hashed password.
477 * Empties the password reset token field just in case.
480 * @param string $pasword_hashed
484 private static function updatePasswordHashed($uid, $pasword_hashed)
487 'password' => $pasword_hashed,
489 'pwdreset_time' => null,
490 'legacy_password' => false
492 return DBA::update('user', $fields, ['uid' => $uid]);
496 * @brief Checks if a nickname is in the list of the forbidden nicknames
498 * Check if a nickname is forbidden from registration on the node by the
499 * admin. Forbidden nicknames (e.g. role namess) can be configured in the
502 * @param string $nickname The nickname that should be checked
503 * @return boolean True is the nickname is blocked on the node
504 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
506 public static function isNicknameBlocked($nickname)
508 $forbidden_nicknames = Config::get('system', 'forbidden_nicknames', '');
510 // if the config variable is empty return false
511 if (empty($forbidden_nicknames)) {
515 // check if the nickname is in the list of blocked nicknames
516 $forbidden = explode(',', $forbidden_nicknames);
517 $forbidden = array_map('trim', $forbidden);
518 if (in_array(strtolower($nickname), $forbidden)) {
527 * @brief Catch-all user creation function
529 * Creates a user from the provided data array, either form fields or OpenID.
530 * Required: { username, nickname, email } or { openid_url }
532 * Performs the following:
533 * - Sends to the OpenId auth URL (if relevant)
534 * - Creates new key pairs for crypto
535 * - Create self-contact
536 * - Create profile image
540 * @throws \ErrorException
541 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
542 * @throws \ImagickException
545 public static function create(array $data)
548 $return = ['user' => null, 'password' => ''];
550 $using_invites = Config::get('system', 'invitation_only');
552 $invite_id = !empty($data['invite_id']) ? Strings::escapeTags(trim($data['invite_id'])) : '';
553 $username = !empty($data['username']) ? Strings::escapeTags(trim($data['username'])) : '';
554 $nickname = !empty($data['nickname']) ? Strings::escapeTags(trim($data['nickname'])) : '';
555 $email = !empty($data['email']) ? Strings::escapeTags(trim($data['email'])) : '';
556 $openid_url = !empty($data['openid_url']) ? Strings::escapeTags(trim($data['openid_url'])) : '';
557 $photo = !empty($data['photo']) ? Strings::escapeTags(trim($data['photo'])) : '';
558 $password = !empty($data['password']) ? trim($data['password']) : '';
559 $password1 = !empty($data['password1']) ? trim($data['password1']) : '';
560 $confirm = !empty($data['confirm']) ? trim($data['confirm']) : '';
561 $blocked = !empty($data['blocked']);
562 $verified = !empty($data['verified']);
563 $language = !empty($data['language']) ? Strings::escapeTags(trim($data['language'])) : 'en';
565 $publish = !empty($data['profile_publish_reg']);
566 $netpublish = $publish && Config::get('system', 'directory');
568 if ($password1 != $confirm) {
569 throw new Exception(L10n::t('Passwords do not match. Password unchanged.'));
570 } elseif ($password1 != '') {
571 $password = $password1;
574 if ($using_invites) {
576 throw new Exception(L10n::t('An invitation is required.'));
579 if (!Register::existsByHash($invite_id)) {
580 throw new Exception(L10n::t('Invitation could not be verified.'));
584 if (empty($username) || empty($email) || empty($nickname)) {
586 if (!Network::isUrlValid($openid_url)) {
587 throw new Exception(L10n::t('Invalid OpenID url'));
589 $_SESSION['register'] = 1;
590 $_SESSION['openid'] = $openid_url;
592 $openid = new LightOpenID($a->getHostName());
593 $openid->identity = $openid_url;
594 $openid->returnUrl = System::baseUrl() . '/openid';
595 $openid->required = ['namePerson/friendly', 'contact/email', 'namePerson'];
596 $openid->optional = ['namePerson/first', 'media/image/aspect11', 'media/image/default'];
598 $authurl = $openid->authUrl();
599 } catch (Exception $e) {
600 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);
602 System::externalRedirect($authurl);
606 throw new Exception(L10n::t('Please enter the required information.'));
609 if (!Network::isUrlValid($openid_url)) {
613 // collapse multiple spaces in name
614 $username = preg_replace('/ +/', ' ', $username);
616 $username_min_length = max(1, min(64, intval(Config::get('system', 'username_min_length', 3))));
617 $username_max_length = max(1, min(64, intval(Config::get('system', 'username_max_length', 48))));
619 if ($username_min_length > $username_max_length) {
620 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);
621 $tmp = $username_min_length;
622 $username_min_length = $username_max_length;
623 $username_max_length = $tmp;
626 if (mb_strlen($username) < $username_min_length) {
627 throw new Exception(L10n::tt('Username should be at least %s character.', 'Username should be at least %s characters.', $username_min_length));
630 if (mb_strlen($username) > $username_max_length) {
631 throw new Exception(L10n::tt('Username should be at most %s character.', 'Username should be at most %s characters.', $username_max_length));
634 // So now we are just looking for a space in the full name.
635 $loose_reg = Config::get('system', 'no_regfullname');
637 $username = mb_convert_case($username, MB_CASE_TITLE, 'UTF-8');
638 if (strpos($username, ' ') === false) {
639 throw new Exception(L10n::t("That doesn't appear to be your full (First Last) name."));
643 if (!Network::isEmailDomainAllowed($email)) {
644 throw new Exception(L10n::t('Your email domain is not among those allowed on this site.'));
647 if (!filter_var($email, FILTER_VALIDATE_EMAIL) || !Network::isEmailDomainValid($email)) {
648 throw new Exception(L10n::t('Not a valid email address.'));
650 if (self::isNicknameBlocked($nickname)) {
651 throw new Exception(L10n::t('The nickname was blocked from registration by the nodes admin.'));
654 if (Config::get('system', 'block_extended_register', false) && DBA::exists('user', ['email' => $email])) {
655 throw new Exception(L10n::t('Cannot use that email.'));
658 // Disallow somebody creating an account using openid that uses the admin email address,
659 // since openid bypasses email verification. We'll allow it if there is not yet an admin account.
660 if (Config::get('config', 'admin_email') && strlen($openid_url)) {
661 $adminlist = explode(',', str_replace(' ', '', strtolower(Config::get('config', 'admin_email'))));
662 if (in_array(strtolower($email), $adminlist)) {
663 throw new Exception(L10n::t('Cannot use that email.'));
667 $nickname = $data['nickname'] = strtolower($nickname);
669 if (!preg_match('/^[a-z0-9][a-z0-9\_]*$/', $nickname)) {
670 throw new Exception(L10n::t('Your nickname can only contain a-z, 0-9 and _.'));
673 // Check existing and deleted accounts for this nickname.
674 if (DBA::exists('user', ['nickname' => $nickname])
675 || DBA::exists('userd', ['username' => $nickname])
677 throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
680 $new_password = strlen($password) ? $password : User::generateNewPassword();
681 $new_password_encoded = self::hashPassword($new_password);
683 $return['password'] = $new_password;
685 $keys = Crypto::newKeypair(4096);
686 if ($keys === false) {
687 throw new Exception(L10n::t('SERIOUS ERROR: Generation of security keys failed.'));
690 $prvkey = $keys['prvkey'];
691 $pubkey = $keys['pubkey'];
693 // Create another keypair for signing/verifying salmon protocol messages.
694 $sres = Crypto::newKeypair(512);
695 $sprvkey = $sres['prvkey'];
696 $spubkey = $sres['pubkey'];
698 $insert_result = DBA::insert('user', [
699 'guid' => System::createUUID(),
700 'username' => $username,
701 'password' => $new_password_encoded,
703 'openid' => $openid_url,
704 'nickname' => $nickname,
707 'spubkey' => $spubkey,
708 'sprvkey' => $sprvkey,
709 'verified' => $verified,
710 'blocked' => $blocked,
711 'language' => $language,
713 'register_date' => DateTimeFormat::utcNow(),
714 'default-location' => ''
717 if ($insert_result) {
718 $uid = DBA::lastInsertId();
719 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
721 throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
725 throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
728 // if somebody clicked submit twice very quickly, they could end up with two accounts
729 // due to race condition. Remove this one.
730 $user_count = DBA::count('user', ['nickname' => $nickname]);
731 if ($user_count > 1) {
732 DBA::delete('user', ['uid' => $uid]);
734 throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
737 $insert_result = DBA::insert('profile', [
740 'photo' => System::baseUrl() . "/photo/profile/{$uid}.jpg",
741 'thumb' => System::baseUrl() . "/photo/avatar/{$uid}.jpg",
742 'publish' => $publish,
744 'net-publish' => $netpublish,
745 'profile-name' => L10n::t('default')
747 if (!$insert_result) {
748 DBA::delete('user', ['uid' => $uid]);
750 throw new Exception(L10n::t('An error occurred creating your default profile. Please try again.'));
753 // Create the self contact
754 if (!Contact::createSelfFromUserId($uid)) {
755 DBA::delete('user', ['uid' => $uid]);
757 throw new Exception(L10n::t('An error occurred creating your self contact. Please try again.'));
760 // Create a group with no members. This allows somebody to use it
761 // right away as a default group for new contacts.
762 $def_gid = Group::create($uid, L10n::t('Friends'));
764 DBA::delete('user', ['uid' => $uid]);
766 throw new Exception(L10n::t('An error occurred creating your default contact group. Please try again.'));
769 $fields = ['def_gid' => $def_gid];
770 if (Config::get('system', 'newuser_private') && $def_gid) {
771 $fields['allow_gid'] = '<' . $def_gid . '>';
774 DBA::update('user', $fields, ['uid' => $uid]);
776 // if we have no OpenID photo try to look up an avatar
777 if (!strlen($photo)) {
778 $photo = Network::lookupAvatarByEmail($email);
781 // unless there is no avatar-addon loaded
782 if (strlen($photo)) {
783 $photo_failure = false;
785 $filename = basename($photo);
786 $img_str = Network::fetchUrl($photo, true);
787 // guess mimetype from headers or filename
788 $type = Image::guessType($photo, true);
790 $Image = new Image($img_str, $type);
791 if ($Image->isValid()) {
792 $Image->scaleToSquare(300);
794 $hash = Photo::newResource();
796 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 4);
799 $photo_failure = true;
802 $Image->scaleDown(80);
804 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 5);
807 $photo_failure = true;
810 $Image->scaleDown(48);
812 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 6);
815 $photo_failure = true;
818 if (!$photo_failure) {
819 Photo::update(['profile' => 1], ['resource-id' => $hash]);
824 Hook::callAll('register_account', $uid);
826 $return['user'] = $user;
831 * @brief Sends pending registration confirmation email
833 * @param array $user User record array
834 * @param string $sitename
835 * @param string $siteurl
836 * @param string $password Plaintext password
837 * @return NULL|boolean from notification() and email() inherited
838 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
840 public static function sendRegisterPendingEmail($user, $sitename, $siteurl, $password)
842 $body = Strings::deindent(L10n::t('
844 Thank you for registering at %2$s. Your account is pending for approval by the administrator.
846 Your login details are as follows:
852 $user['username'], $sitename, $siteurl, $user['nickname'], $password
855 return notification([
856 'type' => SYSTEM_EMAIL,
857 'uid' => $user['uid'],
858 'to_email' => $user['email'],
859 'subject' => L10n::t('Registration at %s', $sitename),
865 * @brief Sends registration confirmation
867 * It's here as a function because the mail is sent from different parts
869 * @param array $user User record array
870 * @param string $sitename
871 * @param string $siteurl
872 * @param string $password Plaintext password
873 * @return NULL|boolean from notification() and email() inherited
874 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
876 public static function sendRegisterOpenEmail($user, $sitename, $siteurl, $password)
878 $preamble = Strings::deindent(L10n::t('
880 Thank you for registering at %2$s. Your account has been created.
882 $user['username'], $sitename
884 $body = Strings::deindent(L10n::t('
885 The login details are as follows:
891 You may change your password from your account "Settings" page after logging
894 Please take a few moments to review the other account settings on that page.
896 You may also wish to add some basic information to your default profile
897 ' . "\x28" . 'on the "Profiles" page' . "\x29" . ' so that other people can easily find you.
899 We recommend setting your full name, adding a profile photo,
900 adding some profile "keywords" ' . "\x28" . 'very useful in making new friends' . "\x29" . ' - and
901 perhaps what country you live in; if you do not wish to be more specific
904 We fully respect your right to privacy, and none of these items are necessary.
905 If you are new and do not know anybody here, they may help
906 you to make some new and interesting friends.
908 If you ever want to delete your account, you can do so at %3$s/removeme
910 Thank you and welcome to %2$s.',
911 $user['nickname'], $sitename, $siteurl, $user['username'], $password
914 return notification([
915 'uid' => $user['uid'],
916 'language' => $user['language'],
917 'type' => SYSTEM_EMAIL,
918 'to_email' => $user['email'],
919 'subject' => L10n::t('Registration details for %s', $sitename),
920 'preamble' => $preamble,
926 * @param object $uid user to remove
928 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
930 public static function remove($uid)
936 Logger::log('Removing user: ' . $uid);
938 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
940 Hook::callAll('remove_user', $user);
942 // save username (actually the nickname as it is guaranteed
943 // unique), so it cannot be re-registered in the future.
944 DBA::insert('userd', ['username' => $user['nickname']]);
946 // The user and related data will be deleted in "cron_expire_and_remove_users" (cronjobs.php)
947 DBA::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utc('now + 7 day')], ['uid' => $uid]);
948 Worker::add(PRIORITY_HIGH, 'Notifier', Delivery::REMOVAL, $uid);
950 // Send an update to the directory
951 $self = DBA::selectFirst('contact', ['url'], ['uid' => $uid, 'self' => true]);
952 Worker::add(PRIORITY_LOW, 'Directory', $self['url']);
954 // Remove the user relevant data
955 Worker::add(PRIORITY_NEGLIGIBLE, 'RemoveUser', $uid);
961 * Return all identities to a user
963 * @param int $uid The user id
964 * @return array All identities for this user
966 * Example for a return:
970 * 'username' => 'maxmuster',
971 * 'nickname' => 'Max Mustermann'
975 * 'username' => 'johndoe',
976 * 'nickname' => 'John Doe'
981 public static function identities($uid)
985 $user = DBA::selectFirst('user', ['uid', 'nickname', 'username', 'parent-uid'], ['uid' => $uid]);
986 if (!DBA::isResult($user)) {
990 if ($user['parent-uid'] == 0) {
991 // First add our own entry
992 $identities = [['uid' => $user['uid'],
993 'username' => $user['username'],
994 'nickname' => $user['nickname']]];
996 // Then add all the children
997 $r = DBA::select('user', ['uid', 'username', 'nickname'],
998 ['parent-uid' => $user['uid'], 'account_removed' => false]);
999 if (DBA::isResult($r)) {
1000 $identities = array_merge($identities, DBA::toArray($r));
1003 // First entry is our parent
1004 $r = DBA::select('user', ['uid', 'username', 'nickname'],
1005 ['uid' => $user['parent-uid'], 'account_removed' => false]);
1006 if (DBA::isResult($r)) {
1007 $identities = DBA::toArray($r);
1010 // Then add all siblings
1011 $r = DBA::select('user', ['uid', 'username', 'nickname'],
1012 ['parent-uid' => $user['parent-uid'], 'account_removed' => false]);
1013 if (DBA::isResult($r)) {
1014 $identities = array_merge($identities, DBA::toArray($r));
1018 $r = DBA::p("SELECT `user`.`uid`, `user`.`username`, `user`.`nickname`
1020 INNER JOIN `user` ON `manage`.`mid` = `user`.`uid`
1021 WHERE `user`.`account_removed` = 0 AND `manage`.`uid` = ?",
1024 if (DBA::isResult($r)) {
1025 $identities = array_merge($identities, DBA::toArray($r));
1032 * Returns statistical information about the current users of this node
1038 public static function getStatistics()
1042 'active_users_halfyear' => 0,
1043 'active_users_monthly' => 0,
1046 $userStmt = DBA::p("SELECT `user`.`uid`, `user`.`login_date`, `contact`.`last-item`
1048 INNER JOIN `profile` ON `profile`.`uid` = `user`.`uid` AND `profile`.`is-default`
1049 INNER JOIN `contact` ON `contact`.`uid` = `user`.`uid` AND `contact`.`self`
1050 WHERE (`profile`.`publish` OR `profile`.`net-publish`) AND `user`.`verified`
1051 AND NOT `user`.`blocked` AND NOT `user`.`account_removed`
1052 AND NOT `user`.`account_expired`");
1054 if (!DBA::isResult($userStmt)) {
1058 $halfyear = time() - (180 * 24 * 60 * 60);
1059 $month = time() - (30 * 24 * 60 * 60);
1061 while ($user = DBA::fetch($userStmt)) {
1062 $statistics['total_users']++;
1064 if ((strtotime($user['login_date']) > $halfyear) ||
1065 (strtotime($user['last-item']) > $halfyear)) {
1066 $statistics['active_users_halfyear']++;
1069 if ((strtotime($user['login_date']) > $month) ||
1070 (strtotime($user['last-item']) > $month)) {
1071 $statistics['active_users_monthly']++;