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