]> git.mxchange.org Git - friendica.git/blob - src/Model/User.php
Merge pull request #10788 from fabrixxm/fix/gettext
[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 id
845          *
846          * @param integer $uid     user id
847          * @param string  $size    One of the ProxyUtils::SIZE_* constants
848          * @return string avatar link
849          */
850         public static function getAvatarUrlForId(int $uid, string $size = ''):string
851         {
852                 $url = DI::baseUrl() . '/photo/';
853
854                 switch ($size) {
855                         case Proxy::SIZE_MICRO:
856                                 $url .= 'micro/';
857                                 $scale = 6;
858                                 break;
859                         case Proxy::SIZE_THUMB:
860                                 $url .= 'avatar/';
861                                 $scale = 5;
862                                 break;
863                         default:
864                                 $url .= 'profile/';
865                                 $scale = 4;
866                                 break;
867                 }
868
869                 $updated =  '';
870                 $imagetype = IMAGETYPE_JPEG;
871
872                 $photo = Photo::selectFirst(['type', 'created', 'edited', 'updated'], ["scale" => $scale, 'uid' => $uid, 'profile' => true]);
873                 if (!empty($photo)) {
874                         $updated = max($photo['created'], $photo['edited'], $photo['updated']);
875
876                         switch ($photo['type']) {
877                                 case 'image/png':
878                                         $imagetype = IMAGETYPE_PNG;
879                                         break;
880
881                                 case 'image/gif':
882                                         $imagetype = IMAGETYPE_PNG;
883                                         break;
884
885                                 default:
886                                         $imagetype = IMAGETYPE_JPEG;
887                                         break;
888                         }
889                 }
890
891                 return $url . $uid . image_type_to_extension($imagetype) . ($updated ? '?ts=' . strtotime($updated) : '');
892         }
893
894         /**
895          * Catch-all user creation function
896          *
897          * Creates a user from the provided data array, either form fields or OpenID.
898          * Required: { username, nickname, email } or { openid_url }
899          *
900          * Performs the following:
901          * - Sends to the OpenId auth URL (if relevant)
902          * - Creates new key pairs for crypto
903          * - Create self-contact
904          * - Create profile image
905          *
906          * @param  array $data
907          * @return array
908          * @throws ErrorException
909          * @throws HTTPException\InternalServerErrorException
910          * @throws ImagickException
911          * @throws Exception
912          */
913         public static function create(array $data)
914         {
915                 $return = ['user' => null, 'password' => ''];
916
917                 $using_invites = DI::config()->get('system', 'invitation_only');
918
919                 $invite_id  = !empty($data['invite_id'])  ? Strings::escapeTags(trim($data['invite_id']))  : '';
920                 $username   = !empty($data['username'])   ? Strings::escapeTags(trim($data['username']))   : '';
921                 $nickname   = !empty($data['nickname'])   ? Strings::escapeTags(trim($data['nickname']))   : '';
922                 $email      = !empty($data['email'])      ? Strings::escapeTags(trim($data['email']))      : '';
923                 $openid_url = !empty($data['openid_url']) ? Strings::escapeTags(trim($data['openid_url'])) : '';
924                 $photo      = !empty($data['photo'])      ? Strings::escapeTags(trim($data['photo']))      : '';
925                 $password   = !empty($data['password'])   ? trim($data['password'])           : '';
926                 $password1  = !empty($data['password1'])  ? trim($data['password1'])          : '';
927                 $confirm    = !empty($data['confirm'])    ? trim($data['confirm'])            : '';
928                 $blocked    = !empty($data['blocked']);
929                 $verified   = !empty($data['verified']);
930                 $language   = !empty($data['language'])   ? Strings::escapeTags(trim($data['language']))   : 'en';
931
932                 $netpublish = $publish = !empty($data['profile_publish_reg']);
933
934                 if ($password1 != $confirm) {
935                         throw new Exception(DI::l10n()->t('Passwords do not match. Password unchanged.'));
936                 } elseif ($password1 != '') {
937                         $password = $password1;
938                 }
939
940                 if ($using_invites) {
941                         if (!$invite_id) {
942                                 throw new Exception(DI::l10n()->t('An invitation is required.'));
943                         }
944
945                         if (!Register::existsByHash($invite_id)) {
946                                 throw new Exception(DI::l10n()->t('Invitation could not be verified.'));
947                         }
948                 }
949
950                 /// @todo Check if this part is really needed. We should have fetched all this data in advance
951                 if (empty($username) || empty($email) || empty($nickname)) {
952                         if ($openid_url) {
953                                 if (!Network::isUrlValid($openid_url)) {
954                                         throw new Exception(DI::l10n()->t('Invalid OpenID url'));
955                                 }
956                                 $_SESSION['register'] = 1;
957                                 $_SESSION['openid'] = $openid_url;
958
959                                 $openid = new LightOpenID(DI::baseUrl()->getHostname());
960                                 $openid->identity = $openid_url;
961                                 $openid->returnUrl = DI::baseUrl() . '/openid';
962                                 $openid->required = ['namePerson/friendly', 'contact/email', 'namePerson'];
963                                 $openid->optional = ['namePerson/first', 'media/image/aspect11', 'media/image/default'];
964                                 try {
965                                         $authurl = $openid->authUrl();
966                                 } catch (Exception $e) {
967                                         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);
968                                 }
969                                 System::externalRedirect($authurl);
970                                 // NOTREACHED
971                         }
972
973                         throw new Exception(DI::l10n()->t('Please enter the required information.'));
974                 }
975
976                 if (!Network::isUrlValid($openid_url)) {
977                         $openid_url = '';
978                 }
979
980                 // collapse multiple spaces in name
981                 $username = preg_replace('/ +/', ' ', $username);
982
983                 $username_min_length = max(1, min(64, intval(DI::config()->get('system', 'username_min_length', 3))));
984                 $username_max_length = max(1, min(64, intval(DI::config()->get('system', 'username_max_length', 48))));
985
986                 if ($username_min_length > $username_max_length) {
987                         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);
988                         $tmp = $username_min_length;
989                         $username_min_length = $username_max_length;
990                         $username_max_length = $tmp;
991                 }
992
993                 if (mb_strlen($username) < $username_min_length) {
994                         throw new Exception(DI::l10n()->tt('Username should be at least %s character.', 'Username should be at least %s characters.', $username_min_length));
995                 }
996
997                 if (mb_strlen($username) > $username_max_length) {
998                         throw new Exception(DI::l10n()->tt('Username should be at most %s character.', 'Username should be at most %s characters.', $username_max_length));
999                 }
1000
1001                 // So now we are just looking for a space in the full name.
1002                 $loose_reg = DI::config()->get('system', 'no_regfullname');
1003                 if (!$loose_reg) {
1004                         $username = mb_convert_case($username, MB_CASE_TITLE, 'UTF-8');
1005                         if (strpos($username, ' ') === false) {
1006                                 throw new Exception(DI::l10n()->t("That doesn't appear to be your full (First Last) name."));
1007                         }
1008                 }
1009
1010                 if (!Network::isEmailDomainAllowed($email)) {
1011                         throw new Exception(DI::l10n()->t('Your email domain is not among those allowed on this site.'));
1012                 }
1013
1014                 if (!filter_var($email, FILTER_VALIDATE_EMAIL) || !Network::isEmailDomainValid($email)) {
1015                         throw new Exception(DI::l10n()->t('Not a valid email address.'));
1016                 }
1017                 if (self::isNicknameBlocked($nickname)) {
1018                         throw new Exception(DI::l10n()->t('The nickname was blocked from registration by the nodes admin.'));
1019                 }
1020
1021                 if (DI::config()->get('system', 'block_extended_register', false) && DBA::exists('user', ['email' => $email])) {
1022                         throw new Exception(DI::l10n()->t('Cannot use that email.'));
1023                 }
1024
1025                 // Disallow somebody creating an account using openid that uses the admin email address,
1026                 // since openid bypasses email verification. We'll allow it if there is not yet an admin account.
1027                 if (DI::config()->get('config', 'admin_email') && strlen($openid_url)) {
1028                         $adminlist = explode(',', str_replace(' ', '', strtolower(DI::config()->get('config', 'admin_email'))));
1029                         if (in_array(strtolower($email), $adminlist)) {
1030                                 throw new Exception(DI::l10n()->t('Cannot use that email.'));
1031                         }
1032                 }
1033
1034                 $nickname = $data['nickname'] = strtolower($nickname);
1035
1036                 if (!preg_match('/^[a-z0-9][a-z0-9_]*$/', $nickname)) {
1037                         throw new Exception(DI::l10n()->t('Your nickname can only contain a-z, 0-9 and _.'));
1038                 }
1039
1040                 // Check existing and deleted accounts for this nickname.
1041                 if (
1042                         DBA::exists('user', ['nickname' => $nickname])
1043                         || DBA::exists('userd', ['username' => $nickname])
1044                 ) {
1045                         throw new Exception(DI::l10n()->t('Nickname is already registered. Please choose another.'));
1046                 }
1047
1048                 $new_password = strlen($password) ? $password : User::generateNewPassword();
1049                 $new_password_encoded = self::hashPassword($new_password);
1050
1051                 $return['password'] = $new_password;
1052
1053                 $keys = Crypto::newKeypair(4096);
1054                 if ($keys === false) {
1055                         throw new Exception(DI::l10n()->t('SERIOUS ERROR: Generation of security keys failed.'));
1056                 }
1057
1058                 $prvkey = $keys['prvkey'];
1059                 $pubkey = $keys['pubkey'];
1060
1061                 // Create another keypair for signing/verifying salmon protocol messages.
1062                 $sres = Crypto::newKeypair(512);
1063                 $sprvkey = $sres['prvkey'];
1064                 $spubkey = $sres['pubkey'];
1065
1066                 $insert_result = DBA::insert('user', [
1067                         'guid'     => System::createUUID(),
1068                         'username' => $username,
1069                         'password' => $new_password_encoded,
1070                         'email'    => $email,
1071                         'openid'   => $openid_url,
1072                         'nickname' => $nickname,
1073                         'pubkey'   => $pubkey,
1074                         'prvkey'   => $prvkey,
1075                         'spubkey'  => $spubkey,
1076                         'sprvkey'  => $sprvkey,
1077                         'verified' => $verified,
1078                         'blocked'  => $blocked,
1079                         'language' => $language,
1080                         'timezone' => 'UTC',
1081                         'register_date' => DateTimeFormat::utcNow(),
1082                         'default-location' => ''
1083                 ]);
1084
1085                 if ($insert_result) {
1086                         $uid = DBA::lastInsertId();
1087                         $user = DBA::selectFirst('user', [], ['uid' => $uid]);
1088                 } else {
1089                         throw new Exception(DI::l10n()->t('An error occurred during registration. Please try again.'));
1090                 }
1091
1092                 if (!$uid) {
1093                         throw new Exception(DI::l10n()->t('An error occurred during registration. Please try again.'));
1094                 }
1095
1096                 // if somebody clicked submit twice very quickly, they could end up with two accounts
1097                 // due to race condition. Remove this one.
1098                 $user_count = DBA::count('user', ['nickname' => $nickname]);
1099                 if ($user_count > 1) {
1100                         DBA::delete('user', ['uid' => $uid]);
1101
1102                         throw new Exception(DI::l10n()->t('Nickname is already registered. Please choose another.'));
1103                 }
1104
1105                 $insert_result = DBA::insert('profile', [
1106                         'uid' => $uid,
1107                         'name' => $username,
1108                         'photo' => self::getAvatarUrlForId($uid),
1109                         'thumb' => self::getAvatarUrlForId($uid, Proxy::SIZE_THUMB),
1110                         'publish' => $publish,
1111                         'net-publish' => $netpublish,
1112                 ]);
1113                 if (!$insert_result) {
1114                         DBA::delete('user', ['uid' => $uid]);
1115
1116                         throw new Exception(DI::l10n()->t('An error occurred creating your default profile. Please try again.'));
1117                 }
1118
1119                 // Create the self contact
1120                 if (!Contact::createSelfFromUserId($uid)) {
1121                         DBA::delete('user', ['uid' => $uid]);
1122
1123                         throw new Exception(DI::l10n()->t('An error occurred creating your self contact. Please try again.'));
1124                 }
1125
1126                 // Create a group with no members. This allows somebody to use it
1127                 // right away as a default group for new contacts.
1128                 $def_gid = Group::create($uid, DI::l10n()->t('Friends'));
1129                 if (!$def_gid) {
1130                         DBA::delete('user', ['uid' => $uid]);
1131
1132                         throw new Exception(DI::l10n()->t('An error occurred creating your default contact group. Please try again.'));
1133                 }
1134
1135                 $fields = ['def_gid' => $def_gid];
1136                 if (DI::config()->get('system', 'newuser_private') && $def_gid) {
1137                         $fields['allow_gid'] = '<' . $def_gid . '>';
1138                 }
1139
1140                 DBA::update('user', $fields, ['uid' => $uid]);
1141
1142                 // if we have no OpenID photo try to look up an avatar
1143                 if (!strlen($photo)) {
1144                         $photo = Network::lookupAvatarByEmail($email);
1145                 }
1146
1147                 // unless there is no avatar-addon loaded
1148                 if (strlen($photo)) {
1149                         $photo_failure = false;
1150
1151                         $filename = basename($photo);
1152                         $curlResult = DI::httpClient()->get($photo);
1153                         if ($curlResult->isSuccess()) {
1154                                 $img_str = $curlResult->getBody();
1155                                 $type = $curlResult->getContentType();
1156                         } else {
1157                                 $img_str = '';
1158                                 $type = '';
1159                         }
1160
1161                         $type = Images::getMimeTypeByData($img_str, $photo, $type);
1162
1163                         $Image = new Image($img_str, $type);
1164                         if ($Image->isValid()) {
1165                                 $Image->scaleToSquare(300);
1166
1167                                 $resource_id = Photo::newResource();
1168
1169                                 $r = Photo::store($Image, $uid, 0, $resource_id, $filename, DI::l10n()->t('Profile Photos'), 4);
1170
1171                                 if ($r === false) {
1172                                         $photo_failure = true;
1173                                 }
1174
1175                                 $Image->scaleDown(80);
1176
1177                                 $r = Photo::store($Image, $uid, 0, $resource_id, $filename, DI::l10n()->t('Profile Photos'), 5);
1178
1179                                 if ($r === false) {
1180                                         $photo_failure = true;
1181                                 }
1182
1183                                 $Image->scaleDown(48);
1184
1185                                 $r = Photo::store($Image, $uid, 0, $resource_id, $filename, DI::l10n()->t('Profile Photos'), 6);
1186
1187                                 if ($r === false) {
1188                                         $photo_failure = true;
1189                                 }
1190
1191                                 if (!$photo_failure) {
1192                                         Photo::update(['profile' => 1], ['resource-id' => $resource_id]);
1193                                 }
1194                         }
1195
1196                         Contact::updateSelfFromUserID($uid, true);
1197                 }
1198
1199                 Hook::callAll('register_account', $uid);
1200
1201                 $return['user'] = $user;
1202                 return $return;
1203         }
1204
1205         /**
1206          * Update a user entry and distribute the changes if needed
1207          *
1208          * @param array $fields
1209          * @param integer $uid
1210          * @return boolean
1211          */
1212         public static function update(array $fields, int $uid): bool
1213         {
1214                 $old_owner = self::getOwnerDataById($uid);
1215                 if (empty($old_owner)) {
1216                         return false;
1217                 }
1218
1219                 if (!DBA::update('user', $fields, ['uid' => $uid])) {
1220                         return false;
1221                 }
1222
1223                 $update = Contact::updateSelfFromUserID($uid);
1224
1225                 $owner = self::getOwnerDataById($uid);
1226                 if (empty($owner)) {
1227                         return false;
1228                 }
1229
1230                 if ($old_owner['name'] != $owner['name']) {
1231                         Profile::update(['name' => $owner['name']], $uid);
1232                 }
1233
1234                 if ($update) {
1235                         Profile::publishUpdate($uid);
1236                 }
1237
1238                 return true;
1239         }
1240
1241         /**
1242          * Sets block state for a given user
1243          *
1244          * @param int  $uid   The user id
1245          * @param bool $block Block state (default is true)
1246          *
1247          * @return bool True, if successfully blocked
1248
1249          * @throws Exception
1250          */
1251         public static function block(int $uid, bool $block = true)
1252         {
1253                 return DBA::update('user', ['blocked' => $block], ['uid' => $uid]);
1254         }
1255
1256         /**
1257          * Allows a registration based on a hash
1258          *
1259          * @param string $hash
1260          *
1261          * @return bool True, if the allow was successful
1262          *
1263          * @throws HTTPException\InternalServerErrorException
1264          * @throws Exception
1265          */
1266         public static function allow(string $hash)
1267         {
1268                 $register = Register::getByHash($hash);
1269                 if (!DBA::isResult($register)) {
1270                         return false;
1271                 }
1272
1273                 $user = User::getById($register['uid']);
1274                 if (!DBA::isResult($user)) {
1275                         return false;
1276                 }
1277
1278                 Register::deleteByHash($hash);
1279
1280                 DBA::update('user', ['blocked' => false, 'verified' => true], ['uid' => $register['uid']]);
1281
1282                 $profile = DBA::selectFirst('profile', ['net-publish'], ['uid' => $register['uid']]);
1283
1284                 if (DBA::isResult($profile) && $profile['net-publish'] && DI::config()->get('system', 'directory')) {
1285                         $url = DI::baseUrl() . '/profile/' . $user['nickname'];
1286                         Worker::add(PRIORITY_LOW, "Directory", $url);
1287                 }
1288
1289                 $l10n = DI::l10n()->withLang($register['language']);
1290
1291                 return User::sendRegisterOpenEmail(
1292                         $l10n,
1293                         $user,
1294                         DI::config()->get('config', 'sitename'),
1295                         DI::baseUrl()->get(),
1296                         ($register['password'] ?? '') ?: 'Sent in a previous email'
1297                 );
1298         }
1299
1300         /**
1301          * Denys a pending registration
1302          *
1303          * @param string $hash The hash of the pending user
1304          *
1305          * This does not have to go through user_remove() and save the nickname
1306          * permanently against re-registration, as the person was not yet
1307          * allowed to have friends on this system
1308          *
1309          * @return bool True, if the deny was successfull
1310          * @throws Exception
1311          */
1312         public static function deny(string $hash)
1313         {
1314                 $register = Register::getByHash($hash);
1315                 if (!DBA::isResult($register)) {
1316                         return false;
1317                 }
1318
1319                 $user = User::getById($register['uid']);
1320                 if (!DBA::isResult($user)) {
1321                         return false;
1322                 }
1323
1324                 // Delete the avatar
1325                 Photo::delete(['uid' => $register['uid']]);
1326
1327                 return DBA::delete('user', ['uid' => $register['uid']]) &&
1328                        Register::deleteByHash($register['hash']);
1329         }
1330
1331         /**
1332          * Creates a new user based on a minimal set and sends an email to this user
1333          *
1334          * @param string $name  The user's name
1335          * @param string $email The user's email address
1336          * @param string $nick  The user's nick name
1337          * @param string $lang  The user's language (default is english)
1338          *
1339          * @return bool True, if the user was created successfully
1340          * @throws HTTPException\InternalServerErrorException
1341          * @throws ErrorException
1342          * @throws ImagickException
1343          */
1344         public static function createMinimal(string $name, string $email, string $nick, string $lang = L10n::DEFAULT)
1345         {
1346                 if (empty($name) ||
1347                     empty($email) ||
1348                     empty($nick)) {
1349                         throw new HTTPException\InternalServerErrorException('Invalid arguments.');
1350                 }
1351
1352                 $result = self::create([
1353                         'username' => $name,
1354                         'email' => $email,
1355                         'nickname' => $nick,
1356                         'verified' => 1,
1357                         'language' => $lang
1358                 ]);
1359
1360                 $user = $result['user'];
1361                 $preamble = Strings::deindent(DI::l10n()->t('
1362                 Dear %1$s,
1363                         the administrator of %2$s has set up an account for you.'));
1364                 $body = Strings::deindent(DI::l10n()->t('
1365                 The login details are as follows:
1366
1367                 Site Location:  %1$s
1368                 Login Name:             %2$s
1369                 Password:               %3$s
1370
1371                 You may change your password from your account "Settings" page after logging
1372                 in.
1373
1374                 Please take a few moments to review the other account settings on that page.
1375
1376                 You may also wish to add some basic information to your default profile
1377                 (on the "Profiles" page) so that other people can easily find you.
1378
1379                 We recommend setting your full name, adding a profile photo,
1380                 adding some profile "keywords" (very useful in making new friends) - and
1381                 perhaps what country you live in; if you do not wish to be more specific
1382                 than that.
1383
1384                 We fully respect your right to privacy, and none of these items are necessary.
1385                 If you are new and do not know anybody here, they may help
1386                 you to make some new and interesting friends.
1387
1388                 If you ever want to delete your account, you can do so at %1$s/removeme
1389
1390                 Thank you and welcome to %4$s.'));
1391
1392                 $preamble = sprintf($preamble, $user['username'], DI::config()->get('config', 'sitename'));
1393                 $body = sprintf($body, DI::baseUrl()->get(), $user['nickname'], $result['password'], DI::config()->get('config', 'sitename'));
1394
1395                 $email = DI::emailer()
1396                         ->newSystemMail()
1397                         ->withMessage(DI::l10n()->t('Registration details for %s', DI::config()->get('config', 'sitename')), $preamble, $body)
1398                         ->forUser($user)
1399                         ->withRecipient($user['email'])
1400                         ->build();
1401                 return DI::emailer()->send($email);
1402         }
1403
1404         /**
1405          * Sends pending registration confirmation email
1406          *
1407          * @param array  $user     User record array
1408          * @param string $sitename
1409          * @param string $siteurl
1410          * @param string $password Plaintext password
1411          * @return NULL|boolean from notification() and email() inherited
1412          * @throws HTTPException\InternalServerErrorException
1413          */
1414         public static function sendRegisterPendingEmail($user, $sitename, $siteurl, $password)
1415         {
1416                 $body = Strings::deindent(DI::l10n()->t(
1417                         '
1418                         Dear %1$s,
1419                                 Thank you for registering at %2$s. Your account is pending for approval by the administrator.
1420
1421                         Your login details are as follows:
1422
1423                         Site Location:  %3$s
1424                         Login Name:             %4$s
1425                         Password:               %5$s
1426                 ',
1427                         $user['username'],
1428                         $sitename,
1429                         $siteurl,
1430                         $user['nickname'],
1431                         $password
1432                 ));
1433
1434                 $email = DI::emailer()
1435                         ->newSystemMail()
1436                         ->withMessage(DI::l10n()->t('Registration at %s', $sitename), $body)
1437                         ->forUser($user)
1438                         ->withRecipient($user['email'])
1439                         ->build();
1440                 return DI::emailer()->send($email);
1441         }
1442
1443         /**
1444          * Sends registration confirmation
1445          *
1446          * It's here as a function because the mail is sent from different parts
1447          *
1448          * @param L10n   $l10n     The used language
1449          * @param array  $user     User record array
1450          * @param string $sitename
1451          * @param string $siteurl
1452          * @param string $password Plaintext password
1453          *
1454          * @return NULL|boolean from notification() and email() inherited
1455          * @throws HTTPException\InternalServerErrorException
1456          */
1457         public static function sendRegisterOpenEmail(L10n $l10n, $user, $sitename, $siteurl, $password)
1458         {
1459                 $preamble = Strings::deindent($l10n->t(
1460                         '
1461                                 Dear %1$s,
1462                                 Thank you for registering at %2$s. Your account has been created.
1463                         ',
1464                         $user['username'],
1465                         $sitename
1466                 ));
1467                 $body = Strings::deindent($l10n->t(
1468                         '
1469                         The login details are as follows:
1470
1471                         Site Location:  %3$s
1472                         Login Name:             %1$s
1473                         Password:               %5$s
1474
1475                         You may change your password from your account "Settings" page after logging
1476                         in.
1477
1478                         Please take a few moments to review the other account settings on that page.
1479
1480                         You may also wish to add some basic information to your default profile
1481                         ' . "\x28" . 'on the "Profiles" page' . "\x29" . ' so that other people can easily find you.
1482
1483                         We recommend setting your full name, adding a profile photo,
1484                         adding some profile "keywords" ' . "\x28" . 'very useful in making new friends' . "\x29" . ' - and
1485                         perhaps what country you live in; if you do not wish to be more specific
1486                         than that.
1487
1488                         We fully respect your right to privacy, and none of these items are necessary.
1489                         If you are new and do not know anybody here, they may help
1490                         you to make some new and interesting friends.
1491
1492                         If you ever want to delete your account, you can do so at %3$s/removeme
1493
1494                         Thank you and welcome to %2$s.',
1495                         $user['nickname'],
1496                         $sitename,
1497                         $siteurl,
1498                         $user['username'],
1499                         $password
1500                 ));
1501
1502                 $email = DI::emailer()
1503                         ->newSystemMail()
1504                         ->withMessage(DI::l10n()->t('Registration details for %s', $sitename), $preamble, $body)
1505                         ->forUser($user)
1506                         ->withRecipient($user['email'])
1507                         ->build();
1508                 return DI::emailer()->send($email);
1509         }
1510
1511         /**
1512          * @param int $uid user to remove
1513          * @return bool
1514          * @throws HTTPException\InternalServerErrorException
1515          */
1516         public static function remove(int $uid)
1517         {
1518                 if (empty($uid)) {
1519                         return false;
1520                 }
1521
1522                 Logger::log('Removing user: ' . $uid);
1523
1524                 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
1525
1526                 Hook::callAll('remove_user', $user);
1527
1528                 // save username (actually the nickname as it is guaranteed
1529                 // unique), so it cannot be re-registered in the future.
1530                 DBA::insert('userd', ['username' => $user['nickname']]);
1531
1532                 // Remove all personal settings, especially connector settings
1533                 DBA::delete('pconfig', ['uid' => $uid]);
1534
1535                 // The user and related data will be deleted in Friendica\Worker\ExpireAndRemoveUsers
1536                 DBA::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utc('now + 7 day')], ['uid' => $uid]);
1537                 Worker::add(PRIORITY_HIGH, 'Notifier', Delivery::REMOVAL, $uid);
1538
1539                 // Send an update to the directory
1540                 $self = DBA::selectFirst('contact', ['url'], ['uid' => $uid, 'self' => true]);
1541                 Worker::add(PRIORITY_LOW, 'Directory', $self['url']);
1542
1543                 // Remove the user relevant data
1544                 Worker::add(PRIORITY_NEGLIGIBLE, 'RemoveUser', $uid);
1545
1546                 return true;
1547         }
1548
1549         /**
1550          * Return all identities to a user
1551          *
1552          * @param int $uid The user id
1553          * @return array All identities for this user
1554          *
1555          * Example for a return:
1556          *    [
1557          *        [
1558          *            'uid' => 1,
1559          *            'username' => 'maxmuster',
1560          *            'nickname' => 'Max Mustermann'
1561          *        ],
1562          *        [
1563          *            'uid' => 2,
1564          *            'username' => 'johndoe',
1565          *            'nickname' => 'John Doe'
1566          *        ]
1567          *    ]
1568          * @throws Exception
1569          */
1570         public static function identities($uid)
1571         {
1572                 if (empty($uid)) {
1573                         return [];
1574                 }
1575
1576                 $identities = [];
1577
1578                 $user = DBA::selectFirst('user', ['uid', 'nickname', 'username', 'parent-uid'], ['uid' => $uid]);
1579                 if (!DBA::isResult($user)) {
1580                         return $identities;
1581                 }
1582
1583                 if ($user['parent-uid'] == 0) {
1584                         // First add our own entry
1585                         $identities = [[
1586                                 'uid' => $user['uid'],
1587                                 'username' => $user['username'],
1588                                 'nickname' => $user['nickname']
1589                         ]];
1590
1591                         // Then add all the children
1592                         $r = DBA::select(
1593                                 'user',
1594                                 ['uid', 'username', 'nickname'],
1595                                 ['parent-uid' => $user['uid'], 'account_removed' => false]
1596                         );
1597                         if (DBA::isResult($r)) {
1598                                 $identities = array_merge($identities, DBA::toArray($r));
1599                         }
1600                 } else {
1601                         // First entry is our parent
1602                         $r = DBA::select(
1603                                 'user',
1604                                 ['uid', 'username', 'nickname'],
1605                                 ['uid' => $user['parent-uid'], 'account_removed' => false]
1606                         );
1607                         if (DBA::isResult($r)) {
1608                                 $identities = DBA::toArray($r);
1609                         }
1610
1611                         // Then add all siblings
1612                         $r = DBA::select(
1613                                 'user',
1614                                 ['uid', 'username', 'nickname'],
1615                                 ['parent-uid' => $user['parent-uid'], 'account_removed' => false]
1616                         );
1617                         if (DBA::isResult($r)) {
1618                                 $identities = array_merge($identities, DBA::toArray($r));
1619                         }
1620                 }
1621
1622                 $r = DBA::p(
1623                         "SELECT `user`.`uid`, `user`.`username`, `user`.`nickname`
1624                         FROM `manage`
1625                         INNER JOIN `user` ON `manage`.`mid` = `user`.`uid`
1626                         WHERE `user`.`account_removed` = 0 AND `manage`.`uid` = ?",
1627                         $user['uid']
1628                 );
1629                 if (DBA::isResult($r)) {
1630                         $identities = array_merge($identities, DBA::toArray($r));
1631                 }
1632
1633                 return $identities;
1634         }
1635
1636         /**
1637          * Check if the given user id has delegations or is delegated
1638          *
1639          * @param int $uid
1640          * @return bool
1641          */
1642         public static function hasIdentities(int $uid):bool
1643         {
1644                 if (empty($uid)) {
1645                         return false;
1646                 }
1647
1648                 $user = DBA::selectFirst('user', ['parent-uid'], ['uid' => $uid, 'account_removed' => false]);
1649                 if (!DBA::isResult($user)) {
1650                         return false;
1651                 }
1652
1653                 if ($user['parent-uid'] != 0) {
1654                         return true;
1655                 }
1656
1657                 if (DBA::exists('user', ['parent-uid' => $uid, 'account_removed' => false])) {
1658                         return true;
1659                 }
1660
1661                 if (DBA::exists('manage', ['uid' => $uid])) {
1662                         return true;
1663                 }
1664
1665                 return false;
1666         }
1667
1668         /**
1669          * Returns statistical information about the current users of this node
1670          *
1671          * @return array
1672          *
1673          * @throws Exception
1674          */
1675         public static function getStatistics()
1676         {
1677                 $statistics = [
1678                         'total_users'           => 0,
1679                         'active_users_halfyear' => 0,
1680                         'active_users_monthly'  => 0,
1681                         'active_users_weekly'   => 0,
1682                 ];
1683
1684                 $userStmt = DBA::select('owner-view', ['uid', 'login_date', 'last-item'],
1685                         ["`verified` AND `login_date` > ? AND NOT `blocked`
1686                         AND NOT `account_removed` AND NOT `account_expired`",
1687                         DBA::NULL_DATETIME]);
1688                 if (!DBA::isResult($userStmt)) {
1689                         return $statistics;
1690                 }
1691
1692                 $halfyear = time() - (180 * 24 * 60 * 60);
1693                 $month = time() - (30 * 24 * 60 * 60);
1694                 $week = time() - (7 * 24 * 60 * 60);
1695
1696                 while ($user = DBA::fetch($userStmt)) {
1697                         $statistics['total_users']++;
1698
1699                         if ((strtotime($user['login_date']) > $halfyear) || (strtotime($user['last-item']) > $halfyear)
1700                         ) {
1701                                 $statistics['active_users_halfyear']++;
1702                         }
1703
1704                         if ((strtotime($user['login_date']) > $month) || (strtotime($user['last-item']) > $month)
1705                         ) {
1706                                 $statistics['active_users_monthly']++;
1707                         }
1708
1709                         if ((strtotime($user['login_date']) > $week) || (strtotime($user['last-item']) > $week)
1710                         ) {
1711                                 $statistics['active_users_weekly']++;
1712                         }
1713                 }
1714                 DBA::close($userStmt);
1715
1716                 return $statistics;
1717         }
1718
1719         /**
1720          * Get all users of the current node
1721          *
1722          * @param int    $start Start count (Default is 0)
1723          * @param int    $count Count of the items per page (Default is @see Pager::ITEMS_PER_PAGE)
1724          * @param string $type  The type of users, which should get (all, bocked, removed)
1725          * @param string $order Order of the user list (Default is 'contact.name')
1726          * @param bool   $descending Order direction (Default is ascending)
1727          *
1728          * @return array The list of the users
1729          * @throws Exception
1730          */
1731         public static function getList($start = 0, $count = Pager::ITEMS_PER_PAGE, $type = 'all', $order = 'name', bool $descending = false)
1732         {
1733                 $param = ['limit' => [$start, $count], 'order' => [$order => $descending]];
1734                 $condition = [];
1735                 switch ($type) {
1736                         case 'active':
1737                                 $condition['account_removed'] = false;
1738                                 $condition['blocked'] = false;
1739                                 break;
1740                         case 'blocked':
1741                                 $condition['account_removed'] = false;
1742                                 $condition['blocked'] = true;
1743                                 $condition['verified'] = true;
1744                                 break;
1745                         case 'removed':
1746                                 $condition['account_removed'] = true;
1747                                 break;
1748                 }
1749
1750                 return DBA::selectToArray('owner-view', [], $condition, $param);
1751         }
1752 }