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