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