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