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