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