4 * @file src/Model/User.php
5 * @brief This file includes the User class with user related database functions
8 namespace Friendica\Model;
10 use DivineOmega\PasswordExposed;
13 use Friendica\Core\Config;
14 use Friendica\Core\Hook;
15 use Friendica\Core\L10n;
16 use Friendica\Core\Logger;
17 use Friendica\Core\PConfig;
18 use Friendica\Core\Protocol;
19 use Friendica\Core\Session;
20 use Friendica\Core\System;
21 use Friendica\Core\Worker;
22 use Friendica\Database\DBA;
23 use Friendica\Model\TwoFactor\AppSpecificPassword;
24 use Friendica\Object\Image;
25 use Friendica\Util\Crypto;
26 use Friendica\Util\DateTimeFormat;
27 use Friendica\Util\Images;
28 use Friendica\Util\Network;
29 use Friendica\Util\Strings;
30 use Friendica\Worker\Delivery;
34 * @brief This class handles User related functions
41 * PAGE_FLAGS_NORMAL is a typical personal profile account
42 * PAGE_FLAGS_SOAPBOX automatically approves all friend requests as Contact::SHARING, (readonly)
43 * PAGE_FLAGS_COMMUNITY automatically approves all friend requests as Contact::SHARING, but with
44 * write access to wall and comments (no email and not included in page owner's ACL lists)
45 * PAGE_FLAGS_FREELOVE automatically approves all friend requests as full friends (Contact::FRIEND).
49 const PAGE_FLAGS_NORMAL = 0;
50 const PAGE_FLAGS_SOAPBOX = 1;
51 const PAGE_FLAGS_COMMUNITY = 2;
52 const PAGE_FLAGS_FREELOVE = 3;
53 const PAGE_FLAGS_BLOG = 4;
54 const PAGE_FLAGS_PRVGROUP = 5;
62 * ACCOUNT_TYPE_PERSON - the account belongs to a person
63 * Associated page types: PAGE_FLAGS_NORMAL, PAGE_FLAGS_SOAPBOX, PAGE_FLAGS_FREELOVE
65 * ACCOUNT_TYPE_ORGANISATION - the account belongs to an organisation
66 * Associated page type: PAGE_FLAGS_SOAPBOX
68 * ACCOUNT_TYPE_NEWS - the account is a news reflector
69 * Associated page type: PAGE_FLAGS_SOAPBOX
71 * ACCOUNT_TYPE_COMMUNITY - the account is community forum
72 * Associated page types: PAGE_COMMUNITY, PAGE_FLAGS_PRVGROUP
74 * ACCOUNT_TYPE_RELAY - the account is a relay
75 * This will only be assigned to contacts, not to user accounts
78 const ACCOUNT_TYPE_PERSON = 0;
79 const ACCOUNT_TYPE_ORGANISATION = 1;
80 const ACCOUNT_TYPE_NEWS = 2;
81 const ACCOUNT_TYPE_COMMUNITY = 3;
82 const ACCOUNT_TYPE_RELAY = 4;
88 * Returns true if a user record exists with the provided id
94 public static function exists($uid)
96 return DBA::exists('user', ['uid' => $uid]);
100 * @param integer $uid
101 * @param array $fields
102 * @return array|boolean User record if it exists, false otherwise
105 public static function getById($uid, array $fields = [])
107 return DBA::selectFirst('user', $fields, ['uid' => $uid]);
111 * Returns a user record based on it's GUID
113 * @param string $guid The guid of the user
114 * @param array $fields The fields to retrieve
115 * @param bool $active True, if only active records are searched
117 * @return array|boolean User record if it exists, false otherwise
120 public static function getByGuid(string $guid, array $fields = [], bool $active = true)
123 $cond = ['guid' => $guid, 'account_expired' => false, 'account_removed' => false];
125 $cond = ['guid' => $guid];
128 return DBA::selectFirst('user', $fields, $cond);
132 * @param string $nickname
133 * @param array $fields
134 * @return array|boolean User record if it exists, false otherwise
137 public static function getByNickname($nickname, array $fields = [])
139 return DBA::selectFirst('user', $fields, ['nickname' => $nickname]);
143 * @brief Returns the user id of a given profile URL
147 * @return integer user id
150 public static function getIdForURL($url)
152 $self = DBA::selectFirst('contact', ['uid'], ['nurl' => Strings::normaliseLink($url), 'self' => true]);
153 if (!DBA::isResult($self)) {
161 * Get a user based on its email
163 * @param string $email
164 * @param array $fields
166 * @return array|boolean User record if it exists, false otherwise
170 public static function getByEmail($email, array $fields = [])
172 return DBA::selectFirst('user', $fields, ['email' => $email]);
176 * @brief Get owner data by user id
179 * @param boolean $check_valid Test if data is invalid and correct it
180 * @return boolean|array
183 public static function getOwnerDataById($uid, $check_valid = true)
185 $r = DBA::fetchFirst(
188 `user`.`prvkey` AS `uprvkey`,
194 `user`.`account-type`,
196 `user`.`account_removed`,
200 ON `user`.`uid` = `contact`.`uid`
201 WHERE `contact`.`uid` = ?
206 if (!DBA::isResult($r)) {
210 if (empty($r['nickname'])) {
218 // Check if the returned data is valid, otherwise fix it. See issue #6122
220 // Check for correct url and normalised nurl
221 $url = System::baseUrl() . '/profile/' . $r['nickname'];
222 $repair = ($r['url'] != $url) || ($r['nurl'] != Strings::normaliseLink($r['url']));
225 // Check if "addr" is present and correct
226 $addr = $r['nickname'] . '@' . substr(System::baseUrl(), strpos(System::baseUrl(), '://') + 3);
227 $repair = ($addr != $r['addr']);
231 // Check if the avatar field is filled and the photo directs to the correct path
232 $avatar = Photo::selectFirst(['resource-id'], ['uid' => $uid, 'profile' => true]);
233 if (DBA::isResult($avatar)) {
234 $repair = empty($r['avatar']) || !strpos($r['photo'], $avatar['resource-id']);
239 Contact::updateSelfFromUserID($uid);
240 // Return the corrected data and avoid a loop
241 $r = self::getOwnerDataById($uid, false);
248 * @brief Get owner data by nick name
251 * @return boolean|array
254 public static function getOwnerDataByNick($nick)
256 $user = DBA::selectFirst('user', ['uid'], ['nickname' => $nick]);
258 if (!DBA::isResult($user)) {
262 return self::getOwnerDataById($user['uid']);
266 * @brief Returns the default group for a given user and network
268 * @param int $uid User id
269 * @param string $network network name
271 * @return int group id
272 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
274 public static function getDefaultGroup($uid, $network = '')
278 if ($network == Protocol::OSTATUS) {
279 $default_group = PConfig::get($uid, "ostatus", "default_group");
282 if ($default_group != 0) {
283 return $default_group;
286 $user = DBA::selectFirst('user', ['def_gid'], ['uid' => $uid]);
288 if (DBA::isResult($user)) {
289 $default_group = $user["def_gid"];
292 return $default_group;
297 * Authenticate a user with a clear text password
299 * @brief Authenticate a user with a clear text password
300 * @param mixed $user_info
301 * @param string $password
302 * @param bool $third_party
303 * @return int|boolean
304 * @deprecated since version 3.6
305 * @see User::getIdFromPasswordAuthentication()
307 public static function authenticate($user_info, $password, $third_party = false)
310 return self::getIdFromPasswordAuthentication($user_info, $password, $third_party);
311 } catch (Exception $ex) {
317 * Returns the user id associated with a successful password authentication
319 * @brief Authenticate a user with a clear text password
320 * @param mixed $user_info
321 * @param string $password
322 * @param bool $third_party
323 * @return int User Id if authentication is successful
326 public static function getIdFromPasswordAuthentication($user_info, $password, $third_party = false)
328 $user = self::getAuthenticationInfo($user_info);
330 if ($third_party && PConfig::get($user['uid'], '2fa', 'verified')) {
331 // Third-party apps can't verify two-factor authentication, we use app-specific passwords instead
332 if (AppSpecificPassword::authenticateUser($user['uid'], $password)) {
335 } elseif (strpos($user['password'], '$') === false) {
336 //Legacy hash that has not been replaced by a new hash yet
337 if (self::hashPasswordLegacy($password) === $user['password']) {
338 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
342 } elseif (!empty($user['legacy_password'])) {
343 //Legacy hash that has been double-hashed and not replaced by a new hash yet
344 //Warning: `legacy_password` is not necessary in sync with the content of `password`
345 if (password_verify(self::hashPasswordLegacy($password), $user['password'])) {
346 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
350 } elseif (password_verify($password, $user['password'])) {
352 if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
353 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
359 throw new Exception(L10n::t('Login failed'));
363 * Returns authentication info from various parameters types
365 * User info can be any of the following:
368 * - User email or username or nickname
369 * - User array with at least the uid and the hashed password
371 * @param mixed $user_info
375 private static function getAuthenticationInfo($user_info)
379 if (is_object($user_info) || is_array($user_info)) {
380 if (is_object($user_info)) {
381 $user = (array) $user_info;
388 || !isset($user['password'])
389 || !isset($user['legacy_password'])
391 throw new Exception(L10n::t('Not enough information to authenticate'));
393 } elseif (is_int($user_info) || is_string($user_info)) {
394 if (is_int($user_info)) {
395 $user = DBA::selectFirst(
397 ['uid', 'password', 'legacy_password'],
401 'account_expired' => 0,
402 'account_removed' => 0,
407 $fields = ['uid', 'password', 'legacy_password'];
409 "(`email` = ? OR `username` = ? OR `nickname` = ?)
410 AND NOT `blocked` AND NOT `account_expired` AND NOT `account_removed` AND `verified`",
411 $user_info, $user_info, $user_info
413 $user = DBA::selectFirst('user', $fields, $condition);
416 if (!DBA::isResult($user)) {
417 throw new Exception(L10n::t('User not found'));
425 * Generates a human-readable random password
429 public static function generateNewPassword()
431 return ucfirst(Strings::getRandomName(8)) . random_int(1000, 9999);
435 * Checks if the provided plaintext password has been exposed or not
437 * @param string $password
441 public static function isPasswordExposed($password)
443 $cache = new \DivineOmega\DOFileCachePSR6\CacheItemPool();
444 $cache->changeConfig([
445 'cacheDirectory' => get_temppath() . '/password-exposed-cache/',
449 $passwordExposedChecker = new PasswordExposed\PasswordExposedChecker(null, $cache);
451 return $passwordExposedChecker->passwordExposed($password) === PasswordExposed\PasswordStatus::EXPOSED;
452 } catch (\Exception $e) {
453 Logger::error('Password Exposed Exception: ' . $e->getMessage(), [
454 'code' => $e->getCode(),
455 'file' => $e->getFile(),
456 'line' => $e->getLine(),
457 'trace' => $e->getTraceAsString()
465 * Legacy hashing function, kept for password migration purposes
467 * @param string $password
470 private static function hashPasswordLegacy($password)
472 return hash('whirlpool', $password);
476 * Global user password hashing function
478 * @param string $password
482 public static function hashPassword($password)
484 if (!trim($password)) {
485 throw new Exception(L10n::t('Password can\'t be empty'));
488 return password_hash($password, PASSWORD_DEFAULT);
492 * Updates a user row with a new plaintext password
495 * @param string $password
499 public static function updatePassword($uid, $password)
501 $password = trim($password);
503 if (empty($password)) {
504 throw new Exception(L10n::t('Empty passwords are not allowed.'));
507 if (!Config::get('system', 'disable_password_exposed', false) && self::isPasswordExposed($password)) {
508 throw new Exception(L10n::t('The new password has been exposed in a public data dump, please choose another.'));
511 $allowed_characters = '!"#$%&\'()*+,-./;<=>?@[\]^_`{|}~';
513 if (!preg_match('/^[a-z0-9' . preg_quote($allowed_characters, '/') . ']+$/i', $password)) {
514 throw new Exception(L10n::t('The password can\'t contain accentuated letters, white spaces or colons (:)'));
517 return self::updatePasswordHashed($uid, self::hashPassword($password));
521 * Updates a user row with a new hashed password.
522 * Empties the password reset token field just in case.
525 * @param string $pasword_hashed
529 private static function updatePasswordHashed($uid, $pasword_hashed)
532 'password' => $pasword_hashed,
534 'pwdreset_time' => null,
535 'legacy_password' => false
537 return DBA::update('user', $fields, ['uid' => $uid]);
541 * @brief Checks if a nickname is in the list of the forbidden nicknames
543 * Check if a nickname is forbidden from registration on the node by the
544 * admin. Forbidden nicknames (e.g. role namess) can be configured in the
547 * @param string $nickname The nickname that should be checked
548 * @return boolean True is the nickname is blocked on the node
549 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
551 public static function isNicknameBlocked($nickname)
553 $forbidden_nicknames = Config::get('system', 'forbidden_nicknames', '');
555 // if the config variable is empty return false
556 if (empty($forbidden_nicknames)) {
560 // check if the nickname is in the list of blocked nicknames
561 $forbidden = explode(',', $forbidden_nicknames);
562 $forbidden = array_map('trim', $forbidden);
563 if (in_array(strtolower($nickname), $forbidden)) {
572 * @brief Catch-all user creation function
574 * Creates a user from the provided data array, either form fields or OpenID.
575 * Required: { username, nickname, email } or { openid_url }
577 * Performs the following:
578 * - Sends to the OpenId auth URL (if relevant)
579 * - Creates new key pairs for crypto
580 * - Create self-contact
581 * - Create profile image
585 * @throws \ErrorException
586 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
587 * @throws \ImagickException
590 public static function create(array $data)
593 $return = ['user' => null, 'password' => ''];
595 $using_invites = Config::get('system', 'invitation_only');
597 $invite_id = !empty($data['invite_id']) ? Strings::escapeTags(trim($data['invite_id'])) : '';
598 $username = !empty($data['username']) ? Strings::escapeTags(trim($data['username'])) : '';
599 $nickname = !empty($data['nickname']) ? Strings::escapeTags(trim($data['nickname'])) : '';
600 $email = !empty($data['email']) ? Strings::escapeTags(trim($data['email'])) : '';
601 $openid_url = !empty($data['openid_url']) ? Strings::escapeTags(trim($data['openid_url'])) : '';
602 $photo = !empty($data['photo']) ? Strings::escapeTags(trim($data['photo'])) : '';
603 $password = !empty($data['password']) ? trim($data['password']) : '';
604 $password1 = !empty($data['password1']) ? trim($data['password1']) : '';
605 $confirm = !empty($data['confirm']) ? trim($data['confirm']) : '';
606 $blocked = !empty($data['blocked']);
607 $verified = !empty($data['verified']);
608 $language = !empty($data['language']) ? Strings::escapeTags(trim($data['language'])) : 'en';
610 $publish = !empty($data['profile_publish_reg']);
611 $netpublish = $publish && Config::get('system', 'directory');
613 if ($password1 != $confirm) {
614 throw new Exception(L10n::t('Passwords do not match. Password unchanged.'));
615 } elseif ($password1 != '') {
616 $password = $password1;
619 if ($using_invites) {
621 throw new Exception(L10n::t('An invitation is required.'));
624 if (!Register::existsByHash($invite_id)) {
625 throw new Exception(L10n::t('Invitation could not be verified.'));
629 /// @todo Check if this part is really needed. We should have fetched all this data in advance
630 if (empty($username) || empty($email) || empty($nickname)) {
632 if (!Network::isUrlValid($openid_url)) {
633 throw new Exception(L10n::t('Invalid OpenID url'));
635 $_SESSION['register'] = 1;
636 $_SESSION['openid'] = $openid_url;
638 $openid = new LightOpenID($a->getHostName());
639 $openid->identity = $openid_url;
640 $openid->returnUrl = System::baseUrl() . '/openid';
641 $openid->required = ['namePerson/friendly', 'contact/email', 'namePerson'];
642 $openid->optional = ['namePerson/first', 'media/image/aspect11', 'media/image/default'];
644 $authurl = $openid->authUrl();
645 } catch (Exception $e) {
646 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);
648 System::externalRedirect($authurl);
652 throw new Exception(L10n::t('Please enter the required information.'));
655 if (!Network::isUrlValid($openid_url)) {
659 // collapse multiple spaces in name
660 $username = preg_replace('/ +/', ' ', $username);
662 $username_min_length = max(1, min(64, intval(Config::get('system', 'username_min_length', 3))));
663 $username_max_length = max(1, min(64, intval(Config::get('system', 'username_max_length', 48))));
665 if ($username_min_length > $username_max_length) {
666 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);
667 $tmp = $username_min_length;
668 $username_min_length = $username_max_length;
669 $username_max_length = $tmp;
672 if (mb_strlen($username) < $username_min_length) {
673 throw new Exception(L10n::tt('Username should be at least %s character.', 'Username should be at least %s characters.', $username_min_length));
676 if (mb_strlen($username) > $username_max_length) {
677 throw new Exception(L10n::tt('Username should be at most %s character.', 'Username should be at most %s characters.', $username_max_length));
680 // So now we are just looking for a space in the full name.
681 $loose_reg = Config::get('system', 'no_regfullname');
683 $username = mb_convert_case($username, MB_CASE_TITLE, 'UTF-8');
684 if (strpos($username, ' ') === false) {
685 throw new Exception(L10n::t("That doesn't appear to be your full (First Last) name."));
689 if (!Network::isEmailDomainAllowed($email)) {
690 throw new Exception(L10n::t('Your email domain is not among those allowed on this site.'));
693 if (!filter_var($email, FILTER_VALIDATE_EMAIL) || !Network::isEmailDomainValid($email)) {
694 throw new Exception(L10n::t('Not a valid email address.'));
696 if (self::isNicknameBlocked($nickname)) {
697 throw new Exception(L10n::t('The nickname was blocked from registration by the nodes admin.'));
700 if (Config::get('system', 'block_extended_register', false) && DBA::exists('user', ['email' => $email])) {
701 throw new Exception(L10n::t('Cannot use that email.'));
704 // Disallow somebody creating an account using openid that uses the admin email address,
705 // since openid bypasses email verification. We'll allow it if there is not yet an admin account.
706 if (Config::get('config', 'admin_email') && strlen($openid_url)) {
707 $adminlist = explode(',', str_replace(' ', '', strtolower(Config::get('config', 'admin_email'))));
708 if (in_array(strtolower($email), $adminlist)) {
709 throw new Exception(L10n::t('Cannot use that email.'));
713 $nickname = $data['nickname'] = strtolower($nickname);
715 if (!preg_match('/^[a-z0-9][a-z0-9\_]*$/', $nickname)) {
716 throw new Exception(L10n::t('Your nickname can only contain a-z, 0-9 and _.'));
719 // Check existing and deleted accounts for this nickname.
721 DBA::exists('user', ['nickname' => $nickname])
722 || DBA::exists('userd', ['username' => $nickname])
724 throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
727 $new_password = strlen($password) ? $password : User::generateNewPassword();
728 $new_password_encoded = self::hashPassword($new_password);
730 $return['password'] = $new_password;
732 $keys = Crypto::newKeypair(4096);
733 if ($keys === false) {
734 throw new Exception(L10n::t('SERIOUS ERROR: Generation of security keys failed.'));
737 $prvkey = $keys['prvkey'];
738 $pubkey = $keys['pubkey'];
740 // Create another keypair for signing/verifying salmon protocol messages.
741 $sres = Crypto::newKeypair(512);
742 $sprvkey = $sres['prvkey'];
743 $spubkey = $sres['pubkey'];
745 $insert_result = DBA::insert('user', [
746 'guid' => System::createUUID(),
747 'username' => $username,
748 'password' => $new_password_encoded,
750 'openid' => $openid_url,
751 'nickname' => $nickname,
754 'spubkey' => $spubkey,
755 'sprvkey' => $sprvkey,
756 'verified' => $verified,
757 'blocked' => $blocked,
758 'language' => $language,
760 'register_date' => DateTimeFormat::utcNow(),
761 'default-location' => ''
764 if ($insert_result) {
765 $uid = DBA::lastInsertId();
766 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
768 throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
772 throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
775 // if somebody clicked submit twice very quickly, they could end up with two accounts
776 // due to race condition. Remove this one.
777 $user_count = DBA::count('user', ['nickname' => $nickname]);
778 if ($user_count > 1) {
779 DBA::delete('user', ['uid' => $uid]);
781 throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
784 $insert_result = DBA::insert('profile', [
787 'photo' => System::baseUrl() . "/photo/profile/{$uid}.jpg",
788 'thumb' => System::baseUrl() . "/photo/avatar/{$uid}.jpg",
789 'publish' => $publish,
791 'net-publish' => $netpublish,
792 'profile-name' => L10n::t('default')
794 if (!$insert_result) {
795 DBA::delete('user', ['uid' => $uid]);
797 throw new Exception(L10n::t('An error occurred creating your default profile. Please try again.'));
800 // Create the self contact
801 if (!Contact::createSelfFromUserId($uid)) {
802 DBA::delete('user', ['uid' => $uid]);
804 throw new Exception(L10n::t('An error occurred creating your self contact. Please try again.'));
807 // Create a group with no members. This allows somebody to use it
808 // right away as a default group for new contacts.
809 $def_gid = Group::create($uid, L10n::t('Friends'));
811 DBA::delete('user', ['uid' => $uid]);
813 throw new Exception(L10n::t('An error occurred creating your default contact group. Please try again.'));
816 $fields = ['def_gid' => $def_gid];
817 if (Config::get('system', 'newuser_private') && $def_gid) {
818 $fields['allow_gid'] = '<' . $def_gid . '>';
821 DBA::update('user', $fields, ['uid' => $uid]);
823 // if we have no OpenID photo try to look up an avatar
824 if (!strlen($photo)) {
825 $photo = Network::lookupAvatarByEmail($email);
828 // unless there is no avatar-addon loaded
829 if (strlen($photo)) {
830 $photo_failure = false;
832 $filename = basename($photo);
833 $img_str = Network::fetchUrl($photo, true);
834 // guess mimetype from headers or filename
835 $type = Images::guessType($photo, true);
837 $Image = new Image($img_str, $type);
838 if ($Image->isValid()) {
839 $Image->scaleToSquare(300);
841 $resource_id = Photo::newResource();
843 $r = Photo::store($Image, $uid, 0, $resource_id, $filename, L10n::t('Profile Photos'), 4);
846 $photo_failure = true;
849 $Image->scaleDown(80);
851 $r = Photo::store($Image, $uid, 0, $resource_id, $filename, L10n::t('Profile Photos'), 5);
854 $photo_failure = true;
857 $Image->scaleDown(48);
859 $r = Photo::store($Image, $uid, 0, $resource_id, $filename, L10n::t('Profile Photos'), 6);
862 $photo_failure = true;
865 if (!$photo_failure) {
866 Photo::update(['profile' => 1], ['resource-id' => $resource_id]);
871 Hook::callAll('register_account', $uid);
873 $return['user'] = $user;
878 * @brief Sends pending registration confirmation email
880 * @param array $user User record array
881 * @param string $sitename
882 * @param string $siteurl
883 * @param string $password Plaintext password
884 * @return NULL|boolean from notification() and email() inherited
885 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
887 public static function sendRegisterPendingEmail($user, $sitename, $siteurl, $password)
889 $body = Strings::deindent(L10n::t(
892 Thank you for registering at %2$s. Your account is pending for approval by the administrator.
894 Your login details are as follows:
907 return notification([
908 'type' => SYSTEM_EMAIL,
909 'uid' => $user['uid'],
910 'to_email' => $user['email'],
911 'subject' => L10n::t('Registration at %s', $sitename),
917 * @brief Sends registration confirmation
919 * It's here as a function because the mail is sent from different parts
921 * @param array $user User record array
922 * @param string $sitename
923 * @param string $siteurl
924 * @param string $password Plaintext password
925 * @return NULL|boolean from notification() and email() inherited
926 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
928 public static function sendRegisterOpenEmail($user, $sitename, $siteurl, $password)
930 $preamble = Strings::deindent(L10n::t(
933 Thank you for registering at %2$s. Your account has been created.
938 $body = Strings::deindent(L10n::t(
940 The login details are as follows:
946 You may change your password from your account "Settings" page after logging
949 Please take a few moments to review the other account settings on that page.
951 You may also wish to add some basic information to your default profile
952 ' . "\x28" . 'on the "Profiles" page' . "\x29" . ' so that other people can easily find you.
954 We recommend setting your full name, adding a profile photo,
955 adding some profile "keywords" ' . "\x28" . 'very useful in making new friends' . "\x29" . ' - and
956 perhaps what country you live in; if you do not wish to be more specific
959 We fully respect your right to privacy, and none of these items are necessary.
960 If you are new and do not know anybody here, they may help
961 you to make some new and interesting friends.
963 If you ever want to delete your account, you can do so at %3$s/removeme
965 Thank you and welcome to %2$s.',
973 return notification([
974 'uid' => $user['uid'],
975 'language' => $user['language'],
976 'type' => SYSTEM_EMAIL,
977 'to_email' => $user['email'],
978 'subject' => L10n::t('Registration details for %s', $sitename),
979 'preamble' => $preamble,
985 * @param object $uid user to remove
987 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
989 public static function remove($uid)
995 Logger::log('Removing user: ' . $uid);
997 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
999 Hook::callAll('remove_user', $user);
1001 // save username (actually the nickname as it is guaranteed
1002 // unique), so it cannot be re-registered in the future.
1003 DBA::insert('userd', ['username' => $user['nickname']]);
1005 // The user and related data will be deleted in "cron_expire_and_remove_users" (cronjobs.php)
1006 DBA::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utc('now + 7 day')], ['uid' => $uid]);
1007 Worker::add(PRIORITY_HIGH, 'Notifier', Delivery::REMOVAL, $uid);
1009 // Send an update to the directory
1010 $self = DBA::selectFirst('contact', ['url'], ['uid' => $uid, 'self' => true]);
1011 Worker::add(PRIORITY_LOW, 'Directory', $self['url']);
1013 // Remove the user relevant data
1014 Worker::add(PRIORITY_NEGLIGIBLE, 'RemoveUser', $uid);
1020 * Return all identities to a user
1022 * @param int $uid The user id
1023 * @return array All identities for this user
1025 * Example for a return:
1029 * 'username' => 'maxmuster',
1030 * 'nickname' => 'Max Mustermann'
1034 * 'username' => 'johndoe',
1035 * 'nickname' => 'John Doe'
1040 public static function identities($uid)
1044 $user = DBA::selectFirst('user', ['uid', 'nickname', 'username', 'parent-uid'], ['uid' => $uid]);
1045 if (!DBA::isResult($user)) {
1049 if ($user['parent-uid'] == 0) {
1050 // First add our own entry
1052 'uid' => $user['uid'],
1053 'username' => $user['username'],
1054 'nickname' => $user['nickname']
1057 // Then add all the children
1060 ['uid', 'username', 'nickname'],
1061 ['parent-uid' => $user['uid'], 'account_removed' => false]
1063 if (DBA::isResult($r)) {
1064 $identities = array_merge($identities, DBA::toArray($r));
1067 // First entry is our parent
1070 ['uid', 'username', 'nickname'],
1071 ['uid' => $user['parent-uid'], 'account_removed' => false]
1073 if (DBA::isResult($r)) {
1074 $identities = DBA::toArray($r);
1077 // Then add all siblings
1080 ['uid', 'username', 'nickname'],
1081 ['parent-uid' => $user['parent-uid'], 'account_removed' => false]
1083 if (DBA::isResult($r)) {
1084 $identities = array_merge($identities, DBA::toArray($r));
1089 "SELECT `user`.`uid`, `user`.`username`, `user`.`nickname`
1091 INNER JOIN `user` ON `manage`.`mid` = `user`.`uid`
1092 WHERE `user`.`account_removed` = 0 AND `manage`.`uid` = ?",
1095 if (DBA::isResult($r)) {
1096 $identities = array_merge($identities, DBA::toArray($r));
1103 * Returns statistical information about the current users of this node
1109 public static function getStatistics()
1113 'active_users_halfyear' => 0,
1114 'active_users_monthly' => 0,
1117 $userStmt = DBA::p("SELECT `user`.`uid`, `user`.`login_date`, `contact`.`last-item`
1119 INNER JOIN `profile` ON `profile`.`uid` = `user`.`uid` AND `profile`.`is-default`
1120 INNER JOIN `contact` ON `contact`.`uid` = `user`.`uid` AND `contact`.`self`
1121 WHERE (`profile`.`publish` OR `profile`.`net-publish`) AND `user`.`verified`
1122 AND NOT `user`.`blocked` AND NOT `user`.`account_removed`
1123 AND NOT `user`.`account_expired`");
1125 if (!DBA::isResult($userStmt)) {
1129 $halfyear = time() - (180 * 24 * 60 * 60);
1130 $month = time() - (30 * 24 * 60 * 60);
1132 while ($user = DBA::fetch($userStmt)) {
1133 $statistics['total_users']++;
1135 if ((strtotime($user['login_date']) > $halfyear) || (strtotime($user['last-item']) > $halfyear)
1137 $statistics['active_users_halfyear']++;
1140 if ((strtotime($user['login_date']) > $month) || (strtotime($user['last-item']) > $month)
1142 $statistics['active_users_monthly']++;