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