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