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