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