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