]> git.mxchange.org Git - friendica.git/blob - src/Model/User.php
92f50dd25cdc89e920e67c80dd0f664cf25b4e9e
[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' => System::getTempPath() . '/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::error(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));
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                                 // Not using Photo::PROFILE_PHOTOS here, so that it is discovered as translateble string
1165                                 $profile_album = DI::l10n()->t('Profile Photos');
1166
1167                                 $r = Photo::store($Image, $uid, 0, $resource_id, $filename, $profile_album, 4);
1168
1169                                 if ($r === false) {
1170                                         $photo_failure = true;
1171                                 }
1172
1173                                 $Image->scaleDown(80);
1174
1175                                 $r = Photo::store($Image, $uid, 0, $resource_id, $filename, $profile_album, 5);
1176
1177                                 if ($r === false) {
1178                                         $photo_failure = true;
1179                                 }
1180
1181                                 $Image->scaleDown(48);
1182
1183                                 $r = Photo::store($Image, $uid, 0, $resource_id, $filename, $profile_album, 6);
1184
1185                                 if ($r === false) {
1186                                         $photo_failure = true;
1187                                 }
1188
1189                                 if (!$photo_failure) {
1190                                         Photo::update(['profile' => true, 'photo-type' => Photo::USER_AVATAR], ['resource-id' => $resource_id]);
1191                                 }
1192                         }
1193
1194                         Contact::updateSelfFromUserID($uid, true);
1195                 }
1196
1197                 Hook::callAll('register_account', $uid);
1198
1199                 $return['user'] = $user;
1200                 return $return;
1201         }
1202
1203         /**
1204          * Update a user entry and distribute the changes if needed
1205          *
1206          * @param array $fields
1207          * @param integer $uid
1208          * @return boolean
1209          */
1210         public static function update(array $fields, int $uid): bool
1211         {
1212                 $old_owner = self::getOwnerDataById($uid);
1213                 if (empty($old_owner)) {
1214                         return false;
1215                 }
1216
1217                 if (!DBA::update('user', $fields, ['uid' => $uid])) {
1218                         return false;
1219                 }
1220
1221                 $update = Contact::updateSelfFromUserID($uid);
1222
1223                 $owner = self::getOwnerDataById($uid);
1224                 if (empty($owner)) {
1225                         return false;
1226                 }
1227
1228                 if ($old_owner['name'] != $owner['name']) {
1229                         Profile::update(['name' => $owner['name']], $uid);
1230                 }
1231
1232                 if ($update) {
1233                         Profile::publishUpdate($uid);
1234                 }
1235
1236                 return true;
1237         }
1238
1239         /**
1240          * Sets block state for a given user
1241          *
1242          * @param int  $uid   The user id
1243          * @param bool $block Block state (default is true)
1244          *
1245          * @return bool True, if successfully blocked
1246
1247          * @throws Exception
1248          */
1249         public static function block(int $uid, bool $block = true)
1250         {
1251                 return DBA::update('user', ['blocked' => $block], ['uid' => $uid]);
1252         }
1253
1254         /**
1255          * Allows a registration based on a hash
1256          *
1257          * @param string $hash
1258          *
1259          * @return bool True, if the allow was successful
1260          *
1261          * @throws HTTPException\InternalServerErrorException
1262          * @throws Exception
1263          */
1264         public static function allow(string $hash)
1265         {
1266                 $register = Register::getByHash($hash);
1267                 if (!DBA::isResult($register)) {
1268                         return false;
1269                 }
1270
1271                 $user = User::getById($register['uid']);
1272                 if (!DBA::isResult($user)) {
1273                         return false;
1274                 }
1275
1276                 Register::deleteByHash($hash);
1277
1278                 DBA::update('user', ['blocked' => false, 'verified' => true], ['uid' => $register['uid']]);
1279
1280                 $profile = DBA::selectFirst('profile', ['net-publish'], ['uid' => $register['uid']]);
1281
1282                 if (DBA::isResult($profile) && $profile['net-publish'] && DI::config()->get('system', 'directory')) {
1283                         $url = DI::baseUrl() . '/profile/' . $user['nickname'];
1284                         Worker::add(PRIORITY_LOW, "Directory", $url);
1285                 }
1286
1287                 $l10n = DI::l10n()->withLang($register['language']);
1288
1289                 return User::sendRegisterOpenEmail(
1290                         $l10n,
1291                         $user,
1292                         DI::config()->get('config', 'sitename'),
1293                         DI::baseUrl()->get(),
1294                         ($register['password'] ?? '') ?: 'Sent in a previous email'
1295                 );
1296         }
1297
1298         /**
1299          * Denys a pending registration
1300          *
1301          * @param string $hash The hash of the pending user
1302          *
1303          * This does not have to go through user_remove() and save the nickname
1304          * permanently against re-registration, as the person was not yet
1305          * allowed to have friends on this system
1306          *
1307          * @return bool True, if the deny was successfull
1308          * @throws Exception
1309          */
1310         public static function deny(string $hash)
1311         {
1312                 $register = Register::getByHash($hash);
1313                 if (!DBA::isResult($register)) {
1314                         return false;
1315                 }
1316
1317                 $user = User::getById($register['uid']);
1318                 if (!DBA::isResult($user)) {
1319                         return false;
1320                 }
1321
1322                 // Delete the avatar
1323                 Photo::delete(['uid' => $register['uid']]);
1324
1325                 return DBA::delete('user', ['uid' => $register['uid']]) &&
1326                        Register::deleteByHash($register['hash']);
1327         }
1328
1329         /**
1330          * Creates a new user based on a minimal set and sends an email to this user
1331          *
1332          * @param string $name  The user's name
1333          * @param string $email The user's email address
1334          * @param string $nick  The user's nick name
1335          * @param string $lang  The user's language (default is english)
1336          *
1337          * @return bool True, if the user was created successfully
1338          * @throws HTTPException\InternalServerErrorException
1339          * @throws ErrorException
1340          * @throws ImagickException
1341          */
1342         public static function createMinimal(string $name, string $email, string $nick, string $lang = L10n::DEFAULT)
1343         {
1344                 if (empty($name) ||
1345                     empty($email) ||
1346                     empty($nick)) {
1347                         throw new HTTPException\InternalServerErrorException('Invalid arguments.');
1348                 }
1349
1350                 $result = self::create([
1351                         'username' => $name,
1352                         'email' => $email,
1353                         'nickname' => $nick,
1354                         'verified' => 1,
1355                         'language' => $lang
1356                 ]);
1357
1358                 $user = $result['user'];
1359                 $preamble = Strings::deindent(DI::l10n()->t('
1360                 Dear %1$s,
1361                         the administrator of %2$s has set up an account for you.'));
1362                 $body = Strings::deindent(DI::l10n()->t('
1363                 The login details are as follows:
1364
1365                 Site Location:  %1$s
1366                 Login Name:             %2$s
1367                 Password:               %3$s
1368
1369                 You may change your password from your account "Settings" page after logging
1370                 in.
1371
1372                 Please take a few moments to review the other account settings on that page.
1373
1374                 You may also wish to add some basic information to your default profile
1375                 (on the "Profiles" page) so that other people can easily find you.
1376
1377                 We recommend setting your full name, adding a profile photo,
1378                 adding some profile "keywords" (very useful in making new friends) - and
1379                 perhaps what country you live in; if you do not wish to be more specific
1380                 than that.
1381
1382                 We fully respect your right to privacy, and none of these items are necessary.
1383                 If you are new and do not know anybody here, they may help
1384                 you to make some new and interesting friends.
1385
1386                 If you ever want to delete your account, you can do so at %1$s/removeme
1387
1388                 Thank you and welcome to %4$s.'));
1389
1390                 $preamble = sprintf($preamble, $user['username'], DI::config()->get('config', 'sitename'));
1391                 $body = sprintf($body, DI::baseUrl()->get(), $user['nickname'], $result['password'], DI::config()->get('config', 'sitename'));
1392
1393                 $email = DI::emailer()
1394                         ->newSystemMail()
1395                         ->withMessage(DI::l10n()->t('Registration details for %s', DI::config()->get('config', 'sitename')), $preamble, $body)
1396                         ->forUser($user)
1397                         ->withRecipient($user['email'])
1398                         ->build();
1399                 return DI::emailer()->send($email);
1400         }
1401
1402         /**
1403          * Sends pending registration confirmation email
1404          *
1405          * @param array  $user     User record array
1406          * @param string $sitename
1407          * @param string $siteurl
1408          * @param string $password Plaintext password
1409          * @return NULL|boolean from notification() and email() inherited
1410          * @throws HTTPException\InternalServerErrorException
1411          */
1412         public static function sendRegisterPendingEmail($user, $sitename, $siteurl, $password)
1413         {
1414                 $body = Strings::deindent(DI::l10n()->t(
1415                         '
1416                         Dear %1$s,
1417                                 Thank you for registering at %2$s. Your account is pending for approval by the administrator.
1418
1419                         Your login details are as follows:
1420
1421                         Site Location:  %3$s
1422                         Login Name:             %4$s
1423                         Password:               %5$s
1424                 ',
1425                         $user['username'],
1426                         $sitename,
1427                         $siteurl,
1428                         $user['nickname'],
1429                         $password
1430                 ));
1431
1432                 $email = DI::emailer()
1433                         ->newSystemMail()
1434                         ->withMessage(DI::l10n()->t('Registration at %s', $sitename), $body)
1435                         ->forUser($user)
1436                         ->withRecipient($user['email'])
1437                         ->build();
1438                 return DI::emailer()->send($email);
1439         }
1440
1441         /**
1442          * Sends registration confirmation
1443          *
1444          * It's here as a function because the mail is sent from different parts
1445          *
1446          * @param L10n   $l10n     The used language
1447          * @param array  $user     User record array
1448          * @param string $sitename
1449          * @param string $siteurl
1450          * @param string $password Plaintext password
1451          *
1452          * @return NULL|boolean from notification() and email() inherited
1453          * @throws HTTPException\InternalServerErrorException
1454          */
1455         public static function sendRegisterOpenEmail(L10n $l10n, $user, $sitename, $siteurl, $password)
1456         {
1457                 $preamble = Strings::deindent($l10n->t(
1458                         '
1459                                 Dear %1$s,
1460                                 Thank you for registering at %2$s. Your account has been created.
1461                         ',
1462                         $user['username'],
1463                         $sitename
1464                 ));
1465                 $body = Strings::deindent($l10n->t(
1466                         '
1467                         The login details are as follows:
1468
1469                         Site Location:  %3$s
1470                         Login Name:             %1$s
1471                         Password:               %5$s
1472
1473                         You may change your password from your account "Settings" page after logging
1474                         in.
1475
1476                         Please take a few moments to review the other account settings on that page.
1477
1478                         You may also wish to add some basic information to your default profile
1479                         ' . "\x28" . 'on the "Profiles" page' . "\x29" . ' so that other people can easily find you.
1480
1481                         We recommend setting your full name, adding a profile photo,
1482                         adding some profile "keywords" ' . "\x28" . 'very useful in making new friends' . "\x29" . ' - and
1483                         perhaps what country you live in; if you do not wish to be more specific
1484                         than that.
1485
1486                         We fully respect your right to privacy, and none of these items are necessary.
1487                         If you are new and do not know anybody here, they may help
1488                         you to make some new and interesting friends.
1489
1490                         If you ever want to delete your account, you can do so at %3$s/removeme
1491
1492                         Thank you and welcome to %2$s.',
1493                         $user['nickname'],
1494                         $sitename,
1495                         $siteurl,
1496                         $user['username'],
1497                         $password
1498                 ));
1499
1500                 $email = DI::emailer()
1501                         ->newSystemMail()
1502                         ->withMessage(DI::l10n()->t('Registration details for %s', $sitename), $preamble, $body)
1503                         ->forUser($user)
1504                         ->withRecipient($user['email'])
1505                         ->build();
1506                 return DI::emailer()->send($email);
1507         }
1508
1509         /**
1510          * @param int $uid user to remove
1511          * @return bool
1512          * @throws HTTPException\InternalServerErrorException
1513          */
1514         public static function remove(int $uid)
1515         {
1516                 if (empty($uid)) {
1517                         return false;
1518                 }
1519
1520                 Logger::notice('Removing user', ['user' => $uid]);
1521
1522                 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
1523
1524                 Hook::callAll('remove_user', $user);
1525
1526                 // save username (actually the nickname as it is guaranteed
1527                 // unique), so it cannot be re-registered in the future.
1528                 DBA::insert('userd', ['username' => $user['nickname']]);
1529
1530                 // Remove all personal settings, especially connector settings
1531                 DBA::delete('pconfig', ['uid' => $uid]);
1532
1533                 // The user and related data will be deleted in Friendica\Worker\ExpireAndRemoveUsers
1534                 DBA::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utc('now + 7 day')], ['uid' => $uid]);
1535                 Worker::add(PRIORITY_HIGH, 'Notifier', Delivery::REMOVAL, $uid);
1536
1537                 // Send an update to the directory
1538                 $self = DBA::selectFirst('contact', ['url'], ['uid' => $uid, 'self' => true]);
1539                 Worker::add(PRIORITY_LOW, 'Directory', $self['url']);
1540
1541                 // Remove the user relevant data
1542                 Worker::add(PRIORITY_NEGLIGIBLE, 'RemoveUser', $uid);
1543
1544                 return true;
1545         }
1546
1547         /**
1548          * Return all identities to a user
1549          *
1550          * @param int $uid The user id
1551          * @return array All identities for this user
1552          *
1553          * Example for a return:
1554          *    [
1555          *        [
1556          *            'uid' => 1,
1557          *            'username' => 'maxmuster',
1558          *            'nickname' => 'Max Mustermann'
1559          *        ],
1560          *        [
1561          *            'uid' => 2,
1562          *            'username' => 'johndoe',
1563          *            'nickname' => 'John Doe'
1564          *        ]
1565          *    ]
1566          * @throws Exception
1567          */
1568         public static function identities($uid)
1569         {
1570                 if (empty($uid)) {
1571                         return [];
1572                 }
1573
1574                 $identities = [];
1575
1576                 $user = DBA::selectFirst('user', ['uid', 'nickname', 'username', 'parent-uid'], ['uid' => $uid]);
1577                 if (!DBA::isResult($user)) {
1578                         return $identities;
1579                 }
1580
1581                 if ($user['parent-uid'] == 0) {
1582                         // First add our own entry
1583                         $identities = [[
1584                                 'uid' => $user['uid'],
1585                                 'username' => $user['username'],
1586                                 'nickname' => $user['nickname']
1587                         ]];
1588
1589                         // Then add all the children
1590                         $r = DBA::select(
1591                                 'user',
1592                                 ['uid', 'username', 'nickname'],
1593                                 ['parent-uid' => $user['uid'], 'account_removed' => false]
1594                         );
1595                         if (DBA::isResult($r)) {
1596                                 $identities = array_merge($identities, DBA::toArray($r));
1597                         }
1598                 } else {
1599                         // First entry is our parent
1600                         $r = DBA::select(
1601                                 'user',
1602                                 ['uid', 'username', 'nickname'],
1603                                 ['uid' => $user['parent-uid'], 'account_removed' => false]
1604                         );
1605                         if (DBA::isResult($r)) {
1606                                 $identities = DBA::toArray($r);
1607                         }
1608
1609                         // Then add all siblings
1610                         $r = DBA::select(
1611                                 'user',
1612                                 ['uid', 'username', 'nickname'],
1613                                 ['parent-uid' => $user['parent-uid'], 'account_removed' => false]
1614                         );
1615                         if (DBA::isResult($r)) {
1616                                 $identities = array_merge($identities, DBA::toArray($r));
1617                         }
1618                 }
1619
1620                 $r = DBA::p(
1621                         "SELECT `user`.`uid`, `user`.`username`, `user`.`nickname`
1622                         FROM `manage`
1623                         INNER JOIN `user` ON `manage`.`mid` = `user`.`uid`
1624                         WHERE `user`.`account_removed` = 0 AND `manage`.`uid` = ?",
1625                         $user['uid']
1626                 );
1627                 if (DBA::isResult($r)) {
1628                         $identities = array_merge($identities, DBA::toArray($r));
1629                 }
1630
1631                 return $identities;
1632         }
1633
1634         /**
1635          * Check if the given user id has delegations or is delegated
1636          *
1637          * @param int $uid
1638          * @return bool
1639          */
1640         public static function hasIdentities(int $uid):bool
1641         {
1642                 if (empty($uid)) {
1643                         return false;
1644                 }
1645
1646                 $user = DBA::selectFirst('user', ['parent-uid'], ['uid' => $uid, 'account_removed' => false]);
1647                 if (!DBA::isResult($user)) {
1648                         return false;
1649                 }
1650
1651                 if ($user['parent-uid'] != 0) {
1652                         return true;
1653                 }
1654
1655                 if (DBA::exists('user', ['parent-uid' => $uid, 'account_removed' => false])) {
1656                         return true;
1657                 }
1658
1659                 if (DBA::exists('manage', ['uid' => $uid])) {
1660                         return true;
1661                 }
1662
1663                 return false;
1664         }
1665
1666         /**
1667          * Returns statistical information about the current users of this node
1668          *
1669          * @return array
1670          *
1671          * @throws Exception
1672          */
1673         public static function getStatistics()
1674         {
1675                 $statistics = [
1676                         'total_users'           => 0,
1677                         'active_users_halfyear' => 0,
1678                         'active_users_monthly'  => 0,
1679                         'active_users_weekly'   => 0,
1680                 ];
1681
1682                 $userStmt = DBA::select('owner-view', ['uid', 'login_date', 'last-item'],
1683                         ["`verified` AND `login_date` > ? AND NOT `blocked`
1684                         AND NOT `account_removed` AND NOT `account_expired`",
1685                         DBA::NULL_DATETIME]);
1686                 if (!DBA::isResult($userStmt)) {
1687                         return $statistics;
1688                 }
1689
1690                 $halfyear = time() - (180 * 24 * 60 * 60);
1691                 $month = time() - (30 * 24 * 60 * 60);
1692                 $week = time() - (7 * 24 * 60 * 60);
1693
1694                 while ($user = DBA::fetch($userStmt)) {
1695                         $statistics['total_users']++;
1696
1697                         if ((strtotime($user['login_date']) > $halfyear) || (strtotime($user['last-item']) > $halfyear)
1698                         ) {
1699                                 $statistics['active_users_halfyear']++;
1700                         }
1701
1702                         if ((strtotime($user['login_date']) > $month) || (strtotime($user['last-item']) > $month)
1703                         ) {
1704                                 $statistics['active_users_monthly']++;
1705                         }
1706
1707                         if ((strtotime($user['login_date']) > $week) || (strtotime($user['last-item']) > $week)
1708                         ) {
1709                                 $statistics['active_users_weekly']++;
1710                         }
1711                 }
1712                 DBA::close($userStmt);
1713
1714                 return $statistics;
1715         }
1716
1717         /**
1718          * Get all users of the current node
1719          *
1720          * @param int    $start Start count (Default is 0)
1721          * @param int    $count Count of the items per page (Default is @see Pager::ITEMS_PER_PAGE)
1722          * @param string $type  The type of users, which should get (all, bocked, removed)
1723          * @param string $order Order of the user list (Default is 'contact.name')
1724          * @param bool   $descending Order direction (Default is ascending)
1725          *
1726          * @return array The list of the users
1727          * @throws Exception
1728          */
1729         public static function getList($start = 0, $count = Pager::ITEMS_PER_PAGE, $type = 'all', $order = 'name', bool $descending = false)
1730         {
1731                 $param = ['limit' => [$start, $count], 'order' => [$order => $descending]];
1732                 $condition = [];
1733                 switch ($type) {
1734                         case 'active':
1735                                 $condition['account_removed'] = false;
1736                                 $condition['blocked'] = false;
1737                                 break;
1738                         case 'blocked':
1739                                 $condition['account_removed'] = false;
1740                                 $condition['blocked'] = true;
1741                                 $condition['verified'] = true;
1742                                 break;
1743                         case 'removed':
1744                                 $condition['account_removed'] = true;
1745                                 break;
1746                 }
1747
1748                 return DBA::selectToArray('owner-view', [], $condition, $param);
1749         }
1750 }