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