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