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