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