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