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