]> git.mxchange.org Git - friendica.git/blob - src/Model/User.php
Allow setting user avatar in the console at creation
[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          * @param string $avatar URL to an image to use as avatar (default is to prompt user at first login)
1561          * @return bool True, if the user was created successfully
1562          * @throws HTTPException\InternalServerErrorException
1563          * @throws ErrorException
1564          * @throws ImagickException
1565          */
1566         public static function createMinimal(string $name, string $email, string $nick, string $lang = L10n::DEFAULT, string $avatar = ''): bool
1567         {
1568                 if (empty($name) ||
1569                     empty($email) ||
1570                     empty($nick)) {
1571                         throw new HTTPException\InternalServerErrorException('Invalid arguments.');
1572                 }
1573
1574                 $result = self::create([
1575                         'username' => $name,
1576                         'email' => $email,
1577                         'nickname' => $nick,
1578                         'verified' => 1,
1579                         'language' => $lang,
1580                         'photo' => $avatar
1581                 ]);
1582
1583                 $user = $result['user'];
1584                 $preamble = Strings::deindent(DI::l10n()->t('
1585                 Dear %1$s,
1586                         the administrator of %2$s has set up an account for you.'));
1587                 $body = Strings::deindent(DI::l10n()->t('
1588                 The login details are as follows:
1589
1590                 Site Location:  %1$s
1591                 Login Name:             %2$s
1592                 Password:               %3$s
1593
1594                 You may change your password from your account "Settings" page after logging
1595                 in.
1596
1597                 Please take a few moments to review the other account settings on that page.
1598
1599                 You may also wish to add some basic information to your default profile
1600                 (on the "Profiles" page) so that other people can easily find you.
1601
1602                 We recommend adding a profile photo, adding some profile "keywords" 
1603                 (very useful in making new friends) - and perhaps what country you live in; 
1604                 if you do not wish to be more specific than that.
1605
1606                 We fully respect your right to privacy, and none of these items are necessary.
1607                 If you are new and do not know anybody here, they may help
1608                 you to make some new and interesting friends.
1609
1610                 If you ever want to delete your account, you can do so at %1$s/settings/removeme
1611
1612                 Thank you and welcome to %4$s.'));
1613
1614                 $preamble = sprintf($preamble, $user['username'], DI::config()->get('config', 'sitename'));
1615                 $body = sprintf($body, DI::baseUrl(), $user['nickname'], $result['password'], DI::config()->get('config', 'sitename'));
1616
1617                 $email = DI::emailer()
1618                         ->newSystemMail()
1619                         ->withMessage(DI::l10n()->t('Registration details for %s', DI::config()->get('config', 'sitename')), $preamble, $body)
1620                         ->forUser($user)
1621                         ->withRecipient($user['email'])
1622                         ->build();
1623                 return DI::emailer()->send($email);
1624         }
1625
1626         /**
1627          * Sends pending registration confirmation email
1628          *
1629          * @param array  $user     User record array
1630          * @param string $sitename
1631          * @param string $siteurl
1632          * @param string $password Plaintext password
1633          * @return NULL|boolean from notification() and email() inherited
1634          * @throws HTTPException\InternalServerErrorException
1635          */
1636         public static function sendRegisterPendingEmail(array $user, string $sitename, string $siteurl, string $password)
1637         {
1638                 $body = Strings::deindent(DI::l10n()->t(
1639                         '
1640                         Dear %1$s,
1641                                 Thank you for registering at %2$s. Your account is pending for approval by the administrator.
1642
1643                         Your login details are as follows:
1644
1645                         Site Location:  %3$s
1646                         Login Name:             %4$s
1647                         Password:               %5$s
1648                 ',
1649                         $user['username'],
1650                         $sitename,
1651                         $siteurl,
1652                         $user['nickname'],
1653                         $password
1654                 ));
1655
1656                 $email = DI::emailer()
1657                         ->newSystemMail()
1658                         ->withMessage(DI::l10n()->t('Registration at %s', $sitename), $body)
1659                         ->forUser($user)
1660                         ->withRecipient($user['email'])
1661                         ->build();
1662                 return DI::emailer()->send($email);
1663         }
1664
1665         /**
1666          * Sends registration confirmation
1667          *
1668          * It's here as a function because the mail is sent from different parts
1669          *
1670          * @param L10n   $l10n     The used language
1671          * @param array  $user     User record array
1672          * @param string $sitename
1673          * @param string $siteurl
1674          * @param string $password Plaintext password
1675          *
1676          * @return NULL|boolean from notification() and email() inherited
1677          * @throws HTTPException\InternalServerErrorException
1678          */
1679         public static function sendRegisterOpenEmail(L10n $l10n, array $user, string $sitename, string $siteurl, string $password)
1680         {
1681                 $preamble = Strings::deindent($l10n->t(
1682                         '
1683                                 Dear %1$s,
1684                                 Thank you for registering at %2$s. Your account has been created.
1685                         ',
1686                         $user['username'],
1687                         $sitename
1688                 ));
1689                 $body = Strings::deindent($l10n->t(
1690                         '
1691                         The login details are as follows:
1692
1693                         Site Location:  %3$s
1694                         Login Name:             %1$s
1695                         Password:               %5$s
1696
1697                         You may change your password from your account "Settings" page after logging
1698                         in.
1699
1700                         Please take a few moments to review the other account settings on that page.
1701
1702                         You may also wish to add some basic information to your default profile
1703                         ' . "\x28" . 'on the "Profiles" page' . "\x29" . ' so that other people can easily find you.
1704
1705                         We recommend adding a profile photo, adding some profile "keywords" ' . "\x28" . 'very useful
1706                         in making new friends' . "\x29" . ' - and perhaps what country you live in; if you do not wish
1707                         to be more specific than that.
1708
1709                         We fully respect your right to privacy, and none of these items are necessary.
1710                         If you are new and do not know anybody here, they may help
1711                         you to make some new and interesting friends.
1712
1713                         If you ever want to delete your account, you can do so at %3$s/settings/removeme
1714
1715                         Thank you and welcome to %2$s.',
1716                         $user['nickname'],
1717                         $sitename,
1718                         $siteurl,
1719                         $user['username'],
1720                         $password
1721                 ));
1722
1723                 $email = DI::emailer()
1724                         ->newSystemMail()
1725                         ->withMessage(DI::l10n()->t('Registration details for %s', $sitename), $preamble, $body)
1726                         ->forUser($user)
1727                         ->withRecipient($user['email'])
1728                         ->build();
1729                 return DI::emailer()->send($email);
1730         }
1731
1732         /**
1733          * @param int $uid user to remove
1734          * @return bool
1735          * @throws HTTPException\InternalServerErrorException
1736          * @throws HTTPException\NotFoundException
1737          */
1738         public static function remove(int $uid): bool
1739         {
1740                 if (empty($uid)) {
1741                         throw new \InvalidArgumentException('uid needs to be greater than 0');
1742                 }
1743
1744                 Logger::notice('Removing user', ['user' => $uid]);
1745
1746                 $user = self::getById($uid);
1747                 if (!$user) {
1748                         throw new HTTPException\NotFoundException('User not found with uid: ' . $uid);
1749                 }
1750
1751                 if (DBA::exists('user', ['parent-uid' => $uid])) {
1752                         throw new \RuntimeException(DI::l10n()->t("User with delegates can't be removed, please remove delegate users first"));
1753                 }
1754
1755                 Hook::callAll('remove_user', $user);
1756
1757                 // save username (actually the nickname as it is guaranteed
1758                 // unique), so it cannot be re-registered in the future.
1759                 DBA::insert('userd', ['username' => $user['nickname']]);
1760
1761                 // Remove all personal settings, especially connector settings
1762                 DBA::delete('pconfig', ['uid' => $uid]);
1763
1764                 // The user and related data will be deleted in Friendica\Worker\ExpireAndRemoveUsers
1765                 DBA::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utc('now + 7 day')], ['uid' => $uid]);
1766                 Worker::add(Worker::PRIORITY_HIGH, 'Notifier', Delivery::REMOVAL, $uid);
1767
1768                 // Send an update to the directory
1769                 $self = DBA::selectFirst('contact', ['url'], ['uid' => $uid, 'self' => true]);
1770                 Worker::add(Worker::PRIORITY_LOW, 'Directory', $self['url']);
1771
1772                 // Remove the user relevant data
1773                 Worker::add(Worker::PRIORITY_NEGLIGIBLE, 'RemoveUser', $uid);
1774
1775                 self::setRegisterMethodByUserCount();
1776                 return true;
1777         }
1778
1779         /**
1780          * Return all identities to a user
1781          *
1782          * @param int $uid The user id
1783          * @return array All identities for this user
1784          *
1785          * Example for a return:
1786          *    [
1787          *        [
1788          *            'uid' => 1,
1789          *            'username' => 'maxmuster',
1790          *            'nickname' => 'Max Mustermann'
1791          *        ],
1792          *        [
1793          *            'uid' => 2,
1794          *            'username' => 'johndoe',
1795          *            'nickname' => 'John Doe'
1796          *        ]
1797          *    ]
1798          * @throws Exception
1799          */
1800         public static function identities(int $uid): array
1801         {
1802                 if (!$uid) {
1803                         return [];
1804                 }
1805
1806                 $identities = [];
1807
1808                 $user = DBA::selectFirst('user', ['uid', 'nickname', 'username', 'parent-uid'], ['uid' => $uid, 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]);
1809                 if (!DBA::isResult($user)) {
1810                         return $identities;
1811                 }
1812
1813                 if (!$user['parent-uid']) {
1814                         // First add our own entry
1815                         $identities = [[
1816                                 'uid' => $user['uid'],
1817                                 'username' => $user['username'],
1818                                 'nickname' => $user['nickname']
1819                         ]];
1820
1821                         // Then add all the children
1822                         $r = DBA::select(
1823                                 'user',
1824                                 ['uid', 'username', 'nickname'],
1825                                 ['parent-uid' => $user['uid'], 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]
1826                         );
1827                         if (DBA::isResult($r)) {
1828                                 $identities = array_merge($identities, DBA::toArray($r));
1829                         }
1830                 } else {
1831                         // First entry is our parent
1832                         $r = DBA::select(
1833                                 'user',
1834                                 ['uid', 'username', 'nickname'],
1835                                 ['uid' => $user['parent-uid'], 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]
1836                         );
1837                         if (DBA::isResult($r)) {
1838                                 $identities = DBA::toArray($r);
1839                         }
1840
1841                         // Then add all siblings
1842                         $r = DBA::select(
1843                                 'user',
1844                                 ['uid', 'username', 'nickname'],
1845                                 ['parent-uid' => $user['parent-uid'], 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]
1846                         );
1847                         if (DBA::isResult($r)) {
1848                                 $identities = array_merge($identities, DBA::toArray($r));
1849                         }
1850                 }
1851
1852                 $r = DBA::p(
1853                         "SELECT `user`.`uid`, `user`.`username`, `user`.`nickname`
1854                         FROM `manage`
1855                         INNER JOIN `user` ON `manage`.`mid` = `user`.`uid`
1856                         WHERE NOT `user`.`account_removed` AND `manage`.`uid` = ?",
1857                         $user['uid']
1858                 );
1859                 if (DBA::isResult($r)) {
1860                         $identities = array_merge($identities, DBA::toArray($r));
1861                 }
1862
1863                 return $identities;
1864         }
1865
1866         /**
1867          * Check if the given user id has delegations or is delegated
1868          *
1869          * @param int $uid
1870          * @return bool
1871          */
1872         public static function hasIdentities(int $uid): bool
1873         {
1874                 if (!$uid) {
1875                         return false;
1876                 }
1877
1878                 $user = DBA::selectFirst('user', ['parent-uid'], ['uid' => $uid, 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]);
1879                 if (!DBA::isResult($user)) {
1880                         return false;
1881                 }
1882
1883                 if ($user['parent-uid']) {
1884                         return true;
1885                 }
1886
1887                 if (DBA::exists('user', ['parent-uid' => $uid, 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false])) {
1888                         return true;
1889                 }
1890
1891                 if (DBA::exists('manage', ['uid' => $uid])) {
1892                         return true;
1893                 }
1894
1895                 return false;
1896         }
1897
1898         /**
1899          * Returns statistical information about the current users of this node
1900          *
1901          * @return array
1902          *
1903          * @throws Exception
1904          */
1905         public static function getStatistics(): array
1906         {
1907                 $statistics = [
1908                         'total_users'           => 0,
1909                         'active_users_halfyear' => 0,
1910                         'active_users_monthly'  => 0,
1911                         'active_users_weekly'   => 0,
1912                 ];
1913
1914                 $userStmt = DBA::select('owner-view', ['uid', 'last-activity', 'last-item'],
1915                         ["`verified` AND `last-activity` > ? AND NOT `blocked`
1916                         AND NOT `account_removed` AND NOT `account_expired`",
1917                         DBA::NULL_DATETIME]);
1918                 if (!DBA::isResult($userStmt)) {
1919                         return $statistics;
1920                 }
1921
1922                 $halfyear = time() - (180 * 24 * 60 * 60);
1923                 $month = time() - (30 * 24 * 60 * 60);
1924                 $week = time() - (7 * 24 * 60 * 60);
1925
1926                 while ($user = DBA::fetch($userStmt)) {
1927                         $statistics['total_users']++;
1928
1929                         if ((strtotime($user['last-activity']) > $halfyear) || (strtotime($user['last-item']) > $halfyear)
1930                         ) {
1931                                 $statistics['active_users_halfyear']++;
1932                         }
1933
1934                         if ((strtotime($user['last-activity']) > $month) || (strtotime($user['last-item']) > $month)
1935                         ) {
1936                                 $statistics['active_users_monthly']++;
1937                         }
1938
1939                         if ((strtotime($user['last-activity']) > $week) || (strtotime($user['last-item']) > $week)
1940                         ) {
1941                                 $statistics['active_users_weekly']++;
1942                         }
1943                 }
1944                 DBA::close($userStmt);
1945
1946                 return $statistics;
1947         }
1948
1949         /**
1950          * Get all users of the current node
1951          *
1952          * @param int    $start Start count (Default is 0)
1953          * @param int    $count Count of the items per page (Default is @see Pager::ITEMS_PER_PAGE)
1954          * @param string $type  The type of users, which should get (all, blocked, removed)
1955          * @param string $order Order of the user list (Default is 'contact.name')
1956          * @param bool   $descending Order direction (Default is ascending)
1957          * @return array|bool The list of the users
1958          * @throws Exception
1959          */
1960         public static function getList(int $start = 0, int $count = Pager::ITEMS_PER_PAGE, string $type = 'all', string $order = 'name', bool $descending = false)
1961         {
1962                 $param = ['limit' => [$start, $count], 'order' => [$order => $descending]];
1963                 $condition = [];
1964                 switch ($type) {
1965                         case 'active':
1966                                 $condition['account_removed'] = false;
1967                                 $condition['blocked'] = false;
1968                                 break;
1969
1970                         case 'blocked':
1971                                 $condition['account_removed'] = false;
1972                                 $condition['blocked'] = true;
1973                                 $condition['verified'] = true;
1974                                 break;
1975
1976                         case 'removed':
1977                                 $condition['account_removed'] = true;
1978                                 break;
1979                 }
1980
1981                 return DBA::selectToArray('owner-view', [], $condition, $param);
1982         }
1983
1984         /**
1985          * Returns a list of lowercase admin email addresses from the comma-separated list in the config
1986          *
1987          * @return array
1988          */
1989         public static function getAdminEmailList(): array
1990         {
1991                 $adminEmails = strtolower(str_replace(' ', '', DI::config()->get('config', 'admin_email')));
1992                 if (!$adminEmails) {
1993                         return [];
1994                 }
1995
1996                 return explode(',', $adminEmails);
1997         }
1998
1999         /**
2000          * Returns the complete list of admin user accounts
2001          *
2002          * @param array $fields
2003          * @return array
2004          * @throws Exception
2005          */
2006         public static function getAdminList(array $fields = []): array
2007         {
2008                 $condition = [
2009                         'email'           => self::getAdminEmailList(),
2010                         'parent-uid'      => null,
2011                         'blocked'         => false,
2012                         'verified'        => true,
2013                         'account_removed' => false,
2014                         'account_expired' => false,
2015                 ];
2016
2017                 return DBA::selectToArray('user', $fields, $condition, ['order' => ['uid']]);
2018         }
2019
2020         /**
2021          * Return a list of admin user accounts where each unique email address appears only once.
2022          *
2023          * This method is meant for admin notifications that do not need to be sent multiple times to the same email address.
2024          *
2025          * @param array $fields
2026          * @return array
2027          * @throws Exception
2028          */
2029         public static function getAdminListForEmailing(array $fields = []): array
2030         {
2031                 return array_filter(self::getAdminList($fields), function ($user) {
2032                         static $emails = [];
2033
2034                         if (in_array($user['email'], $emails)) {
2035                                 return false;
2036                         }
2037
2038                         $emails[] = $user['email'];
2039
2040                         return true;
2041                 });
2042         }
2043
2044         public static function setRegisterMethodByUserCount()
2045         {
2046                 $max_registered_users = DI::config()->get('config', 'max_registered_users');
2047                 if ($max_registered_users <= 0) {
2048                         return;
2049                 }
2050
2051                 $register_policy = DI::config()->get('config', 'register_policy');
2052                 if (!in_array($register_policy, [Module\Register::OPEN, Module\Register::CLOSED])) {
2053                         Logger::debug('Unsupported register policy.', ['policy' => $register_policy]);
2054                         return;
2055                 }
2056
2057                 $users = DBA::count('user', ['blocked' => false, 'account_removed' => false, 'account_expired' => false]);
2058                 if (($users >= $max_registered_users) && ($register_policy == Module\Register::OPEN)) {
2059                         DI::config()->set('config', 'register_policy', Module\Register::CLOSED);
2060                         Logger::notice('Max users reached, registration is closed.', ['users' => $users, 'max' => $max_registered_users]);
2061                 } elseif (($users < $max_registered_users) && ($register_policy == Module\Register::CLOSED)) {
2062                         DI::config()->set('config', 'register_policy', Module\Register::OPEN);
2063                         Logger::notice('Below maximum users, registration is opened.', ['users' => $users, 'max' => $max_registered_users]);
2064                 } else {
2065                         Logger::debug('Unchanged register policy', ['policy' => $register_policy, 'users' => $users, 'max' => $max_registered_users]);
2066                 }
2067         }
2068 }