]> git.mxchange.org Git - friendica.git/blob - src/Model/User.php
Fix security vulnerbilities.
[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 DivineOmega\PasswordExposed;
11 use Exception;
12 use Friendica\Core\Config;
13 use Friendica\Core\Hook;
14 use Friendica\Core\L10n;
15 use Friendica\Core\Logger;
16 use Friendica\Core\PConfig;
17 use Friendica\Core\Protocol;
18 use Friendica\Core\System;
19 use Friendica\Core\Worker;
20 use Friendica\Database\DBA;
21 use Friendica\Model\Photo;
22 use Friendica\Model\TwoFactor\AppSpecificPassword;
23 use Friendica\Object\Image;
24 use Friendica\Util\Crypto;
25 use Friendica\Util\DateTimeFormat;
26 use Friendica\Util\Network;
27 use Friendica\Util\Strings;
28 use Friendica\Worker\Delivery;
29 use LightOpenID;
30
31 /**
32  * @brief This class handles User related functions
33  */
34 class User
35 {
36         /**
37          * Page/profile types
38          *
39          * PAGE_FLAGS_NORMAL is a typical personal profile account
40          * PAGE_FLAGS_SOAPBOX automatically approves all friend requests as Contact::SHARING, (readonly)
41          * PAGE_FLAGS_COMMUNITY automatically approves all friend requests as Contact::SHARING, but with
42          *      write access to wall and comments (no email and not included in page owner's ACL lists)
43          * PAGE_FLAGS_FREELOVE automatically approves all friend requests as full friends (Contact::FRIEND).
44          *
45          * @{
46          */
47         const PAGE_FLAGS_NORMAL    = 0;
48         const PAGE_FLAGS_SOAPBOX   = 1;
49         const PAGE_FLAGS_COMMUNITY = 2;
50         const PAGE_FLAGS_FREELOVE  = 3;
51         const PAGE_FLAGS_BLOG      = 4;
52         const PAGE_FLAGS_PRVGROUP  = 5;
53         /**
54          * @}
55          */
56
57         /**
58          * Account types
59          *
60          * ACCOUNT_TYPE_PERSON - the account belongs to a person
61          *      Associated page types: PAGE_FLAGS_NORMAL, PAGE_FLAGS_SOAPBOX, PAGE_FLAGS_FREELOVE
62          *
63          * ACCOUNT_TYPE_ORGANISATION - the account belongs to an organisation
64          *      Associated page type: PAGE_FLAGS_SOAPBOX
65          *
66          * ACCOUNT_TYPE_NEWS - the account is a news reflector
67          *      Associated page type: PAGE_FLAGS_SOAPBOX
68          *
69          * ACCOUNT_TYPE_COMMUNITY - the account is community forum
70          *      Associated page types: PAGE_COMMUNITY, PAGE_FLAGS_PRVGROUP
71          *
72          * ACCOUNT_TYPE_RELAY - the account is a relay
73          *      This will only be assigned to contacts, not to user accounts
74          * @{
75          */
76         const ACCOUNT_TYPE_PERSON =       0;
77         const ACCOUNT_TYPE_ORGANISATION = 1;
78         const ACCOUNT_TYPE_NEWS =         2;
79         const ACCOUNT_TYPE_COMMUNITY =    3;
80         const ACCOUNT_TYPE_RELAY =        4;
81         /**
82          * @}
83          */
84
85         /**
86          * Returns true if a user record exists with the provided id
87          *
88          * @param  integer $uid
89          * @return boolean
90          * @throws Exception
91          */
92         public static function exists($uid)
93         {
94                 return DBA::exists('user', ['uid' => $uid]);
95         }
96
97         /**
98          * @param  integer       $uid
99          * @param array          $fields
100          * @return array|boolean User record if it exists, false otherwise
101          * @throws Exception
102          */
103         public static function getById($uid, array $fields = [])
104         {
105                 return DBA::selectFirst('user', $fields, ['uid' => $uid]);
106         }
107
108         /**
109          * @param  string        $nickname
110          * @param array          $fields
111          * @return array|boolean User record if it exists, false otherwise
112          * @throws Exception
113          */
114         public static function getByNickname($nickname, array $fields = [])
115         {
116                 return DBA::selectFirst('user', $fields, ['nickname' => $nickname]);
117         }
118
119         /**
120          * @brief Returns the user id of a given profile URL
121          *
122          * @param string $url
123          *
124          * @return integer user id
125          * @throws Exception
126          */
127         public static function getIdForURL($url)
128         {
129                 $self = DBA::selectFirst('contact', ['uid'], ['nurl' => Strings::normaliseLink($url), 'self' => true]);
130                 if (!DBA::isResult($self)) {
131                         return false;
132                 } else {
133                         return $self['uid'];
134                 }
135         }
136
137         /**
138          * Get a user based on its email
139          *
140          * @param string        $email
141          * @param array          $fields
142          *
143          * @return array|boolean User record if it exists, false otherwise
144          *
145          * @throws Exception
146          */
147         public static function getByEmail($email, array $fields = [])
148         {
149                 return DBA::selectFirst('user', $fields, ['email' => $email]);
150         }
151
152         /**
153          * @brief Get owner data by user id
154          *
155          * @param int $uid
156          * @param boolean $check_valid Test if data is invalid and correct it
157          * @return boolean|array
158          * @throws Exception
159          */
160         public static function getOwnerDataById($uid, $check_valid = true)
161         {
162                 $r = DBA::fetchFirst(
163                         "SELECT
164                         `contact`.*,
165                         `user`.`prvkey` AS `uprvkey`,
166                         `user`.`timezone`,
167                         `user`.`nickname`,
168                         `user`.`sprvkey`,
169                         `user`.`spubkey`,
170                         `user`.`page-flags`,
171                         `user`.`account-type`,
172                         `user`.`prvnets`,
173                         `user`.`account_removed`
174                         FROM `contact`
175                         INNER JOIN `user`
176                                 ON `user`.`uid` = `contact`.`uid`
177                         WHERE `contact`.`uid` = ?
178                         AND `contact`.`self`
179                         LIMIT 1",
180                         $uid
181                 );
182                 if (!DBA::isResult($r)) {
183                         return false;
184                 }
185
186                 if (empty($r['nickname'])) {
187                         return false;
188                 }
189
190                 if (!$check_valid) {
191                         return $r;
192                 }
193
194                 // Check if the returned data is valid, otherwise fix it. See issue #6122
195
196                 // Check for correct url and normalised nurl
197                 $url = System::baseUrl() . '/profile/' . $r['nickname'];
198                 $repair = ($r['url'] != $url) || ($r['nurl'] != Strings::normaliseLink($r['url']));
199
200                 if (!$repair) {
201                         // Check if "addr" is present and correct
202                         $addr = $r['nickname'] . '@' . substr(System::baseUrl(), strpos(System::baseUrl(), '://') + 3);
203                         $repair = ($addr != $r['addr']);
204                 }
205
206                 if (!$repair) {
207                         // Check if the avatar field is filled and the photo directs to the correct path
208                         $avatar = Photo::selectFirst(['resource-id'], ['uid' => $uid, 'profile' => true]);
209                         if (DBA::isResult($avatar)) {
210                                 $repair = empty($r['avatar']) || !strpos($r['photo'], $avatar['resource-id']);
211                         }
212                 }
213
214                 if ($repair) {
215                         Contact::updateSelfFromUserID($uid);
216                         // Return the corrected data and avoid a loop
217                         $r = self::getOwnerDataById($uid, false);
218                 }
219
220                 return $r;
221         }
222
223         /**
224          * @brief Get owner data by nick name
225          *
226          * @param int $nick
227          * @return boolean|array
228          * @throws Exception
229          */
230         public static function getOwnerDataByNick($nick)
231         {
232                 $user = DBA::selectFirst('user', ['uid'], ['nickname' => $nick]);
233
234                 if (!DBA::isResult($user)) {
235                         return false;
236                 }
237
238                 return self::getOwnerDataById($user['uid']);
239         }
240
241         /**
242          * @brief Returns the default group for a given user and network
243          *
244          * @param int $uid User id
245          * @param string $network network name
246          *
247          * @return int group id
248          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
249          */
250         public static function getDefaultGroup($uid, $network = '')
251         {
252                 $default_group = 0;
253
254                 if ($network == Protocol::OSTATUS) {
255                         $default_group = PConfig::get($uid, "ostatus", "default_group");
256                 }
257
258                 if ($default_group != 0) {
259                         return $default_group;
260                 }
261
262                 $user = DBA::selectFirst('user', ['def_gid'], ['uid' => $uid]);
263
264                 if (DBA::isResult($user)) {
265                         $default_group = $user["def_gid"];
266                 }
267
268                 return $default_group;
269         }
270
271
272         /**
273          * Authenticate a user with a clear text password
274          *
275          * @brief      Authenticate a user with a clear text password
276          * @param mixed  $user_info
277          * @param string $password
278          * @param bool   $third_party
279          * @return int|boolean
280          * @deprecated since version 3.6
281          * @see        User::getIdFromPasswordAuthentication()
282          */
283         public static function authenticate($user_info, $password, $third_party = false)
284         {
285                 try {
286                         return self::getIdFromPasswordAuthentication($user_info, $password, $third_party);
287                 } catch (Exception $ex) {
288                         return false;
289                 }
290         }
291
292         /**
293          * Returns the user id associated with a successful password authentication
294          *
295          * @brief Authenticate a user with a clear text password
296          * @param mixed  $user_info
297          * @param string $password
298          * @param bool   $third_party
299          * @return int User Id if authentication is successful
300          * @throws Exception
301          */
302         public static function getIdFromPasswordAuthentication($user_info, $password, $third_party = false)
303         {
304                 $user = self::getAuthenticationInfo($user_info);
305
306                 if ($third_party && PConfig::get($user['uid'], '2fa', 'verified')) {
307                         // Third-party apps can't verify two-factor authentication, we use app-specific passwords instead
308                         if (AppSpecificPassword::authenticateUser($user['uid'], $password)) {
309                                 return $user['uid'];
310                         }
311                 } elseif (strpos($user['password'], '$') === false) {
312                         //Legacy hash that has not been replaced by a new hash yet
313                         if (self::hashPasswordLegacy($password) === $user['password']) {
314                                 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
315
316                                 return $user['uid'];
317                         }
318                 } elseif (!empty($user['legacy_password'])) {
319                         //Legacy hash that has been double-hashed and not replaced by a new hash yet
320                         //Warning: `legacy_password` is not necessary in sync with the content of `password`
321                         if (password_verify(self::hashPasswordLegacy($password), $user['password'])) {
322                                 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
323
324                                 return $user['uid'];
325                         }
326                 } elseif (password_verify($password, $user['password'])) {
327                         //New password hash
328                         if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
329                                 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
330                         }
331
332                         return $user['uid'];
333                 }
334
335                 throw new Exception(L10n::t('Login failed'));
336         }
337
338         /**
339          * Returns authentication info from various parameters types
340          *
341          * User info can be any of the following:
342          * - User DB object
343          * - User Id
344          * - User email or username or nickname
345          * - User array with at least the uid and the hashed password
346          *
347          * @param mixed $user_info
348          * @return array
349          * @throws Exception
350          */
351         private static function getAuthenticationInfo($user_info)
352         {
353                 $user = null;
354
355                 if (is_object($user_info) || is_array($user_info)) {
356                         if (is_object($user_info)) {
357                                 $user = (array) $user_info;
358                         } else {
359                                 $user = $user_info;
360                         }
361
362                         if (
363                                 !isset($user['uid'])
364                                 || !isset($user['password'])
365                                 || !isset($user['legacy_password'])
366                         ) {
367                                 throw new Exception(L10n::t('Not enough information to authenticate'));
368                         }
369                 } elseif (is_int($user_info) || is_string($user_info)) {
370                         if (is_int($user_info)) {
371                                 $user = DBA::selectFirst(
372                                         'user',
373                                         ['uid', 'password', 'legacy_password'],
374                                         [
375                                                 'uid' => $user_info,
376                                                 'blocked' => 0,
377                                                 'account_expired' => 0,
378                                                 'account_removed' => 0,
379                                                 'verified' => 1
380                                         ]
381                                 );
382                         } else {
383                                 $fields = ['uid', 'password', 'legacy_password'];
384                                 $condition = [
385                                         "(`email` = ? OR `username` = ? OR `nickname` = ?)
386                                         AND NOT `blocked` AND NOT `account_expired` AND NOT `account_removed` AND `verified`",
387                                         $user_info, $user_info, $user_info
388                                 ];
389                                 $user = DBA::selectFirst('user', $fields, $condition);
390                         }
391
392                         if (!DBA::isResult($user)) {
393                                 throw new Exception(L10n::t('User not found'));
394                         }
395                 }
396
397                 return $user;
398         }
399
400         /**
401          * Generates a human-readable random password
402          *
403          * @return string
404          */
405         public static function generateNewPassword()
406         {
407                 return ucfirst(Strings::getRandomName(8)) . random_int(1000, 9999);
408         }
409
410         /**
411          * Checks if the provided plaintext password has been exposed or not
412          *
413          * @param string $password
414          * @return bool
415          */
416         public static function isPasswordExposed($password)
417         {
418                 $cache = new \DivineOmega\DOFileCachePSR6\CacheItemPool();
419                 $cache->changeConfig([
420                         'cacheDirectory' => get_temppath() . '/password-exposed-cache/',
421                 ]);
422
423                 $PasswordExposedCHecker = new PasswordExposed\PasswordExposedChecker(null, $cache);
424
425                 return $PasswordExposedCHecker->passwordExposed($password) === PasswordExposed\PasswordStatus::EXPOSED;
426         }
427
428         /**
429          * Legacy hashing function, kept for password migration purposes
430          *
431          * @param string $password
432          * @return string
433          */
434         private static function hashPasswordLegacy($password)
435         {
436                 return hash('whirlpool', $password);
437         }
438
439         /**
440          * Global user password hashing function
441          *
442          * @param string $password
443          * @return string
444          * @throws Exception
445          */
446         public static function hashPassword($password)
447         {
448                 if (!trim($password)) {
449                         throw new Exception(L10n::t('Password can\'t be empty'));
450                 }
451
452                 return password_hash($password, PASSWORD_DEFAULT);
453         }
454
455         /**
456          * Updates a user row with a new plaintext password
457          *
458          * @param int    $uid
459          * @param string $password
460          * @return bool
461          * @throws Exception
462          */
463         public static function updatePassword($uid, $password)
464         {
465                 $password = trim($password);
466
467                 if (empty($password)) {
468                         throw new Exception(L10n::t('Empty passwords are not allowed.'));
469                 }
470
471                 if (!Config::get('system', 'disable_password_exposed', false) && self::isPasswordExposed($password)) {
472                         throw new Exception(L10n::t('The new password has been exposed in a public data dump, please choose another.'));
473                 }
474
475                 $allowed_characters = '!"#$%&\'()*+,-./;<=>?@[\]^_`{|}~';
476
477                 if (!preg_match('/^[a-z0-9' . preg_quote($allowed_characters, '/') . ']+$/i', $password)) {
478                         throw new Exception(L10n::t('The password can\'t contain accentuated letters, white spaces or colons (:)'));
479                 }
480
481                 return self::updatePasswordHashed($uid, self::hashPassword($password));
482         }
483
484         /**
485          * Updates a user row with a new hashed password.
486          * Empties the password reset token field just in case.
487          *
488          * @param int    $uid
489          * @param string $pasword_hashed
490          * @return bool
491          * @throws Exception
492          */
493         private static function updatePasswordHashed($uid, $pasword_hashed)
494         {
495                 $fields = [
496                         'password' => $pasword_hashed,
497                         'pwdreset' => null,
498                         'pwdreset_time' => null,
499                         'legacy_password' => false
500                 ];
501                 return DBA::update('user', $fields, ['uid' => $uid]);
502         }
503
504         /**
505          * @brief Checks if a nickname is in the list of the forbidden nicknames
506          *
507          * Check if a nickname is forbidden from registration on the node by the
508          * admin. Forbidden nicknames (e.g. role namess) can be configured in the
509          * admin panel.
510          *
511          * @param string $nickname The nickname that should be checked
512          * @return boolean True is the nickname is blocked on the node
513          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
514          */
515         public static function isNicknameBlocked($nickname)
516         {
517                 $forbidden_nicknames = Config::get('system', 'forbidden_nicknames', '');
518
519                 // if the config variable is empty return false
520                 if (empty($forbidden_nicknames)) {
521                         return false;
522                 }
523
524                 // check if the nickname is in the list of blocked nicknames
525                 $forbidden = explode(',', $forbidden_nicknames);
526                 $forbidden = array_map('trim', $forbidden);
527                 if (in_array(strtolower($nickname), $forbidden)) {
528                         return true;
529                 }
530
531                 // else return false
532                 return false;
533         }
534
535         /**
536          * @brief Catch-all user creation function
537          *
538          * Creates a user from the provided data array, either form fields or OpenID.
539          * Required: { username, nickname, email } or { openid_url }
540          *
541          * Performs the following:
542          * - Sends to the OpenId auth URL (if relevant)
543          * - Creates new key pairs for crypto
544          * - Create self-contact
545          * - Create profile image
546          *
547          * @param  array $data
548          * @return array
549          * @throws \ErrorException
550          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
551          * @throws \ImagickException
552          * @throws Exception
553          */
554         public static function create(array $data)
555         {
556                 $a = \get_app();
557                 $return = ['user' => null, 'password' => ''];
558
559                 $using_invites = Config::get('system', 'invitation_only');
560
561                 $invite_id  = !empty($data['invite_id'])  ? Strings::escapeTags(trim($data['invite_id']))  : '';
562                 $username   = !empty($data['username'])   ? Strings::escapeTags(trim($data['username']))   : '';
563                 $nickname   = !empty($data['nickname'])   ? Strings::escapeTags(trim($data['nickname']))   : '';
564                 $email      = !empty($data['email'])      ? Strings::escapeTags(trim($data['email']))      : '';
565                 $openid_url = !empty($data['openid_url']) ? Strings::escapeTags(trim($data['openid_url'])) : '';
566                 $photo      = !empty($data['photo'])      ? Strings::escapeTags(trim($data['photo']))      : '';
567                 $password   = !empty($data['password'])   ? trim($data['password'])           : '';
568                 $password1  = !empty($data['password1'])  ? trim($data['password1'])          : '';
569                 $confirm    = !empty($data['confirm'])    ? trim($data['confirm'])            : '';
570                 $blocked    = !empty($data['blocked']);
571                 $verified   = !empty($data['verified']);
572                 $language   = !empty($data['language'])   ? Strings::escapeTags(trim($data['language']))   : 'en';
573
574                 $publish = !empty($data['profile_publish_reg']);
575                 $netpublish = $publish && Config::get('system', 'directory');
576
577                 if ($password1 != $confirm) {
578                         throw new Exception(L10n::t('Passwords do not match. Password unchanged.'));
579                 } elseif ($password1 != '') {
580                         $password = $password1;
581                 }
582
583                 if ($using_invites) {
584                         if (!$invite_id) {
585                                 throw new Exception(L10n::t('An invitation is required.'));
586                         }
587
588                         if (!Register::existsByHash($invite_id)) {
589                                 throw new Exception(L10n::t('Invitation could not be verified.'));
590                         }
591                 }
592
593                 if (empty($username) || empty($email) || empty($nickname)) {
594                         if ($openid_url) {
595                                 if (!Network::isUrlValid($openid_url)) {
596                                         throw new Exception(L10n::t('Invalid OpenID url'));
597                                 }
598                                 $_SESSION['register'] = 1;
599                                 $_SESSION['openid'] = $openid_url;
600
601                                 $openid = new LightOpenID($a->getHostName());
602                                 $openid->identity = $openid_url;
603                                 $openid->returnUrl = System::baseUrl() . '/openid';
604                                 $openid->required = ['namePerson/friendly', 'contact/email', 'namePerson'];
605                                 $openid->optional = ['namePerson/first', 'media/image/aspect11', 'media/image/default'];
606                                 try {
607                                         $authurl = $openid->authUrl();
608                                 } catch (Exception $e) {
609                                         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);
610                                 }
611                                 System::externalRedirect($authurl);
612                                 // NOTREACHED
613                         }
614
615                         throw new Exception(L10n::t('Please enter the required information.'));
616                 }
617
618                 if (!Network::isUrlValid($openid_url)) {
619                         $openid_url = '';
620                 }
621
622                 // collapse multiple spaces in name
623                 $username = preg_replace('/ +/', ' ', $username);
624
625                 $username_min_length = max(1, min(64, intval(Config::get('system', 'username_min_length', 3))));
626                 $username_max_length = max(1, min(64, intval(Config::get('system', 'username_max_length', 48))));
627
628                 if ($username_min_length > $username_max_length) {
629                         Logger::log(L10n::t('system.username_min_length (%s) and system.username_max_length (%s) are excluding each other, swapping values.', $username_min_length, $username_max_length), Logger::WARNING);
630                         $tmp = $username_min_length;
631                         $username_min_length = $username_max_length;
632                         $username_max_length = $tmp;
633                 }
634
635                 if (mb_strlen($username) < $username_min_length) {
636                         throw new Exception(L10n::tt('Username should be at least %s character.', 'Username should be at least %s characters.', $username_min_length));
637                 }
638
639                 if (mb_strlen($username) > $username_max_length) {
640                         throw new Exception(L10n::tt('Username should be at most %s character.', 'Username should be at most %s characters.', $username_max_length));
641                 }
642
643                 // So now we are just looking for a space in the full name.
644                 $loose_reg = Config::get('system', 'no_regfullname');
645                 if (!$loose_reg) {
646                         $username = mb_convert_case($username, MB_CASE_TITLE, 'UTF-8');
647                         if (strpos($username, ' ') === false) {
648                                 throw new Exception(L10n::t("That doesn't appear to be your full (First Last) name."));
649                         }
650                 }
651
652                 if (!Network::isEmailDomainAllowed($email)) {
653                         throw new Exception(L10n::t('Your email domain is not among those allowed on this site.'));
654                 }
655
656                 if (!filter_var($email, FILTER_VALIDATE_EMAIL) || !Network::isEmailDomainValid($email)) {
657                         throw new Exception(L10n::t('Not a valid email address.'));
658                 }
659                 if (self::isNicknameBlocked($nickname)) {
660                         throw new Exception(L10n::t('The nickname was blocked from registration by the nodes admin.'));
661                 }
662
663                 if (Config::get('system', 'block_extended_register', false) && DBA::exists('user', ['email' => $email])) {
664                         throw new Exception(L10n::t('Cannot use that email.'));
665                 }
666
667                 // Disallow somebody creating an account using openid that uses the admin email address,
668                 // since openid bypasses email verification. We'll allow it if there is not yet an admin account.
669                 if (Config::get('config', 'admin_email') && strlen($openid_url)) {
670                         $adminlist = explode(',', str_replace(' ', '', strtolower(Config::get('config', 'admin_email'))));
671                         if (in_array(strtolower($email), $adminlist)) {
672                                 throw new Exception(L10n::t('Cannot use that email.'));
673                         }
674                 }
675
676                 $nickname = $data['nickname'] = strtolower($nickname);
677
678                 if (!preg_match('/^[a-z0-9][a-z0-9\_]*$/', $nickname)) {
679                         throw new Exception(L10n::t('Your nickname can only contain a-z, 0-9 and _.'));
680                 }
681
682                 // Check existing and deleted accounts for this nickname.
683                 if (
684                         DBA::exists('user', ['nickname' => $nickname])
685                         || DBA::exists('userd', ['username' => $nickname])
686                 ) {
687                         throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
688                 }
689
690                 $new_password = strlen($password) ? $password : User::generateNewPassword();
691                 $new_password_encoded = self::hashPassword($new_password);
692
693                 $return['password'] = $new_password;
694
695                 $keys = Crypto::newKeypair(4096);
696                 if ($keys === false) {
697                         throw new Exception(L10n::t('SERIOUS ERROR: Generation of security keys failed.'));
698                 }
699
700                 $prvkey = $keys['prvkey'];
701                 $pubkey = $keys['pubkey'];
702
703                 // Create another keypair for signing/verifying salmon protocol messages.
704                 $sres = Crypto::newKeypair(512);
705                 $sprvkey = $sres['prvkey'];
706                 $spubkey = $sres['pubkey'];
707
708                 $insert_result = DBA::insert('user', [
709                         'guid'     => System::createUUID(),
710                         'username' => $username,
711                         'password' => $new_password_encoded,
712                         'email'    => $email,
713                         'openid'   => $openid_url,
714                         'nickname' => $nickname,
715                         'pubkey'   => $pubkey,
716                         'prvkey'   => $prvkey,
717                         'spubkey'  => $spubkey,
718                         'sprvkey'  => $sprvkey,
719                         'verified' => $verified,
720                         'blocked'  => $blocked,
721                         'language' => $language,
722                         'timezone' => 'UTC',
723                         'register_date' => DateTimeFormat::utcNow(),
724                         'default-location' => ''
725                 ]);
726
727                 if ($insert_result) {
728                         $uid = DBA::lastInsertId();
729                         $user = DBA::selectFirst('user', [], ['uid' => $uid]);
730                 } else {
731                         throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
732                 }
733
734                 if (!$uid) {
735                         throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
736                 }
737
738                 // if somebody clicked submit twice very quickly, they could end up with two accounts
739                 // due to race condition. Remove this one.
740                 $user_count = DBA::count('user', ['nickname' => $nickname]);
741                 if ($user_count > 1) {
742                         DBA::delete('user', ['uid' => $uid]);
743
744                         throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
745                 }
746
747                 $insert_result = DBA::insert('profile', [
748                         'uid' => $uid,
749                         'name' => $username,
750                         'photo' => System::baseUrl() . "/photo/profile/{$uid}.jpg",
751                         'thumb' => System::baseUrl() . "/photo/avatar/{$uid}.jpg",
752                         'publish' => $publish,
753                         'is-default' => 1,
754                         'net-publish' => $netpublish,
755                         'profile-name' => L10n::t('default')
756                 ]);
757                 if (!$insert_result) {
758                         DBA::delete('user', ['uid' => $uid]);
759
760                         throw new Exception(L10n::t('An error occurred creating your default profile. Please try again.'));
761                 }
762
763                 // Create the self contact
764                 if (!Contact::createSelfFromUserId($uid)) {
765                         DBA::delete('user', ['uid' => $uid]);
766
767                         throw new Exception(L10n::t('An error occurred creating your self contact. Please try again.'));
768                 }
769
770                 // Create a group with no members. This allows somebody to use it
771                 // right away as a default group for new contacts.
772                 $def_gid = Group::create($uid, L10n::t('Friends'));
773                 if (!$def_gid) {
774                         DBA::delete('user', ['uid' => $uid]);
775
776                         throw new Exception(L10n::t('An error occurred creating your default contact group. Please try again.'));
777                 }
778
779                 $fields = ['def_gid' => $def_gid];
780                 if (Config::get('system', 'newuser_private') && $def_gid) {
781                         $fields['allow_gid'] = '<' . $def_gid . '>';
782                 }
783
784                 DBA::update('user', $fields, ['uid' => $uid]);
785
786                 // if we have no OpenID photo try to look up an avatar
787                 if (!strlen($photo)) {
788                         $photo = Network::lookupAvatarByEmail($email);
789                 }
790
791                 // unless there is no avatar-addon loaded
792                 if (strlen($photo)) {
793                         $photo_failure = false;
794
795                         $filename = basename($photo);
796                         $img_str = Network::fetchUrl($photo, true);
797                         // guess mimetype from headers or filename
798                         $type = Image::guessType($photo, true);
799
800                         $Image = new Image($img_str, $type);
801                         if ($Image->isValid()) {
802                                 $Image->scaleToSquare(300);
803
804                                 $hash = Photo::newResource();
805
806                                 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 4);
807
808                                 if ($r === false) {
809                                         $photo_failure = true;
810                                 }
811
812                                 $Image->scaleDown(80);
813
814                                 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 5);
815
816                                 if ($r === false) {
817                                         $photo_failure = true;
818                                 }
819
820                                 $Image->scaleDown(48);
821
822                                 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 6);
823
824                                 if ($r === false) {
825                                         $photo_failure = true;
826                                 }
827
828                                 if (!$photo_failure) {
829                                         Photo::update(['profile' => 1], ['resource-id' => $hash]);
830                                 }
831                         }
832                 }
833
834                 Hook::callAll('register_account', $uid);
835
836                 $return['user'] = $user;
837                 return $return;
838         }
839
840         /**
841          * @brief Sends pending registration confirmation email
842          *
843          * @param array  $user     User record array
844          * @param string $sitename
845          * @param string $siteurl
846          * @param string $password Plaintext password
847          * @return NULL|boolean from notification() and email() inherited
848          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
849          */
850         public static function sendRegisterPendingEmail($user, $sitename, $siteurl, $password)
851         {
852                 $body = Strings::deindent(L10n::t(
853                         '
854                         Dear %1$s,
855                                 Thank you for registering at %2$s. Your account is pending for approval by the administrator.
856
857                         Your login details are as follows:
858
859                         Site Location:  %3$s
860                         Login Name:             %4$s
861                         Password:               %5$s
862                 ',
863                         $user['username'],
864                         $sitename,
865                         $siteurl,
866                         $user['nickname'],
867                         $password
868                 ));
869
870                 return notification([
871                         'type'     => SYSTEM_EMAIL,
872                         'uid'      => $user['uid'],
873                         'to_email' => $user['email'],
874                         'subject'  => L10n::t('Registration at %s', $sitename),
875                         'body'     => $body
876                 ]);
877         }
878
879         /**
880          * @brief Sends registration confirmation
881          *
882          * It's here as a function because the mail is sent from different parts
883          *
884          * @param array  $user     User record array
885          * @param string $sitename
886          * @param string $siteurl
887          * @param string $password Plaintext password
888          * @return NULL|boolean from notification() and email() inherited
889          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
890          */
891         public static function sendRegisterOpenEmail($user, $sitename, $siteurl, $password)
892         {
893                 $preamble = Strings::deindent(L10n::t(
894                         '
895                                 Dear %1$s,
896                                 Thank you for registering at %2$s. Your account has been created.
897                         ',
898                         $user['username'],
899                         $sitename
900                 ));
901                 $body = Strings::deindent(L10n::t(
902                         '
903                         The login details are as follows:
904
905                         Site Location:  %3$s
906                         Login Name:             %1$s
907                         Password:               %5$s
908
909                         You may change your password from your account "Settings" page after logging
910                         in.
911
912                         Please take a few moments to review the other account settings on that page.
913
914                         You may also wish to add some basic information to your default profile
915                         ' . "\x28" . 'on the "Profiles" page' . "\x29" . ' so that other people can easily find you.
916
917                         We recommend setting your full name, adding a profile photo,
918                         adding some profile "keywords" ' . "\x28" . 'very useful in making new friends' . "\x29" . ' - and
919                         perhaps what country you live in; if you do not wish to be more specific
920                         than that.
921
922                         We fully respect your right to privacy, and none of these items are necessary.
923                         If you are new and do not know anybody here, they may help
924                         you to make some new and interesting friends.
925
926                         If you ever want to delete your account, you can do so at %3$s/removeme
927
928                         Thank you and welcome to %2$s.',
929                         $user['nickname'],
930                         $sitename,
931                         $siteurl,
932                         $user['username'],
933                         $password
934                 ));
935
936                 return notification([
937                         'uid'      => $user['uid'],
938                         'language' => $user['language'],
939                         'type'     => SYSTEM_EMAIL,
940                         'to_email' => $user['email'],
941                         'subject'  => L10n::t('Registration details for %s', $sitename),
942                         'preamble' => $preamble,
943                         'body'     => $body
944                 ]);
945         }
946
947         /**
948          * @param object $uid user to remove
949          * @return bool
950          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
951          */
952         public static function remove($uid)
953         {
954                 if (!$uid) {
955                         return false;
956                 }
957
958                 Logger::log('Removing user: ' . $uid);
959
960                 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
961
962                 Hook::callAll('remove_user', $user);
963
964                 // save username (actually the nickname as it is guaranteed
965                 // unique), so it cannot be re-registered in the future.
966                 DBA::insert('userd', ['username' => $user['nickname']]);
967
968                 // The user and related data will be deleted in "cron_expire_and_remove_users" (cronjobs.php)
969                 DBA::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utc('now + 7 day')], ['uid' => $uid]);
970                 Worker::add(PRIORITY_HIGH, 'Notifier', Delivery::REMOVAL, $uid);
971
972                 // Send an update to the directory
973                 $self = DBA::selectFirst('contact', ['url'], ['uid' => $uid, 'self' => true]);
974                 Worker::add(PRIORITY_LOW, 'Directory', $self['url']);
975
976                 // Remove the user relevant data
977                 Worker::add(PRIORITY_NEGLIGIBLE, 'RemoveUser', $uid);
978
979                 return true;
980         }
981
982         /**
983          * Return all identities to a user
984          *
985          * @param int $uid The user id
986          * @return array All identities for this user
987          *
988          * Example for a return:
989          *    [
990          *        [
991          *            'uid' => 1,
992          *            'username' => 'maxmuster',
993          *            'nickname' => 'Max Mustermann'
994          *        ],
995          *        [
996          *            'uid' => 2,
997          *            'username' => 'johndoe',
998          *            'nickname' => 'John Doe'
999          *        ]
1000          *    ]
1001          * @throws Exception
1002          */
1003         public static function identities($uid)
1004         {
1005                 $identities = [];
1006
1007                 $user = DBA::selectFirst('user', ['uid', 'nickname', 'username', 'parent-uid'], ['uid' => $uid]);
1008                 if (!DBA::isResult($user)) {
1009                         return $identities;
1010                 }
1011
1012                 if ($user['parent-uid'] == 0) {
1013                         // First add our own entry
1014                         $identities = [[
1015                                 'uid' => $user['uid'],
1016                                 'username' => $user['username'],
1017                                 'nickname' => $user['nickname']
1018                         ]];
1019
1020                         // Then add all the children
1021                         $r = DBA::select(
1022                                 'user',
1023                                 ['uid', 'username', 'nickname'],
1024                                 ['parent-uid' => $user['uid'], 'account_removed' => false]
1025                         );
1026                         if (DBA::isResult($r)) {
1027                                 $identities = array_merge($identities, DBA::toArray($r));
1028                         }
1029                 } else {
1030                         // First entry is our parent
1031                         $r = DBA::select(
1032                                 'user',
1033                                 ['uid', 'username', 'nickname'],
1034                                 ['uid' => $user['parent-uid'], 'account_removed' => false]
1035                         );
1036                         if (DBA::isResult($r)) {
1037                                 $identities = DBA::toArray($r);
1038                         }
1039
1040                         // Then add all siblings
1041                         $r = DBA::select(
1042                                 'user',
1043                                 ['uid', 'username', 'nickname'],
1044                                 ['parent-uid' => $user['parent-uid'], 'account_removed' => false]
1045                         );
1046                         if (DBA::isResult($r)) {
1047                                 $identities = array_merge($identities, DBA::toArray($r));
1048                         }
1049                 }
1050
1051                 $r = DBA::p(
1052                         "SELECT `user`.`uid`, `user`.`username`, `user`.`nickname`
1053                         FROM `manage`
1054                         INNER JOIN `user` ON `manage`.`mid` = `user`.`uid`
1055                         WHERE `user`.`account_removed` = 0 AND `manage`.`uid` = ?",
1056                         $user['uid']
1057                 );
1058                 if (DBA::isResult($r)) {
1059                         $identities = array_merge($identities, DBA::toArray($r));
1060                 }
1061
1062                 return $identities;
1063         }
1064
1065         /**
1066          * Returns statistical information about the current users of this node
1067          *
1068          * @return array
1069          *
1070          * @throws Exception
1071          */
1072         public static function getStatistics()
1073         {
1074                 $statistics = [
1075                         'total_users'           => 0,
1076                         'active_users_halfyear' => 0,
1077                         'active_users_monthly'  => 0,
1078                 ];
1079
1080                 $userStmt = DBA::p("SELECT `user`.`uid`, `user`.`login_date`, `contact`.`last-item`
1081                         FROM `user`
1082                         INNER JOIN `profile` ON `profile`.`uid` = `user`.`uid` AND `profile`.`is-default`
1083                         INNER JOIN `contact` ON `contact`.`uid` = `user`.`uid` AND `contact`.`self`
1084                         WHERE (`profile`.`publish` OR `profile`.`net-publish`) AND `user`.`verified`
1085                                 AND NOT `user`.`blocked` AND NOT `user`.`account_removed`
1086                                 AND NOT `user`.`account_expired`");
1087
1088                 if (!DBA::isResult($userStmt)) {
1089                         return $statistics;
1090                 }
1091
1092                 $halfyear = time() - (180 * 24 * 60 * 60);
1093                 $month = time() - (30 * 24 * 60 * 60);
1094
1095                 while ($user = DBA::fetch($userStmt)) {
1096                         $statistics['total_users']++;
1097
1098                         if ((strtotime($user['login_date']) > $halfyear) || (strtotime($user['last-item']) > $halfyear)
1099                         ) {
1100                                 $statistics['active_users_halfyear']++;
1101                         }
1102
1103                         if ((strtotime($user['login_date']) > $month) || (strtotime($user['last-item']) > $month)
1104                         ) {
1105                                 $statistics['active_users_monthly']++;
1106                         }
1107                 }
1108
1109                 return $statistics;
1110         }
1111 }