]> git.mxchange.org Git - friendica.git/blob - src/Model/User.php
Merge remote-tracking branch 'upstream/develop' into public-redir
[friendica.git] / src / Model / User.php
1 <?php
2 /**
3  * @file src/Model/User.php
4  * @brief This file includes the User class with user related database functions
5  */
6 namespace Friendica\Model;
7
8 use DivineOmega\PasswordExposed\PasswordStatus;
9 use Friendica\Core\Addon;
10 use Friendica\Core\Config;
11 use Friendica\Core\L10n;
12 use Friendica\Core\PConfig;
13 use Friendica\Core\System;
14 use Friendica\Core\Worker;
15 use Friendica\Database\DBM;
16 use Friendica\Model\Contact;
17 use Friendica\Model\Group;
18 use Friendica\Model\Photo;
19 use Friendica\Object\Image;
20 use Friendica\Util\Crypto;
21 use Friendica\Util\DateTimeFormat;
22 use Friendica\Util\Network;
23 use dba;
24 use Exception;
25 use LightOpenID;
26 use function password_exposed;
27
28 require_once 'boot.php';
29 require_once 'include/dba.php';
30 require_once 'include/enotify.php';
31 require_once 'include/text.php';
32 /**
33  * @brief This class handles User related functions
34  */
35 class User
36 {
37         /**
38          * @brief Get owner data by user id
39          *
40          * @param int $uid
41          * @return boolean|array
42          */
43         public static function getOwnerDataById($uid) {
44                 $r = dba::fetch_first("SELECT
45                         `contact`.*,
46                         `user`.`prvkey` AS `uprvkey`,
47                         `user`.`timezone`,
48                         `user`.`nickname`,
49                         `user`.`sprvkey`,
50                         `user`.`spubkey`,
51                         `user`.`page-flags`,
52                         `user`.`account-type`,
53                         `user`.`prvnets`
54                         FROM `contact`
55                         INNER JOIN `user`
56                                 ON `user`.`uid` = `contact`.`uid`
57                         WHERE `contact`.`uid` = ?
58                         AND `contact`.`self`
59                         LIMIT 1",
60                         $uid
61                 );
62                 if (!DBM::is_result($r)) {
63                         return false;
64                 }
65                 return $r;
66         }
67
68         /**
69          * @brief Returns the default group for a given user and network
70          *
71          * @param int $uid User id
72          * @param string $network network name
73          *
74          * @return int group id
75          */
76         public static function getDefaultGroup($uid, $network = '')
77         {
78                 $default_group = 0;
79
80                 if ($network == NETWORK_OSTATUS) {
81                         $default_group = PConfig::get($uid, "ostatus", "default_group");
82                 }
83
84                 if ($default_group != 0) {
85                         return $default_group;
86                 }
87
88                 $user = dba::selectFirst('user', ['def_gid'], ['uid' => $uid]);
89
90                 if (DBM::is_result($user)) {
91                         $default_group = $user["def_gid"];
92                 }
93
94                 return $default_group;
95         }
96
97
98         /**
99          * Authenticate a user with a clear text password
100          *
101          * @brief Authenticate a user with a clear text password
102          * @param mixed $user_info
103          * @param string $password
104          * @return int|boolean
105          * @deprecated since version 3.6
106          * @see User::getIdFromPasswordAuthentication()
107          */
108         public static function authenticate($user_info, $password)
109         {
110                 try {
111                         return self::getIdFromPasswordAuthentication($user_info, $password);
112                 } catch (Exception $ex) {
113                         return false;
114                 }
115         }
116
117         /**
118          * Returns the user id associated with a successful password authentication
119          *
120          * @brief Authenticate a user with a clear text password
121          * @param mixed $user_info
122          * @param string $password
123          * @return int User Id if authentication is successful
124          * @throws Exception
125          */
126         public static function getIdFromPasswordAuthentication($user_info, $password)
127         {
128                 $user = self::getAuthenticationInfo($user_info);
129
130                 if (strpos($user['password'], '$') === false) {
131                         //Legacy hash that has not been replaced by a new hash yet
132                         if (self::hashPasswordLegacy($password) === $user['password']) {
133                                 self::updatePassword($user['uid'], $password);
134
135                                 return $user['uid'];
136                         }
137                 } elseif (!empty($user['legacy_password'])) {
138                         //Legacy hash that has been double-hashed and not replaced by a new hash yet
139                         //Warning: `legacy_password` is not necessary in sync with the content of `password`
140                         if (password_verify(self::hashPasswordLegacy($password), $user['password'])) {
141                                 self::updatePassword($user['uid'], $password);
142
143                                 return $user['uid'];
144                         }
145                 } elseif (password_verify($password, $user['password'])) {
146                         //New password hash
147                         if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
148                                 self::updatePassword($user['uid'], $password);
149                         }
150
151                         return $user['uid'];
152                 }
153
154                 throw new Exception(L10n::t('Login failed'));
155         }
156
157         /**
158          * Returns authentication info from various parameters types
159          *
160          * User info can be any of the following:
161          * - User DB object
162          * - User Id
163          * - User email or username or nickname
164          * - User array with at least the uid and the hashed password
165          *
166          * @param mixed $user_info
167          * @return array
168          * @throws Exception
169          */
170         private static function getAuthenticationInfo($user_info)
171         {
172                 $user = null;
173
174                 if (is_object($user_info) || is_array($user_info)) {
175                         if (is_object($user_info)) {
176                                 $user = (array) $user_info;
177                         } else {
178                                 $user = $user_info;
179                         }
180
181                         if (!isset($user['uid'])
182                                 || !isset($user['password'])
183                                 || !isset($user['legacy_password'])
184                         ) {
185                                 throw new Exception(L10n::t('Not enough information to authenticate'));
186                         }
187                 } elseif (is_int($user_info) || is_string($user_info)) {
188                         if (is_int($user_info)) {
189                                 $user = dba::selectFirst('user', ['uid', 'password', 'legacy_password'],
190                                         [
191                                                 'uid' => $user_info,
192                                                 'blocked' => 0,
193                                                 'account_expired' => 0,
194                                                 'account_removed' => 0,
195                                                 'verified' => 1
196                                         ]
197                                 );
198                         } else {
199                                 $user = dba::fetch_first('SELECT `uid`, `password`, `legacy_password`
200                                         FROM `user`
201                                         WHERE (`email` = ? OR `username` = ? OR `nickname` = ?)
202                                         AND `blocked` = 0
203                                         AND `account_expired` = 0
204                                         AND `account_removed` = 0
205                                         AND `verified` = 1
206                                         LIMIT 1',
207                                         $user_info,
208                                         $user_info,
209                                         $user_info
210                                 );
211                         }
212
213                         if (!DBM::is_result($user)) {
214                                 throw new Exception(L10n::t('User not found'));
215                         }
216                 }
217
218                 return $user;
219         }
220
221         /**
222          * Generates a human-readable random password
223          *
224          * @return string
225          */
226         public static function generateNewPassword()
227         {
228                 return autoname(6) . mt_rand(100, 9999);
229         }
230
231         /**
232          * Checks if the provided plaintext password has been exposed or not
233          *
234          * @param string $password
235          * @return bool
236          */
237         public static function isPasswordExposed($password)
238         {
239                 return password_exposed($password) === PasswordStatus::EXPOSED;
240         }
241
242         /**
243          * Legacy hashing function, kept for password migration purposes
244          *
245          * @param string $password
246          * @return string
247          */
248         private static function hashPasswordLegacy($password)
249         {
250                 return hash('whirlpool', $password);
251         }
252
253         /**
254          * Global user password hashing function
255          *
256          * @param string $password
257          * @return string
258          */
259         public static function hashPassword($password)
260         {
261                 if (!trim($password)) {
262                         throw new Exception(L10n::t('Password can\'t be empty'));
263                 }
264
265                 return password_hash($password, PASSWORD_DEFAULT);
266         }
267
268         /**
269          * Updates a user row with a new plaintext password
270          *
271          * @param int    $uid
272          * @param string $password
273          * @return bool
274          */
275         public static function updatePassword($uid, $password)
276         {
277                 return self::updatePasswordHashed($uid, self::hashPassword($password));
278         }
279
280         /**
281          * Updates a user row with a new hashed password.
282          * Empties the password reset token field just in case.
283          *
284          * @param int    $uid
285          * @param string $pasword_hashed
286          * @return bool
287          */
288         private static function updatePasswordHashed($uid, $pasword_hashed)
289         {
290                 $fields = [
291                         'password' => $pasword_hashed,
292                         'pwdreset' => null,
293                         'pwdreset_time' => null,
294                         'legacy_password' => false
295                 ];
296                 return dba::update('user', $fields, ['uid' => $uid]);
297         }
298
299         /**
300          * @brief Catch-all user creation function
301          *
302          * Creates a user from the provided data array, either form fields or OpenID.
303          * Required: { username, nickname, email } or { openid_url }
304          *
305          * Performs the following:
306          * - Sends to the OpenId auth URL (if relevant)
307          * - Creates new key pairs for crypto
308          * - Create self-contact
309          * - Create profile image
310          *
311          * @param array $data
312          * @return string
313          * @throw Exception
314          */
315         public static function create(array $data)
316         {
317                 $a = get_app();
318                 $return = ['user' => null, 'password' => ''];
319
320                 $using_invites = Config::get('system', 'invitation_only');
321                 $num_invites   = Config::get('system', 'number_invites');
322
323                 $invite_id  = x($data, 'invite_id')  ? notags(trim($data['invite_id']))  : '';
324                 $username   = x($data, 'username')   ? notags(trim($data['username']))   : '';
325                 $nickname   = x($data, 'nickname')   ? notags(trim($data['nickname']))   : '';
326                 $email      = x($data, 'email')      ? notags(trim($data['email']))      : '';
327                 $openid_url = x($data, 'openid_url') ? notags(trim($data['openid_url'])) : '';
328                 $photo      = x($data, 'photo')      ? notags(trim($data['photo']))      : '';
329                 $password   = x($data, 'password')   ? trim($data['password'])           : '';
330                 $password1  = x($data, 'password1')  ? trim($data['password1'])          : '';
331                 $confirm    = x($data, 'confirm')    ? trim($data['confirm'])            : '';
332                 $blocked    = x($data, 'blocked')    ? intval($data['blocked'])          : 0;
333                 $verified   = x($data, 'verified')   ? intval($data['verified'])         : 0;
334                 $language   = x($data, 'language')   ? notags(trim($data['language'])) : 'en';
335
336                 $publish = x($data, 'profile_publish_reg') && intval($data['profile_publish_reg']) ? 1 : 0;
337                 $netpublish = strlen(Config::get('system', 'directory')) ? $publish : 0;
338
339                 if ($password1 != $confirm) {
340                         throw new Exception(L10n::t('Passwords do not match. Password unchanged.'));
341                 } elseif ($password1 != '') {
342                         $password = $password1;
343                 }
344
345                 if ($using_invites) {
346                         if (!$invite_id) {
347                                 throw new Exception(L10n::t('An invitation is required.'));
348                         }
349
350                         if (!dba::exists('register', ['hash' => $invite_id])) {
351                                 throw new Exception(L10n::t('Invitation could not be verified.'));
352                         }
353                 }
354
355                 if (!x($username) || !x($email) || !x($nickname)) {
356                         if ($openid_url) {
357                                 if (!Network::isUrlValid($openid_url)) {
358                                         throw new Exception(L10n::t('Invalid OpenID url'));
359                                 }
360                                 $_SESSION['register'] = 1;
361                                 $_SESSION['openid'] = $openid_url;
362
363                                 $openid = new LightOpenID($a->get_hostname());
364                                 $openid->identity = $openid_url;
365                                 $openid->returnUrl = System::baseUrl() . '/openid';
366                                 $openid->required = ['namePerson/friendly', 'contact/email', 'namePerson'];
367                                 $openid->optional = ['namePerson/first', 'media/image/aspect11', 'media/image/default'];
368                                 try {
369                                         $authurl = $openid->authUrl();
370                                 } catch (Exception $e) {
371                                         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);
372                                 }
373                                 goaway($authurl);
374                                 // NOTREACHED
375                         }
376
377                         throw new Exception(L10n::t('Please enter the required information.'));
378                 }
379
380                 if (!Network::isUrlValid($openid_url)) {
381                         $openid_url = '';
382                 }
383
384                 $err = '';
385
386                 // collapse multiple spaces in name
387                 $username = preg_replace('/ +/', ' ', $username);
388
389                 if (mb_strlen($username) > 48) {
390                         throw new Exception(L10n::t('Please use a shorter name.'));
391                 }
392                 if (mb_strlen($username) < 3) {
393                         throw new Exception(L10n::t('Name too short.'));
394                 }
395
396                 // So now we are just looking for a space in the full name.
397                 $loose_reg = Config::get('system', 'no_regfullname');
398                 if (!$loose_reg) {
399                         $username = mb_convert_case($username, MB_CASE_TITLE, 'UTF-8');
400                         if (!strpos($username, ' ')) {
401                                 throw new Exception(L10n::t("That doesn't appear to be your full \x28First Last\x29 name."));
402                         }
403                 }
404
405                 if (!Network::isEmailDomainAllowed($email)) {
406                         throw new Exception(L10n::t('Your email domain is not among those allowed on this site.'));
407                 }
408
409                 if (!valid_email($email) || !Network::isEmailDomainValid($email)) {
410                         throw new Exception(L10n::t('Not a valid email address.'));
411                 }
412
413                 if (Config::get('system', 'block_extended_register', false) && dba::exists('user', ['email' => $email])) {
414                         throw new Exception(L10n::t('Cannot use that email.'));
415                 }
416
417                 // Disallow somebody creating an account using openid that uses the admin email address,
418                 // since openid bypasses email verification. We'll allow it if there is not yet an admin account.
419                 if (x($a->config, 'admin_email') && strlen($openid_url)) {
420                         $adminlist = explode(',', str_replace(' ', '', strtolower($a->config['admin_email'])));
421                         if (in_array(strtolower($email), $adminlist)) {
422                                 throw new Exception(L10n::t('Cannot use that email.'));
423                         }
424                 }
425
426                 $nickname = $data['nickname'] = strtolower($nickname);
427
428                 if (!preg_match('/^[a-z0-9][a-z0-9\_]*$/', $nickname)) {
429                         throw new Exception(L10n::t('Your nickname can only contain a-z, 0-9 and _.'));
430                 }
431
432                 // Check existing and deleted accounts for this nickname.
433                 if (dba::exists('user', ['nickname' => $nickname])
434                         || dba::exists('userd', ['username' => $nickname])
435                 ) {
436                         throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
437                 }
438
439                 $new_password = strlen($password) ? $password : User::generateNewPassword();
440                 $new_password_encoded = self::hashPassword($new_password);
441
442                 $return['password'] = $new_password;
443
444                 $keys = Crypto::newKeypair(4096);
445                 if ($keys === false) {
446                         throw new Exception(L10n::t('SERIOUS ERROR: Generation of security keys failed.'));
447                 }
448
449                 $prvkey = $keys['prvkey'];
450                 $pubkey = $keys['pubkey'];
451
452                 // Create another keypair for signing/verifying salmon protocol messages.
453                 $sres = Crypto::newKeypair(512);
454                 $sprvkey = $sres['prvkey'];
455                 $spubkey = $sres['pubkey'];
456
457                 $insert_result = dba::insert('user', [
458                         'guid'     => generate_user_guid(),
459                         'username' => $username,
460                         'password' => $new_password_encoded,
461                         'email'    => $email,
462                         'openid'   => $openid_url,
463                         'nickname' => $nickname,
464                         'pubkey'   => $pubkey,
465                         'prvkey'   => $prvkey,
466                         'spubkey'  => $spubkey,
467                         'sprvkey'  => $sprvkey,
468                         'verified' => $verified,
469                         'blocked'  => $blocked,
470                         'language' => $language,
471                         'timezone' => 'UTC',
472                         'register_date' => DateTimeFormat::utcNow(),
473                         'default-location' => ''
474                 ]);
475
476                 if ($insert_result) {
477                         $uid = dba::lastInsertId();
478                         $user = dba::selectFirst('user', [], ['uid' => $uid]);
479                 } else {
480                         throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
481                 }
482
483                 if (!$uid) {
484                         throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
485                 }
486
487                 // if somebody clicked submit twice very quickly, they could end up with two accounts
488                 // due to race condition. Remove this one.
489                 $user_count = dba::count('user', ['nickname' => $nickname]);
490                 if ($user_count > 1) {
491                         dba::delete('user', ['uid' => $uid]);
492
493                         throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
494                 }
495
496                 $insert_result = dba::insert('profile', [
497                         'uid' => $uid,
498                         'name' => $username,
499                         'photo' => System::baseUrl() . "/photo/profile/{$uid}.jpg",
500                         'thumb' => System::baseUrl() . "/photo/avatar/{$uid}.jpg",
501                         'publish' => $publish,
502                         'is-default' => 1,
503                         'net-publish' => $netpublish,
504                         'profile-name' => L10n::t('default')
505                 ]);
506                 if (!$insert_result) {
507                         dba::delete('user', ['uid' => $uid]);
508
509                         throw new Exception(L10n::t('An error occurred creating your default profile. Please try again.'));
510                 }
511
512                 // Create the self contact
513                 if (!Contact::createSelfFromUserId($uid)) {
514                         dba::delete('user', ['uid' => $uid]);
515
516                         throw new Exception(L10n::t('An error occurred creating your self contact. Please try again.'));
517                 }
518
519                 // Create a group with no members. This allows somebody to use it
520                 // right away as a default group for new contacts.
521                 $def_gid = Group::create($uid, L10n::t('Friends'));
522                 if (!$def_gid) {
523                         dba::delete('user', ['uid' => $uid]);
524
525                         throw new Exception(L10n::t('An error occurred creating your default contact group. Please try again.'));
526                 }
527
528                 $fields = ['def_gid' => $def_gid];
529                 if (Config::get('system', 'newuser_private') && $def_gid) {
530                         $fields['allow_gid'] = '<' . $def_gid . '>';
531                 }
532
533                 dba::update('user', $fields, ['uid' => $uid]);
534
535                 // if we have no OpenID photo try to look up an avatar
536                 if (!strlen($photo)) {
537                         $photo = Network::lookupAvatarByEmail($email);
538                 }
539
540                 // unless there is no avatar-addon loaded
541                 if (strlen($photo)) {
542                         $photo_failure = false;
543
544                         $filename = basename($photo);
545                         $img_str = Network::fetchUrl($photo, true);
546                         // guess mimetype from headers or filename
547                         $type = Image::guessType($photo, true);
548
549                         $Image = new Image($img_str, $type);
550                         if ($Image->isValid()) {
551                                 $Image->scaleToSquare(175);
552
553                                 $hash = Photo::newResource();
554
555                                 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 4);
556
557                                 if ($r === false) {
558                                         $photo_failure = true;
559                                 }
560
561                                 $Image->scaleDown(80);
562
563                                 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 5);
564
565                                 if ($r === false) {
566                                         $photo_failure = true;
567                                 }
568
569                                 $Image->scaleDown(48);
570
571                                 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 6);
572
573                                 if ($r === false) {
574                                         $photo_failure = true;
575                                 }
576
577                                 if (!$photo_failure) {
578                                         dba::update('photo', ['profile' => 1], ['resource-id' => $hash]);
579                                 }
580                         }
581                 }
582
583                 Addon::callHooks('register_account', $uid);
584
585                 $return['user'] = $user;
586                 return $return;
587         }
588
589         /**
590          * @brief Sends pending registration confiƕmation email
591          *
592          * @param string $email
593          * @param string $sitename
594          * @param string $username
595          * @return NULL|boolean from notification() and email() inherited
596          */
597         public static function sendRegisterPendingEmail($email, $sitename, $username)
598         {
599                 $body = deindent(L10n::t('
600                         Dear %1$s,
601                                 Thank you for registering at %2$s. Your account is pending for approval by the administrator.
602                 '));
603
604                 $body = sprintf($body, $username, $sitename);
605
606                 return notification([
607                         'type' => SYSTEM_EMAIL,
608                         'to_email' => $email,
609                         'subject'=> L10n::t('Registration at %s', $sitename),
610                         'body' => $body]);
611         }
612
613         /**
614          * @brief Sends registration confirmation
615          *
616          * It's here as a function because the mail is sent from different parts
617          *
618          * @param string $email
619          * @param string $sitename
620          * @param string $siteurl
621          * @param string $username
622          * @param string $password
623          * @return NULL|boolean from notification() and email() inherited
624          */
625         public static function sendRegisterOpenEmail($email, $sitename, $siteurl, $username, $password)
626         {
627                 $preamble = deindent(L10n::t('
628                         Dear %1$s,
629                                 Thank you for registering at %2$s. Your account has been created.
630                 '));
631                 $body = deindent(L10n::t('
632                         The login details are as follows:
633
634                         Site Location:  %3$s
635                         Login Name:             %1$s
636                         Password:               %5$s
637
638                         You may change your password from your account "Settings" page after logging
639                         in.
640
641                         Please take a few moments to review the other account settings on that page.
642
643                         You may also wish to add some basic information to your default profile
644                         ' . "\x28" . 'on the "Profiles" page' . "\x29" . ' so that other people can easily find you.
645
646                         We recommend setting your full name, adding a profile photo,
647                         adding some profile "keywords" ' . "\x28" . 'very useful in making new friends' . "\x29" . ' - and
648                         perhaps what country you live in; if you do not wish to be more specific
649                         than that.
650
651                         We fully respect your right to privacy, and none of these items are necessary.
652                         If you are new and do not know anybody here, they may help
653                         you to make some new and interesting friends.
654
655                         If you ever want to delete your account, you can do so at %3$s/removeme
656
657                         Thank you and welcome to %2$s.'));
658
659                 $preamble = sprintf($preamble, $username, $sitename);
660                 $body = sprintf($body, $email, $sitename, $siteurl, $username, $password);
661
662                 return notification([
663                         'type' => SYSTEM_EMAIL,
664                         'to_email' => $email,
665                         'subject'=> L10n::t('Registration details for %s', $sitename),
666                         'preamble'=> $preamble,
667                         'body' => $body]);
668         }
669
670         /**
671          * @param object $uid user to remove
672          * @return void
673          */
674         public static function remove($uid)
675         {
676                 if (!$uid) {
677                         return;
678                 }
679
680                 logger('Removing user: ' . $uid);
681
682                 $user = dba::selectFirst('user', [], ['uid' => $uid]);
683
684                 Addon::callHooks('remove_user', $user);
685
686                 // save username (actually the nickname as it is guaranteed
687                 // unique), so it cannot be re-registered in the future.
688                 dba::insert('userd', ['username' => $user['nickname']]);
689
690                 // The user and related data will be deleted in "cron_expire_and_remove_users" (cronjobs.php)
691                 dba::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utcNow()], ['uid' => $uid]);
692                 Worker::add(PRIORITY_HIGH, "Notifier", "removeme", $uid);
693
694                 // Send an update to the directory
695                 Worker::add(PRIORITY_LOW, "Directory", $user['url']);
696
697                 if ($uid == local_user()) {
698                         unset($_SESSION['authenticated']);
699                         unset($_SESSION['uid']);
700                         goaway(System::baseUrl());
701                 }
702         }
703 }