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