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