]> git.mxchange.org Git - friendica.git/blob - src/Model/User.php
Merge pull request #5022 from Rudloff/feature/test_api
[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
335                 $publish = x($data, 'profile_publish_reg') && intval($data['profile_publish_reg']) ? 1 : 0;
336                 $netpublish = strlen(Config::get('system', 'directory')) ? $publish : 0;
337
338                 if ($password1 != $confirm) {
339                         throw new Exception(L10n::t('Passwords do not match. Password unchanged.'));
340                 } elseif ($password1 != '') {
341                         $password = $password1;
342                 }
343
344                 if ($using_invites) {
345                         if (!$invite_id) {
346                                 throw new Exception(L10n::t('An invitation is required.'));
347                         }
348
349                         if (!dba::exists('register', ['hash' => $invite_id])) {
350                                 throw new Exception(L10n::t('Invitation could not be verified.'));
351                         }
352                 }
353
354                 if (!x($username) || !x($email) || !x($nickname)) {
355                         if ($openid_url) {
356                                 if (!Network::isUrlValid($openid_url)) {
357                                         throw new Exception(L10n::t('Invalid OpenID url'));
358                                 }
359                                 $_SESSION['register'] = 1;
360                                 $_SESSION['openid'] = $openid_url;
361
362                                 $openid = new LightOpenID;
363                                 $openid->identity = $openid_url;
364                                 $openid->returnUrl = System::baseUrl() . '/openid';
365                                 $openid->required = ['namePerson/friendly', 'contact/email', 'namePerson'];
366                                 $openid->optional = ['namePerson/first', 'media/image/aspect11', 'media/image/default'];
367                                 try {
368                                         $authurl = $openid->authUrl();
369                                 } catch (Exception $e) {
370                                         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);
371                                 }
372                                 goaway($authurl);
373                                 // NOTREACHED
374                         }
375
376                         throw new Exception(L10n::t('Please enter the required information.'));
377                 }
378
379                 if (!Network::isUrlValid($openid_url)) {
380                         $openid_url = '';
381                 }
382
383                 $err = '';
384
385                 // collapse multiple spaces in name
386                 $username = preg_replace('/ +/', ' ', $username);
387
388                 if (mb_strlen($username) > 48) {
389                         throw new Exception(L10n::t('Please use a shorter name.'));
390                 }
391                 if (mb_strlen($username) < 3) {
392                         throw new Exception(L10n::t('Name too short.'));
393                 }
394
395                 // So now we are just looking for a space in the full name.
396                 $loose_reg = Config::get('system', 'no_regfullname');
397                 if (!$loose_reg) {
398                         $username = mb_convert_case($username, MB_CASE_TITLE, 'UTF-8');
399                         if (!strpos($username, ' ')) {
400                                 throw new Exception(L10n::t("That doesn't appear to be your full \x28First Last\x29 name."));
401                         }
402                 }
403
404                 if (!Network::isEmailDomainAllowed($email)) {
405                         throw new Exception(L10n::t('Your email domain is not among those allowed on this site.'));
406                 }
407
408                 if (!valid_email($email) || !Network::isEmailDomainValid($email)) {
409                         throw new Exception(L10n::t('Not a valid email address.'));
410                 }
411
412                 if (Config::get('system', 'block_extended_register', false) && dba::exists('user', ['email' => $email])) {
413                         throw new Exception(L10n::t('Cannot use that email.'));
414                 }
415
416                 // Disallow somebody creating an account using openid that uses the admin email address,
417                 // since openid bypasses email verification. We'll allow it if there is not yet an admin account.
418                 if (x($a->config, 'admin_email') && strlen($openid_url)) {
419                         $adminlist = explode(',', str_replace(' ', '', strtolower($a->config['admin_email'])));
420                         if (in_array(strtolower($email), $adminlist)) {
421                                 throw new Exception(L10n::t('Cannot use that email.'));
422                         }
423                 }
424
425                 $nickname = $data['nickname'] = strtolower($nickname);
426
427                 if (!preg_match('/^[a-z0-9][a-z0-9\_]*$/', $nickname)) {
428                         throw new Exception(L10n::t('Your nickname can only contain a-z, 0-9 and _.'));
429                 }
430
431                 // Check existing and deleted accounts for this nickname.
432                 if (dba::exists('user', ['nickname' => $nickname])
433                         || dba::exists('userd', ['username' => $nickname])
434                 ) {
435                         throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
436                 }
437
438                 $new_password = strlen($password) ? $password : User::generateNewPassword();
439                 $new_password_encoded = self::hashPassword($new_password);
440
441                 $return['password'] = $new_password;
442
443                 $keys = Crypto::newKeypair(4096);
444                 if ($keys === false) {
445                         throw new Exception(L10n::t('SERIOUS ERROR: Generation of security keys failed.'));
446                 }
447
448                 $prvkey = $keys['prvkey'];
449                 $pubkey = $keys['pubkey'];
450
451                 // Create another keypair for signing/verifying salmon protocol messages.
452                 $sres = Crypto::newKeypair(512);
453                 $sprvkey = $sres['prvkey'];
454                 $spubkey = $sres['pubkey'];
455
456                 $insert_result = dba::insert('user', [
457                         'guid'     => generate_user_guid(),
458                         'username' => $username,
459                         'password' => $new_password_encoded,
460                         'email'    => $email,
461                         'openid'   => $openid_url,
462                         'nickname' => $nickname,
463                         'pubkey'   => $pubkey,
464                         'prvkey'   => $prvkey,
465                         'spubkey'  => $spubkey,
466                         'sprvkey'  => $sprvkey,
467                         'verified' => $verified,
468                         'blocked'  => $blocked,
469                         'timezone' => 'UTC',
470                         'register_date' => DateTimeFormat::utcNow(),
471                         'default-location' => ''
472                 ]);
473
474                 if ($insert_result) {
475                         $uid = dba::lastInsertId();
476                         $user = dba::selectFirst('user', [], ['uid' => $uid]);
477                 } else {
478                         throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
479                 }
480
481                 if (!$uid) {
482                         throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
483                 }
484
485                 // if somebody clicked submit twice very quickly, they could end up with two accounts
486                 // due to race condition. Remove this one.
487                 $user_count = dba::count('user', ['nickname' => $nickname]);
488                 if ($user_count > 1) {
489                         dba::delete('user', ['uid' => $uid]);
490
491                         throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
492                 }
493
494                 $insert_result = dba::insert('profile', [
495                         'uid' => $uid,
496                         'name' => $username,
497                         'photo' => System::baseUrl() . "/photo/profile/{$uid}.jpg",
498                         'thumb' => System::baseUrl() . "/photo/avatar/{$uid}.jpg",
499                         'publish' => $publish,
500                         'is-default' => 1,
501                         'net-publish' => $netpublish,
502                         'profile-name' => L10n::t('default')
503                 ]);
504                 if (!$insert_result) {
505                         dba::delete('user', ['uid' => $uid]);
506
507                         throw new Exception(L10n::t('An error occurred creating your default profile. Please try again.'));
508                 }
509
510                 // Create the self contact
511                 if (!Contact::createSelfFromUserId($uid)) {
512                         dba::delete('user', ['uid' => $uid]);
513
514                         throw new Exception(L10n::t('An error occurred creating your self contact. Please try again.'));
515                 }
516
517                 // Create a group with no members. This allows somebody to use it
518                 // right away as a default group for new contacts.
519                 $def_gid = Group::create($uid, L10n::t('Friends'));
520                 if (!$def_gid) {
521                         dba::delete('user', ['uid' => $uid]);
522
523                         throw new Exception(L10n::t('An error occurred creating your default contact group. Please try again.'));
524                 }
525
526                 $fields = ['def_gid' => $def_gid];
527                 if (Config::get('system', 'newuser_private') && $def_gid) {
528                         $fields['allow_gid'] = '<' . $def_gid . '>';
529                 }
530
531                 dba::update('user', $fields, ['uid' => $uid]);
532
533                 // if we have no OpenID photo try to look up an avatar
534                 if (!strlen($photo)) {
535                         $photo = Network::lookupAvatarByEmail($email);
536                 }
537
538                 // unless there is no avatar-addon loaded
539                 if (strlen($photo)) {
540                         $photo_failure = false;
541
542                         $filename = basename($photo);
543                         $img_str = Network::fetchUrl($photo, true);
544                         // guess mimetype from headers or filename
545                         $type = Image::guessType($photo, true);
546
547                         $Image = new Image($img_str, $type);
548                         if ($Image->isValid()) {
549                                 $Image->scaleToSquare(175);
550
551                                 $hash = Photo::newResource();
552
553                                 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 4);
554
555                                 if ($r === false) {
556                                         $photo_failure = true;
557                                 }
558
559                                 $Image->scaleDown(80);
560
561                                 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 5);
562
563                                 if ($r === false) {
564                                         $photo_failure = true;
565                                 }
566
567                                 $Image->scaleDown(48);
568
569                                 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 6);
570
571                                 if ($r === false) {
572                                         $photo_failure = true;
573                                 }
574
575                                 if (!$photo_failure) {
576                                         dba::update('photo', ['profile' => 1], ['resource-id' => $hash]);
577                                 }
578                         }
579                 }
580
581                 Addon::callHooks('register_account', $uid);
582
583                 $return['user'] = $user;
584                 return $return;
585         }
586
587         /**
588          * @brief Sends pending registration confiƕmation email
589          *
590          * @param string $email
591          * @param string $sitename
592          * @param string $username
593          * @return NULL|boolean from notification() and email() inherited
594          */
595         public static function sendRegisterPendingEmail($email, $sitename, $username)
596         {
597                 $body = deindent(L10n::t('
598                         Dear %1$s,
599                                 Thank you for registering at %2$s. Your account is pending for approval by the administrator.
600                 '));
601
602                 $body = sprintf($body, $username, $sitename);
603
604                 return notification([
605                         'type' => SYSTEM_EMAIL,
606                         'to_email' => $email,
607                         'subject'=> L10n::t('Registration at %s', $sitename),
608                         'body' => $body]);
609         }
610
611         /**
612          * @brief Sends registration confirmation
613          *
614          * It's here as a function because the mail is sent from different parts
615          *
616          * @param string $email
617          * @param string $sitename
618          * @param string $siteurl
619          * @param string $username
620          * @param string $password
621          * @return NULL|boolean from notification() and email() inherited
622          */
623         public static function sendRegisterOpenEmail($email, $sitename, $siteurl, $username, $password)
624         {
625                 $preamble = deindent(L10n::t('
626                         Dear %1$s,
627                                 Thank you for registering at %2$s. Your account has been created.
628                 '));
629                 $body = deindent(L10n::t('
630                         The login details are as follows:
631
632                         Site Location:  %3$s
633                         Login Name:             %1$s
634                         Password:               %5$s
635
636                         You may change your password from your account "Settings" page after logging
637                         in.
638
639                         Please take a few moments to review the other account settings on that page.
640
641                         You may also wish to add some basic information to your default profile
642                         ' . "\x28" . 'on the "Profiles" page' . "\x29" . ' so that other people can easily find you.
643
644                         We recommend setting your full name, adding a profile photo,
645                         adding some profile "keywords" ' . "\x28" . 'very useful in making new friends' . "\x29" . ' - and
646                         perhaps what country you live in; if you do not wish to be more specific
647                         than that.
648
649                         We fully respect your right to privacy, and none of these items are necessary.
650                         If you are new and do not know anybody here, they may help
651                         you to make some new and interesting friends.
652
653                         If you ever want to delete your account, you can do so at %3$s/removeme
654
655                         Thank you and welcome to %2$s.'));
656
657                 $preamble = sprintf($preamble, $username, $sitename);
658                 $body = sprintf($body, $email, $sitename, $siteurl, $username, $password);
659
660                 return notification([
661                         'type' => SYSTEM_EMAIL,
662                         'to_email' => $email,
663                         'subject'=> L10n::t('Registration details for %s', $sitename),
664                         'preamble'=> $preamble,
665                         'body' => $body]);
666         }
667
668         /**
669          * @param object $uid user to remove
670          * @return void
671          */
672         public static function remove($uid)
673         {
674                 if (!$uid) {
675                         return;
676                 }
677
678                 logger('Removing user: ' . $uid);
679
680                 $user = dba::selectFirst('user', [], ['uid' => $uid]);
681
682                 Addon::callHooks('remove_user', $user);
683
684                 // save username (actually the nickname as it is guaranteed
685                 // unique), so it cannot be re-registered in the future.
686                 dba::insert('userd', ['username' => $user['nickname']]);
687
688                 // The user and related data will be deleted in "cron_expire_and_remove_users" (cronjobs.php)
689                 dba::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utcNow()], ['uid' => $uid]);
690                 Worker::add(PRIORITY_HIGH, "Notifier", "removeme", $uid);
691
692                 // Send an update to the directory
693                 Worker::add(PRIORITY_LOW, "Directory", $user['url']);
694
695                 if ($uid == local_user()) {
696                         unset($_SESSION['authenticated']);
697                         unset($_SESSION['uid']);
698                         goaway(System::baseUrl());
699                 }
700         }
701 }