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