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