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