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 Friendica\Core\Addon;
9 use Friendica\Core\Config;
10 use Friendica\Core\L10n;
11 use Friendica\Core\PConfig;
12 use Friendica\Core\System;
13 use Friendica\Core\Worker;
14 use Friendica\Database\DBM;
15 use Friendica\Model\Contact;
16 use Friendica\Model\Group;
17 use Friendica\Model\Photo;
18 use Friendica\Object\Image;
19 use Friendica\Util\Crypto;
20 use Friendica\Util\DateTimeFormat;
21 use Friendica\Util\Network;
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 * @brief Get owner data by user id
39 * @return boolean|array
41 public static function getOwnerDataById($uid) {
42 $r = dba::fetch_first("SELECT
44 `user`.`prvkey` AS `uprvkey`,
50 `user`.`account-type`,
54 ON `user`.`uid` = `contact`.`uid`
55 WHERE `contact`.`uid` = ?
60 if (!DBM::is_result($r)) {
67 * @brief Returns the default group for a given user and network
69 * @param int $uid User id
70 * @param string $network network name
72 * @return int group id
74 public static function getDefaultGroup($uid, $network = '')
78 if ($network == NETWORK_OSTATUS) {
79 $default_group = PConfig::get($uid, "ostatus", "default_group");
82 if ($default_group != 0) {
83 return $default_group;
86 $user = dba::selectFirst('user', ['def_gid'], ['uid' => $uid]);
88 if (DBM::is_result($user)) {
89 $default_group = $user["def_gid"];
92 return $default_group;
97 * Authenticate a user with a clear text password
99 * @brief Authenticate a user with a clear text password
100 * @param mixed $user_info
101 * @param string $password
102 * @return int|boolean
103 * @deprecated since version 3.6
104 * @see Friendica\Model\User::getIdFromPasswordAuthentication()
106 public static function authenticate($user_info, $password)
109 return self::getIdFromPasswordAuthentication($user_info, $password);
110 } catch (Exception $ex) {
116 * Returns the user id associated with a successful password authentication
118 * @brief Authenticate a user with a clear text password
119 * @param mixed $user_info
120 * @param string $password
121 * @return int User Id if authentication is successful
124 public static function getIdFromPasswordAuthentication($user_info, $password)
126 $user = self::getAuthenticationInfo($user_info);
128 if ($user['legacy_password']) {
129 if (password_verify(self::hashPasswordLegacy($password), $user['password'])) {
130 self::updatePassword($user['uid'], $password);
134 } elseif (password_verify($password, $user['password'])) {
135 if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
136 self::updatePassword($user['uid'], $password);
142 throw new Exception(L10n::t('Login failed'));
146 * Returns authentication info from various parameters types
148 * User info can be any of the following:
151 * - User email or username or nickname
152 * - User array with at least the uid and the hashed password
154 * @param mixed $user_info
158 private static function getAuthenticationInfo($user_info)
162 if (is_object($user_info) || is_array($user_info)) {
163 if (is_object($user_info)) {
164 $user = (array) $user_info;
169 if (!isset($user['uid'])
170 || !isset($user['password'])
171 || !isset($user['legacy_password'])
173 throw new Exception(L10n::t('Not enough information to authenticate'));
175 } elseif (is_int($user_info) || is_string($user_info)) {
176 if (is_int($user_info)) {
177 $user = dba::selectFirst('user', ['uid', 'password', 'legacy_password'],
181 'account_expired' => 0,
182 'account_removed' => 0,
187 $user = dba::fetch_first('SELECT `uid`, `password`, `legacy_password`
189 WHERE (`email` = ? OR `username` = ? OR `nickname` = ?)
191 AND `account_expired` = 0
192 AND `account_removed` = 0
201 if (!DBM::is_result($user)) {
202 throw new Exception(L10n::t('User not found'));
210 * Generates a human-readable random password
214 public static function generateNewPassword()
216 return autoname(6) . mt_rand(100, 9999);
220 * Legacy hashing function, kept for password migration purposes
222 * @param string $password
225 private static function hashPasswordLegacy($password)
227 return hash('whirlpool', $password);
231 * Global user password hashing function
233 * @param string $password
236 public static function hashPassword($password)
238 return password_hash($password, PASSWORD_DEFAULT);
242 * Updates a user row with a new plaintext password
245 * @param string $password
248 public static function updatePassword($uid, $password)
250 return self::updatePasswordHashed($uid, self::hashPassword($password));
254 * Updates a user row with a new hashed password.
255 * Empties the password reset token field just in case.
258 * @param string $pasword_hashed
261 private static function updatePasswordHashed($uid, $pasword_hashed)
264 'password' => $pasword_hashed,
266 'pwdreset_time' => null,
267 'legacy_password' => false
269 return dba::update('user', $fields, ['uid' => $uid]);
273 * @brief Catch-all user creation function
275 * Creates a user from the provided data array, either form fields or OpenID.
276 * Required: { username, nickname, email } or { openid_url }
278 * Performs the following:
279 * - Sends to the OpenId auth URL (if relevant)
280 * - Creates new key pairs for crypto
281 * - Create self-contact
282 * - Create profile image
288 public static function create(array $data)
291 $return = ['user' => null, 'password' => ''];
293 $using_invites = Config::get('system', 'invitation_only');
294 $num_invites = Config::get('system', 'number_invites');
296 $invite_id = x($data, 'invite_id') ? notags(trim($data['invite_id'])) : '';
297 $username = x($data, 'username') ? notags(trim($data['username'])) : '';
298 $nickname = x($data, 'nickname') ? notags(trim($data['nickname'])) : '';
299 $email = x($data, 'email') ? notags(trim($data['email'])) : '';
300 $openid_url = x($data, 'openid_url') ? notags(trim($data['openid_url'])) : '';
301 $photo = x($data, 'photo') ? notags(trim($data['photo'])) : '';
302 $password = x($data, 'password') ? trim($data['password']) : '';
303 $password1 = x($data, 'password1') ? trim($data['password1']) : '';
304 $confirm = x($data, 'confirm') ? trim($data['confirm']) : '';
305 $blocked = x($data, 'blocked') ? intval($data['blocked']) : 0;
306 $verified = x($data, 'verified') ? intval($data['verified']) : 0;
308 $publish = x($data, 'profile_publish_reg') && intval($data['profile_publish_reg']) ? 1 : 0;
309 $netpublish = strlen(Config::get('system', 'directory')) ? $publish : 0;
311 if ($password1 != $confirm) {
312 throw new Exception(L10n::t('Passwords do not match. Password unchanged.'));
313 } elseif ($password1 != '') {
314 $password = $password1;
317 if ($using_invites) {
319 throw new Exception(L10n::t('An invitation is required.'));
322 if (!dba::exists('register', ['hash' => $invite_id])) {
323 throw new Exception(L10n::t('Invitation could not be verified.'));
327 if (!x($username) || !x($email) || !x($nickname)) {
329 if (!Network::isUrlValid($openid_url)) {
330 throw new Exception(L10n::t('Invalid OpenID url'));
332 $_SESSION['register'] = 1;
333 $_SESSION['openid'] = $openid_url;
335 $openid = new LightOpenID;
336 $openid->identity = $openid_url;
337 $openid->returnUrl = System::baseUrl() . '/openid';
338 $openid->required = ['namePerson/friendly', 'contact/email', 'namePerson'];
339 $openid->optional = ['namePerson/first', 'media/image/aspect11', 'media/image/default'];
341 $authurl = $openid->authUrl();
342 } catch (Exception $e) {
343 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);
349 throw new Exception(L10n::t('Please enter the required information.'));
352 if (!Network::isUrlValid($openid_url)) {
358 // collapse multiple spaces in name
359 $username = preg_replace('/ +/', ' ', $username);
361 if (mb_strlen($username) > 48) {
362 throw new Exception(L10n::t('Please use a shorter name.'));
364 if (mb_strlen($username) < 3) {
365 throw new Exception(L10n::t('Name too short.'));
368 // So now we are just looking for a space in the full name.
369 $loose_reg = Config::get('system', 'no_regfullname');
371 $username = mb_convert_case($username, MB_CASE_TITLE, 'UTF-8');
372 if (!strpos($username, ' ')) {
373 throw new Exception(L10n::t("That doesn't appear to be your full \x28First Last\x29 name."));
377 if (!Network::isEmailDomainAllowed($email)) {
378 throw new Exception(L10n::t('Your email domain is not among those allowed on this site.'));
381 if (!valid_email($email) || !Network::isEmailDomainValid($email)) {
382 throw new Exception(L10n::t('Not a valid email address.'));
385 if (dba::exists('user', ['email' => $email])) {
386 throw new Exception(L10n::t('Cannot use that email.'));
389 // Disallow somebody creating an account using openid that uses the admin email address,
390 // since openid bypasses email verification. We'll allow it if there is not yet an admin account.
391 if (x($a->config, 'admin_email') && strlen($openid_url)) {
392 $adminlist = explode(',', str_replace(' ', '', strtolower($a->config['admin_email'])));
393 if (in_array(strtolower($email), $adminlist)) {
394 throw new Exception(L10n::t('Cannot use that email.'));
398 $nickname = $data['nickname'] = strtolower($nickname);
400 if (!preg_match('/^[a-z0-9][a-z0-9\_]*$/', $nickname)) {
401 throw new Exception(L10n::t('Your nickname can only contain a-z, 0-9 and _.'));
404 // Check existing and deleted accounts for this nickname.
405 if (dba::exists('user', ['nickname' => $nickname])
406 || dba::exists('userd', ['username' => $nickname])
408 throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
411 $new_password = strlen($password) ? $password : User::generateNewPassword();
412 $new_password_encoded = self::hashPassword($new_password);
414 $return['password'] = $new_password;
416 $keys = Crypto::newKeypair(4096);
417 if ($keys === false) {
418 throw new Exception(L10n::t('SERIOUS ERROR: Generation of security keys failed.'));
421 $prvkey = $keys['prvkey'];
422 $pubkey = $keys['pubkey'];
424 // Create another keypair for signing/verifying salmon protocol messages.
425 $sres = Crypto::newKeypair(512);
426 $sprvkey = $sres['prvkey'];
427 $spubkey = $sres['pubkey'];
429 $insert_result = dba::insert('user', [
430 'guid' => generate_user_guid(),
431 'username' => $username,
432 'password' => $new_password_encoded,
434 'openid' => $openid_url,
435 'nickname' => $nickname,
438 'spubkey' => $spubkey,
439 'sprvkey' => $sprvkey,
440 'verified' => $verified,
441 'blocked' => $blocked,
443 'register_date' => DateTimeFormat::utcNow(),
444 'default-location' => ''
447 if ($insert_result) {
448 $uid = dba::lastInsertId();
449 $user = dba::selectFirst('user', [], ['uid' => $uid]);
451 throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
455 throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
458 // if somebody clicked submit twice very quickly, they could end up with two accounts
459 // due to race condition. Remove this one.
460 $user_count = dba::count('user', ['nickname' => $nickname]);
461 if ($user_count > 1) {
462 dba::delete('user', ['uid' => $uid]);
464 throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
467 $insert_result = dba::insert('profile', [
470 'photo' => System::baseUrl() . "/photo/profile/{$uid}.jpg",
471 'thumb' => System::baseUrl() . "/photo/avatar/{$uid}.jpg",
472 'publish' => $publish,
474 'net-publish' => $netpublish,
475 'profile-name' => L10n::t('default')
477 if (!$insert_result) {
478 dba::delete('user', ['uid' => $uid]);
480 throw new Exception(L10n::t('An error occurred creating your default profile. Please try again.'));
483 // Create the self contact
484 if (!Contact::createSelfFromUserId($uid)) {
485 dba::delete('user', ['uid' => $uid]);
487 throw new Exception(L10n::t('An error occurred creating your self contact. Please try again.'));
490 // Create a group with no members. This allows somebody to use it
491 // right away as a default group for new contacts.
492 $def_gid = Group::create($uid, L10n::t('Friends'));
494 dba::delete('user', ['uid' => $uid]);
496 throw new Exception(L10n::t('An error occurred creating your default contact group. Please try again.'));
499 $fields = ['def_gid' => $def_gid];
500 if (Config::get('system', 'newuser_private') && $def_gid) {
501 $fields['allow_gid'] = '<' . $def_gid . '>';
504 dba::update('user', $fields, ['uid' => $uid]);
506 // if we have no OpenID photo try to look up an avatar
507 if (!strlen($photo)) {
508 $photo = Network::lookupAvatarByEmail($email);
511 // unless there is no avatar-addon loaded
512 if (strlen($photo)) {
513 $photo_failure = false;
515 $filename = basename($photo);
516 $img_str = Network::fetchUrl($photo, true);
517 // guess mimetype from headers or filename
518 $type = Image::guessType($photo, true);
520 $Image = new Image($img_str, $type);
521 if ($Image->isValid()) {
522 $Image->scaleToSquare(175);
524 $hash = photo_new_resource();
526 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 4);
529 $photo_failure = true;
532 $Image->scaleDown(80);
534 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 5);
537 $photo_failure = true;
540 $Image->scaleDown(48);
542 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 6);
545 $photo_failure = true;
548 if (!$photo_failure) {
549 dba::update('photo', ['profile' => 1], ['resource-id' => $hash]);
554 Addon::callHooks('register_account', $uid);
556 $return['user'] = $user;
561 * @brief Sends pending registration confiĆmation email
563 * @param string $email
564 * @param string $sitename
565 * @param string $username
566 * @return NULL|boolean from notification() and email() inherited
568 public static function sendRegisterPendingEmail($email, $sitename, $username)
570 $body = deindent(L10n::t('
572 Thank you for registering at %2$s. Your account is pending for approval by the administrator.
575 $body = sprintf($body, $username, $sitename);
577 return notification([
578 'type' => SYSTEM_EMAIL,
579 'to_email' => $email,
580 'subject'=> L10n::t('Registration at %s', $sitename),
585 * @brief Sends registration confirmation
587 * It's here as a function because the mail is sent from different parts
589 * @param string $email
590 * @param string $sitename
591 * @param string $siteurl
592 * @param string $username
593 * @param string $password
594 * @return NULL|boolean from notification() and email() inherited
596 public static function sendRegisterOpenEmail($email, $sitename, $siteurl, $username, $password)
598 $preamble = deindent(L10n::t('
600 Thank you for registering at %2$s. Your account has been created.
602 $body = deindent(L10n::t('
603 The login details are as follows:
608 You may change your password from your account Settings page after logging
611 Please take a few moments to review the other account settings on that page.
613 You may also wish to add some basic information to your default profile
614 ' . "\x28" . 'on the "Profiles" page' . "\x29" . ' so that other people can easily find you.
616 We recommend setting your full name, adding a profile photo,
617 adding some profile keywords ' . "\x28" . 'very useful in making new friends' . "\x29" . ' - and
618 perhaps what country you live in; if you do not wish to be more specific
621 We fully respect your right to privacy, and none of these items are necessary.
622 If you are new and do not know anybody here, they may help
623 you to make some new and interesting friends.
626 Thank you and welcome to %2$s.'));
628 $preamble = sprintf($preamble, $username, $sitename);
629 $body = sprintf($body, $email, $sitename, $siteurl, $username, $password);
631 return notification([
632 'type' => SYSTEM_EMAIL,
633 'to_email' => $email,
634 'subject'=> L10n::t('Registration details for %s', $sitename),
635 'preamble'=> $preamble,
640 * @param object $uid user to remove
643 public static function remove($uid)
649 logger('Removing user: ' . $uid);
651 $user = dba::selectFirst('user', [], ['uid' => $uid]);
653 Addon::callHooks('remove_user', $user);
655 // save username (actually the nickname as it is guaranteed
656 // unique), so it cannot be re-registered in the future.
657 dba::insert('userd', ['username' => $user['nickname']]);
659 // The user and related data will be deleted in "cron_expire_and_remove_users" (cronjobs.php)
660 dba::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utcNow()], ['uid' => $uid]);
661 Worker::add(PRIORITY_HIGH, "Notifier", "removeme", $uid);
663 // Send an update to the directory
664 Worker::add(PRIORITY_LOW, "Directory", $user['url']);
666 if ($uid == local_user()) {
667 unset($_SESSION['authenticated']);
668 unset($_SESSION['uid']);
669 goaway(System::baseUrl());