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