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