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