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