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