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