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\Addon;
11 use Friendica\Core\Config;
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;
26 require_once 'boot.php';
27 require_once 'include/dba.php';
28 require_once 'include/enotify.php';
29 require_once 'include/text.php';
31 * @brief This class handles User related functions
36 * Returns true if a user record exists with the provided id
41 public static function exists($uid)
43 return DBA::exists('user', ['uid' => $uid]);
48 * @return array|boolean User record if it exists, false otherwise
50 public static function getById($uid)
52 return DBA::selectFirst('user', [], ['uid' => $uid]);
56 * @brief Returns the user id of a given profile URL
60 * @return integer user id
62 public static function getIdForURL($url)
64 $self = DBA::selectFirst('contact', ['uid'], ['nurl' => Strings::normaliseLink($url), 'self' => true]);
65 if (!DBA::isResult($self)) {
73 * @brief Get owner data by user id
76 * @return boolean|array
78 public static function getOwnerDataById($uid) {
79 $r = DBA::fetchFirst("SELECT
81 `user`.`prvkey` AS `uprvkey`,
87 `user`.`account-type`,
91 ON `user`.`uid` = `contact`.`uid`
92 WHERE `contact`.`uid` = ?
97 if (!DBA::isResult($r)) {
104 * @brief Get owner data by nick name
107 * @return boolean|array
109 public static function getOwnerDataByNick($nick)
111 $user = DBA::selectFirst('user', ['uid'], ['nickname' => $nick]);
113 if (!DBA::isResult($user)) {
117 return self::getOwnerDataById($user['uid']);
121 * @brief Returns the default group for a given user and network
123 * @param int $uid User id
124 * @param string $network network name
126 * @return int group id
128 public static function getDefaultGroup($uid, $network = '')
132 if ($network == Protocol::OSTATUS) {
133 $default_group = PConfig::get($uid, "ostatus", "default_group");
136 if ($default_group != 0) {
137 return $default_group;
140 $user = DBA::selectFirst('user', ['def_gid'], ['uid' => $uid]);
142 if (DBA::isResult($user)) {
143 $default_group = $user["def_gid"];
146 return $default_group;
151 * Authenticate a user with a clear text password
153 * @brief Authenticate a user with a clear text password
154 * @param mixed $user_info
155 * @param string $password
156 * @return int|boolean
157 * @deprecated since version 3.6
158 * @see User::getIdFromPasswordAuthentication()
160 public static function authenticate($user_info, $password)
163 return self::getIdFromPasswordAuthentication($user_info, $password);
164 } catch (Exception $ex) {
170 * Returns the user id associated with a successful password authentication
172 * @brief Authenticate a user with a clear text password
173 * @param mixed $user_info
174 * @param string $password
175 * @return int User Id if authentication is successful
178 public static function getIdFromPasswordAuthentication($user_info, $password)
180 $user = self::getAuthenticationInfo($user_info);
182 if (strpos($user['password'], '$') === false) {
183 //Legacy hash that has not been replaced by a new hash yet
184 if (self::hashPasswordLegacy($password) === $user['password']) {
185 self::updatePassword($user['uid'], $password);
189 } elseif (!empty($user['legacy_password'])) {
190 //Legacy hash that has been double-hashed and not replaced by a new hash yet
191 //Warning: `legacy_password` is not necessary in sync with the content of `password`
192 if (password_verify(self::hashPasswordLegacy($password), $user['password'])) {
193 self::updatePassword($user['uid'], $password);
197 } elseif (password_verify($password, $user['password'])) {
199 if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
200 self::updatePassword($user['uid'], $password);
206 throw new Exception(L10n::t('Login failed'));
210 * Returns authentication info from various parameters types
212 * User info can be any of the following:
215 * - User email or username or nickname
216 * - User array with at least the uid and the hashed password
218 * @param mixed $user_info
222 private static function getAuthenticationInfo($user_info)
226 if (is_object($user_info) || is_array($user_info)) {
227 if (is_object($user_info)) {
228 $user = (array) $user_info;
233 if (!isset($user['uid'])
234 || !isset($user['password'])
235 || !isset($user['legacy_password'])
237 throw new Exception(L10n::t('Not enough information to authenticate'));
239 } elseif (is_int($user_info) || is_string($user_info)) {
240 if (is_int($user_info)) {
241 $user = DBA::selectFirst('user', ['uid', 'password', 'legacy_password'],
245 'account_expired' => 0,
246 'account_removed' => 0,
251 $fields = ['uid', 'password', 'legacy_password'];
252 $condition = ["(`email` = ? OR `username` = ? OR `nickname` = ?)
253 AND NOT `blocked` AND NOT `account_expired` AND NOT `account_removed` AND `verified`",
254 $user_info, $user_info, $user_info];
255 $user = DBA::selectFirst('user', $fields, $condition);
258 if (!DBA::isResult($user)) {
259 throw new Exception(L10n::t('User not found'));
267 * Generates a human-readable random password
271 public static function generateNewPassword()
273 return Strings::getRandomName(6) . mt_rand(100, 9999);
277 * Checks if the provided plaintext password has been exposed or not
279 * @param string $password
282 public static function isPasswordExposed($password)
284 $cache = new \DivineOmega\DOFileCachePSR6\CacheItemPool();
285 $cache->changeConfig([
286 'cacheDirectory' => get_temppath() . '/password-exposed-cache/',
289 $PasswordExposedCHecker = new PasswordExposed\PasswordExposedChecker(null, $cache);
291 return $PasswordExposedCHecker->passwordExposed($password) === PasswordExposed\PasswordStatus::EXPOSED;
295 * Legacy hashing function, kept for password migration purposes
297 * @param string $password
300 private static function hashPasswordLegacy($password)
302 return hash('whirlpool', $password);
306 * Global user password hashing function
308 * @param string $password
311 public static function hashPassword($password)
313 if (!trim($password)) {
314 throw new Exception(L10n::t('Password can\'t be empty'));
317 return password_hash($password, PASSWORD_DEFAULT);
321 * Updates a user row with a new plaintext password
324 * @param string $password
327 public static function updatePassword($uid, $password)
329 return self::updatePasswordHashed($uid, self::hashPassword($password));
333 * Updates a user row with a new hashed password.
334 * Empties the password reset token field just in case.
337 * @param string $pasword_hashed
340 private static function updatePasswordHashed($uid, $pasword_hashed)
343 'password' => $pasword_hashed,
345 'pwdreset_time' => null,
346 'legacy_password' => false
348 return DBA::update('user', $fields, ['uid' => $uid]);
352 * @brief Checks if a nickname is in the list of the forbidden nicknames
354 * Check if a nickname is forbidden from registration on the node by the
355 * admin. Forbidden nicknames (e.g. role namess) can be configured in the
358 * @param string $nickname The nickname that should be checked
359 * @return boolean True is the nickname is blocked on the node
361 public static function isNicknameBlocked($nickname)
363 $forbidden_nicknames = Config::get('system', 'forbidden_nicknames', '');
365 // if the config variable is empty return false
366 if (empty($forbidden_nicknames)) {
370 // check if the nickname is in the list of blocked nicknames
371 $forbidden = explode(',', $forbidden_nicknames);
372 $forbidden = array_map('trim', $forbidden);
373 if (in_array(strtolower($nickname), $forbidden)) {
382 * @brief Catch-all user creation function
384 * Creates a user from the provided data array, either form fields or OpenID.
385 * Required: { username, nickname, email } or { openid_url }
387 * Performs the following:
388 * - Sends to the OpenId auth URL (if relevant)
389 * - Creates new key pairs for crypto
390 * - Create self-contact
391 * - Create profile image
397 public static function create(array $data)
400 $return = ['user' => null, 'password' => ''];
402 $using_invites = Config::get('system', 'invitation_only');
403 $num_invites = Config::get('system', 'number_invites');
405 $invite_id = !empty($data['invite_id']) ? Strings::escapeTags(trim($data['invite_id'])) : '';
406 $username = !empty($data['username']) ? Strings::escapeTags(trim($data['username'])) : '';
407 $nickname = !empty($data['nickname']) ? Strings::escapeTags(trim($data['nickname'])) : '';
408 $email = !empty($data['email']) ? Strings::escapeTags(trim($data['email'])) : '';
409 $openid_url = !empty($data['openid_url']) ? Strings::escapeTags(trim($data['openid_url'])) : '';
410 $photo = !empty($data['photo']) ? Strings::escapeTags(trim($data['photo'])) : '';
411 $password = !empty($data['password']) ? trim($data['password']) : '';
412 $password1 = !empty($data['password1']) ? trim($data['password1']) : '';
413 $confirm = !empty($data['confirm']) ? trim($data['confirm']) : '';
414 $blocked = !empty($data['blocked']) ? intval($data['blocked']) : 0;
415 $verified = !empty($data['verified']) ? intval($data['verified']) : 0;
416 $language = !empty($data['language']) ? Strings::escapeTags(trim($data['language'])) : 'en';
418 $publish = !empty($data['profile_publish_reg']) && intval($data['profile_publish_reg']) ? 1 : 0;
419 $netpublish = strlen(Config::get('system', 'directory')) ? $publish : 0;
421 if ($password1 != $confirm) {
422 throw new Exception(L10n::t('Passwords do not match. Password unchanged.'));
423 } elseif ($password1 != '') {
424 $password = $password1;
427 if ($using_invites) {
429 throw new Exception(L10n::t('An invitation is required.'));
432 if (!Register::existsByHash($invite_id)) {
433 throw new Exception(L10n::t('Invitation could not be verified.'));
437 if (empty($username) || empty($email) || empty($nickname)) {
439 if (!Network::isUrlValid($openid_url)) {
440 throw new Exception(L10n::t('Invalid OpenID url'));
442 $_SESSION['register'] = 1;
443 $_SESSION['openid'] = $openid_url;
445 $openid = new LightOpenID($a->getHostName());
446 $openid->identity = $openid_url;
447 $openid->returnUrl = System::baseUrl() . '/openid';
448 $openid->required = ['namePerson/friendly', 'contact/email', 'namePerson'];
449 $openid->optional = ['namePerson/first', 'media/image/aspect11', 'media/image/default'];
451 $authurl = $openid->authUrl();
452 } catch (Exception $e) {
453 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);
455 System::externalRedirect($authurl);
459 throw new Exception(L10n::t('Please enter the required information.'));
462 if (!Network::isUrlValid($openid_url)) {
468 // collapse multiple spaces in name
469 $username = preg_replace('/ +/', ' ', $username);
471 $username_min_length = max(1, min(64, intval(Config::get('system', 'username_min_length', 3))));
472 $username_max_length = max(1, min(64, intval(Config::get('system', 'username_max_length', 48))));
474 if ($username_min_length > $username_max_length) {
475 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);
476 $tmp = $username_min_length;
477 $username_min_length = $username_max_length;
478 $username_max_length = $tmp;
481 if (mb_strlen($username) < $username_min_length) {
482 throw new Exception(L10n::tt('Username should be at least %s character.', 'Username should be at least %s characters.', $username_min_length));
485 if (mb_strlen($username) > $username_max_length) {
486 throw new Exception(L10n::tt('Username should be at most %s character.', 'Username should be at most %s characters.', $username_max_length));
489 // So now we are just looking for a space in the full name.
490 $loose_reg = Config::get('system', 'no_regfullname');
492 $username = mb_convert_case($username, MB_CASE_TITLE, 'UTF-8');
493 if (strpos($username, ' ') === false) {
494 throw new Exception(L10n::t("That doesn't appear to be your full (First Last) name."));
498 if (!Network::isEmailDomainAllowed($email)) {
499 throw new Exception(L10n::t('Your email domain is not among those allowed on this site.'));
502 if (!filter_var($email, FILTER_VALIDATE_EMAIL) || !Network::isEmailDomainValid($email)) {
503 throw new Exception(L10n::t('Not a valid email address.'));
505 if (self::isNicknameBlocked($nickname)) {
506 throw new Exception(L10n::t('The nickname was blocked from registration by the nodes admin.'));
509 if (Config::get('system', 'block_extended_register', false) && DBA::exists('user', ['email' => $email])) {
510 throw new Exception(L10n::t('Cannot use that email.'));
513 // Disallow somebody creating an account using openid that uses the admin email address,
514 // since openid bypasses email verification. We'll allow it if there is not yet an admin account.
515 if (Config::get('config', 'admin_email') && strlen($openid_url)) {
516 $adminlist = explode(',', str_replace(' ', '', strtolower(Config::get('config', 'admin_email'))));
517 if (in_array(strtolower($email), $adminlist)) {
518 throw new Exception(L10n::t('Cannot use that email.'));
522 $nickname = $data['nickname'] = strtolower($nickname);
524 if (!preg_match('/^[a-z0-9][a-z0-9\_]*$/', $nickname)) {
525 throw new Exception(L10n::t('Your nickname can only contain a-z, 0-9 and _.'));
528 // Check existing and deleted accounts for this nickname.
529 if (DBA::exists('user', ['nickname' => $nickname])
530 || DBA::exists('userd', ['username' => $nickname])
532 throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
535 $new_password = strlen($password) ? $password : User::generateNewPassword();
536 $new_password_encoded = self::hashPassword($new_password);
538 $return['password'] = $new_password;
540 $keys = Crypto::newKeypair(4096);
541 if ($keys === false) {
542 throw new Exception(L10n::t('SERIOUS ERROR: Generation of security keys failed.'));
545 $prvkey = $keys['prvkey'];
546 $pubkey = $keys['pubkey'];
548 // Create another keypair for signing/verifying salmon protocol messages.
549 $sres = Crypto::newKeypair(512);
550 $sprvkey = $sres['prvkey'];
551 $spubkey = $sres['pubkey'];
553 $insert_result = DBA::insert('user', [
554 'guid' => System::createUUID(),
555 'username' => $username,
556 'password' => $new_password_encoded,
558 'openid' => $openid_url,
559 'nickname' => $nickname,
562 'spubkey' => $spubkey,
563 'sprvkey' => $sprvkey,
564 'verified' => $verified,
565 'blocked' => $blocked,
566 'language' => $language,
568 'register_date' => DateTimeFormat::utcNow(),
569 'default-location' => ''
572 if ($insert_result) {
573 $uid = DBA::lastInsertId();
574 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
576 throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
580 throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
583 // if somebody clicked submit twice very quickly, they could end up with two accounts
584 // due to race condition. Remove this one.
585 $user_count = DBA::count('user', ['nickname' => $nickname]);
586 if ($user_count > 1) {
587 DBA::delete('user', ['uid' => $uid]);
589 throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
592 $insert_result = DBA::insert('profile', [
595 'photo' => System::baseUrl() . "/photo/profile/{$uid}.jpg",
596 'thumb' => System::baseUrl() . "/photo/avatar/{$uid}.jpg",
597 'publish' => $publish,
599 'net-publish' => $netpublish,
600 'profile-name' => L10n::t('default')
602 if (!$insert_result) {
603 DBA::delete('user', ['uid' => $uid]);
605 throw new Exception(L10n::t('An error occurred creating your default profile. Please try again.'));
608 // Create the self contact
609 if (!Contact::createSelfFromUserId($uid)) {
610 DBA::delete('user', ['uid' => $uid]);
612 throw new Exception(L10n::t('An error occurred creating your self contact. Please try again.'));
615 // Create a group with no members. This allows somebody to use it
616 // right away as a default group for new contacts.
617 $def_gid = Group::create($uid, L10n::t('Friends'));
619 DBA::delete('user', ['uid' => $uid]);
621 throw new Exception(L10n::t('An error occurred creating your default contact group. Please try again.'));
624 $fields = ['def_gid' => $def_gid];
625 if (Config::get('system', 'newuser_private') && $def_gid) {
626 $fields['allow_gid'] = '<' . $def_gid . '>';
629 DBA::update('user', $fields, ['uid' => $uid]);
631 // if we have no OpenID photo try to look up an avatar
632 if (!strlen($photo)) {
633 $photo = Network::lookupAvatarByEmail($email);
636 // unless there is no avatar-addon loaded
637 if (strlen($photo)) {
638 $photo_failure = false;
640 $filename = basename($photo);
641 $img_str = Network::fetchUrl($photo, true);
642 // guess mimetype from headers or filename
643 $type = Image::guessType($photo, true);
645 $Image = new Image($img_str, $type);
646 if ($Image->isValid()) {
647 $Image->scaleToSquare(300);
649 $hash = Photo::newResource();
651 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 4);
654 $photo_failure = true;
657 $Image->scaleDown(80);
659 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 5);
662 $photo_failure = true;
665 $Image->scaleDown(48);
667 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 6);
670 $photo_failure = true;
673 if (!$photo_failure) {
674 DBA::update('photo', ['profile' => 1], ['resource-id' => $hash]);
679 Addon::callHooks('register_account', $uid);
681 $return['user'] = $user;
686 * @brief Sends pending registration confirmation email
688 * @param array $user User record array
689 * @param string $sitename
690 * @param string $siteurl
691 * @param string $password Plaintext password
692 * @return NULL|boolean from notification() and email() inherited
694 public static function sendRegisterPendingEmail($user, $sitename, $siteurl, $password)
696 $body = Strings::deindent(L10n::t('
698 Thank you for registering at %2$s. Your account is pending for approval by the administrator.
700 Your login details are as follows:
706 $user['username'], $sitename, $siteurl, $user['nickname'], $password
709 return notification([
710 'type' => SYSTEM_EMAIL,
711 'uid' => $user['uid'],
712 'to_email' => $user['email'],
713 'subject' => L10n::t('Registration at %s', $sitename),
719 * @brief Sends registration confirmation
721 * It's here as a function because the mail is sent from different parts
723 * @param array $user User record array
724 * @param string $sitename
725 * @param string $siteurl
726 * @param string $password Plaintext password
727 * @return NULL|boolean from notification() and email() inherited
729 public static function sendRegisterOpenEmail($user, $sitename, $siteurl, $password)
731 $preamble = Strings::deindent(L10n::t('
733 Thank you for registering at %2$s. Your account has been created.
735 $preamble, $user['username'], $sitename
737 $body = Strings::deindent(L10n::t('
738 The login details are as follows:
744 You may change your password from your account "Settings" page after logging
747 Please take a few moments to review the other account settings on that page.
749 You may also wish to add some basic information to your default profile
750 ' . "\x28" . 'on the "Profiles" page' . "\x29" . ' so that other people can easily find you.
752 We recommend setting your full name, adding a profile photo,
753 adding some profile "keywords" ' . "\x28" . 'very useful in making new friends' . "\x29" . ' - and
754 perhaps what country you live in; if you do not wish to be more specific
757 We fully respect your right to privacy, and none of these items are necessary.
758 If you are new and do not know anybody here, they may help
759 you to make some new and interesting friends.
761 If you ever want to delete your account, you can do so at %3$s/removeme
763 Thank you and welcome to %2$s.',
764 $user['email'], $sitename, $siteurl, $user['username'], $password
767 return notification([
768 'uid' => $user['uid'],
769 'language' => $user['language'],
770 'type' => SYSTEM_EMAIL,
771 'to_email' => $user['email'],
772 'subject' => L10n::t('Registration details for %s', $sitename),
773 'preamble' => $preamble,
779 * @param object $uid user to remove
782 public static function remove($uid)
790 Logger::log('Removing user: ' . $uid);
792 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
794 Addon::callHooks('remove_user', $user);
796 // save username (actually the nickname as it is guaranteed
797 // unique), so it cannot be re-registered in the future.
798 DBA::insert('userd', ['username' => $user['nickname']]);
800 // The user and related data will be deleted in "cron_expire_and_remove_users" (cronjobs.php)
801 DBA::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utc(DateTimeFormat::utcNow() . " + 7 day")], ['uid' => $uid]);
802 Worker::add(PRIORITY_HIGH, "Notifier", "removeme", $uid);
804 // Send an update to the directory
805 $self = DBA::selectFirst('contact', ['url'], ['uid' => $uid, 'self' => true]);
806 Worker::add(PRIORITY_LOW, "Directory", $self['url']);
808 // Remove the user relevant data
809 Worker::add(PRIORITY_LOW, "RemoveUser", $uid);
811 if ($uid == local_user()) {
812 unset($_SESSION['authenticated']);
813 unset($_SESSION['uid']);
814 $a->internalRedirect();
819 * Return all identities to a user
821 * @param int $uid The user id
822 * @return array All identities for this user
824 * Example for a return:
828 * 'username' => 'maxmuster',
829 * 'nickname' => 'Max Mustermann'
833 * 'username' => 'johndoe',
834 * 'nickname' => 'John Doe'
838 public static function identities($uid)
842 $user = DBA::selectFirst('user', ['uid', 'nickname', 'username', 'parent-uid'], ['uid' => $uid]);
843 if (!DBA::isResult($user)) {
847 if ($user['parent-uid'] == 0) {
848 // First add our own entry
849 $identities = [['uid' => $user['uid'],
850 'username' => $user['username'],
851 'nickname' => $user['nickname']]];
853 // Then add all the children
854 $r = DBA::select('user', ['uid', 'username', 'nickname'],
855 ['parent-uid' => $user['uid'], 'account_removed' => false]);
856 if (DBA::isResult($r)) {
857 $identities = array_merge($identities, DBA::toArray($r));
860 // First entry is our parent
861 $r = DBA::select('user', ['uid', 'username', 'nickname'],
862 ['uid' => $user['parent-uid'], 'account_removed' => false]);
863 if (DBA::isResult($r)) {
864 $identities = DBA::toArray($r);
867 // Then add all siblings
868 $r = DBA::select('user', ['uid', 'username', 'nickname'],
869 ['parent-uid' => $user['parent-uid'], 'account_removed' => false]);
870 if (DBA::isResult($r)) {
871 $identities = array_merge($identities, DBA::toArray($r));
875 $r = DBA::p("SELECT `user`.`uid`, `user`.`username`, `user`.`nickname`
877 INNER JOIN `user` ON `manage`.`mid` = `user`.`uid`
878 WHERE `user`.`account_removed` = 0 AND `manage`.`uid` = ?",
881 if (DBA::isResult($r)) {
882 $identities = array_merge($identities, DBA::toArray($r));