]> git.mxchange.org Git - friendica.git/blob - src/Model/User.php
Refactor ExAuth for DICE
[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 HTTPException\ForbiddenException
325          * @throws HTTPException\NotFoundException
326          */
327         public static function getIdFromPasswordAuthentication($user_info, $password, $third_party = false)
328         {
329                 $user = self::getAuthenticationInfo($user_info);
330
331                 if ($third_party && DI::pConfig()->get($user['uid'], '2fa', 'verified')) {
332                         // Third-party apps can't verify two-factor authentication, we use app-specific passwords instead
333                         if (AppSpecificPassword::authenticateUser($user['uid'], $password)) {
334                                 return $user['uid'];
335                         }
336                 } elseif (strpos($user['password'], '$') === false) {
337                         //Legacy hash that has not been replaced by a new hash yet
338                         if (self::hashPasswordLegacy($password) === $user['password']) {
339                                 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
340
341                                 return $user['uid'];
342                         }
343                 } elseif (!empty($user['legacy_password'])) {
344                         //Legacy hash that has been double-hashed and not replaced by a new hash yet
345                         //Warning: `legacy_password` is not necessary in sync with the content of `password`
346                         if (password_verify(self::hashPasswordLegacy($password), $user['password'])) {
347                                 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
348
349                                 return $user['uid'];
350                         }
351                 } elseif (password_verify($password, $user['password'])) {
352                         //New password hash
353                         if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
354                                 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
355                         }
356
357                         return $user['uid'];
358                 }
359
360                 throw new HTTPException\ForbiddenException(DI::l10n()->t('Login failed'));
361         }
362
363         /**
364          * Returns authentication info from various parameters types
365          *
366          * User info can be any of the following:
367          * - User DB object
368          * - User Id
369          * - User email or username or nickname
370          * - User array with at least the uid and the hashed password
371          *
372          * @param mixed $user_info
373          * @return array
374          * @throws HTTPException\NotFoundException
375          */
376         private static function getAuthenticationInfo($user_info)
377         {
378                 $user = null;
379
380                 if (is_object($user_info) || is_array($user_info)) {
381                         if (is_object($user_info)) {
382                                 $user = (array) $user_info;
383                         } else {
384                                 $user = $user_info;
385                         }
386
387                         if (
388                                 !isset($user['uid'])
389                                 || !isset($user['password'])
390                                 || !isset($user['legacy_password'])
391                         ) {
392                                 throw new Exception(DI::l10n()->t('Not enough information to authenticate'));
393                         }
394                 } elseif (is_int($user_info) || is_string($user_info)) {
395                         if (is_int($user_info)) {
396                                 $user = DBA::selectFirst(
397                                         'user',
398                                         ['uid', 'password', 'legacy_password'],
399                                         [
400                                                 'uid' => $user_info,
401                                                 'blocked' => 0,
402                                                 'account_expired' => 0,
403                                                 'account_removed' => 0,
404                                                 'verified' => 1
405                                         ]
406                                 );
407                         } else {
408                                 $fields = ['uid', 'password', 'legacy_password'];
409                                 $condition = [
410                                         "(`email` = ? OR `username` = ? OR `nickname` = ?)
411                                         AND NOT `blocked` AND NOT `account_expired` AND NOT `account_removed` AND `verified`",
412                                         $user_info, $user_info, $user_info
413                                 ];
414                                 $user = DBA::selectFirst('user', $fields, $condition);
415                         }
416
417                         if (!DBA::isResult($user)) {
418                                 throw new HTTPException\NotFoundException(DI::l10n()->t('User not found'));
419                         }
420                 }
421
422                 return $user;
423         }
424
425         /**
426          * Generates a human-readable random password
427          *
428          * @return string
429          * @throws Exception
430          */
431         public static function generateNewPassword()
432         {
433                 return ucfirst(Strings::getRandomName(8)) . random_int(1000, 9999);
434         }
435
436         /**
437          * Checks if the provided plaintext password has been exposed or not
438          *
439          * @param string $password
440          * @return bool
441          * @throws Exception
442          */
443         public static function isPasswordExposed($password)
444         {
445                 $cache = new CacheItemPool();
446                 $cache->changeConfig([
447                         'cacheDirectory' => get_temppath() . '/password-exposed-cache/',
448                 ]);
449
450                 try {
451                         $passwordExposedChecker = new PasswordExposed\PasswordExposedChecker(null, $cache);
452
453                         return $passwordExposedChecker->passwordExposed($password) === PasswordExposed\PasswordStatus::EXPOSED;
454                 } catch (Exception $e) {
455                         Logger::error('Password Exposed Exception: ' . $e->getMessage(), [
456                                 'code' => $e->getCode(),
457                                 'file' => $e->getFile(),
458                                 'line' => $e->getLine(),
459                                 'trace' => $e->getTraceAsString()
460                         ]);
461
462                         return false;
463                 }
464         }
465
466         /**
467          * Legacy hashing function, kept for password migration purposes
468          *
469          * @param string $password
470          * @return string
471          */
472         private static function hashPasswordLegacy($password)
473         {
474                 return hash('whirlpool', $password);
475         }
476
477         /**
478          * Global user password hashing function
479          *
480          * @param string $password
481          * @return string
482          * @throws Exception
483          */
484         public static function hashPassword($password)
485         {
486                 if (!trim($password)) {
487                         throw new Exception(DI::l10n()->t('Password can\'t be empty'));
488                 }
489
490                 return password_hash($password, PASSWORD_DEFAULT);
491         }
492
493         /**
494          * Updates a user row with a new plaintext password
495          *
496          * @param int    $uid
497          * @param string $password
498          * @return bool
499          * @throws Exception
500          */
501         public static function updatePassword($uid, $password)
502         {
503                 $password = trim($password);
504
505                 if (empty($password)) {
506                         throw new Exception(DI::l10n()->t('Empty passwords are not allowed.'));
507                 }
508
509                 if (!DI::config()->get('system', 'disable_password_exposed', false) && self::isPasswordExposed($password)) {
510                         throw new Exception(DI::l10n()->t('The new password has been exposed in a public data dump, please choose another.'));
511                 }
512
513                 $allowed_characters = '!"#$%&\'()*+,-./;<=>?@[\]^_`{|}~';
514
515                 if (!preg_match('/^[a-z0-9' . preg_quote($allowed_characters, '/') . ']+$/i', $password)) {
516                         throw new Exception(DI::l10n()->t('The password can\'t contain accentuated letters, white spaces or colons (:)'));
517                 }
518
519                 return self::updatePasswordHashed($uid, self::hashPassword($password));
520         }
521
522         /**
523          * Updates a user row with a new hashed password.
524          * Empties the password reset token field just in case.
525          *
526          * @param int    $uid
527          * @param string $pasword_hashed
528          * @return bool
529          * @throws Exception
530          */
531         private static function updatePasswordHashed($uid, $pasword_hashed)
532         {
533                 $fields = [
534                         'password' => $pasword_hashed,
535                         'pwdreset' => null,
536                         'pwdreset_time' => null,
537                         'legacy_password' => false
538                 ];
539                 return DBA::update('user', $fields, ['uid' => $uid]);
540         }
541
542         /**
543          * Checks if a nickname is in the list of the forbidden nicknames
544          *
545          * Check if a nickname is forbidden from registration on the node by the
546          * admin. Forbidden nicknames (e.g. role namess) can be configured in the
547          * admin panel.
548          *
549          * @param string $nickname The nickname that should be checked
550          * @return boolean True is the nickname is blocked on the node
551          */
552         public static function isNicknameBlocked($nickname)
553         {
554                 $forbidden_nicknames = DI::config()->get('system', 'forbidden_nicknames', '');
555
556                 // if the config variable is empty return false
557                 if (empty($forbidden_nicknames)) {
558                         return false;
559                 }
560
561                 // check if the nickname is in the list of blocked nicknames
562                 $forbidden = explode(',', $forbidden_nicknames);
563                 $forbidden = array_map('trim', $forbidden);
564                 if (in_array(strtolower($nickname), $forbidden)) {
565                         return true;
566                 }
567
568                 // else return false
569                 return false;
570         }
571
572         /**
573          * Catch-all user creation function
574          *
575          * Creates a user from the provided data array, either form fields or OpenID.
576          * Required: { username, nickname, email } or { openid_url }
577          *
578          * Performs the following:
579          * - Sends to the OpenId auth URL (if relevant)
580          * - Creates new key pairs for crypto
581          * - Create self-contact
582          * - Create profile image
583          *
584          * @param  array $data
585          * @return array
586          * @throws ErrorException
587          * @throws HTTPException\InternalServerErrorException
588          * @throws ImagickException
589          * @throws Exception
590          */
591         public static function create(array $data)
592         {
593                 $return = ['user' => null, 'password' => ''];
594
595                 $using_invites = DI::config()->get('system', 'invitation_only');
596
597                 $invite_id  = !empty($data['invite_id'])  ? Strings::escapeTags(trim($data['invite_id']))  : '';
598                 $username   = !empty($data['username'])   ? Strings::escapeTags(trim($data['username']))   : '';
599                 $nickname   = !empty($data['nickname'])   ? Strings::escapeTags(trim($data['nickname']))   : '';
600                 $email      = !empty($data['email'])      ? Strings::escapeTags(trim($data['email']))      : '';
601                 $openid_url = !empty($data['openid_url']) ? Strings::escapeTags(trim($data['openid_url'])) : '';
602                 $photo      = !empty($data['photo'])      ? Strings::escapeTags(trim($data['photo']))      : '';
603                 $password   = !empty($data['password'])   ? trim($data['password'])           : '';
604                 $password1  = !empty($data['password1'])  ? trim($data['password1'])          : '';
605                 $confirm    = !empty($data['confirm'])    ? trim($data['confirm'])            : '';
606                 $blocked    = !empty($data['blocked']);
607                 $verified   = !empty($data['verified']);
608                 $language   = !empty($data['language'])   ? Strings::escapeTags(trim($data['language']))   : 'en';
609
610                 $netpublish = $publish = !empty($data['profile_publish_reg']);
611
612                 if ($password1 != $confirm) {
613                         throw new Exception(DI::l10n()->t('Passwords do not match. Password unchanged.'));
614                 } elseif ($password1 != '') {
615                         $password = $password1;
616                 }
617
618                 if ($using_invites) {
619                         if (!$invite_id) {
620                                 throw new Exception(DI::l10n()->t('An invitation is required.'));
621                         }
622
623                         if (!Register::existsByHash($invite_id)) {
624                                 throw new Exception(DI::l10n()->t('Invitation could not be verified.'));
625                         }
626                 }
627
628                 /// @todo Check if this part is really needed. We should have fetched all this data in advance
629                 if (empty($username) || empty($email) || empty($nickname)) {
630                         if ($openid_url) {
631                                 if (!Network::isUrlValid($openid_url)) {
632                                         throw new Exception(DI::l10n()->t('Invalid OpenID url'));
633                                 }
634                                 $_SESSION['register'] = 1;
635                                 $_SESSION['openid'] = $openid_url;
636
637                                 $openid = new LightOpenID(DI::baseUrl()->getHostname());
638                                 $openid->identity = $openid_url;
639                                 $openid->returnUrl = DI::baseUrl() . '/openid';
640                                 $openid->required = ['namePerson/friendly', 'contact/email', 'namePerson'];
641                                 $openid->optional = ['namePerson/first', 'media/image/aspect11', 'media/image/default'];
642                                 try {
643                                         $authurl = $openid->authUrl();
644                                 } catch (Exception $e) {
645                                         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);
646                                 }
647                                 System::externalRedirect($authurl);
648                                 // NOTREACHED
649                         }
650
651                         throw new Exception(DI::l10n()->t('Please enter the required information.'));
652                 }
653
654                 if (!Network::isUrlValid($openid_url)) {
655                         $openid_url = '';
656                 }
657
658                 // collapse multiple spaces in name
659                 $username = preg_replace('/ +/', ' ', $username);
660
661                 $username_min_length = max(1, min(64, intval(DI::config()->get('system', 'username_min_length', 3))));
662                 $username_max_length = max(1, min(64, intval(DI::config()->get('system', 'username_max_length', 48))));
663
664                 if ($username_min_length > $username_max_length) {
665                         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);
666                         $tmp = $username_min_length;
667                         $username_min_length = $username_max_length;
668                         $username_max_length = $tmp;
669                 }
670
671                 if (mb_strlen($username) < $username_min_length) {
672                         throw new Exception(DI::l10n()->tt('Username should be at least %s character.', 'Username should be at least %s characters.', $username_min_length));
673                 }
674
675                 if (mb_strlen($username) > $username_max_length) {
676                         throw new Exception(DI::l10n()->tt('Username should be at most %s character.', 'Username should be at most %s characters.', $username_max_length));
677                 }
678
679                 // So now we are just looking for a space in the full name.
680                 $loose_reg = DI::config()->get('system', 'no_regfullname');
681                 if (!$loose_reg) {
682                         $username = mb_convert_case($username, MB_CASE_TITLE, 'UTF-8');
683                         if (strpos($username, ' ') === false) {
684                                 throw new Exception(DI::l10n()->t("That doesn't appear to be your full (First Last) name."));
685                         }
686                 }
687
688                 if (!Network::isEmailDomainAllowed($email)) {
689                         throw new Exception(DI::l10n()->t('Your email domain is not among those allowed on this site.'));
690                 }
691
692                 if (!filter_var($email, FILTER_VALIDATE_EMAIL) || !Network::isEmailDomainValid($email)) {
693                         throw new Exception(DI::l10n()->t('Not a valid email address.'));
694                 }
695                 if (self::isNicknameBlocked($nickname)) {
696                         throw new Exception(DI::l10n()->t('The nickname was blocked from registration by the nodes admin.'));
697                 }
698
699                 if (DI::config()->get('system', 'block_extended_register', false) && DBA::exists('user', ['email' => $email])) {
700                         throw new Exception(DI::l10n()->t('Cannot use that email.'));
701                 }
702
703                 // Disallow somebody creating an account using openid that uses the admin email address,
704                 // since openid bypasses email verification. We'll allow it if there is not yet an admin account.
705                 if (DI::config()->get('config', 'admin_email') && strlen($openid_url)) {
706                         $adminlist = explode(',', str_replace(' ', '', strtolower(DI::config()->get('config', 'admin_email'))));
707                         if (in_array(strtolower($email), $adminlist)) {
708                                 throw new Exception(DI::l10n()->t('Cannot use that email.'));
709                         }
710                 }
711
712                 $nickname = $data['nickname'] = strtolower($nickname);
713
714                 if (!preg_match('/^[a-z0-9][a-z0-9_]*$/', $nickname)) {
715                         throw new Exception(DI::l10n()->t('Your nickname can only contain a-z, 0-9 and _.'));
716                 }
717
718                 // Check existing and deleted accounts for this nickname.
719                 if (
720                         DBA::exists('user', ['nickname' => $nickname])
721                         || DBA::exists('userd', ['username' => $nickname])
722                 ) {
723                         throw new Exception(DI::l10n()->t('Nickname is already registered. Please choose another.'));
724                 }
725
726                 $new_password = strlen($password) ? $password : User::generateNewPassword();
727                 $new_password_encoded = self::hashPassword($new_password);
728
729                 $return['password'] = $new_password;
730
731                 $keys = Crypto::newKeypair(4096);
732                 if ($keys === false) {
733                         throw new Exception(DI::l10n()->t('SERIOUS ERROR: Generation of security keys failed.'));
734                 }
735
736                 $prvkey = $keys['prvkey'];
737                 $pubkey = $keys['pubkey'];
738
739                 // Create another keypair for signing/verifying salmon protocol messages.
740                 $sres = Crypto::newKeypair(512);
741                 $sprvkey = $sres['prvkey'];
742                 $spubkey = $sres['pubkey'];
743
744                 $insert_result = DBA::insert('user', [
745                         'guid'     => System::createUUID(),
746                         'username' => $username,
747                         'password' => $new_password_encoded,
748                         'email'    => $email,
749                         'openid'   => $openid_url,
750                         'nickname' => $nickname,
751                         'pubkey'   => $pubkey,
752                         'prvkey'   => $prvkey,
753                         'spubkey'  => $spubkey,
754                         'sprvkey'  => $sprvkey,
755                         'verified' => $verified,
756                         'blocked'  => $blocked,
757                         'language' => $language,
758                         'timezone' => 'UTC',
759                         'register_date' => DateTimeFormat::utcNow(),
760                         'default-location' => ''
761                 ]);
762
763                 if ($insert_result) {
764                         $uid = DBA::lastInsertId();
765                         $user = DBA::selectFirst('user', [], ['uid' => $uid]);
766                 } else {
767                         throw new Exception(DI::l10n()->t('An error occurred during registration. Please try again.'));
768                 }
769
770                 if (!$uid) {
771                         throw new Exception(DI::l10n()->t('An error occurred during registration. Please try again.'));
772                 }
773
774                 // if somebody clicked submit twice very quickly, they could end up with two accounts
775                 // due to race condition. Remove this one.
776                 $user_count = DBA::count('user', ['nickname' => $nickname]);
777                 if ($user_count > 1) {
778                         DBA::delete('user', ['uid' => $uid]);
779
780                         throw new Exception(DI::l10n()->t('Nickname is already registered. Please choose another.'));
781                 }
782
783                 $insert_result = DBA::insert('profile', [
784                         'uid' => $uid,
785                         'name' => $username,
786                         'photo' => DI::baseUrl() . "/photo/profile/{$uid}.jpg",
787                         'thumb' => DI::baseUrl() . "/photo/avatar/{$uid}.jpg",
788                         'publish' => $publish,
789                         'net-publish' => $netpublish,
790                 ]);
791                 if (!$insert_result) {
792                         DBA::delete('user', ['uid' => $uid]);
793
794                         throw new Exception(DI::l10n()->t('An error occurred creating your default profile. Please try again.'));
795                 }
796
797                 // Create the self contact
798                 if (!Contact::createSelfFromUserId($uid)) {
799                         DBA::delete('user', ['uid' => $uid]);
800
801                         throw new Exception(DI::l10n()->t('An error occurred creating your self contact. Please try again.'));
802                 }
803
804                 // Create a group with no members. This allows somebody to use it
805                 // right away as a default group for new contacts.
806                 $def_gid = Group::create($uid, DI::l10n()->t('Friends'));
807                 if (!$def_gid) {
808                         DBA::delete('user', ['uid' => $uid]);
809
810                         throw new Exception(DI::l10n()->t('An error occurred creating your default contact group. Please try again.'));
811                 }
812
813                 $fields = ['def_gid' => $def_gid];
814                 if (DI::config()->get('system', 'newuser_private') && $def_gid) {
815                         $fields['allow_gid'] = '<' . $def_gid . '>';
816                 }
817
818                 DBA::update('user', $fields, ['uid' => $uid]);
819
820                 // if we have no OpenID photo try to look up an avatar
821                 if (!strlen($photo)) {
822                         $photo = Network::lookupAvatarByEmail($email);
823                 }
824
825                 // unless there is no avatar-addon loaded
826                 if (strlen($photo)) {
827                         $photo_failure = false;
828
829                         $filename = basename($photo);
830                         $curlResult = Network::curl($photo, true);
831                         if ($curlResult->isSuccess()) {
832                                 $img_str = $curlResult->getBody();
833                                 $type = $curlResult->getContentType();
834                         } else {
835                                 $img_str = '';
836                                 $type = '';
837                         }
838
839                         $type = Images::getMimeTypeByData($img_str, $photo, $type);
840
841                         $Image = new Image($img_str, $type);
842                         if ($Image->isValid()) {
843                                 $Image->scaleToSquare(300);
844
845                                 $resource_id = Photo::newResource();
846
847                                 $r = Photo::store($Image, $uid, 0, $resource_id, $filename, DI::l10n()->t('Profile Photos'), 4);
848
849                                 if ($r === false) {
850                                         $photo_failure = true;
851                                 }
852
853                                 $Image->scaleDown(80);
854
855                                 $r = Photo::store($Image, $uid, 0, $resource_id, $filename, DI::l10n()->t('Profile Photos'), 5);
856
857                                 if ($r === false) {
858                                         $photo_failure = true;
859                                 }
860
861                                 $Image->scaleDown(48);
862
863                                 $r = Photo::store($Image, $uid, 0, $resource_id, $filename, DI::l10n()->t('Profile Photos'), 6);
864
865                                 if ($r === false) {
866                                         $photo_failure = true;
867                                 }
868
869                                 if (!$photo_failure) {
870                                         Photo::update(['profile' => 1], ['resource-id' => $resource_id]);
871                                 }
872                         }
873                 }
874
875                 Hook::callAll('register_account', $uid);
876
877                 $return['user'] = $user;
878                 return $return;
879         }
880
881         /**
882          * Sets block state for a given user
883          *
884          * @param int  $uid   The user id
885          * @param bool $block Block state (default is true)
886          *
887          * @return bool True, if successfully blocked
888
889          * @throws Exception
890          */
891         public static function block(int $uid, bool $block = true)
892         {
893                 return DBA::update('user', ['blocked' => $block], ['uid' => $uid]);
894         }
895
896         /**
897          * Allows a registration based on a hash
898          *
899          * @param string $hash
900          *
901          * @return bool True, if the allow was successful
902          *
903          * @throws HTTPException\InternalServerErrorException
904          * @throws Exception
905          */
906         public static function allow(string $hash)
907         {
908                 $register = Register::getByHash($hash);
909                 if (!DBA::isResult($register)) {
910                         return false;
911                 }
912
913                 $user = User::getById($register['uid']);
914                 if (!DBA::isResult($user)) {
915                         return false;
916                 }
917
918                 Register::deleteByHash($hash);
919
920                 DBA::update('user', ['blocked' => false, 'verified' => true], ['uid' => $register['uid']]);
921
922                 $profile = DBA::selectFirst('profile', ['net-publish'], ['uid' => $register['uid']]);
923
924                 if (DBA::isResult($profile) && $profile['net-publish'] && DI::config()->get('system', 'directory')) {
925                         $url = DI::baseUrl() . '/profile/' . $user['nickname'];
926                         Worker::add(PRIORITY_LOW, "Directory", $url);
927                 }
928
929                 $l10n = DI::l10n()->withLang($register['language']);
930
931                 return User::sendRegisterOpenEmail(
932                         $l10n,
933                         $user,
934                         DI::config()->get('config', 'sitename'),
935                         DI::baseUrl()->get(),
936                         ($register['password'] ?? '') ?: 'Sent in a previous email'
937                 );
938         }
939
940         /**
941          * Denys a pending registration
942          *
943          * @param string $hash The hash of the pending user
944          *
945          * This does not have to go through user_remove() and save the nickname
946          * permanently against re-registration, as the person was not yet
947          * allowed to have friends on this system
948          *
949          * @return bool True, if the deny was successfull
950          * @throws Exception
951          */
952         public static function deny(string $hash)
953         {
954                 $register = Register::getByHash($hash);
955                 if (!DBA::isResult($register)) {
956                         return false;
957                 }
958
959                 $user = User::getById($register['uid']);
960                 if (!DBA::isResult($user)) {
961                         return false;
962                 }
963
964                 return DBA::delete('user', ['uid' => $register['uid']]) &&
965                        Register::deleteByHash($register['hash']);
966         }
967
968         /**
969          * Creates a new user based on a minimal set and sends an email to this user
970          *
971          * @param string $name  The user's name
972          * @param string $email The user's email address
973          * @param string $nick  The user's nick name
974          * @param string $lang  The user's language (default is english)
975          *
976          * @return bool True, if the user was created successfully
977          * @throws HTTPException\InternalServerErrorException
978          * @throws ErrorException
979          * @throws ImagickException
980          */
981         public static function createMinimal(string $name, string $email, string $nick, string $lang = L10n::DEFAULT)
982         {
983                 if (empty($name) ||
984                     empty($email) ||
985                     empty($nick)) {
986                         throw new HTTPException\InternalServerErrorException('Invalid arguments.');
987                 }
988
989                 $result = self::create([
990                         'username' => $name,
991                         'email' => $email,
992                         'nickname' => $nick,
993                         'verified' => 1,
994                         'language' => $lang
995                 ]);
996
997                 $user = $result['user'];
998                 $preamble = Strings::deindent(DI::l10n()->t('
999                 Dear %1$s,
1000                         the administrator of %2$s has set up an account for you.'));
1001                 $body = Strings::deindent(DI::l10n()->t('
1002                 The login details are as follows:
1003
1004                 Site Location:  %1$s
1005                 Login Name:             %2$s
1006                 Password:               %3$s
1007
1008                 You may change your password from your account "Settings" page after logging
1009                 in.
1010
1011                 Please take a few moments to review the other account settings on that page.
1012
1013                 You may also wish to add some basic information to your default profile
1014                 (on the "Profiles" page) so that other people can easily find you.
1015
1016                 We recommend setting your full name, adding a profile photo,
1017                 adding some profile "keywords" (very useful in making new friends) - and
1018                 perhaps what country you live in; if you do not wish to be more specific
1019                 than that.
1020
1021                 We fully respect your right to privacy, and none of these items are necessary.
1022                 If you are new and do not know anybody here, they may help
1023                 you to make some new and interesting friends.
1024
1025                 If you ever want to delete your account, you can do so at %1$s/removeme
1026
1027                 Thank you and welcome to %4$s.'));
1028
1029                 $preamble = sprintf($preamble, $user['username'], DI::config()->get('config', 'sitename'));
1030                 $body = sprintf($body, DI::baseUrl()->get(), $user['nickname'], $result['password'], DI::config()->get('config', 'sitename'));
1031
1032                 $email = DI::emailer()
1033                         ->newSystemMail()
1034                         ->withMessage(DI::l10n()->t('Registration details for %s', DI::config()->get('config', 'sitename')), $preamble, $body)
1035                         ->forUser($user)
1036                         ->withRecipient($user['email'])
1037                         ->build();
1038                 return DI::emailer()->send($email);
1039         }
1040
1041         /**
1042          * Sends pending registration confirmation email
1043          *
1044          * @param array  $user     User record array
1045          * @param string $sitename
1046          * @param string $siteurl
1047          * @param string $password Plaintext password
1048          * @return NULL|boolean from notification() and email() inherited
1049          * @throws HTTPException\InternalServerErrorException
1050          */
1051         public static function sendRegisterPendingEmail($user, $sitename, $siteurl, $password)
1052         {
1053                 $body = Strings::deindent(DI::l10n()->t(
1054                         '
1055                         Dear %1$s,
1056                                 Thank you for registering at %2$s. Your account is pending for approval by the administrator.
1057
1058                         Your login details are as follows:
1059
1060                         Site Location:  %3$s
1061                         Login Name:             %4$s
1062                         Password:               %5$s
1063                 ',
1064                         $user['username'],
1065                         $sitename,
1066                         $siteurl,
1067                         $user['nickname'],
1068                         $password
1069                 ));
1070
1071                 $email = DI::emailer()
1072                         ->newSystemMail()
1073                         ->withMessage(DI::l10n()->t('Registration at %s', $sitename), $body)
1074                         ->forUser($user)
1075                         ->withRecipient($user['email'])
1076                         ->build();
1077                 return DI::emailer()->send($email);
1078         }
1079
1080         /**
1081          * Sends registration confirmation
1082          *
1083          * It's here as a function because the mail is sent from different parts
1084          *
1085          * @param L10n   $l10n     The used language
1086          * @param array  $user     User record array
1087          * @param string $sitename
1088          * @param string $siteurl
1089          * @param string $password Plaintext password
1090          *
1091          * @return NULL|boolean from notification() and email() inherited
1092          * @throws HTTPException\InternalServerErrorException
1093          */
1094         public static function sendRegisterOpenEmail(L10n $l10n, $user, $sitename, $siteurl, $password)
1095         {
1096                 $preamble = Strings::deindent($l10n->t(
1097                         '
1098                                 Dear %1$s,
1099                                 Thank you for registering at %2$s. Your account has been created.
1100                         ',
1101                         $user['username'],
1102                         $sitename
1103                 ));
1104                 $body = Strings::deindent($l10n->t(
1105                         '
1106                         The login details are as follows:
1107
1108                         Site Location:  %3$s
1109                         Login Name:             %1$s
1110                         Password:               %5$s
1111
1112                         You may change your password from your account "Settings" page after logging
1113                         in.
1114
1115                         Please take a few moments to review the other account settings on that page.
1116
1117                         You may also wish to add some basic information to your default profile
1118                         ' . "\x28" . 'on the "Profiles" page' . "\x29" . ' so that other people can easily find you.
1119
1120                         We recommend setting your full name, adding a profile photo,
1121                         adding some profile "keywords" ' . "\x28" . 'very useful in making new friends' . "\x29" . ' - and
1122                         perhaps what country you live in; if you do not wish to be more specific
1123                         than that.
1124
1125                         We fully respect your right to privacy, and none of these items are necessary.
1126                         If you are new and do not know anybody here, they may help
1127                         you to make some new and interesting friends.
1128
1129                         If you ever want to delete your account, you can do so at %3$s/removeme
1130
1131                         Thank you and welcome to %2$s.',
1132                         $user['nickname'],
1133                         $sitename,
1134                         $siteurl,
1135                         $user['username'],
1136                         $password
1137                 ));
1138
1139                 $email = DI::emailer()
1140                         ->newSystemMail()
1141                         ->withMessage(DI::l10n()->t('Registration details for %s', $sitename), $preamble, $body)
1142                         ->forUser($user)
1143                         ->withRecipient($user['email'])
1144                         ->build();
1145                 return DI::emailer()->send($email);
1146         }
1147
1148         /**
1149          * @param int $uid user to remove
1150          * @return bool
1151          * @throws HTTPException\InternalServerErrorException
1152          */
1153         public static function remove(int $uid)
1154         {
1155                 if (!$uid) {
1156                         return false;
1157                 }
1158
1159                 Logger::log('Removing user: ' . $uid);
1160
1161                 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
1162
1163                 Hook::callAll('remove_user', $user);
1164
1165                 // save username (actually the nickname as it is guaranteed
1166                 // unique), so it cannot be re-registered in the future.
1167                 DBA::insert('userd', ['username' => $user['nickname']]);
1168
1169                 // The user and related data will be deleted in Friendica\Worker\CronJobs::expireAndRemoveUsers()
1170                 DBA::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utc('now + 7 day')], ['uid' => $uid]);
1171                 Worker::add(PRIORITY_HIGH, 'Notifier', Delivery::REMOVAL, $uid);
1172
1173                 // Send an update to the directory
1174                 $self = DBA::selectFirst('contact', ['url'], ['uid' => $uid, 'self' => true]);
1175                 Worker::add(PRIORITY_LOW, 'Directory', $self['url']);
1176
1177                 // Remove the user relevant data
1178                 Worker::add(PRIORITY_NEGLIGIBLE, 'RemoveUser', $uid);
1179
1180                 return true;
1181         }
1182
1183         /**
1184          * Return all identities to a user
1185          *
1186          * @param int $uid The user id
1187          * @return array All identities for this user
1188          *
1189          * Example for a return:
1190          *    [
1191          *        [
1192          *            'uid' => 1,
1193          *            'username' => 'maxmuster',
1194          *            'nickname' => 'Max Mustermann'
1195          *        ],
1196          *        [
1197          *            'uid' => 2,
1198          *            'username' => 'johndoe',
1199          *            'nickname' => 'John Doe'
1200          *        ]
1201          *    ]
1202          * @throws Exception
1203          */
1204         public static function identities($uid)
1205         {
1206                 $identities = [];
1207
1208                 $user = DBA::selectFirst('user', ['uid', 'nickname', 'username', 'parent-uid'], ['uid' => $uid]);
1209                 if (!DBA::isResult($user)) {
1210                         return $identities;
1211                 }
1212
1213                 if ($user['parent-uid'] == 0) {
1214                         // First add our own entry
1215                         $identities = [[
1216                                 'uid' => $user['uid'],
1217                                 'username' => $user['username'],
1218                                 'nickname' => $user['nickname']
1219                         ]];
1220
1221                         // Then add all the children
1222                         $r = DBA::select(
1223                                 'user',
1224                                 ['uid', 'username', 'nickname'],
1225                                 ['parent-uid' => $user['uid'], 'account_removed' => false]
1226                         );
1227                         if (DBA::isResult($r)) {
1228                                 $identities = array_merge($identities, DBA::toArray($r));
1229                         }
1230                 } else {
1231                         // First entry is our parent
1232                         $r = DBA::select(
1233                                 'user',
1234                                 ['uid', 'username', 'nickname'],
1235                                 ['uid' => $user['parent-uid'], 'account_removed' => false]
1236                         );
1237                         if (DBA::isResult($r)) {
1238                                 $identities = DBA::toArray($r);
1239                         }
1240
1241                         // Then add all siblings
1242                         $r = DBA::select(
1243                                 'user',
1244                                 ['uid', 'username', 'nickname'],
1245                                 ['parent-uid' => $user['parent-uid'], 'account_removed' => false]
1246                         );
1247                         if (DBA::isResult($r)) {
1248                                 $identities = array_merge($identities, DBA::toArray($r));
1249                         }
1250                 }
1251
1252                 $r = DBA::p(
1253                         "SELECT `user`.`uid`, `user`.`username`, `user`.`nickname`
1254                         FROM `manage`
1255                         INNER JOIN `user` ON `manage`.`mid` = `user`.`uid`
1256                         WHERE `user`.`account_removed` = 0 AND `manage`.`uid` = ?",
1257                         $user['uid']
1258                 );
1259                 if (DBA::isResult($r)) {
1260                         $identities = array_merge($identities, DBA::toArray($r));
1261                 }
1262
1263                 return $identities;
1264         }
1265
1266         /**
1267          * Returns statistical information about the current users of this node
1268          *
1269          * @return array
1270          *
1271          * @throws Exception
1272          */
1273         public static function getStatistics()
1274         {
1275                 $statistics = [
1276                         'total_users'           => 0,
1277                         'active_users_halfyear' => 0,
1278                         'active_users_monthly'  => 0,
1279                         'active_users_weekly'   => 0,
1280                 ];
1281
1282                 $userStmt = DBA::select('owner-view', ['uid', 'login_date', 'last-item'],
1283                         ["`verified` AND `login_date` > ? AND NOT `blocked`
1284                         AND NOT `account_removed` AND NOT `account_expired`",
1285                         DBA::NULL_DATETIME]);
1286                 if (!DBA::isResult($userStmt)) {
1287                         return $statistics;
1288                 }
1289
1290                 $halfyear = time() - (180 * 24 * 60 * 60);
1291                 $month = time() - (30 * 24 * 60 * 60);
1292                 $week = time() - (7 * 24 * 60 * 60);
1293
1294                 while ($user = DBA::fetch($userStmt)) {
1295                         $statistics['total_users']++;
1296
1297                         if ((strtotime($user['login_date']) > $halfyear) || (strtotime($user['last-item']) > $halfyear)
1298                         ) {
1299                                 $statistics['active_users_halfyear']++;
1300                         }
1301
1302                         if ((strtotime($user['login_date']) > $month) || (strtotime($user['last-item']) > $month)
1303                         ) {
1304                                 $statistics['active_users_monthly']++;
1305                         }
1306
1307                         if ((strtotime($user['login_date']) > $week) || (strtotime($user['last-item']) > $week)
1308                         ) {
1309                                 $statistics['active_users_weekly']++;
1310                         }
1311                 }
1312                 DBA::close($userStmt);
1313
1314                 return $statistics;
1315         }
1316
1317         /**
1318          * Get all users of the current node
1319          *
1320          * @param int    $start Start count (Default is 0)
1321          * @param int    $count Count of the items per page (Default is @see Pager::ITEMS_PER_PAGE)
1322          * @param string $type  The type of users, which should get (all, bocked, removed)
1323          * @param string $order Order of the user list (Default is 'contact.name')
1324          * @param bool   $descending Order direction (Default is ascending)
1325          *
1326          * @return array The list of the users
1327          * @throws Exception
1328          */
1329         public static function getList($start = 0, $count = Pager::ITEMS_PER_PAGE, $type = 'all', $order = 'name', bool $descending = false)
1330         {
1331                 $param = ['limit' => [$start, $count], 'order' => [$order => $descending]];
1332                 $condition = [];
1333                 switch ($type) {
1334                         case 'active':
1335                                 $condition['account_removed'] = false;
1336                                 $condition['blocked'] = false;
1337                                 break;
1338                         case 'blocked':
1339                                 $condition['blocked'] = true;
1340                                 break;
1341                         case 'removed':
1342                                 $condition['account_removed'] = true;
1343                                 break;
1344                 }
1345
1346                 return DBA::selectToArray('owner-view', [], $condition, $param);
1347         }
1348 }