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