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