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