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