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\PasswordStatus;
10 use Friendica\Core\Addon;
11 use Friendica\Core\Config;
12 use Friendica\Core\L10n;
13 use Friendica\Core\PConfig;
14 use Friendica\Core\Protocol;
15 use Friendica\Core\System;
16 use Friendica\Core\Worker;
17 use Friendica\Database\DBA;
18 use Friendica\Object\Image;
19 use Friendica\Util\Crypto;
20 use Friendica\Util\DateTimeFormat;
21 use Friendica\Util\Network;
23 use function password_exposed;
25 require_once 'boot.php';
26 require_once 'include/dba.php';
27 require_once 'include/enotify.php';
28 require_once 'include/text.php';
30 * @brief This class handles User related functions
35 * @brief Returns the user id of a given profile url
37 * @param string $profile
39 * @return integer user id
41 public static function getIdForURL($url)
43 $self = DBA::selectFirst('contact', ['uid'], ['nurl' => normalise_link($url), 'self' => true]);
44 if (!DBA::isResult($self)) {
52 * @brief Get owner data by user id
55 * @return boolean|array
57 public static function getOwnerDataById($uid) {
58 $r = DBA::fetchFirst("SELECT
60 `user`.`prvkey` AS `uprvkey`,
66 `user`.`account-type`,
70 ON `user`.`uid` = `contact`.`uid`
71 WHERE `contact`.`uid` = ?
76 if (!DBA::isResult($r)) {
83 * @brief Get owner data by nick name
86 * @return boolean|array
88 public static function getOwnerDataByNick($nick)
90 $user = DBA::selectFirst('user', ['uid'], ['nickname' => $nick]);
92 if (!DBA::isResult($user)) {
96 return self::getOwnerDataById($user['uid']);
100 * @brief Returns the default group for a given user and network
102 * @param int $uid User id
103 * @param string $network network name
105 * @return int group id
107 public static function getDefaultGroup($uid, $network = '')
111 if ($network == Protocol::OSTATUS) {
112 $default_group = PConfig::get($uid, "ostatus", "default_group");
115 if ($default_group != 0) {
116 return $default_group;
119 $user = DBA::selectFirst('user', ['def_gid'], ['uid' => $uid]);
121 if (DBA::isResult($user)) {
122 $default_group = $user["def_gid"];
125 return $default_group;
130 * Authenticate a user with a clear text password
132 * @brief Authenticate a user with a clear text password
133 * @param mixed $user_info
134 * @param string $password
135 * @return int|boolean
136 * @deprecated since version 3.6
137 * @see User::getIdFromPasswordAuthentication()
139 public static function authenticate($user_info, $password)
142 return self::getIdFromPasswordAuthentication($user_info, $password);
143 } catch (Exception $ex) {
149 * Returns the user id associated with a successful password authentication
151 * @brief Authenticate a user with a clear text password
152 * @param mixed $user_info
153 * @param string $password
154 * @return int User Id if authentication is successful
157 public static function getIdFromPasswordAuthentication($user_info, $password)
159 $user = self::getAuthenticationInfo($user_info);
161 if (strpos($user['password'], '$') === false) {
162 //Legacy hash that has not been replaced by a new hash yet
163 if (self::hashPasswordLegacy($password) === $user['password']) {
164 self::updatePassword($user['uid'], $password);
168 } elseif (!empty($user['legacy_password'])) {
169 //Legacy hash that has been double-hashed and not replaced by a new hash yet
170 //Warning: `legacy_password` is not necessary in sync with the content of `password`
171 if (password_verify(self::hashPasswordLegacy($password), $user['password'])) {
172 self::updatePassword($user['uid'], $password);
176 } elseif (password_verify($password, $user['password'])) {
178 if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
179 self::updatePassword($user['uid'], $password);
185 throw new Exception(L10n::t('Login failed'));
189 * Returns authentication info from various parameters types
191 * User info can be any of the following:
194 * - User email or username or nickname
195 * - User array with at least the uid and the hashed password
197 * @param mixed $user_info
201 private static function getAuthenticationInfo($user_info)
205 if (is_object($user_info) || is_array($user_info)) {
206 if (is_object($user_info)) {
207 $user = (array) $user_info;
212 if (!isset($user['uid'])
213 || !isset($user['password'])
214 || !isset($user['legacy_password'])
216 throw new Exception(L10n::t('Not enough information to authenticate'));
218 } elseif (is_int($user_info) || is_string($user_info)) {
219 if (is_int($user_info)) {
220 $user = DBA::selectFirst('user', ['uid', 'password', 'legacy_password'],
224 'account_expired' => 0,
225 'account_removed' => 0,
230 $fields = ['uid', 'password', 'legacy_password'];
231 $condition = ["(`email` = ? OR `username` = ? OR `nickname` = ?)
232 AND NOT `blocked` AND NOT `account_expired` AND NOT `account_removed` AND `verified`",
233 $user_info, $user_info, $user_info];
234 $user = DBA::selectFirst('user', $fields, $condition);
237 if (!DBA::isResult($user)) {
238 throw new Exception(L10n::t('User not found'));
246 * Generates a human-readable random password
250 public static function generateNewPassword()
252 return autoname(6) . mt_rand(100, 9999);
256 * Checks if the provided plaintext password has been exposed or not
258 * @param string $password
261 public static function isPasswordExposed($password)
263 return password_exposed($password) === PasswordStatus::EXPOSED;
267 * Legacy hashing function, kept for password migration purposes
269 * @param string $password
272 private static function hashPasswordLegacy($password)
274 return hash('whirlpool', $password);
278 * Global user password hashing function
280 * @param string $password
283 public static function hashPassword($password)
285 if (!trim($password)) {
286 throw new Exception(L10n::t('Password can\'t be empty'));
289 return password_hash($password, PASSWORD_DEFAULT);
293 * Updates a user row with a new plaintext password
296 * @param string $password
299 public static function updatePassword($uid, $password)
301 return self::updatePasswordHashed($uid, self::hashPassword($password));
305 * Updates a user row with a new hashed password.
306 * Empties the password reset token field just in case.
309 * @param string $pasword_hashed
312 private static function updatePasswordHashed($uid, $pasword_hashed)
315 'password' => $pasword_hashed,
317 'pwdreset_time' => null,
318 'legacy_password' => false
320 return DBA::update('user', $fields, ['uid' => $uid]);
324 * @brief Checks if a nickname is in the list of the forbidden nicknames
326 * Check if a nickname is forbidden from registration on the node by the
327 * admin. Forbidden nicknames (e.g. role namess) can be configured in the
330 * @param string $nickname The nickname that should be checked
331 * @return boolean True is the nickname is blocked on the node
333 public static function isNicknameBlocked($nickname)
335 $forbidden_nicknames = Config::get('system', 'forbidden_nicknames', '');
337 // if the config variable is empty return false
338 if (empty($forbidden_nicknames)) {
342 // check if the nickname is in the list of blocked nicknames
343 $forbidden = explode(',', $forbidden_nicknames);
344 $forbidden = array_map('trim', $forbidden);
345 if (in_array(strtolower($nickname), $forbidden)) {
354 * @brief Catch-all user creation function
356 * Creates a user from the provided data array, either form fields or OpenID.
357 * Required: { username, nickname, email } or { openid_url }
359 * Performs the following:
360 * - Sends to the OpenId auth URL (if relevant)
361 * - Creates new key pairs for crypto
362 * - Create self-contact
363 * - Create profile image
369 public static function create(array $data)
372 $return = ['user' => null, 'password' => ''];
374 $using_invites = Config::get('system', 'invitation_only');
375 $num_invites = Config::get('system', 'number_invites');
377 $invite_id = !empty($data['invite_id']) ? notags(trim($data['invite_id'])) : '';
378 $username = !empty($data['username']) ? notags(trim($data['username'])) : '';
379 $nickname = !empty($data['nickname']) ? notags(trim($data['nickname'])) : '';
380 $email = !empty($data['email']) ? notags(trim($data['email'])) : '';
381 $openid_url = !empty($data['openid_url']) ? notags(trim($data['openid_url'])) : '';
382 $photo = !empty($data['photo']) ? notags(trim($data['photo'])) : '';
383 $password = !empty($data['password']) ? trim($data['password']) : '';
384 $password1 = !empty($data['password1']) ? trim($data['password1']) : '';
385 $confirm = !empty($data['confirm']) ? trim($data['confirm']) : '';
386 $blocked = !empty($data['blocked']) ? intval($data['blocked']) : 0;
387 $verified = !empty($data['verified']) ? intval($data['verified']) : 0;
388 $language = !empty($data['language']) ? notags(trim($data['language'])) : 'en';
390 $publish = !empty($data['profile_publish_reg']) && intval($data['profile_publish_reg']) ? 1 : 0;
391 $netpublish = strlen(Config::get('system', 'directory')) ? $publish : 0;
393 if ($password1 != $confirm) {
394 throw new Exception(L10n::t('Passwords do not match. Password unchanged.'));
395 } elseif ($password1 != '') {
396 $password = $password1;
399 if ($using_invites) {
401 throw new Exception(L10n::t('An invitation is required.'));
404 if (!DBA::exists('register', ['hash' => $invite_id])) {
405 throw new Exception(L10n::t('Invitation could not be verified.'));
409 if (empty($username) || empty($email) || empty($nickname)) {
411 if (!Network::isUrlValid($openid_url)) {
412 throw new Exception(L10n::t('Invalid OpenID url'));
414 $_SESSION['register'] = 1;
415 $_SESSION['openid'] = $openid_url;
417 $openid = new LightOpenID($a->get_hostname());
418 $openid->identity = $openid_url;
419 $openid->returnUrl = System::baseUrl() . '/openid';
420 $openid->required = ['namePerson/friendly', 'contact/email', 'namePerson'];
421 $openid->optional = ['namePerson/first', 'media/image/aspect11', 'media/image/default'];
423 $authurl = $openid->authUrl();
424 } catch (Exception $e) {
425 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);
431 throw new Exception(L10n::t('Please enter the required information.'));
434 if (!Network::isUrlValid($openid_url)) {
440 // collapse multiple spaces in name
441 $username = preg_replace('/ +/', ' ', $username);
443 if (mb_strlen($username) > 48) {
444 throw new Exception(L10n::t('Please use a shorter name.'));
446 if (mb_strlen($username) < 3) {
447 throw new Exception(L10n::t('Name too short.'));
450 // So now we are just looking for a space in the full name.
451 $loose_reg = Config::get('system', 'no_regfullname');
453 $username = mb_convert_case($username, MB_CASE_TITLE, 'UTF-8');
454 if (!strpos($username, ' ')) {
455 throw new Exception(L10n::t("That doesn't appear to be your full \x28First Last\x29 name."));
459 if (!Network::isEmailDomainAllowed($email)) {
460 throw new Exception(L10n::t('Your email domain is not among those allowed on this site.'));
463 if (!valid_email($email) || !Network::isEmailDomainValid($email)) {
464 throw new Exception(L10n::t('Not a valid email address.'));
466 if (self::isNicknameBlocked($nickname)) {
467 throw new Exception(L10n::t('The nickname was blocked from registration by the nodes admin.'));
470 if (Config::get('system', 'block_extended_register', false) && DBA::exists('user', ['email' => $email])) {
471 throw new Exception(L10n::t('Cannot use that email.'));
474 // Disallow somebody creating an account using openid that uses the admin email address,
475 // since openid bypasses email verification. We'll allow it if there is not yet an admin account.
476 if (Config::get('config', 'admin_email') && strlen($openid_url)) {
477 $adminlist = explode(',', str_replace(' ', '', strtolower(Config::get('config', 'admin_email'))));
478 if (in_array(strtolower($email), $adminlist)) {
479 throw new Exception(L10n::t('Cannot use that email.'));
483 $nickname = $data['nickname'] = strtolower($nickname);
485 if (!preg_match('/^[a-z0-9][a-z0-9\_]*$/', $nickname)) {
486 throw new Exception(L10n::t('Your nickname can only contain a-z, 0-9 and _.'));
489 // Check existing and deleted accounts for this nickname.
490 if (DBA::exists('user', ['nickname' => $nickname])
491 || DBA::exists('userd', ['username' => $nickname])
493 throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
496 $new_password = strlen($password) ? $password : User::generateNewPassword();
497 $new_password_encoded = self::hashPassword($new_password);
499 $return['password'] = $new_password;
501 $keys = Crypto::newKeypair(4096);
502 if ($keys === false) {
503 throw new Exception(L10n::t('SERIOUS ERROR: Generation of security keys failed.'));
506 $prvkey = $keys['prvkey'];
507 $pubkey = $keys['pubkey'];
509 // Create another keypair for signing/verifying salmon protocol messages.
510 $sres = Crypto::newKeypair(512);
511 $sprvkey = $sres['prvkey'];
512 $spubkey = $sres['pubkey'];
514 $insert_result = DBA::insert('user', [
515 'guid' => System::createUUID(),
516 'username' => $username,
517 'password' => $new_password_encoded,
519 'openid' => $openid_url,
520 'nickname' => $nickname,
523 'spubkey' => $spubkey,
524 'sprvkey' => $sprvkey,
525 'verified' => $verified,
526 'blocked' => $blocked,
527 'language' => $language,
529 'register_date' => DateTimeFormat::utcNow(),
530 'default-location' => ''
533 if ($insert_result) {
534 $uid = DBA::lastInsertId();
535 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
537 throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
541 throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
544 // if somebody clicked submit twice very quickly, they could end up with two accounts
545 // due to race condition. Remove this one.
546 $user_count = DBA::count('user', ['nickname' => $nickname]);
547 if ($user_count > 1) {
548 DBA::delete('user', ['uid' => $uid]);
550 throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
553 $insert_result = DBA::insert('profile', [
556 'photo' => System::baseUrl() . "/photo/profile/{$uid}.jpg",
557 'thumb' => System::baseUrl() . "/photo/avatar/{$uid}.jpg",
558 'publish' => $publish,
560 'net-publish' => $netpublish,
561 'profile-name' => L10n::t('default')
563 if (!$insert_result) {
564 DBA::delete('user', ['uid' => $uid]);
566 throw new Exception(L10n::t('An error occurred creating your default profile. Please try again.'));
569 // Create the self contact
570 if (!Contact::createSelfFromUserId($uid)) {
571 DBA::delete('user', ['uid' => $uid]);
573 throw new Exception(L10n::t('An error occurred creating your self contact. Please try again.'));
576 // Create a group with no members. This allows somebody to use it
577 // right away as a default group for new contacts.
578 $def_gid = Group::create($uid, L10n::t('Friends'));
580 DBA::delete('user', ['uid' => $uid]);
582 throw new Exception(L10n::t('An error occurred creating your default contact group. Please try again.'));
585 $fields = ['def_gid' => $def_gid];
586 if (Config::get('system', 'newuser_private') && $def_gid) {
587 $fields['allow_gid'] = '<' . $def_gid . '>';
590 DBA::update('user', $fields, ['uid' => $uid]);
592 // if we have no OpenID photo try to look up an avatar
593 if (!strlen($photo)) {
594 $photo = Network::lookupAvatarByEmail($email);
597 // unless there is no avatar-addon loaded
598 if (strlen($photo)) {
599 $photo_failure = false;
601 $filename = basename($photo);
602 $img_str = Network::fetchUrl($photo, true);
603 // guess mimetype from headers or filename
604 $type = Image::guessType($photo, true);
606 $Image = new Image($img_str, $type);
607 if ($Image->isValid()) {
608 $Image->scaleToSquare(175);
610 $hash = Photo::newResource();
612 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 4);
615 $photo_failure = true;
618 $Image->scaleDown(80);
620 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 5);
623 $photo_failure = true;
626 $Image->scaleDown(48);
628 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 6);
631 $photo_failure = true;
634 if (!$photo_failure) {
635 DBA::update('photo', ['profile' => 1], ['resource-id' => $hash]);
640 Addon::callHooks('register_account', $uid);
642 $return['user'] = $user;
647 * @brief Sends pending registration confiĆmation email
649 * @param string $email
650 * @param string $sitename
651 * @param string $username
652 * @return NULL|boolean from notification() and email() inherited
654 public static function sendRegisterPendingEmail($email, $sitename, $username)
656 $body = deindent(L10n::t('
658 Thank you for registering at %2$s. Your account is pending for approval by the administrator.
661 $body = sprintf($body, $username, $sitename);
663 return notification([
664 'type' => SYSTEM_EMAIL,
665 'to_email' => $email,
666 'subject'=> L10n::t('Registration at %s', $sitename),
671 * @brief Sends registration confirmation
673 * It's here as a function because the mail is sent from different parts
675 * @param string $email
676 * @param string $sitename
677 * @param string $siteurl
678 * @param string $username
679 * @param string $password
680 * @return NULL|boolean from notification() and email() inherited
682 public static function sendRegisterOpenEmail($email, $sitename, $siteurl, $username, $password, $user)
684 $preamble = deindent(L10n::t('
686 Thank you for registering at %2$s. Your account has been created.
688 $body = deindent(L10n::t('
689 The login details are as follows:
695 You may change your password from your account "Settings" page after logging
698 Please take a few moments to review the other account settings on that page.
700 You may also wish to add some basic information to your default profile
701 ' . "\x28" . 'on the "Profiles" page' . "\x29" . ' so that other people can easily find you.
703 We recommend setting your full name, adding a profile photo,
704 adding some profile "keywords" ' . "\x28" . 'very useful in making new friends' . "\x29" . ' - and
705 perhaps what country you live in; if you do not wish to be more specific
708 We fully respect your right to privacy, and none of these items are necessary.
709 If you are new and do not know anybody here, they may help
710 you to make some new and interesting friends.
712 If you ever want to delete your account, you can do so at %3$s/removeme
714 Thank you and welcome to %2$s.'));
716 $preamble = sprintf($preamble, $username, $sitename);
717 $body = sprintf($body, $email, $sitename, $siteurl, $username, $password);
719 return notification([
720 'uid' => $user['uid'],
721 'language' => $user['language'],
722 'type' => SYSTEM_EMAIL,
723 'to_email' => $email,
724 'subject'=> L10n::t('Registration details for %s', $sitename),
725 'preamble'=> $preamble,
730 * @param object $uid user to remove
733 public static function remove($uid)
739 logger('Removing user: ' . $uid);
741 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
743 Addon::callHooks('remove_user', $user);
745 // save username (actually the nickname as it is guaranteed
746 // unique), so it cannot be re-registered in the future.
747 DBA::insert('userd', ['username' => $user['nickname']]);
749 // The user and related data will be deleted in "cron_expire_and_remove_users" (cronjobs.php)
750 DBA::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utc(DateTimeFormat::utcNow() . " + 7 day")], ['uid' => $uid]);
751 Worker::add(PRIORITY_HIGH, "Notifier", "removeme", $uid);
753 // Send an update to the directory
754 $self = DBA::selectFirst('contact', ['url'], ['uid' => $uid, 'self' => true]);
755 Worker::add(PRIORITY_LOW, "Directory", $self['url']);
757 // Remove the user relevant data
758 Worker::add(PRIORITY_LOW, "RemoveUser", $uid);
760 if ($uid == local_user()) {
761 unset($_SESSION['authenticated']);
762 unset($_SESSION['uid']);
763 goaway(System::baseUrl());