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