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