]> git.mxchange.org Git - friendica.git/blob - src/Model/User.php
Fixed wrong reference to user.expire in Model\User::getActorName
[friendica.git] / src / Model / User.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2022, the Friendica project
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\DOFileCachePSR6\CacheItemPool;
25 use DivineOmega\PasswordExposed;
26 use ErrorException;
27 use Exception;
28 use Friendica\Content\Pager;
29 use Friendica\Core\Hook;
30 use Friendica\Core\L10n;
31 use Friendica\Core\Logger;
32 use Friendica\Core\Protocol;
33 use Friendica\Core\Search;
34 use Friendica\Core\System;
35 use Friendica\Core\Worker;
36 use Friendica\Database\DBA;
37 use Friendica\DI;
38 use Friendica\Network\HTTPClient\Client\HttpClientAccept;
39 use Friendica\Security\TwoFactor\Model\AppSpecificPassword;
40 use Friendica\Network\HTTPException;
41 use Friendica\Object\Image;
42 use Friendica\Util\Crypto;
43 use Friendica\Util\DateTimeFormat;
44 use Friendica\Util\Images;
45 use Friendica\Util\Network;
46 use Friendica\Util\Proxy;
47 use Friendica\Util\Strings;
48 use Friendica\Worker\Delivery;
49 use ImagickException;
50 use LightOpenID;
51
52 /**
53  * This class handles User related functions
54  */
55 class User
56 {
57         /**
58          * Page/profile types
59          *
60          * PAGE_FLAGS_NORMAL is a typical personal profile account
61          * PAGE_FLAGS_SOAPBOX automatically approves all friend requests as Contact::SHARING, (readonly)
62          * PAGE_FLAGS_COMMUNITY automatically approves all friend requests as Contact::SHARING, but with
63          *      write access to wall and comments (no email and not included in page owner's ACL lists)
64          * PAGE_FLAGS_FREELOVE automatically approves all friend requests as full friends (Contact::FRIEND).
65          *
66          * @{
67          */
68         const PAGE_FLAGS_NORMAL    = 0;
69         const PAGE_FLAGS_SOAPBOX   = 1;
70         const PAGE_FLAGS_COMMUNITY = 2;
71         const PAGE_FLAGS_FREELOVE  = 3;
72         const PAGE_FLAGS_BLOG      = 4;
73         const PAGE_FLAGS_PRVGROUP  = 5;
74         /**
75          * @}
76          */
77
78         /**
79          * Account types
80          *
81          * ACCOUNT_TYPE_PERSON - the account belongs to a person
82          *      Associated page types: PAGE_FLAGS_NORMAL, PAGE_FLAGS_SOAPBOX, PAGE_FLAGS_FREELOVE
83          *
84          * ACCOUNT_TYPE_ORGANISATION - the account belongs to an organisation
85          *      Associated page type: PAGE_FLAGS_SOAPBOX
86          *
87          * ACCOUNT_TYPE_NEWS - the account is a news reflector
88          *      Associated page type: PAGE_FLAGS_SOAPBOX
89          *
90          * ACCOUNT_TYPE_COMMUNITY - the account is community forum
91          *      Associated page types: PAGE_COMMUNITY, PAGE_FLAGS_PRVGROUP
92          *
93          * ACCOUNT_TYPE_RELAY - the account is a relay
94          *      This will only be assigned to contacts, not to user accounts
95          * @{
96          */
97         const ACCOUNT_TYPE_PERSON =       0;
98         const ACCOUNT_TYPE_ORGANISATION = 1;
99         const ACCOUNT_TYPE_NEWS =         2;
100         const ACCOUNT_TYPE_COMMUNITY =    3;
101         const ACCOUNT_TYPE_RELAY =        4;
102         const ACCOUNT_TYPE_DELETED =    127;
103         /**
104          * @}
105          */
106
107         private static $owner;
108
109         /**
110          * Returns the numeric account type by their string
111          *
112          * @param string $accounttype as string constant
113          * @return int|null Numeric account type - or null when not set
114          */
115         public static function getAccountTypeByString(string $accounttype)
116         {
117                 switch ($accounttype) {
118                         case 'person':
119                                 return User::ACCOUNT_TYPE_PERSON;
120
121                         case 'organisation':
122                                 return User::ACCOUNT_TYPE_ORGANISATION;
123
124                         case 'news':
125                                 return User::ACCOUNT_TYPE_NEWS;
126
127                         case 'community':
128                                 return User::ACCOUNT_TYPE_COMMUNITY;
129
130                 }
131                 return null;
132         }
133
134         /**
135          * Fetch the system account
136          *
137          * @return array system account
138          */
139         public static function getSystemAccount(): array
140         {
141                 $system = Contact::selectFirst([], ['self' => true, 'uid' => 0]);
142                 if (!DBA::isResult($system)) {
143                         self::createSystemAccount();
144                         $system = Contact::selectFirst([], ['self' => true, 'uid' => 0]);
145                         if (!DBA::isResult($system)) {
146                                 return [];
147                         }
148                 }
149
150                 $system['sprvkey'] = $system['uprvkey'] = $system['prvkey'];
151                 $system['spubkey'] = $system['upubkey'] = $system['pubkey'];
152                 $system['nickname'] = $system['nick'];
153                 $system['page-flags'] = User::PAGE_FLAGS_SOAPBOX;
154                 $system['account-type'] = $system['contact-type'];
155                 $system['guid'] = '';
156                 $system['picdate'] = '';
157                 $system['theme'] = '';
158                 $system['publish'] = false;
159                 $system['net-publish'] = false;
160                 $system['hide-friends'] = true;
161                 $system['prv_keywords'] = '';
162                 $system['pub_keywords'] = '';
163                 $system['address'] = '';
164                 $system['locality'] = '';
165                 $system['region'] = '';
166                 $system['postal-code'] = '';
167                 $system['country-name'] = '';
168                 $system['homepage'] = DI::baseUrl()->get();
169                 $system['dob'] = '0000-00-00';
170
171                 // Ensure that the user contains data
172                 $user = DBA::selectFirst('user', ['prvkey', 'guid'], ['uid' => 0]);
173                 if (empty($user['prvkey']) || empty($user['guid'])) {
174                         $fields = [
175                                 'username' => $system['name'],
176                                 'nickname' => $system['nick'],
177                                 'register_date' => $system['created'],
178                                 'pubkey' => $system['pubkey'],
179                                 'prvkey' => $system['prvkey'],
180                                 'spubkey' => $system['spubkey'],
181                                 'sprvkey' => $system['sprvkey'],
182                                 'guid' => System::createUUID(),
183                                 'verified' => true,
184                                 'page-flags' => User::PAGE_FLAGS_SOAPBOX,
185                                 'account-type' => User::ACCOUNT_TYPE_RELAY,
186                         ];
187
188                         DBA::update('user', $fields, ['uid' => 0]);
189
190                         $system['guid'] = $fields['guid'];
191                 } else {
192                         $system['guid'] = $user['guid'];
193                 }
194
195                 return $system;
196         }
197
198         /**
199          * Create the system account
200          *
201          * @return void
202          */
203         private static function createSystemAccount()
204         {
205                 $system_actor_name = self::getActorName();
206                 if (empty($system_actor_name)) {
207                         return;
208                 }
209
210                 $keys = Crypto::newKeypair(4096);
211                 if ($keys === false) {
212                         throw new Exception(DI::l10n()->t('SERIOUS ERROR: Generation of security keys failed.'));
213                 }
214
215                 $system = [
216                         'uid'          => 0,
217                         'created'      => DateTimeFormat::utcNow(),
218                         'self'         => true,
219                         'network'      => Protocol::ACTIVITYPUB,
220                         'name'         => 'System Account',
221                         'addr'         => $system_actor_name . '@' . DI::baseUrl()->getHostname(),
222                         'nick'         => $system_actor_name,
223                         'url'          => DI::baseUrl() . '/friendica',
224                         'pubkey'       => $keys['pubkey'],
225                         'prvkey'       => $keys['prvkey'],
226                         'blocked'      => 0,
227                         'pending'      => 0,
228                         'contact-type' => Contact::TYPE_RELAY, // In AP this is translated to 'Application'
229                         'name-date'    => DateTimeFormat::utcNow(),
230                         'uri-date'     => DateTimeFormat::utcNow(),
231                         'avatar-date'  => DateTimeFormat::utcNow(),
232                         'closeness'    => 0,
233                         'baseurl'      => DI::baseUrl(),
234                 ];
235
236                 $system['avatar'] = $system['photo'] = Contact::getDefaultAvatar($system, Proxy::SIZE_SMALL);
237                 $system['thumb']  = Contact::getDefaultAvatar($system, Proxy::SIZE_THUMB);
238                 $system['micro']  = Contact::getDefaultAvatar($system, Proxy::SIZE_MICRO);
239                 $system['nurl']   = Strings::normaliseLink($system['url']);
240                 $system['gsid']   = GServer::getID($system['baseurl']);
241
242                 Contact::insert($system);
243         }
244
245         /**
246          * Detect a usable actor name
247          *
248          * @return string actor account name
249          */
250         public static function getActorName(): string
251         {
252                 $system_actor_name = DI::config()->get('system', 'actor_name');
253                 if (!empty($system_actor_name)) {
254                         $self = Contact::selectFirst(['nick'], ['uid' => 0, 'self' => true]);
255                         if (!empty($self['nick'])) {
256                                 if ($self['nick'] != $system_actor_name) {
257                                         // Reset the actor name to the already used name
258                                         DI::config()->set('system', 'actor_name', $self['nick']);
259                                         $system_actor_name = $self['nick'];
260                                 }
261                         }
262                         return $system_actor_name;
263                 }
264
265                 // List of possible actor names
266                 $possible_accounts = ['friendica', 'actor', 'system', 'internal'];
267                 foreach ($possible_accounts as $name) {
268                         if (!DBA::exists('user', ['nickname' => $name, 'account_removed' => false, 'account_expired' => false]) &&
269                                 !DBA::exists('userd', ['username' => $name])) {
270                                 DI::config()->set('system', 'actor_name', $name);
271                                 return $name;
272                         }
273                 }
274                 return '';
275         }
276
277         /**
278          * Returns true if a user record exists with the provided id
279          *
280          * @param  int $uid
281          *
282          * @return boolean
283          * @throws Exception
284          */
285         public static function exists(int $uid): bool
286         {
287                 return DBA::exists('user', ['uid' => $uid]);
288         }
289
290         /**
291          * @param  integer       $uid
292          * @param array          $fields
293          * @return array|boolean User record if it exists, false otherwise
294          * @throws Exception
295          */
296         public static function getById(int $uid, array $fields = [])
297         {
298                 return !empty($uid) ? DBA::selectFirst('user', $fields, ['uid' => $uid]) : [];
299         }
300
301         /**
302          * Returns a user record based on it's GUID
303          *
304          * @param string $guid   The guid of the user
305          * @param array  $fields The fields to retrieve
306          * @param bool   $active True, if only active records are searched
307          *
308          * @return array|boolean User record if it exists, false otherwise
309          * @throws Exception
310          */
311         public static function getByGuid(string $guid, array $fields = [], bool $active = true)
312         {
313                 if ($active) {
314                         $cond = ['guid' => $guid, 'account_expired' => false, 'account_removed' => false];
315                 } else {
316                         $cond = ['guid' => $guid];
317                 }
318
319                 return DBA::selectFirst('user', $fields, $cond);
320         }
321
322         /**
323          * @param  string        $nickname
324          * @param array          $fields
325          * @return array|boolean User record if it exists, false otherwise
326          * @throws Exception
327          */
328         public static function getByNickname(string $nickname, array $fields = [])
329         {
330                 return DBA::selectFirst('user', $fields, ['nickname' => $nickname]);
331         }
332
333         /**
334          * Returns the user id of a given profile URL
335          *
336          * @param string $url
337          *
338          * @return integer user id
339          * @throws Exception
340          */
341         public static function getIdForURL(string $url): int
342         {
343                 // Avoid database queries when the local node hostname isn't even part of the url.
344                 if (!Contact::isLocal($url)) {
345                         return 0;
346                 }
347
348                 $self = Contact::selectFirst(['uid'], ['self' => true, 'nurl' => Strings::normaliseLink($url)]);
349                 if (!empty($self['uid'])) {
350                         return $self['uid'];
351                 }
352
353                 $self = Contact::selectFirst(['uid'], ['self' => true, 'addr' => $url]);
354                 if (!empty($self['uid'])) {
355                         return $self['uid'];
356                 }
357
358                 $self = Contact::selectFirst(['uid'], ['self' => true, 'alias' => [$url, Strings::normaliseLink($url)]]);
359                 if (!empty($self['uid'])) {
360                         return $self['uid'];
361                 }
362
363                 return 0;
364         }
365
366         /**
367          * Get a user based on its email
368          *
369          * @param string $email
370          * @param array  $fields
371          * @return array|boolean User record if it exists, false otherwise
372          * @throws Exception
373          */
374         public static function getByEmail(string $email, array $fields = [])
375         {
376                 return DBA::selectFirst('user', $fields, ['email' => $email]);
377         }
378
379         /**
380          * Fetch the user array of the administrator. The first one if there are several.
381          *
382          * @param array $fields
383          * @return array user
384          */
385         public static function getFirstAdmin(array $fields = []) : array
386         {
387                 if (!empty(DI::config()->get('config', 'admin_nickname'))) {
388                         return self::getByNickname(DI::config()->get('config', 'admin_nickname'), $fields);
389                 } elseif (!empty(DI::config()->get('config', 'admin_email'))) {
390                         $adminList = explode(',', str_replace(' ', '', DI::config()->get('config', 'admin_email')));
391                         return self::getByEmail($adminList[0], $fields);
392                 } else {
393                         return [];
394                 }
395         }
396
397         /**
398          * Get owner data by user id
399          *
400          * @param int     $uid
401          * @param boolean $repairMissing Repair the owner data if it's missing
402          * @return boolean|array
403          * @throws Exception
404          */
405         public static function getOwnerDataById(int $uid, bool $repairMissing = true)
406         {
407                 if ($uid == 0) {
408                         return self::getSystemAccount();
409                 }
410
411                 if (!empty(self::$owner[$uid])) {
412                         return self::$owner[$uid];
413                 }
414
415                 $owner = DBA::selectFirst('owner-view', [], ['uid' => $uid]);
416                 if (!DBA::isResult($owner)) {
417                         if (!self::exists($uid) || !$repairMissing) {
418                                 return false;
419                         }
420                         if (!DBA::exists('profile', ['uid' => $uid])) {
421                                 DBA::insert('profile', ['uid' => $uid]);
422                         }
423                         if (!DBA::exists('contact', ['uid' => $uid, 'self' => true])) {
424                                 Contact::createSelfFromUserId($uid);
425                         }
426                         $owner = self::getOwnerDataById($uid, false);
427                 }
428
429                 if (empty($owner['nickname'])) {
430                         return false;
431                 }
432
433                 if (!$repairMissing || $owner['account_expired']) {
434                         return $owner;
435                 }
436
437                 // Check if the returned data is valid, otherwise fix it. See issue #6122
438
439                 // Check for correct url and normalised nurl
440                 $url = DI::baseUrl() . '/profile/' . $owner['nickname'];
441                 $repair = empty($owner['network']) || ($owner['url'] != $url) || ($owner['nurl'] != Strings::normaliseLink($owner['url']));
442
443                 if (!$repair) {
444                         // Check if "addr" is present and correct
445                         $addr = $owner['nickname'] . '@' . substr(DI::baseUrl(), strpos(DI::baseUrl(), '://') + 3);
446                         $repair = ($addr != $owner['addr']) || empty($owner['prvkey']) || empty($owner['pubkey']);
447                 }
448
449                 if (!$repair) {
450                         // Check if the avatar field is filled and the photo directs to the correct path
451                         $avatar = Photo::selectFirst(['resource-id'], ['uid' => $uid, 'profile' => true]);
452                         if (DBA::isResult($avatar)) {
453                                 $repair = empty($owner['avatar']) || !strpos($owner['photo'], $avatar['resource-id']);
454                         }
455                 }
456
457                 if ($repair) {
458                         Contact::updateSelfFromUserID($uid);
459                         // Return the corrected data and avoid a loop
460                         $owner = self::getOwnerDataById($uid, false);
461                 }
462
463                 self::$owner[$uid] = $owner;
464                 return $owner;
465         }
466
467         /**
468          * Get owner data by nick name
469          *
470          * @param int $nick
471          * @return boolean|array
472          * @throws Exception
473          */
474         public static function getOwnerDataByNick(string $nick)
475         {
476                 $user = DBA::selectFirst('user', ['uid'], ['nickname' => $nick]);
477
478                 if (!DBA::isResult($user)) {
479                         return false;
480                 }
481
482                 return self::getOwnerDataById($user['uid']);
483         }
484
485         /**
486          * Returns the default group for a given user and network
487          *
488          * @param int $uid User id
489          *
490          * @return int group id
491          * @throws Exception
492          */
493         public static function getDefaultGroup(int $uid): int
494         {
495                 $user = DBA::selectFirst('user', ['def_gid'], ['uid' => $uid]);
496                 if (DBA::isResult($user)) {
497                         $default_group = $user["def_gid"];
498                 } else {
499                         $default_group = 0;
500                 }
501
502                 return $default_group;
503         }
504
505         /**
506          * Authenticate a user with a clear text password
507          *
508          * Returns the user id associated with a successful password authentication
509          *
510          * @param mixed  $user_info
511          * @param string $password
512          * @param bool   $third_party
513          * @return int User Id if authentication is successful
514          * @throws HTTPException\ForbiddenException
515          * @throws HTTPException\NotFoundException
516          */
517         public static function getIdFromPasswordAuthentication($user_info, string $password, bool $third_party = false): int
518         {
519                 // Addons registered with the "authenticate" hook may create the user on the
520                 // fly. `getAuthenticationInfo` will fail if the user doesn't exist yet. If
521                 // the user doesn't exist, we should give the addons a chance to create the
522                 // user in our database, if applicable, before re-throwing the exception if
523                 // they fail.
524                 try {
525                         $user = self::getAuthenticationInfo($user_info);
526                 } catch (Exception $e) {
527                         $username = (is_string($user_info) ? $user_info : $user_info['nickname'] ?? '');
528
529                         // Addons can create users, and since this 'catch' branch should only
530                         // execute if getAuthenticationInfo can't find an existing user, that's
531                         // exactly what will happen here. Creating a numeric username would create
532                         // abiguity with user IDs, possibly opening up an attack vector.
533                         // So let's be very careful about that.
534                         if (empty($username) || is_numeric($username)) {
535                                 throw $e;
536                         }
537
538                         return self::getIdFromAuthenticateHooks($username, $password);
539                 }
540
541                 if ($third_party && DI::pConfig()->get($user['uid'], '2fa', 'verified')) {
542                         // Third-party apps can't verify two-factor authentication, we use app-specific passwords instead
543                         if (AppSpecificPassword::authenticateUser($user['uid'], $password)) {
544                                 return $user['uid'];
545                         }
546                 } elseif (strpos($user['password'], '$') === false) {
547                         //Legacy hash that has not been replaced by a new hash yet
548                         if (self::hashPasswordLegacy($password) === $user['password']) {
549                                 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
550
551                                 return $user['uid'];
552                         }
553                 } elseif (!empty($user['legacy_password'])) {
554                         //Legacy hash that has been double-hashed and not replaced by a new hash yet
555                         //Warning: `legacy_password` is not necessary in sync with the content of `password`
556                         if (password_verify(self::hashPasswordLegacy($password), $user['password'])) {
557                                 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
558
559                                 return $user['uid'];
560                         }
561                 } elseif (password_verify($password, $user['password'])) {
562                         //New password hash
563                         if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
564                                 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
565                         }
566
567                         return $user['uid'];
568                 } else {
569                         return self::getIdFromAuthenticateHooks($user['nickname'], $password); // throws
570                 }
571
572                 throw new HTTPException\ForbiddenException(DI::l10n()->t('Login failed'));
573         }
574
575         /**
576          * Try to obtain a user ID via "authenticate" hook addons
577          *
578          * Returns the user id associated with a successful password authentication
579          *
580          * @param string $username
581          * @param string $password
582          * @return int User Id if authentication is successful
583          * @throws HTTPException\ForbiddenException
584          */
585         public static function getIdFromAuthenticateHooks(string $username, string $password): int
586         {
587                 $addon_auth = [
588                         'username'      => $username,
589                         'password'      => $password,
590                         'authenticated' => 0,
591                         'user_record'   => null
592                 ];
593
594                 /*
595                  * An addon indicates successful login by setting 'authenticated' to non-zero value and returning a user record
596                  * Addons should never set 'authenticated' except to indicate success - as hooks may be chained
597                  * and later addons should not interfere with an earlier one that succeeded.
598                  */
599                 Hook::callAll('authenticate', $addon_auth);
600
601                 if ($addon_auth['authenticated'] && $addon_auth['user_record']) {
602                         return $addon_auth['user_record']['uid'];
603                 }
604
605                 throw new HTTPException\ForbiddenException(DI::l10n()->t('Login failed'));
606         }
607
608         /**
609          * Returns authentication info from various parameters types
610          *
611          * User info can be any of the following:
612          * - User DB object
613          * - User Id
614          * - User email or username or nickname
615          * - User array with at least the uid and the hashed password
616          *
617          * @param mixed $user_info
618          * @return array|null Null if not found/determined
619          * @throws HTTPException\NotFoundException
620          */
621         public static function getAuthenticationInfo($user_info)
622         {
623                 $user = null;
624
625                 if (is_object($user_info) || is_array($user_info)) {
626                         if (is_object($user_info)) {
627                                 $user = (array) $user_info;
628                         } else {
629                                 $user = $user_info;
630                         }
631
632                         if (
633                                 !isset($user['uid'])
634                                 || !isset($user['password'])
635                                 || !isset($user['legacy_password'])
636                         ) {
637                                 throw new Exception(DI::l10n()->t('Not enough information to authenticate'));
638                         }
639                 } elseif (is_int($user_info) || is_string($user_info)) {
640                         if (is_int($user_info)) {
641                                 $user = DBA::selectFirst(
642                                         'user',
643                                         ['uid', 'nickname', 'password', 'legacy_password'],
644                                         [
645                                                 'uid' => $user_info,
646                                                 'blocked' => 0,
647                                                 'account_expired' => 0,
648                                                 'account_removed' => 0,
649                                                 'verified' => 1
650                                         ]
651                                 );
652                         } else {
653                                 $fields = ['uid', 'nickname', 'password', 'legacy_password'];
654                                 $condition = [
655                                         "(`email` = ? OR `username` = ? OR `nickname` = ?)
656                                         AND NOT `blocked` AND NOT `account_expired` AND NOT `account_removed` AND `verified`",
657                                         $user_info, $user_info, $user_info
658                                 ];
659                                 $user = DBA::selectFirst('user', $fields, $condition);
660                         }
661
662                         if (!DBA::isResult($user)) {
663                                 throw new HTTPException\NotFoundException(DI::l10n()->t('User not found'));
664                         }
665                 }
666
667                 return $user;
668         }
669
670         /**
671          * Generates a human-readable random password
672          *
673          * @return string
674          * @throws Exception
675          */
676         public static function generateNewPassword(): string
677         {
678                 return ucfirst(Strings::getRandomName(8)) . random_int(1000, 9999);
679         }
680
681         /**
682          * Checks if the provided plaintext password has been exposed or not
683          *
684          * @param string $password
685          * @return bool
686          * @throws Exception
687          */
688         public static function isPasswordExposed(string $password): bool
689         {
690                 $cache = new CacheItemPool();
691                 $cache->changeConfig([
692                         'cacheDirectory' => System::getTempPath() . '/password-exposed-cache/',
693                 ]);
694
695                 try {
696                         $passwordExposedChecker = new PasswordExposed\PasswordExposedChecker(null, $cache);
697
698                         return $passwordExposedChecker->passwordExposed($password) === PasswordExposed\PasswordStatus::EXPOSED;
699                 } catch (Exception $e) {
700                         Logger::error('Password Exposed Exception: ' . $e->getMessage(), [
701                                 'code' => $e->getCode(),
702                                 'file' => $e->getFile(),
703                                 'line' => $e->getLine(),
704                                 'trace' => $e->getTraceAsString()
705                         ]);
706
707                         return false;
708                 }
709         }
710
711         /**
712          * Legacy hashing function, kept for password migration purposes
713          *
714          * @param string $password
715          * @return string
716          */
717         private static function hashPasswordLegacy(string $password): string
718         {
719                 return hash('whirlpool', $password);
720         }
721
722         /**
723          * Global user password hashing function
724          *
725          * @param string $password
726          * @return string
727          * @throws Exception
728          */
729         public static function hashPassword(string $password): string
730         {
731                 if (!trim($password)) {
732                         throw new Exception(DI::l10n()->t('Password can\'t be empty'));
733                 }
734
735                 return password_hash($password, PASSWORD_DEFAULT);
736         }
737
738         /**
739          * Allowed characters are a-z, A-Z, 0-9 and special characters except white spaces, accentuated letters and colon (:).
740          *
741          * Password length is limited to 72 characters if the current default password hashing algorithm is Blowfish.
742          * From the manual: "Using the PASSWORD_BCRYPT as the algorithm, will result in the password parameter being
743          * truncated to a maximum length of 72 bytes."
744          *
745          * @see https://www.php.net/manual/en/function.password-hash.php#refsect1-function.password-hash-parameters
746          *
747          * @param string|null $delimiter Whether the regular expression is meant to be wrapper in delimiter characters
748          * @return string
749          */
750         public static function getPasswordRegExp(string $delimiter = null): string
751         {
752                 $allowed_characters = '!"#$%&\'()*+,-./;<=>?@[\]^_`{|}~';
753
754                 if ($delimiter) {
755                         $allowed_characters = preg_quote($allowed_characters, $delimiter);
756                 }
757
758                 return '^[a-zA-Z0-9' . $allowed_characters . ']' . (PASSWORD_DEFAULT !== PASSWORD_BCRYPT ? '{1,72}' : '+') . '$';
759         }
760
761         /**
762          * Updates a user row with a new plaintext password
763          *
764          * @param int    $uid
765          * @param string $password
766          * @return bool
767          * @throws Exception
768          */
769         public static function updatePassword(int $uid, string $password): bool
770         {
771                 $password = trim($password);
772
773                 if (empty($password)) {
774                         throw new Exception(DI::l10n()->t('Empty passwords are not allowed.'));
775                 }
776
777                 if (!DI::config()->get('system', 'disable_password_exposed', false) && self::isPasswordExposed($password)) {
778                         throw new Exception(DI::l10n()->t('The new password has been exposed in a public data dump, please choose another.'));
779                 }
780
781                 if (PASSWORD_DEFAULT === PASSWORD_BCRYPT && strlen($password) > 72) {
782                         throw new Exception(DI::l10n()->t('The password length is limited to 72 characters.'));
783                 }
784
785                 if (!preg_match('/' . self::getPasswordRegExp('/') . '/', $password)) {
786                         throw new Exception(DI::l10n()->t('The password can\'t contain accentuated letters, white spaces or colons (:)'));
787                 }
788
789                 return self::updatePasswordHashed($uid, self::hashPassword($password));
790         }
791
792         /**
793          * Updates a user row with a new hashed password.
794          * Empties the password reset token field just in case.
795          *
796          * @param int    $uid
797          * @param string $pasword_hashed
798          * @return bool
799          * @throws Exception
800          */
801         private static function updatePasswordHashed(int $uid, string $pasword_hashed): bool
802         {
803                 $fields = [
804                         'password' => $pasword_hashed,
805                         'pwdreset' => null,
806                         'pwdreset_time' => null,
807                         'legacy_password' => false
808                 ];
809                 return DBA::update('user', $fields, ['uid' => $uid]);
810         }
811
812         /**
813          * Checks if a nickname is in the list of the forbidden nicknames
814          *
815          * Check if a nickname is forbidden from registration on the node by the
816          * admin. Forbidden nicknames (e.g. role namess) can be configured in the
817          * admin panel.
818          *
819          * @param string $nickname The nickname that should be checked
820          * @return boolean True is the nickname is blocked on the node
821          */
822         public static function isNicknameBlocked(string $nickname): bool
823         {
824                 $forbidden_nicknames = DI::config()->get('system', 'forbidden_nicknames', '');
825                 if (!empty($forbidden_nicknames)) {
826                         $forbidden = explode(',', $forbidden_nicknames);
827                         $forbidden = array_map('trim', $forbidden);
828                 } else {
829                         $forbidden = [];
830                 }
831
832                 // Add the name of the internal actor to the "forbidden" list
833                 $actor_name = self::getActorName();
834                 if (!empty($actor_name)) {
835                         $forbidden[] = $actor_name;
836                 }
837
838                 if (empty($forbidden)) {
839                         return false;
840                 }
841
842                 // check if the nickname is in the list of blocked nicknames
843                 if (in_array(strtolower($nickname), $forbidden)) {
844                         return true;
845                 }
846
847                 // else return false
848                 return false;
849         }
850
851         /**
852          * Get avatar link for given user
853          *
854          * @param array  $user
855          * @param string $size One of the Proxy::SIZE_* constants
856          * @return string avatar link
857          * @throws Exception
858          */
859         public static function getAvatarUrl(array $user, string $size = ''): string
860         {
861                 if (empty($user['nickname'])) {
862                         DI::logger()->warning('Missing user nickname key', ['trace' => System::callstack(20)]);
863                 }
864
865                 $url = DI::baseUrl() . '/photo/';
866
867                 switch ($size) {
868                         case Proxy::SIZE_MICRO:
869                                 $url .= 'micro/';
870                                 $scale = 6;
871                                 break;
872                         case Proxy::SIZE_THUMB:
873                                 $url .= 'avatar/';
874                                 $scale = 5;
875                                 break;
876                         default:
877                                 $url .= 'profile/';
878                                 $scale = 4;
879                                 break;
880                 }
881
882                 $updated  =  '';
883                 $mimetype = '';
884
885                 $photo = Photo::selectFirst(['type', 'created', 'edited', 'updated'], ["scale" => $scale, 'uid' => $user['uid'], 'profile' => true]);
886                 if (!empty($photo)) {
887                         $updated  = max($photo['created'], $photo['edited'], $photo['updated']);
888                         $mimetype = $photo['type'];
889                 }
890
891                 return $url . $user['nickname'] . Images::getExtensionByMimeType($mimetype) . ($updated ? '?ts=' . strtotime($updated) : '');
892         }
893
894         /**
895          * Get banner link for given user
896          *
897          * @param array  $user
898          * @return string banner link
899          * @throws Exception
900          */
901         public static function getBannerUrl(array $user): string
902         {
903                 if (empty($user['nickname'])) {
904                         DI::logger()->warning('Missing user nickname key', ['trace' => System::callstack(20)]);
905                 }
906
907                 $url = DI::baseUrl() . '/photo/banner/';
908
909                 $updated  = '';
910                 $mimetype = '';
911
912                 $photo = Photo::selectFirst(['type', 'created', 'edited', 'updated'], ["scale" => 3, 'uid' => $user['uid'], 'photo-type' => Photo::USER_BANNER]);
913                 if (!empty($photo)) {
914                         $updated  = max($photo['created'], $photo['edited'], $photo['updated']);
915                         $mimetype = $photo['type'];
916                 } else {
917                         // Only for the RC phase: Don't return an image link for the default picture
918                         return '';
919                 }
920
921                 return $url . $user['nickname'] . Images::getExtensionByMimeType($mimetype) . ($updated ? '?ts=' . strtotime($updated) : '');
922         }
923
924         /**
925          * Catch-all user creation function
926          *
927          * Creates a user from the provided data array, either form fields or OpenID.
928          * Required: { username, nickname, email } or { openid_url }
929          *
930          * Performs the following:
931          * - Sends to the OpenId auth URL (if relevant)
932          * - Creates new key pairs for crypto
933          * - Create self-contact
934          * - Create profile image
935          *
936          * @param  array $data
937          * @return array
938          * @throws ErrorException
939          * @throws HTTPException\InternalServerErrorException
940          * @throws ImagickException
941          * @throws Exception
942          */
943         public static function create(array $data): array
944         {
945                 $return = ['user' => null, 'password' => ''];
946
947                 $using_invites = DI::config()->get('system', 'invitation_only');
948
949                 $invite_id  = !empty($data['invite_id'])  ? trim($data['invite_id'])  : '';
950                 $username   = !empty($data['username'])   ? trim($data['username'])   : '';
951                 $nickname   = !empty($data['nickname'])   ? trim($data['nickname'])   : '';
952                 $email      = !empty($data['email'])      ? trim($data['email'])      : '';
953                 $openid_url = !empty($data['openid_url']) ? trim($data['openid_url']) : '';
954                 $photo      = !empty($data['photo'])      ? trim($data['photo'])      : '';
955                 $password   = !empty($data['password'])   ? trim($data['password'])   : '';
956                 $password1  = !empty($data['password1'])  ? trim($data['password1'])  : '';
957                 $confirm    = !empty($data['confirm'])    ? trim($data['confirm'])    : '';
958                 $blocked    = !empty($data['blocked']);
959                 $verified   = !empty($data['verified']);
960                 $language   = !empty($data['language'])   ? trim($data['language'])   : 'en';
961
962                 $netpublish = $publish = !empty($data['profile_publish_reg']);
963
964                 if ($password1 != $confirm) {
965                         throw new Exception(DI::l10n()->t('Passwords do not match. Password unchanged.'));
966                 } elseif ($password1 != '') {
967                         $password = $password1;
968                 }
969
970                 if ($using_invites) {
971                         if (!$invite_id) {
972                                 throw new Exception(DI::l10n()->t('An invitation is required.'));
973                         }
974
975                         if (!Register::existsByHash($invite_id)) {
976                                 throw new Exception(DI::l10n()->t('Invitation could not be verified.'));
977                         }
978                 }
979
980                 /// @todo Check if this part is really needed. We should have fetched all this data in advance
981                 if (empty($username) || empty($email) || empty($nickname)) {
982                         if ($openid_url) {
983                                 if (!Network::isUrlValid($openid_url)) {
984                                         throw new Exception(DI::l10n()->t('Invalid OpenID url'));
985                                 }
986                                 $_SESSION['register'] = 1;
987                                 $_SESSION['openid'] = $openid_url;
988
989                                 $openid = new LightOpenID(DI::baseUrl()->getHostname());
990                                 $openid->identity = $openid_url;
991                                 $openid->returnUrl = DI::baseUrl() . '/openid';
992                                 $openid->required = ['namePerson/friendly', 'contact/email', 'namePerson'];
993                                 $openid->optional = ['namePerson/first', 'media/image/aspect11', 'media/image/default'];
994                                 try {
995                                         $authurl = $openid->authUrl();
996                                 } catch (Exception $e) {
997                                         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.') . '<br />' . DI::l10n()->t('The error message was:') . $e->getMessage(), 0, $e);
998                                 }
999                                 System::externalRedirect($authurl);
1000                                 // NOTREACHED
1001                         }
1002
1003                         throw new Exception(DI::l10n()->t('Please enter the required information.'));
1004                 }
1005
1006                 if (!Network::isUrlValid($openid_url)) {
1007                         $openid_url = '';
1008                 }
1009
1010                 // collapse multiple spaces in name
1011                 $username = preg_replace('/ +/', ' ', $username);
1012
1013                 $username_min_length = max(1, min(64, intval(DI::config()->get('system', 'username_min_length', 3))));
1014                 $username_max_length = max(1, min(64, intval(DI::config()->get('system', 'username_max_length', 48))));
1015
1016                 if ($username_min_length > $username_max_length) {
1017                         Logger::error(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));
1018                         $tmp = $username_min_length;
1019                         $username_min_length = $username_max_length;
1020                         $username_max_length = $tmp;
1021                 }
1022
1023                 if (mb_strlen($username) < $username_min_length) {
1024                         throw new Exception(DI::l10n()->tt('Username should be at least %s character.', 'Username should be at least %s characters.', $username_min_length));
1025                 }
1026
1027                 if (mb_strlen($username) > $username_max_length) {
1028                         throw new Exception(DI::l10n()->tt('Username should be at most %s character.', 'Username should be at most %s characters.', $username_max_length));
1029                 }
1030
1031                 // So now we are just looking for a space in the full name.
1032                 $loose_reg = DI::config()->get('system', 'no_regfullname');
1033                 if (!$loose_reg) {
1034                         $username = mb_convert_case($username, MB_CASE_TITLE, 'UTF-8');
1035                         if (strpos($username, ' ') === false) {
1036                                 throw new Exception(DI::l10n()->t("That doesn't appear to be your full (First Last) name."));
1037                         }
1038                 }
1039
1040                 if (!Network::isEmailDomainAllowed($email)) {
1041                         throw new Exception(DI::l10n()->t('Your email domain is not among those allowed on this site.'));
1042                 }
1043
1044                 if (!filter_var($email, FILTER_VALIDATE_EMAIL) || !Network::isEmailDomainValid($email)) {
1045                         throw new Exception(DI::l10n()->t('Not a valid email address.'));
1046                 }
1047                 if (self::isNicknameBlocked($nickname)) {
1048                         throw new Exception(DI::l10n()->t('The nickname was blocked from registration by the nodes admin.'));
1049                 }
1050
1051                 if (DI::config()->get('system', 'block_extended_register', false) && DBA::exists('user', ['email' => $email])) {
1052                         throw new Exception(DI::l10n()->t('Cannot use that email.'));
1053                 }
1054
1055                 // Disallow somebody creating an account using openid that uses the admin email address,
1056                 // since openid bypasses email verification. We'll allow it if there is not yet an admin account.
1057                 if (DI::config()->get('config', 'admin_email') && strlen($openid_url)) {
1058                         $adminlist = explode(',', str_replace(' ', '', strtolower(DI::config()->get('config', 'admin_email'))));
1059                         if (in_array(strtolower($email), $adminlist)) {
1060                                 throw new Exception(DI::l10n()->t('Cannot use that email.'));
1061                         }
1062                 }
1063
1064                 $nickname = $data['nickname'] = strtolower($nickname);
1065
1066                 if (!preg_match('/^[a-z0-9][a-z0-9_]*$/', $nickname)) {
1067                         throw new Exception(DI::l10n()->t('Your nickname can only contain a-z, 0-9 and _.'));
1068                 }
1069
1070                 // Check existing and deleted accounts for this nickname.
1071                 if (
1072                         DBA::exists('user', ['nickname' => $nickname])
1073                         || DBA::exists('userd', ['username' => $nickname])
1074                 ) {
1075                         throw new Exception(DI::l10n()->t('Nickname is already registered. Please choose another.'));
1076                 }
1077
1078                 $new_password = strlen($password) ? $password : User::generateNewPassword();
1079                 $new_password_encoded = self::hashPassword($new_password);
1080
1081                 $return['password'] = $new_password;
1082
1083                 $keys = Crypto::newKeypair(4096);
1084                 if ($keys === false) {
1085                         throw new Exception(DI::l10n()->t('SERIOUS ERROR: Generation of security keys failed.'));
1086                 }
1087
1088                 $prvkey = $keys['prvkey'];
1089                 $pubkey = $keys['pubkey'];
1090
1091                 // Create another keypair for signing/verifying salmon protocol messages.
1092                 $sres = Crypto::newKeypair(512);
1093                 $sprvkey = $sres['prvkey'];
1094                 $spubkey = $sres['pubkey'];
1095
1096                 $insert_result = DBA::insert('user', [
1097                         'guid'     => System::createUUID(),
1098                         'username' => $username,
1099                         'password' => $new_password_encoded,
1100                         'email'    => $email,
1101                         'openid'   => $openid_url,
1102                         'nickname' => $nickname,
1103                         'pubkey'   => $pubkey,
1104                         'prvkey'   => $prvkey,
1105                         'spubkey'  => $spubkey,
1106                         'sprvkey'  => $sprvkey,
1107                         'verified' => $verified,
1108                         'blocked'  => $blocked,
1109                         'language' => $language,
1110                         'timezone' => 'UTC',
1111                         'register_date' => DateTimeFormat::utcNow(),
1112                         'default-location' => ''
1113                 ]);
1114
1115                 if ($insert_result) {
1116                         $uid = DBA::lastInsertId();
1117                         $user = DBA::selectFirst('user', [], ['uid' => $uid]);
1118                 } else {
1119                         throw new Exception(DI::l10n()->t('An error occurred during registration. Please try again.'));
1120                 }
1121
1122                 if (!$uid) {
1123                         throw new Exception(DI::l10n()->t('An error occurred during registration. Please try again.'));
1124                 }
1125
1126                 // if somebody clicked submit twice very quickly, they could end up with two accounts
1127                 // due to race condition. Remove this one.
1128                 $user_count = DBA::count('user', ['nickname' => $nickname]);
1129                 if ($user_count > 1) {
1130                         DBA::delete('user', ['uid' => $uid]);
1131
1132                         throw new Exception(DI::l10n()->t('Nickname is already registered. Please choose another.'));
1133                 }
1134
1135                 $insert_result = DBA::insert('profile', [
1136                         'uid' => $uid,
1137                         'name' => $username,
1138                         'photo' => self::getAvatarUrl($user),
1139                         'thumb' => self::getAvatarUrl($user, Proxy::SIZE_THUMB),
1140                         'publish' => $publish,
1141                         'net-publish' => $netpublish,
1142                 ]);
1143                 if (!$insert_result) {
1144                         DBA::delete('user', ['uid' => $uid]);
1145
1146                         throw new Exception(DI::l10n()->t('An error occurred creating your default profile. Please try again.'));
1147                 }
1148
1149                 // Create the self contact
1150                 if (!Contact::createSelfFromUserId($uid)) {
1151                         DBA::delete('user', ['uid' => $uid]);
1152
1153                         throw new Exception(DI::l10n()->t('An error occurred creating your self contact. Please try again.'));
1154                 }
1155
1156                 // Create a group with no members. This allows somebody to use it
1157                 // right away as a default group for new contacts.
1158                 $def_gid = Group::create($uid, DI::l10n()->t('Friends'));
1159                 if (!$def_gid) {
1160                         DBA::delete('user', ['uid' => $uid]);
1161
1162                         throw new Exception(DI::l10n()->t('An error occurred creating your default contact group. Please try again.'));
1163                 }
1164
1165                 $fields = ['def_gid' => $def_gid];
1166                 if (DI::config()->get('system', 'newuser_private') && $def_gid) {
1167                         $fields['allow_gid'] = '<' . $def_gid . '>';
1168                 }
1169
1170                 DBA::update('user', $fields, ['uid' => $uid]);
1171
1172                 // if we have no OpenID photo try to look up an avatar
1173                 if (!strlen($photo)) {
1174                         $photo = Network::lookupAvatarByEmail($email);
1175                 }
1176
1177                 // unless there is no avatar-addon loaded
1178                 if (strlen($photo)) {
1179                         $photo_failure = false;
1180
1181                         $filename = basename($photo);
1182                         $curlResult = DI::httpClient()->get($photo, HttpClientAccept::IMAGE);
1183                         if ($curlResult->isSuccess()) {
1184                                 Logger::debug('Got picture', ['Content-Type' => $curlResult->getHeader('Content-Type'), 'url' => $photo]);
1185                                 $img_str = $curlResult->getBody();
1186                                 $type = $curlResult->getContentType();
1187                         } else {
1188                                 $img_str = '';
1189                                 $type = '';
1190                         }
1191
1192                         $type = Images::getMimeTypeByData($img_str, $photo, $type);
1193
1194                         $image = new Image($img_str, $type);
1195                         if ($image->isValid()) {
1196                                 $image->scaleToSquare(300);
1197
1198                                 $resource_id = Photo::newResource();
1199
1200                                 // Not using Photo::PROFILE_PHOTOS here, so that it is discovered as translateble string
1201                                 $profile_album = DI::l10n()->t('Profile Photos');
1202
1203                                 $r = Photo::store($image, $uid, 0, $resource_id, $filename, $profile_album, 4);
1204
1205                                 if ($r === false) {
1206                                         $photo_failure = true;
1207                                 }
1208
1209                                 $image->scaleDown(80);
1210
1211                                 $r = Photo::store($image, $uid, 0, $resource_id, $filename, $profile_album, 5);
1212
1213                                 if ($r === false) {
1214                                         $photo_failure = true;
1215                                 }
1216
1217                                 $image->scaleDown(48);
1218
1219                                 $r = Photo::store($image, $uid, 0, $resource_id, $filename, $profile_album, 6);
1220
1221                                 if ($r === false) {
1222                                         $photo_failure = true;
1223                                 }
1224
1225                                 if (!$photo_failure) {
1226                                         Photo::update(['profile' => true, 'photo-type' => Photo::USER_AVATAR], ['resource-id' => $resource_id]);
1227                                 }
1228                         }
1229
1230                         Contact::updateSelfFromUserID($uid, true);
1231                 }
1232
1233                 Hook::callAll('register_account', $uid);
1234
1235                 $return['user'] = $user;
1236                 return $return;
1237         }
1238
1239         /**
1240          * Update a user entry and distribute the changes if needed
1241          *
1242          * @param array $fields
1243          * @param integer $uid
1244          * @return boolean
1245          */
1246         public static function update(array $fields, int $uid): bool
1247         {
1248                 $old_owner = self::getOwnerDataById($uid);
1249                 if (empty($old_owner)) {
1250                         return false;
1251                 }
1252
1253                 if (!DBA::update('user', $fields, ['uid' => $uid])) {
1254                         return false;
1255                 }
1256
1257                 $update = Contact::updateSelfFromUserID($uid);
1258
1259                 $owner = self::getOwnerDataById($uid);
1260                 if (empty($owner)) {
1261                         return false;
1262                 }
1263
1264                 if ($old_owner['name'] != $owner['name']) {
1265                         Profile::update(['name' => $owner['name']], $uid);
1266                 }
1267
1268                 if ($update) {
1269                         Profile::publishUpdate($uid);
1270                 }
1271
1272                 return true;
1273         }
1274
1275         /**
1276          * Sets block state for a given user
1277          *
1278          * @param int  $uid   The user id
1279          * @param bool $block Block state (default is true)
1280          *
1281          * @return bool True, if successfully blocked
1282
1283          * @throws Exception
1284          */
1285         public static function block(int $uid, bool $block = true): bool
1286         {
1287                 return DBA::update('user', ['blocked' => $block], ['uid' => $uid]);
1288         }
1289
1290         /**
1291          * Allows a registration based on a hash
1292          *
1293          * @param string $hash
1294          *
1295          * @return bool True, if the allow was successful
1296          *
1297          * @throws HTTPException\InternalServerErrorException
1298          * @throws Exception
1299          */
1300         public static function allow(string $hash): bool
1301         {
1302                 $register = Register::getByHash($hash);
1303                 if (!DBA::isResult($register)) {
1304                         return false;
1305                 }
1306
1307                 $user = User::getById($register['uid']);
1308                 if (!DBA::isResult($user)) {
1309                         return false;
1310                 }
1311
1312                 Register::deleteByHash($hash);
1313
1314                 DBA::update('user', ['blocked' => false, 'verified' => true], ['uid' => $register['uid']]);
1315
1316                 $profile = DBA::selectFirst('profile', ['net-publish'], ['uid' => $register['uid']]);
1317
1318                 if (DBA::isResult($profile) && $profile['net-publish'] && Search::getGlobalDirectory()) {
1319                         $url = DI::baseUrl() . '/profile/' . $user['nickname'];
1320                         Worker::add(Worker::PRIORITY_LOW, "Directory", $url);
1321                 }
1322
1323                 $l10n = DI::l10n()->withLang($register['language']);
1324
1325                 return User::sendRegisterOpenEmail(
1326                         $l10n,
1327                         $user,
1328                         DI::config()->get('config', 'sitename'),
1329                         DI::baseUrl()->get(),
1330                         ($register['password'] ?? '') ?: 'Sent in a previous email'
1331                 );
1332         }
1333
1334         /**
1335          * Denys a pending registration
1336          *
1337          * @param string $hash The hash of the pending user
1338          *
1339          * This does not have to go through user_remove() and save the nickname
1340          * permanently against re-registration, as the person was not yet
1341          * allowed to have friends on this system
1342          *
1343          * @return bool True, if the deny was successfull
1344          * @throws Exception
1345          */
1346         public static function deny(string $hash): bool
1347         {
1348                 $register = Register::getByHash($hash);
1349                 if (!DBA::isResult($register)) {
1350                         return false;
1351                 }
1352
1353                 $user = User::getById($register['uid']);
1354                 if (!DBA::isResult($user)) {
1355                         return false;
1356                 }
1357
1358                 // Delete the avatar
1359                 Photo::delete(['uid' => $register['uid']]);
1360
1361                 return DBA::delete('user', ['uid' => $register['uid']]) &&
1362                        Register::deleteByHash($register['hash']);
1363         }
1364
1365         /**
1366          * Creates a new user based on a minimal set and sends an email to this user
1367          *
1368          * @param string $name  The user's name
1369          * @param string $email The user's email address
1370          * @param string $nick  The user's nick name
1371          * @param string $lang  The user's language (default is english)
1372          * @return bool True, if the user was created successfully
1373          * @throws HTTPException\InternalServerErrorException
1374          * @throws ErrorException
1375          * @throws ImagickException
1376          */
1377         public static function createMinimal(string $name, string $email, string $nick, string $lang = L10n::DEFAULT): bool
1378         {
1379                 if (empty($name) ||
1380                     empty($email) ||
1381                     empty($nick)) {
1382                         throw new HTTPException\InternalServerErrorException('Invalid arguments.');
1383                 }
1384
1385                 $result = self::create([
1386                         'username' => $name,
1387                         'email' => $email,
1388                         'nickname' => $nick,
1389                         'verified' => 1,
1390                         'language' => $lang
1391                 ]);
1392
1393                 $user = $result['user'];
1394                 $preamble = Strings::deindent(DI::l10n()->t('
1395                 Dear %1$s,
1396                         the administrator of %2$s has set up an account for you.'));
1397                 $body = Strings::deindent(DI::l10n()->t('
1398                 The login details are as follows:
1399
1400                 Site Location:  %1$s
1401                 Login Name:             %2$s
1402                 Password:               %3$s
1403
1404                 You may change your password from your account "Settings" page after logging
1405                 in.
1406
1407                 Please take a few moments to review the other account settings on that page.
1408
1409                 You may also wish to add some basic information to your default profile
1410                 (on the "Profiles" page) so that other people can easily find you.
1411
1412                 We recommend setting your full name, adding a profile photo,
1413                 adding some profile "keywords" (very useful in making new friends) - and
1414                 perhaps what country you live in; if you do not wish to be more specific
1415                 than that.
1416
1417                 We fully respect your right to privacy, and none of these items are necessary.
1418                 If you are new and do not know anybody here, they may help
1419                 you to make some new and interesting friends.
1420
1421                 If you ever want to delete your account, you can do so at %1$s/settings/removeme
1422
1423                 Thank you and welcome to %4$s.'));
1424
1425                 $preamble = sprintf($preamble, $user['username'], DI::config()->get('config', 'sitename'));
1426                 $body = sprintf($body, DI::baseUrl()->get(), $user['nickname'], $result['password'], DI::config()->get('config', 'sitename'));
1427
1428                 $email = DI::emailer()
1429                         ->newSystemMail()
1430                         ->withMessage(DI::l10n()->t('Registration details for %s', DI::config()->get('config', 'sitename')), $preamble, $body)
1431                         ->forUser($user)
1432                         ->withRecipient($user['email'])
1433                         ->build();
1434                 return DI::emailer()->send($email);
1435         }
1436
1437         /**
1438          * Sends pending registration confirmation email
1439          *
1440          * @param array  $user     User record array
1441          * @param string $sitename
1442          * @param string $siteurl
1443          * @param string $password Plaintext password
1444          * @return NULL|boolean from notification() and email() inherited
1445          * @throws HTTPException\InternalServerErrorException
1446          */
1447         public static function sendRegisterPendingEmail(array $user, string $sitename, string $siteurl, string $password)
1448         {
1449                 $body = Strings::deindent(DI::l10n()->t(
1450                         '
1451                         Dear %1$s,
1452                                 Thank you for registering at %2$s. Your account is pending for approval by the administrator.
1453
1454                         Your login details are as follows:
1455
1456                         Site Location:  %3$s
1457                         Login Name:             %4$s
1458                         Password:               %5$s
1459                 ',
1460                         $user['username'],
1461                         $sitename,
1462                         $siteurl,
1463                         $user['nickname'],
1464                         $password
1465                 ));
1466
1467                 $email = DI::emailer()
1468                         ->newSystemMail()
1469                         ->withMessage(DI::l10n()->t('Registration at %s', $sitename), $body)
1470                         ->forUser($user)
1471                         ->withRecipient($user['email'])
1472                         ->build();
1473                 return DI::emailer()->send($email);
1474         }
1475
1476         /**
1477          * Sends registration confirmation
1478          *
1479          * It's here as a function because the mail is sent from different parts
1480          *
1481          * @param L10n   $l10n     The used language
1482          * @param array  $user     User record array
1483          * @param string $sitename
1484          * @param string $siteurl
1485          * @param string $password Plaintext password
1486          *
1487          * @return NULL|boolean from notification() and email() inherited
1488          * @throws HTTPException\InternalServerErrorException
1489          */
1490         public static function sendRegisterOpenEmail(L10n $l10n, array $user, string $sitename, string $siteurl, string $password)
1491         {
1492                 $preamble = Strings::deindent($l10n->t(
1493                         '
1494                                 Dear %1$s,
1495                                 Thank you for registering at %2$s. Your account has been created.
1496                         ',
1497                         $user['username'],
1498                         $sitename
1499                 ));
1500                 $body = Strings::deindent($l10n->t(
1501                         '
1502                         The login details are as follows:
1503
1504                         Site Location:  %3$s
1505                         Login Name:             %1$s
1506                         Password:               %5$s
1507
1508                         You may change your password from your account "Settings" page after logging
1509                         in.
1510
1511                         Please take a few moments to review the other account settings on that page.
1512
1513                         You may also wish to add some basic information to your default profile
1514                         ' . "\x28" . 'on the "Profiles" page' . "\x29" . ' so that other people can easily find you.
1515
1516                         We recommend setting your full name, adding a profile photo,
1517                         adding some profile "keywords" ' . "\x28" . 'very useful in making new friends' . "\x29" . ' - and
1518                         perhaps what country you live in; if you do not wish to be more specific
1519                         than that.
1520
1521                         We fully respect your right to privacy, and none of these items are necessary.
1522                         If you are new and do not know anybody here, they may help
1523                         you to make some new and interesting friends.
1524
1525                         If you ever want to delete your account, you can do so at %3$s/settings/removeme
1526
1527                         Thank you and welcome to %2$s.',
1528                         $user['nickname'],
1529                         $sitename,
1530                         $siteurl,
1531                         $user['username'],
1532                         $password
1533                 ));
1534
1535                 $email = DI::emailer()
1536                         ->newSystemMail()
1537                         ->withMessage(DI::l10n()->t('Registration details for %s', $sitename), $preamble, $body)
1538                         ->forUser($user)
1539                         ->withRecipient($user['email'])
1540                         ->build();
1541                 return DI::emailer()->send($email);
1542         }
1543
1544         /**
1545          * @param int $uid user to remove
1546          * @return bool
1547          * @throws HTTPException\InternalServerErrorException
1548          */
1549         public static function remove(int $uid): bool
1550         {
1551                 if (empty($uid)) {
1552                         return false;
1553                 }
1554
1555                 Logger::notice('Removing user', ['user' => $uid]);
1556
1557                 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
1558
1559                 Hook::callAll('remove_user', $user);
1560
1561                 // save username (actually the nickname as it is guaranteed
1562                 // unique), so it cannot be re-registered in the future.
1563                 DBA::insert('userd', ['username' => $user['nickname']]);
1564
1565                 // Remove all personal settings, especially connector settings
1566                 DBA::delete('pconfig', ['uid' => $uid]);
1567
1568                 // The user and related data will be deleted in Friendica\Worker\ExpireAndRemoveUsers
1569                 DBA::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utc('now + 7 day')], ['uid' => $uid]);
1570                 Worker::add(Worker::PRIORITY_HIGH, 'Notifier', Delivery::REMOVAL, $uid);
1571
1572                 // Send an update to the directory
1573                 $self = DBA::selectFirst('contact', ['url'], ['uid' => $uid, 'self' => true]);
1574                 Worker::add(Worker::PRIORITY_LOW, 'Directory', $self['url']);
1575
1576                 // Remove the user relevant data
1577                 Worker::add(Worker::PRIORITY_NEGLIGIBLE, 'RemoveUser', $uid);
1578
1579                 return true;
1580         }
1581
1582         /**
1583          * Return all identities to a user
1584          *
1585          * @param int $uid The user id
1586          * @return array All identities for this user
1587          *
1588          * Example for a return:
1589          *    [
1590          *        [
1591          *            'uid' => 1,
1592          *            'username' => 'maxmuster',
1593          *            'nickname' => 'Max Mustermann'
1594          *        ],
1595          *        [
1596          *            'uid' => 2,
1597          *            'username' => 'johndoe',
1598          *            'nickname' => 'John Doe'
1599          *        ]
1600          *    ]
1601          * @throws Exception
1602          */
1603         public static function identities(int $uid): array
1604         {
1605                 if (empty($uid)) {
1606                         return [];
1607                 }
1608
1609                 $identities = [];
1610
1611                 $user = DBA::selectFirst('user', ['uid', 'nickname', 'username', 'parent-uid'], ['uid' => $uid]);
1612                 if (!DBA::isResult($user)) {
1613                         return $identities;
1614                 }
1615
1616                 if ($user['parent-uid'] == 0) {
1617                         // First add our own entry
1618                         $identities = [[
1619                                 'uid' => $user['uid'],
1620                                 'username' => $user['username'],
1621                                 'nickname' => $user['nickname']
1622                         ]];
1623
1624                         // Then add all the children
1625                         $r = DBA::select(
1626                                 'user',
1627                                 ['uid', 'username', 'nickname'],
1628                                 ['parent-uid' => $user['uid'], 'account_removed' => false]
1629                         );
1630                         if (DBA::isResult($r)) {
1631                                 $identities = array_merge($identities, DBA::toArray($r));
1632                         }
1633                 } else {
1634                         // First entry is our parent
1635                         $r = DBA::select(
1636                                 'user',
1637                                 ['uid', 'username', 'nickname'],
1638                                 ['uid' => $user['parent-uid'], 'account_removed' => false]
1639                         );
1640                         if (DBA::isResult($r)) {
1641                                 $identities = DBA::toArray($r);
1642                         }
1643
1644                         // Then add all siblings
1645                         $r = DBA::select(
1646                                 'user',
1647                                 ['uid', 'username', 'nickname'],
1648                                 ['parent-uid' => $user['parent-uid'], 'account_removed' => false]
1649                         );
1650                         if (DBA::isResult($r)) {
1651                                 $identities = array_merge($identities, DBA::toArray($r));
1652                         }
1653                 }
1654
1655                 $r = DBA::p(
1656                         "SELECT `user`.`uid`, `user`.`username`, `user`.`nickname`
1657                         FROM `manage`
1658                         INNER JOIN `user` ON `manage`.`mid` = `user`.`uid`
1659                         WHERE `user`.`account_removed` = 0 AND `manage`.`uid` = ?",
1660                         $user['uid']
1661                 );
1662                 if (DBA::isResult($r)) {
1663                         $identities = array_merge($identities, DBA::toArray($r));
1664                 }
1665
1666                 return $identities;
1667         }
1668
1669         /**
1670          * Check if the given user id has delegations or is delegated
1671          *
1672          * @param int $uid
1673          * @return bool
1674          */
1675         public static function hasIdentities(int $uid): bool
1676         {
1677                 if (empty($uid)) {
1678                         return false;
1679                 }
1680
1681                 $user = DBA::selectFirst('user', ['parent-uid'], ['uid' => $uid, 'account_removed' => false]);
1682                 if (!DBA::isResult($user)) {
1683                         return false;
1684                 }
1685
1686                 if ($user['parent-uid'] != 0) {
1687                         return true;
1688                 }
1689
1690                 if (DBA::exists('user', ['parent-uid' => $uid, 'account_removed' => false])) {
1691                         return true;
1692                 }
1693
1694                 if (DBA::exists('manage', ['uid' => $uid])) {
1695                         return true;
1696                 }
1697
1698                 return false;
1699         }
1700
1701         /**
1702          * Returns statistical information about the current users of this node
1703          *
1704          * @return array
1705          *
1706          * @throws Exception
1707          */
1708         public static function getStatistics(): array
1709         {
1710                 $statistics = [
1711                         'total_users'           => 0,
1712                         'active_users_halfyear' => 0,
1713                         'active_users_monthly'  => 0,
1714                         'active_users_weekly'   => 0,
1715                 ];
1716
1717                 $userStmt = DBA::select('owner-view', ['uid', 'login_date', 'last-item'],
1718                         ["`verified` AND `login_date` > ? AND NOT `blocked`
1719                         AND NOT `account_removed` AND NOT `account_expired`",
1720                         DBA::NULL_DATETIME]);
1721                 if (!DBA::isResult($userStmt)) {
1722                         return $statistics;
1723                 }
1724
1725                 $halfyear = time() - (180 * 24 * 60 * 60);
1726                 $month = time() - (30 * 24 * 60 * 60);
1727                 $week = time() - (7 * 24 * 60 * 60);
1728
1729                 while ($user = DBA::fetch($userStmt)) {
1730                         $statistics['total_users']++;
1731
1732                         if ((strtotime($user['login_date']) > $halfyear) || (strtotime($user['last-item']) > $halfyear)
1733                         ) {
1734                                 $statistics['active_users_halfyear']++;
1735                         }
1736
1737                         if ((strtotime($user['login_date']) > $month) || (strtotime($user['last-item']) > $month)
1738                         ) {
1739                                 $statistics['active_users_monthly']++;
1740                         }
1741
1742                         if ((strtotime($user['login_date']) > $week) || (strtotime($user['last-item']) > $week)
1743                         ) {
1744                                 $statistics['active_users_weekly']++;
1745                         }
1746                 }
1747                 DBA::close($userStmt);
1748
1749                 return $statistics;
1750         }
1751
1752         /**
1753          * Get all users of the current node
1754          *
1755          * @param int    $start Start count (Default is 0)
1756          * @param int    $count Count of the items per page (Default is @see Pager::ITEMS_PER_PAGE)
1757          * @param string $type  The type of users, which should get (all, bocked, removed)
1758          * @param string $order Order of the user list (Default is 'contact.name')
1759          * @param bool   $descending Order direction (Default is ascending)
1760          * @return array|bool The list of the users
1761          * @throws Exception
1762          */
1763         public static function getList(int $start = 0, int $count = Pager::ITEMS_PER_PAGE, string $type = 'all', string $order = 'name', bool $descending = false)
1764         {
1765                 $param = ['limit' => [$start, $count], 'order' => [$order => $descending]];
1766                 $condition = [];
1767                 switch ($type) {
1768                         case 'active':
1769                                 $condition['account_removed'] = false;
1770                                 $condition['blocked'] = false;
1771                                 break;
1772
1773                         case 'blocked':
1774                                 $condition['account_removed'] = false;
1775                                 $condition['blocked'] = true;
1776                                 $condition['verified'] = true;
1777                                 break;
1778
1779                         case 'removed':
1780                                 $condition['account_removed'] = true;
1781                                 break;
1782                 }
1783
1784                 return DBA::selectToArray('owner-view', [], $condition, $param);
1785         }
1786 }