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