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