]> git.mxchange.org Git - friendica.git/blob - src/Model/User.php
Added contact
[friendica.git] / src / Model / User.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2021, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Model;
23
24 use DivineOmega\DOFileCachePSR6\CacheItemPool;
25 use DivineOmega\PasswordExposed;
26 use ErrorException;
27 use Exception;
28 use Friendica\Content\Pager;
29 use Friendica\Core\Hook;
30 use Friendica\Core\L10n;
31 use Friendica\Core\Logger;
32 use Friendica\Core\Protocol;
33 use Friendica\Core\System;
34 use Friendica\Core\Worker;
35 use Friendica\Database\DBA;
36 use Friendica\DI;
37 use Friendica\Security\TwoFactor\Model\AppSpecificPassword;
38 use Friendica\Network\HTTPException;
39 use Friendica\Object\Image;
40 use Friendica\Util\Crypto;
41 use Friendica\Util\DateTimeFormat;
42 use Friendica\Util\Images;
43 use Friendica\Util\Network;
44 use Friendica\Util\Proxy;
45 use Friendica\Util\Strings;
46 use Friendica\Worker\Delivery;
47 use ImagickException;
48 use LightOpenID;
49
50 /**
51  * This class handles User related functions
52  */
53 class User
54 {
55         /**
56          * Page/profile types
57          *
58          * PAGE_FLAGS_NORMAL is a typical personal profile account
59          * PAGE_FLAGS_SOAPBOX automatically approves all friend requests as Contact::SHARING, (readonly)
60          * PAGE_FLAGS_COMMUNITY automatically approves all friend requests as Contact::SHARING, but with
61          *      write access to wall and comments (no email and not included in page owner's ACL lists)
62          * PAGE_FLAGS_FREELOVE automatically approves all friend requests as full friends (Contact::FRIEND).
63          *
64          * @{
65          */
66         const PAGE_FLAGS_NORMAL    = 0;
67         const PAGE_FLAGS_SOAPBOX   = 1;
68         const PAGE_FLAGS_COMMUNITY = 2;
69         const PAGE_FLAGS_FREELOVE  = 3;
70         const PAGE_FLAGS_BLOG      = 4;
71         const PAGE_FLAGS_PRVGROUP  = 5;
72         /**
73          * @}
74          */
75
76         /**
77          * Account types
78          *
79          * ACCOUNT_TYPE_PERSON - the account belongs to a person
80          *      Associated page types: PAGE_FLAGS_NORMAL, PAGE_FLAGS_SOAPBOX, PAGE_FLAGS_FREELOVE
81          *
82          * ACCOUNT_TYPE_ORGANISATION - the account belongs to an organisation
83          *      Associated page type: PAGE_FLAGS_SOAPBOX
84          *
85          * ACCOUNT_TYPE_NEWS - the account is a news reflector
86          *      Associated page type: PAGE_FLAGS_SOAPBOX
87          *
88          * ACCOUNT_TYPE_COMMUNITY - the account is community forum
89          *      Associated page types: PAGE_COMMUNITY, PAGE_FLAGS_PRVGROUP
90          *
91          * ACCOUNT_TYPE_RELAY - the account is a relay
92          *      This will only be assigned to contacts, not to user accounts
93          * @{
94          */
95         const ACCOUNT_TYPE_PERSON =       0;
96         const ACCOUNT_TYPE_ORGANISATION = 1;
97         const ACCOUNT_TYPE_NEWS =         2;
98         const ACCOUNT_TYPE_COMMUNITY =    3;
99         const ACCOUNT_TYPE_RELAY =        4;
100         const ACCOUNT_TYPE_DELETED =    127;
101         /**
102          * @}
103          */
104
105         private static $owner;
106
107         /**
108          * Returns the numeric account type by their string
109          *
110          * @param string $accounttype as string constant
111          * @return int|null Numeric account type - or null when not set
112          */
113         public static function getAccountTypeByString(string $accounttype)
114         {
115                 switch ($accounttype) {
116                         case 'person':
117                                 return User::ACCOUNT_TYPE_PERSON;
118                         case 'organisation':
119                                 return User::ACCOUNT_TYPE_ORGANISATION;
120                         case 'news':
121                                 return User::ACCOUNT_TYPE_NEWS;
122                         case 'community':
123                                 return User::ACCOUNT_TYPE_COMMUNITY;
124                         default:
125                                 return null;
126                         break;
127                 }
128         }
129
130         /**
131          * Fetch the system account
132          *
133          * @return array system account
134          */
135         public static function getSystemAccount()
136         {
137                 $system = Contact::selectFirst([], ['self' => true, 'uid' => 0]);
138                 if (!DBA::isResult($system)) {
139                         self::createSystemAccount();
140                         $system = Contact::selectFirst([], ['self' => true, 'uid' => 0]);
141                         if (!DBA::isResult($system)) {
142                                 return [];
143                         }
144                 }
145
146                 $system['sprvkey'] = $system['uprvkey'] = $system['prvkey'];
147                 $system['spubkey'] = $system['upubkey'] = $system['pubkey'];
148                 $system['nickname'] = $system['nick'];
149                 $system['page-flags'] = User::PAGE_FLAGS_SOAPBOX;
150                 $system['account-type'] = $system['contact-type'];
151                 $system['guid'] = '';
152                 $system['picdate'] = '';
153                 $system['theme'] = '';
154                 $system['publish'] = false;
155                 $system['net-publish'] = false;
156                 $system['hide-friends'] = true;
157                 $system['prv_keywords'] = '';
158                 $system['pub_keywords'] = '';
159                 $system['address'] = '';
160                 $system['locality'] = '';
161                 $system['region'] = '';
162                 $system['postal-code'] = '';
163                 $system['country-name'] = '';
164                 $system['homepage'] = DI::baseUrl()->get();
165                 $system['dob'] = '0000-00-00';
166
167                 // Ensure that the user contains data
168                 $user = DBA::selectFirst('user', ['prvkey', 'guid'], ['uid' => 0]);
169                 if (empty($user['prvkey']) || empty($user['guid'])) {
170                         $fields = [
171                                 'username' => $system['name'],
172                                 'nickname' => $system['nick'],
173                                 'register_date' => $system['created'],
174                                 'pubkey' => $system['pubkey'],
175                                 'prvkey' => $system['prvkey'],
176                                 'spubkey' => $system['spubkey'],
177                                 'sprvkey' => $system['sprvkey'],
178                                 'guid' => System::createUUID(),
179                                 'verified' => true,
180                                 'page-flags' => User::PAGE_FLAGS_SOAPBOX,
181                                 'account-type' => User::ACCOUNT_TYPE_RELAY,
182                         ];
183
184                         DBA::update('user', $fields, ['uid' => 0]);
185
186                         $system['guid'] = $fields['guid'];
187                 } else {
188                         $system['guid'] = $user['guid'];
189                 }
190
191                 return $system;
192         }
193
194         /**
195          * Create the system account
196          *
197          * @return void
198          */
199         private static function createSystemAccount()
200         {
201                 $system_actor_name = self::getActorName();
202                 if (empty($system_actor_name)) {
203                         return;
204                 }
205
206                 $keys = Crypto::newKeypair(4096);
207                 if ($keys === false) {
208                         throw new Exception(DI::l10n()->t('SERIOUS ERROR: Generation of security keys failed.'));
209                 }
210
211                 $system = [];
212                 $system['uid'] = 0;
213                 $system['created'] = DateTimeFormat::utcNow();
214                 $system['self'] = true;
215                 $system['network'] = Protocol::ACTIVITYPUB;
216                 $system['name'] = 'System Account';
217                 $system['addr'] = $system_actor_name . '@' . DI::baseUrl()->getHostname();
218                 $system['nick'] = $system_actor_name;
219                 $system['url'] = DI::baseUrl() . '/friendica';
220
221                 $system['avatar'] = $system['photo'] = Contact::getDefaultAvatar($system, Proxy::SIZE_SMALL);
222                 $system['thumb'] = Contact::getDefaultAvatar($system, Proxy::SIZE_THUMB);
223                 $system['micro'] = Contact::getDefaultAvatar($system, Proxy::SIZE_MICRO);
224
225                 $system['nurl'] = Strings::normaliseLink($system['url']);
226                 $system['pubkey'] = $keys['pubkey'];
227                 $system['prvkey'] = $keys['prvkey'];
228                 $system['blocked'] = 0;
229                 $system['pending'] = 0;
230                 $system['contact-type'] = Contact::TYPE_RELAY; // In AP this is translated to 'Application'
231                 $system['name-date'] = DateTimeFormat::utcNow();
232                 $system['uri-date'] = DateTimeFormat::utcNow();
233                 $system['avatar-date'] = DateTimeFormat::utcNow();
234                 $system['closeness'] = 0;
235                 $system['baseurl'] = DI::baseUrl();
236                 $system['gsid'] = GServer::getID($system['baseurl']);
237                 Contact::insert($system);
238         }
239
240         /**
241          * Detect a usable actor name
242          *
243          * @return string actor account name
244          */
245         public static function getActorName()
246         {
247                 $system_actor_name = DI::config()->get('system', 'actor_name');
248                 if (!empty($system_actor_name)) {
249                         $self = Contact::selectFirst(['nick'], ['uid' => 0, 'self' => true]);
250                         if (!empty($self['nick'])) {
251                                 if ($self['nick'] != $system_actor_name) {
252                                         // Reset the actor name to the already used name
253                                         DI::config()->set('system', 'actor_name', $self['nick']);
254                                         $system_actor_name = $self['nick'];
255                                 }
256                         }
257                         return $system_actor_name;
258                 }
259
260                 // List of possible actor names
261                 $possible_accounts = ['friendica', 'actor', 'system', 'internal'];
262                 foreach ($possible_accounts as $name) {
263                         if (!DBA::exists('user', ['nickname' => $name, 'account_removed' => false, 'expire' => false]) &&
264                                 !DBA::exists('userd', ['username' => $name])) {
265                                 DI::config()->set('system', 'actor_name', $name);
266                                 return $name;
267                         }
268                 }
269                 return '';
270         }
271
272         /**
273          * Returns true if a user record exists with the provided id
274          *
275          * @param  integer $uid
276          * @return boolean
277          * @throws Exception
278          */
279         public static function exists($uid)
280         {
281                 return DBA::exists('user', ['uid' => $uid]);
282         }
283
284         /**
285          * @param  integer       $uid
286          * @param array          $fields
287          * @return array|boolean User record if it exists, false otherwise
288          * @throws Exception
289          */
290         public static function getById($uid, array $fields = [])
291         {
292                 return !empty($uid) ? DBA::selectFirst('user', $fields, ['uid' => $uid]) : [];
293         }
294
295         /**
296          * Returns a user record based on it's GUID
297          *
298          * @param string $guid   The guid of the user
299          * @param array  $fields The fields to retrieve
300          * @param bool   $active True, if only active records are searched
301          *
302          * @return array|boolean User record if it exists, false otherwise
303          * @throws Exception
304          */
305         public static function getByGuid(string $guid, array $fields = [], bool $active = true)
306         {
307                 if ($active) {
308                         $cond = ['guid' => $guid, 'account_expired' => false, 'account_removed' => false];
309                 } else {
310                         $cond = ['guid' => $guid];
311                 }
312
313                 return DBA::selectFirst('user', $fields, $cond);
314         }
315
316         /**
317          * @param  string        $nickname
318          * @param array          $fields
319          * @return array|boolean User record if it exists, false otherwise
320          * @throws Exception
321          */
322         public static function getByNickname($nickname, array $fields = [])
323         {
324                 return DBA::selectFirst('user', $fields, ['nickname' => $nickname]);
325         }
326
327         /**
328          * Returns the user id of a given profile URL
329          *
330          * @param string $url
331          *
332          * @return integer user id
333          * @throws Exception
334          */
335         public static function getIdForURL(string $url)
336         {
337                 // Avoid database queries when the local node hostname isn't even part of the url.
338                 if (!Contact::isLocal($url)) {
339                         return 0;
340                 }
341
342                 $self = Contact::selectFirst(['uid'], ['self' => true, 'nurl' => Strings::normaliseLink($url)]);
343                 if (!empty($self['uid'])) {
344                         return $self['uid'];
345                 }
346
347                 $self = Contact::selectFirst(['uid'], ['self' => true, 'addr' => $url]);
348                 if (!empty($self['uid'])) {
349                         return $self['uid'];
350                 }
351
352                 $self = Contact::selectFirst(['uid'], ['self' => true, 'alias' => [$url, Strings::normaliseLink($url)]]);
353                 if (!empty($self['uid'])) {
354                         return $self['uid'];
355                 }
356
357                 return 0;
358         }
359
360         /**
361          * Get a user based on its email
362          *
363          * @param string        $email
364          * @param array          $fields
365          *
366          * @return array|boolean User record if it exists, false otherwise
367          *
368          * @throws Exception
369          */
370         public static function getByEmail($email, array $fields = [])
371         {
372                 return DBA::selectFirst('user', $fields, ['email' => $email]);
373         }
374
375         /**
376          * Fetch the user array of the administrator. The first one if there are several.
377          *
378          * @param array $fields
379          * @return array user
380          */
381         public static function getFirstAdmin(array $fields = [])
382         {
383                 if (!empty(DI::config()->get('config', 'admin_nickname'))) {
384                         return self::getByNickname(DI::config()->get('config', 'admin_nickname'), $fields);
385                 } elseif (!empty(DI::config()->get('config', 'admin_email'))) {
386                         $adminList = explode(',', str_replace(' ', '', DI::config()->get('config', 'admin_email')));
387                         return self::getByEmail($adminList[0], $fields);
388                 } else {
389                         return [];
390                 }
391         }
392
393         /**
394          * Get owner data by user id
395          *
396          * @param int     $uid
397          * @param boolean $repairMissing Repair the owner data if it's missing
398          * @return boolean|array
399          * @throws Exception
400          */
401         public static function getOwnerDataById(int $uid, bool $repairMissing = true)
402         {
403                 if ($uid == 0) {
404                         return self::getSystemAccount();
405                 }
406
407                 if (!empty(self::$owner[$uid])) {
408                         return self::$owner[$uid];
409                 }
410
411                 $owner = DBA::selectFirst('owner-view', [], ['uid' => $uid]);
412                 if (!DBA::isResult($owner)) {
413                         if (!DBA::exists('user', ['uid' => $uid]) || !$repairMissing) {
414                                 return false;
415                         }
416                         if (!DBA::exists('profile', ['uid' => $uid])) {
417                                 DBA::insert('profile', ['uid' => $uid]);
418                         }
419                         if (!DBA::exists('contact', ['uid' => $uid, 'self' => true])) {
420                                 Contact::createSelfFromUserId($uid);
421                         }
422                         $owner = self::getOwnerDataById($uid, false);
423                 }
424
425                 if (empty($owner['nickname'])) {
426                         return false;
427                 }
428
429                 if (!$repairMissing || $owner['account_expired']) {
430                         return $owner;
431                 }
432
433                 // Check if the returned data is valid, otherwise fix it. See issue #6122
434
435                 // Check for correct url and normalised nurl
436                 $url = DI::baseUrl() . '/profile/' . $owner['nickname'];
437                 $repair = empty($owner['network']) || ($owner['url'] != $url) || ($owner['nurl'] != Strings::normaliseLink($owner['url']));
438
439                 if (!$repair) {
440                         // Check if "addr" is present and correct
441                         $addr = $owner['nickname'] . '@' . substr(DI::baseUrl(), strpos(DI::baseUrl(), '://') + 3);
442                         $repair = ($addr != $owner['addr']) || empty($owner['prvkey']) || empty($owner['pubkey']);
443                 }
444
445                 if (!$repair) {
446                         // Check if the avatar field is filled and the photo directs to the correct path
447                         $avatar = Photo::selectFirst(['resource-id'], ['uid' => $uid, 'profile' => true]);
448                         if (DBA::isResult($avatar)) {
449                                 $repair = empty($owner['avatar']) || !strpos($owner['photo'], $avatar['resource-id']);
450                         }
451                 }
452
453                 if ($repair) {
454                         Contact::updateSelfFromUserID($uid);
455                         // Return the corrected data and avoid a loop
456                         $owner = self::getOwnerDataById($uid, false);
457                 }
458
459                 self::$owner[$uid] = $owner;
460                 return $owner;
461         }
462
463         /**
464          * Get owner data by nick name
465          *
466          * @param int $nick
467          * @return boolean|array
468          * @throws Exception
469          */
470         public static function getOwnerDataByNick($nick)
471         {
472                 $user = DBA::selectFirst('user', ['uid'], ['nickname' => $nick]);
473
474                 if (!DBA::isResult($user)) {
475                         return false;
476                 }
477
478                 return self::getOwnerDataById($user['uid']);
479         }
480
481         /**
482          * Returns the default group for a given user and network
483          *
484          * @param int    $uid     User id
485          * @param string $network network name
486          *
487          * @return int group id
488          * @throws Exception
489          */
490         public static function getDefaultGroup($uid, $network = '')
491         {
492                 $user = DBA::selectFirst('user', ['def_gid'], ['uid' => $uid]);
493                 if (DBA::isResult($user)) {
494                         $default_group = $user["def_gid"];
495                 } else {
496                         $default_group = 0;
497                 }
498
499                 return $default_group;
500         }
501
502         /**
503          * 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                 $imagetype = IMAGETYPE_JPEG;
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
861                         if (in_array($photo['type'], ['image/png', 'image/gif'])) {
862                                 $imagetype = IMAGETYPE_PNG;
863                         }
864                 }
865
866                 return $url . $user['nickname'] . image_type_to_extension($imagetype) . ($updated ? '?ts=' . strtotime($updated) : '');
867         }
868
869         /**
870          * Catch-all user creation function
871          *
872          * Creates a user from the provided data array, either form fields or OpenID.
873          * Required: { username, nickname, email } or { openid_url }
874          *
875          * Performs the following:
876          * - Sends to the OpenId auth URL (if relevant)
877          * - Creates new key pairs for crypto
878          * - Create self-contact
879          * - Create profile image
880          *
881          * @param  array $data
882          * @return array
883          * @throws ErrorException
884          * @throws HTTPException\InternalServerErrorException
885          * @throws ImagickException
886          * @throws Exception
887          */
888         public static function create(array $data)
889         {
890                 $return = ['user' => null, 'password' => ''];
891
892                 $using_invites = DI::config()->get('system', 'invitation_only');
893
894                 $invite_id  = !empty($data['invite_id'])  ? trim($data['invite_id'])  : '';
895                 $username   = !empty($data['username'])   ? trim($data['username'])   : '';
896                 $nickname   = !empty($data['nickname'])   ? trim($data['nickname'])   : '';
897                 $email      = !empty($data['email'])      ? trim($data['email'])      : '';
898                 $openid_url = !empty($data['openid_url']) ? trim($data['openid_url']) : '';
899                 $photo      = !empty($data['photo'])      ? trim($data['photo'])      : '';
900                 $password   = !empty($data['password'])   ? trim($data['password'])   : '';
901                 $password1  = !empty($data['password1'])  ? trim($data['password1'])  : '';
902                 $confirm    = !empty($data['confirm'])    ? trim($data['confirm'])    : '';
903                 $blocked    = !empty($data['blocked']);
904                 $verified   = !empty($data['verified']);
905                 $language   = !empty($data['language'])   ? trim($data['language'])   : 'en';
906
907                 $netpublish = $publish = !empty($data['profile_publish_reg']);
908
909                 if ($password1 != $confirm) {
910                         throw new Exception(DI::l10n()->t('Passwords do not match. Password unchanged.'));
911                 } elseif ($password1 != '') {
912                         $password = $password1;
913                 }
914
915                 if ($using_invites) {
916                         if (!$invite_id) {
917                                 throw new Exception(DI::l10n()->t('An invitation is required.'));
918                         }
919
920                         if (!Register::existsByHash($invite_id)) {
921                                 throw new Exception(DI::l10n()->t('Invitation could not be verified.'));
922                         }
923                 }
924
925                 /// @todo Check if this part is really needed. We should have fetched all this data in advance
926                 if (empty($username) || empty($email) || empty($nickname)) {
927                         if ($openid_url) {
928                                 if (!Network::isUrlValid($openid_url)) {
929                                         throw new Exception(DI::l10n()->t('Invalid OpenID url'));
930                                 }
931                                 $_SESSION['register'] = 1;
932                                 $_SESSION['openid'] = $openid_url;
933
934                                 $openid = new LightOpenID(DI::baseUrl()->getHostname());
935                                 $openid->identity = $openid_url;
936                                 $openid->returnUrl = DI::baseUrl() . '/openid';
937                                 $openid->required = ['namePerson/friendly', 'contact/email', 'namePerson'];
938                                 $openid->optional = ['namePerson/first', 'media/image/aspect11', 'media/image/default'];
939                                 try {
940                                         $authurl = $openid->authUrl();
941                                 } catch (Exception $e) {
942                                         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);
943                                 }
944                                 System::externalRedirect($authurl);
945                                 // NOTREACHED
946                         }
947
948                         throw new Exception(DI::l10n()->t('Please enter the required information.'));
949                 }
950
951                 if (!Network::isUrlValid($openid_url)) {
952                         $openid_url = '';
953                 }
954
955                 // collapse multiple spaces in name
956                 $username = preg_replace('/ +/', ' ', $username);
957
958                 $username_min_length = max(1, min(64, intval(DI::config()->get('system', 'username_min_length', 3))));
959                 $username_max_length = max(1, min(64, intval(DI::config()->get('system', 'username_max_length', 48))));
960
961                 if ($username_min_length > $username_max_length) {
962                         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));
963                         $tmp = $username_min_length;
964                         $username_min_length = $username_max_length;
965                         $username_max_length = $tmp;
966                 }
967
968                 if (mb_strlen($username) < $username_min_length) {
969                         throw new Exception(DI::l10n()->tt('Username should be at least %s character.', 'Username should be at least %s characters.', $username_min_length));
970                 }
971
972                 if (mb_strlen($username) > $username_max_length) {
973                         throw new Exception(DI::l10n()->tt('Username should be at most %s character.', 'Username should be at most %s characters.', $username_max_length));
974                 }
975
976                 // So now we are just looking for a space in the full name.
977                 $loose_reg = DI::config()->get('system', 'no_regfullname');
978                 if (!$loose_reg) {
979                         $username = mb_convert_case($username, MB_CASE_TITLE, 'UTF-8');
980                         if (strpos($username, ' ') === false) {
981                                 throw new Exception(DI::l10n()->t("That doesn't appear to be your full (First Last) name."));
982                         }
983                 }
984
985                 if (!Network::isEmailDomainAllowed($email)) {
986                         throw new Exception(DI::l10n()->t('Your email domain is not among those allowed on this site.'));
987                 }
988
989                 if (!filter_var($email, FILTER_VALIDATE_EMAIL) || !Network::isEmailDomainValid($email)) {
990                         throw new Exception(DI::l10n()->t('Not a valid email address.'));
991                 }
992                 if (self::isNicknameBlocked($nickname)) {
993                         throw new Exception(DI::l10n()->t('The nickname was blocked from registration by the nodes admin.'));
994                 }
995
996                 if (DI::config()->get('system', 'block_extended_register', false) && DBA::exists('user', ['email' => $email])) {
997                         throw new Exception(DI::l10n()->t('Cannot use that email.'));
998                 }
999
1000                 // Disallow somebody creating an account using openid that uses the admin email address,
1001                 // since openid bypasses email verification. We'll allow it if there is not yet an admin account.
1002                 if (DI::config()->get('config', 'admin_email') && strlen($openid_url)) {
1003                         $adminlist = explode(',', str_replace(' ', '', strtolower(DI::config()->get('config', 'admin_email'))));
1004                         if (in_array(strtolower($email), $adminlist)) {
1005                                 throw new Exception(DI::l10n()->t('Cannot use that email.'));
1006                         }
1007                 }
1008
1009                 $nickname = $data['nickname'] = strtolower($nickname);
1010
1011                 if (!preg_match('/^[a-z0-9][a-z0-9_]*$/', $nickname)) {
1012                         throw new Exception(DI::l10n()->t('Your nickname can only contain a-z, 0-9 and _.'));
1013                 }
1014
1015                 // Check existing and deleted accounts for this nickname.
1016                 if (
1017                         DBA::exists('user', ['nickname' => $nickname])
1018                         || DBA::exists('userd', ['username' => $nickname])
1019                 ) {
1020                         throw new Exception(DI::l10n()->t('Nickname is already registered. Please choose another.'));
1021                 }
1022
1023                 $new_password = strlen($password) ? $password : User::generateNewPassword();
1024                 $new_password_encoded = self::hashPassword($new_password);
1025
1026                 $return['password'] = $new_password;
1027
1028                 $keys = Crypto::newKeypair(4096);
1029                 if ($keys === false) {
1030                         throw new Exception(DI::l10n()->t('SERIOUS ERROR: Generation of security keys failed.'));
1031                 }
1032
1033                 $prvkey = $keys['prvkey'];
1034                 $pubkey = $keys['pubkey'];
1035
1036                 // Create another keypair for signing/verifying salmon protocol messages.
1037                 $sres = Crypto::newKeypair(512);
1038                 $sprvkey = $sres['prvkey'];
1039                 $spubkey = $sres['pubkey'];
1040
1041                 $insert_result = DBA::insert('user', [
1042                         'guid'     => System::createUUID(),
1043                         'username' => $username,
1044                         'password' => $new_password_encoded,
1045                         'email'    => $email,
1046                         'openid'   => $openid_url,
1047                         'nickname' => $nickname,
1048                         'pubkey'   => $pubkey,
1049                         'prvkey'   => $prvkey,
1050                         'spubkey'  => $spubkey,
1051                         'sprvkey'  => $sprvkey,
1052                         'verified' => $verified,
1053                         'blocked'  => $blocked,
1054                         'language' => $language,
1055                         'timezone' => 'UTC',
1056                         'register_date' => DateTimeFormat::utcNow(),
1057                         'default-location' => ''
1058                 ]);
1059
1060                 if ($insert_result) {
1061                         $uid = DBA::lastInsertId();
1062                         $user = DBA::selectFirst('user', [], ['uid' => $uid]);
1063                 } else {
1064                         throw new Exception(DI::l10n()->t('An error occurred during registration. Please try again.'));
1065                 }
1066
1067                 if (!$uid) {
1068                         throw new Exception(DI::l10n()->t('An error occurred during registration. Please try again.'));
1069                 }
1070
1071                 // if somebody clicked submit twice very quickly, they could end up with two accounts
1072                 // due to race condition. Remove this one.
1073                 $user_count = DBA::count('user', ['nickname' => $nickname]);
1074                 if ($user_count > 1) {
1075                         DBA::delete('user', ['uid' => $uid]);
1076
1077                         throw new Exception(DI::l10n()->t('Nickname is already registered. Please choose another.'));
1078                 }
1079
1080                 $insert_result = DBA::insert('profile', [
1081                         'uid' => $uid,
1082                         'name' => $username,
1083                         'photo' => self::getAvatarUrl($user),
1084                         'thumb' => self::getAvatarUrl($user, Proxy::SIZE_THUMB),
1085                         'publish' => $publish,
1086                         'net-publish' => $netpublish,
1087                 ]);
1088                 if (!$insert_result) {
1089                         DBA::delete('user', ['uid' => $uid]);
1090
1091                         throw new Exception(DI::l10n()->t('An error occurred creating your default profile. Please try again.'));
1092                 }
1093
1094                 // Create the self contact
1095                 if (!Contact::createSelfFromUserId($uid)) {
1096                         DBA::delete('user', ['uid' => $uid]);
1097
1098                         throw new Exception(DI::l10n()->t('An error occurred creating your self contact. Please try again.'));
1099                 }
1100
1101                 // Create a group with no members. This allows somebody to use it
1102                 // right away as a default group for new contacts.
1103                 $def_gid = Group::create($uid, DI::l10n()->t('Friends'));
1104                 if (!$def_gid) {
1105                         DBA::delete('user', ['uid' => $uid]);
1106
1107                         throw new Exception(DI::l10n()->t('An error occurred creating your default contact group. Please try again.'));
1108                 }
1109
1110                 $fields = ['def_gid' => $def_gid];
1111                 if (DI::config()->get('system', 'newuser_private') && $def_gid) {
1112                         $fields['allow_gid'] = '<' . $def_gid . '>';
1113                 }
1114
1115                 DBA::update('user', $fields, ['uid' => $uid]);
1116
1117                 // if we have no OpenID photo try to look up an avatar
1118                 if (!strlen($photo)) {
1119                         $photo = Network::lookupAvatarByEmail($email);
1120                 }
1121
1122                 // unless there is no avatar-addon loaded
1123                 if (strlen($photo)) {
1124                         $photo_failure = false;
1125
1126                         $filename = basename($photo);
1127                         $curlResult = DI::httpClient()->get($photo);
1128                         if ($curlResult->isSuccess()) {
1129                                 $img_str = $curlResult->getBody();
1130                                 $type = $curlResult->getContentType();
1131                         } else {
1132                                 $img_str = '';
1133                                 $type = '';
1134                         }
1135
1136                         $type = Images::getMimeTypeByData($img_str, $photo, $type);
1137
1138                         $Image = new Image($img_str, $type);
1139                         if ($Image->isValid()) {
1140                                 $Image->scaleToSquare(300);
1141
1142                                 $resource_id = Photo::newResource();
1143
1144                                 // Not using Photo::PROFILE_PHOTOS here, so that it is discovered as translateble string
1145                                 $profile_album = DI::l10n()->t('Profile Photos');
1146
1147                                 $r = Photo::store($Image, $uid, 0, $resource_id, $filename, $profile_album, 4);
1148
1149                                 if ($r === false) {
1150                                         $photo_failure = true;
1151                                 }
1152
1153                                 $Image->scaleDown(80);
1154
1155                                 $r = Photo::store($Image, $uid, 0, $resource_id, $filename, $profile_album, 5);
1156
1157                                 if ($r === false) {
1158                                         $photo_failure = true;
1159                                 }
1160
1161                                 $Image->scaleDown(48);
1162
1163                                 $r = Photo::store($Image, $uid, 0, $resource_id, $filename, $profile_album, 6);
1164
1165                                 if ($r === false) {
1166                                         $photo_failure = true;
1167                                 }
1168
1169                                 if (!$photo_failure) {
1170                                         Photo::update(['profile' => true, 'photo-type' => Photo::USER_AVATAR], ['resource-id' => $resource_id]);
1171                                 }
1172                         }
1173
1174                         Contact::updateSelfFromUserID($uid, true);
1175                 }
1176
1177                 Hook::callAll('register_account', $uid);
1178
1179                 $return['user'] = $user;
1180                 return $return;
1181         }
1182
1183         /**
1184          * Update a user entry and distribute the changes if needed
1185          *
1186          * @param array $fields
1187          * @param integer $uid
1188          * @return boolean
1189          */
1190         public static function update(array $fields, int $uid): bool
1191         {
1192                 $old_owner = self::getOwnerDataById($uid);
1193                 if (empty($old_owner)) {
1194                         return false;
1195                 }
1196
1197                 if (!DBA::update('user', $fields, ['uid' => $uid])) {
1198                         return false;
1199                 }
1200
1201                 $update = Contact::updateSelfFromUserID($uid);
1202
1203                 $owner = self::getOwnerDataById($uid);
1204                 if (empty($owner)) {
1205                         return false;
1206                 }
1207
1208                 if ($old_owner['name'] != $owner['name']) {
1209                         Profile::update(['name' => $owner['name']], $uid);
1210                 }
1211
1212                 if ($update) {
1213                         Profile::publishUpdate($uid);
1214                 }
1215
1216                 return true;
1217         }
1218
1219         /**
1220          * Sets block state for a given user
1221          *
1222          * @param int  $uid   The user id
1223          * @param bool $block Block state (default is true)
1224          *
1225          * @return bool True, if successfully blocked
1226
1227          * @throws Exception
1228          */
1229         public static function block(int $uid, bool $block = true)
1230         {
1231                 return DBA::update('user', ['blocked' => $block], ['uid' => $uid]);
1232         }
1233
1234         /**
1235          * Allows a registration based on a hash
1236          *
1237          * @param string $hash
1238          *
1239          * @return bool True, if the allow was successful
1240          *
1241          * @throws HTTPException\InternalServerErrorException
1242          * @throws Exception
1243          */
1244         public static function allow(string $hash)
1245         {
1246                 $register = Register::getByHash($hash);
1247                 if (!DBA::isResult($register)) {
1248                         return false;
1249                 }
1250
1251                 $user = User::getById($register['uid']);
1252                 if (!DBA::isResult($user)) {
1253                         return false;
1254                 }
1255
1256                 Register::deleteByHash($hash);
1257
1258                 DBA::update('user', ['blocked' => false, 'verified' => true], ['uid' => $register['uid']]);
1259
1260                 $profile = DBA::selectFirst('profile', ['net-publish'], ['uid' => $register['uid']]);
1261
1262                 if (DBA::isResult($profile) && $profile['net-publish'] && DI::config()->get('system', 'directory')) {
1263                         $url = DI::baseUrl() . '/profile/' . $user['nickname'];
1264                         Worker::add(PRIORITY_LOW, "Directory", $url);
1265                 }
1266
1267                 $l10n = DI::l10n()->withLang($register['language']);
1268
1269                 return User::sendRegisterOpenEmail(
1270                         $l10n,
1271                         $user,
1272                         DI::config()->get('config', 'sitename'),
1273                         DI::baseUrl()->get(),
1274                         ($register['password'] ?? '') ?: 'Sent in a previous email'
1275                 );
1276         }
1277
1278         /**
1279          * Denys a pending registration
1280          *
1281          * @param string $hash The hash of the pending user
1282          *
1283          * This does not have to go through user_remove() and save the nickname
1284          * permanently against re-registration, as the person was not yet
1285          * allowed to have friends on this system
1286          *
1287          * @return bool True, if the deny was successfull
1288          * @throws Exception
1289          */
1290         public static function deny(string $hash)
1291         {
1292                 $register = Register::getByHash($hash);
1293                 if (!DBA::isResult($register)) {
1294                         return false;
1295                 }
1296
1297                 $user = User::getById($register['uid']);
1298                 if (!DBA::isResult($user)) {
1299                         return false;
1300                 }
1301
1302                 // Delete the avatar
1303                 Photo::delete(['uid' => $register['uid']]);
1304
1305                 return DBA::delete('user', ['uid' => $register['uid']]) &&
1306                        Register::deleteByHash($register['hash']);
1307         }
1308
1309         /**
1310          * Creates a new user based on a minimal set and sends an email to this user
1311          *
1312          * @param string $name  The user's name
1313          * @param string $email The user's email address
1314          * @param string $nick  The user's nick name
1315          * @param string $lang  The user's language (default is english)
1316          *
1317          * @return bool True, if the user was created successfully
1318          * @throws HTTPException\InternalServerErrorException
1319          * @throws ErrorException
1320          * @throws ImagickException
1321          */
1322         public static function createMinimal(string $name, string $email, string $nick, string $lang = L10n::DEFAULT)
1323         {
1324                 if (empty($name) ||
1325                     empty($email) ||
1326                     empty($nick)) {
1327                         throw new HTTPException\InternalServerErrorException('Invalid arguments.');
1328                 }
1329
1330                 $result = self::create([
1331                         'username' => $name,
1332                         'email' => $email,
1333                         'nickname' => $nick,
1334                         'verified' => 1,
1335                         'language' => $lang
1336                 ]);
1337
1338                 $user = $result['user'];
1339                 $preamble = Strings::deindent(DI::l10n()->t('
1340                 Dear %1$s,
1341                         the administrator of %2$s has set up an account for you.'));
1342                 $body = Strings::deindent(DI::l10n()->t('
1343                 The login details are as follows:
1344
1345                 Site Location:  %1$s
1346                 Login Name:             %2$s
1347                 Password:               %3$s
1348
1349                 You may change your password from your account "Settings" page after logging
1350                 in.
1351
1352                 Please take a few moments to review the other account settings on that page.
1353
1354                 You may also wish to add some basic information to your default profile
1355                 (on the "Profiles" page) so that other people can easily find you.
1356
1357                 We recommend setting your full name, adding a profile photo,
1358                 adding some profile "keywords" (very useful in making new friends) - and
1359                 perhaps what country you live in; if you do not wish to be more specific
1360                 than that.
1361
1362                 We fully respect your right to privacy, and none of these items are necessary.
1363                 If you are new and do not know anybody here, they may help
1364                 you to make some new and interesting friends.
1365
1366                 If you ever want to delete your account, you can do so at %1$s/removeme
1367
1368                 Thank you and welcome to %4$s.'));
1369
1370                 $preamble = sprintf($preamble, $user['username'], DI::config()->get('config', 'sitename'));
1371                 $body = sprintf($body, DI::baseUrl()->get(), $user['nickname'], $result['password'], DI::config()->get('config', 'sitename'));
1372
1373                 $email = DI::emailer()
1374                         ->newSystemMail()
1375                         ->withMessage(DI::l10n()->t('Registration details for %s', DI::config()->get('config', 'sitename')), $preamble, $body)
1376                         ->forUser($user)
1377                         ->withRecipient($user['email'])
1378                         ->build();
1379                 return DI::emailer()->send($email);
1380         }
1381
1382         /**
1383          * Sends pending registration confirmation email
1384          *
1385          * @param array  $user     User record array
1386          * @param string $sitename
1387          * @param string $siteurl
1388          * @param string $password Plaintext password
1389          * @return NULL|boolean from notification() and email() inherited
1390          * @throws HTTPException\InternalServerErrorException
1391          */
1392         public static function sendRegisterPendingEmail($user, $sitename, $siteurl, $password)
1393         {
1394                 $body = Strings::deindent(DI::l10n()->t(
1395                         '
1396                         Dear %1$s,
1397                                 Thank you for registering at %2$s. Your account is pending for approval by the administrator.
1398
1399                         Your login details are as follows:
1400
1401                         Site Location:  %3$s
1402                         Login Name:             %4$s
1403                         Password:               %5$s
1404                 ',
1405                         $user['username'],
1406                         $sitename,
1407                         $siteurl,
1408                         $user['nickname'],
1409                         $password
1410                 ));
1411
1412                 $email = DI::emailer()
1413                         ->newSystemMail()
1414                         ->withMessage(DI::l10n()->t('Registration at %s', $sitename), $body)
1415                         ->forUser($user)
1416                         ->withRecipient($user['email'])
1417                         ->build();
1418                 return DI::emailer()->send($email);
1419         }
1420
1421         /**
1422          * Sends registration confirmation
1423          *
1424          * It's here as a function because the mail is sent from different parts
1425          *
1426          * @param L10n   $l10n     The used language
1427          * @param array  $user     User record array
1428          * @param string $sitename
1429          * @param string $siteurl
1430          * @param string $password Plaintext password
1431          *
1432          * @return NULL|boolean from notification() and email() inherited
1433          * @throws HTTPException\InternalServerErrorException
1434          */
1435         public static function sendRegisterOpenEmail(L10n $l10n, $user, $sitename, $siteurl, $password)
1436         {
1437                 $preamble = Strings::deindent($l10n->t(
1438                         '
1439                                 Dear %1$s,
1440                                 Thank you for registering at %2$s. Your account has been created.
1441                         ',
1442                         $user['username'],
1443                         $sitename
1444                 ));
1445                 $body = Strings::deindent($l10n->t(
1446                         '
1447                         The login details are as follows:
1448
1449                         Site Location:  %3$s
1450                         Login Name:             %1$s
1451                         Password:               %5$s
1452
1453                         You may change your password from your account "Settings" page after logging
1454                         in.
1455
1456                         Please take a few moments to review the other account settings on that page.
1457
1458                         You may also wish to add some basic information to your default profile
1459                         ' . "\x28" . 'on the "Profiles" page' . "\x29" . ' so that other people can easily find you.
1460
1461                         We recommend setting your full name, adding a profile photo,
1462                         adding some profile "keywords" ' . "\x28" . 'very useful in making new friends' . "\x29" . ' - and
1463                         perhaps what country you live in; if you do not wish to be more specific
1464                         than that.
1465
1466                         We fully respect your right to privacy, and none of these items are necessary.
1467                         If you are new and do not know anybody here, they may help
1468                         you to make some new and interesting friends.
1469
1470                         If you ever want to delete your account, you can do so at %3$s/removeme
1471
1472                         Thank you and welcome to %2$s.',
1473                         $user['nickname'],
1474                         $sitename,
1475                         $siteurl,
1476                         $user['username'],
1477                         $password
1478                 ));
1479
1480                 $email = DI::emailer()
1481                         ->newSystemMail()
1482                         ->withMessage(DI::l10n()->t('Registration details for %s', $sitename), $preamble, $body)
1483                         ->forUser($user)
1484                         ->withRecipient($user['email'])
1485                         ->build();
1486                 return DI::emailer()->send($email);
1487         }
1488
1489         /**
1490          * @param int $uid user to remove
1491          * @return bool
1492          * @throws HTTPException\InternalServerErrorException
1493          */
1494         public static function remove(int $uid)
1495         {
1496                 if (empty($uid)) {
1497                         return false;
1498                 }
1499
1500                 Logger::notice('Removing user', ['user' => $uid]);
1501
1502                 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
1503
1504                 Hook::callAll('remove_user', $user);
1505
1506                 // save username (actually the nickname as it is guaranteed
1507                 // unique), so it cannot be re-registered in the future.
1508                 DBA::insert('userd', ['username' => $user['nickname']]);
1509
1510                 // Remove all personal settings, especially connector settings
1511                 DBA::delete('pconfig', ['uid' => $uid]);
1512
1513                 // The user and related data will be deleted in Friendica\Worker\ExpireAndRemoveUsers
1514                 DBA::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utc('now + 7 day')], ['uid' => $uid]);
1515                 Worker::add(PRIORITY_HIGH, 'Notifier', Delivery::REMOVAL, $uid);
1516
1517                 // Send an update to the directory
1518                 $self = DBA::selectFirst('contact', ['url'], ['uid' => $uid, 'self' => true]);
1519                 Worker::add(PRIORITY_LOW, 'Directory', $self['url']);
1520
1521                 // Remove the user relevant data
1522                 Worker::add(PRIORITY_NEGLIGIBLE, 'RemoveUser', $uid);
1523
1524                 return true;
1525         }
1526
1527         /**
1528          * Return all identities to a user
1529          *
1530          * @param int $uid The user id
1531          * @return array All identities for this user
1532          *
1533          * Example for a return:
1534          *    [
1535          *        [
1536          *            'uid' => 1,
1537          *            'username' => 'maxmuster',
1538          *            'nickname' => 'Max Mustermann'
1539          *        ],
1540          *        [
1541          *            'uid' => 2,
1542          *            'username' => 'johndoe',
1543          *            'nickname' => 'John Doe'
1544          *        ]
1545          *    ]
1546          * @throws Exception
1547          */
1548         public static function identities($uid)
1549         {
1550                 if (empty($uid)) {
1551                         return [];
1552                 }
1553
1554                 $identities = [];
1555
1556                 $user = DBA::selectFirst('user', ['uid', 'nickname', 'username', 'parent-uid'], ['uid' => $uid]);
1557                 if (!DBA::isResult($user)) {
1558                         return $identities;
1559                 }
1560
1561                 if ($user['parent-uid'] == 0) {
1562                         // First add our own entry
1563                         $identities = [[
1564                                 'uid' => $user['uid'],
1565                                 'username' => $user['username'],
1566                                 'nickname' => $user['nickname']
1567                         ]];
1568
1569                         // Then add all the children
1570                         $r = DBA::select(
1571                                 'user',
1572                                 ['uid', 'username', 'nickname'],
1573                                 ['parent-uid' => $user['uid'], 'account_removed' => false]
1574                         );
1575                         if (DBA::isResult($r)) {
1576                                 $identities = array_merge($identities, DBA::toArray($r));
1577                         }
1578                 } else {
1579                         // First entry is our parent
1580                         $r = DBA::select(
1581                                 'user',
1582                                 ['uid', 'username', 'nickname'],
1583                                 ['uid' => $user['parent-uid'], 'account_removed' => false]
1584                         );
1585                         if (DBA::isResult($r)) {
1586                                 $identities = DBA::toArray($r);
1587                         }
1588
1589                         // Then add all siblings
1590                         $r = DBA::select(
1591                                 'user',
1592                                 ['uid', 'username', 'nickname'],
1593                                 ['parent-uid' => $user['parent-uid'], 'account_removed' => false]
1594                         );
1595                         if (DBA::isResult($r)) {
1596                                 $identities = array_merge($identities, DBA::toArray($r));
1597                         }
1598                 }
1599
1600                 $r = DBA::p(
1601                         "SELECT `user`.`uid`, `user`.`username`, `user`.`nickname`
1602                         FROM `manage`
1603                         INNER JOIN `user` ON `manage`.`mid` = `user`.`uid`
1604                         WHERE `user`.`account_removed` = 0 AND `manage`.`uid` = ?",
1605                         $user['uid']
1606                 );
1607                 if (DBA::isResult($r)) {
1608                         $identities = array_merge($identities, DBA::toArray($r));
1609                 }
1610
1611                 return $identities;
1612         }
1613
1614         /**
1615          * Check if the given user id has delegations or is delegated
1616          *
1617          * @param int $uid
1618          * @return bool
1619          */
1620         public static function hasIdentities(int $uid):bool
1621         {
1622                 if (empty($uid)) {
1623                         return false;
1624                 }
1625
1626                 $user = DBA::selectFirst('user', ['parent-uid'], ['uid' => $uid, 'account_removed' => false]);
1627                 if (!DBA::isResult($user)) {
1628                         return false;
1629                 }
1630
1631                 if ($user['parent-uid'] != 0) {
1632                         return true;
1633                 }
1634
1635                 if (DBA::exists('user', ['parent-uid' => $uid, 'account_removed' => false])) {
1636                         return true;
1637                 }
1638
1639                 if (DBA::exists('manage', ['uid' => $uid])) {
1640                         return true;
1641                 }
1642
1643                 return false;
1644         }
1645
1646         /**
1647          * Returns statistical information about the current users of this node
1648          *
1649          * @return array
1650          *
1651          * @throws Exception
1652          */
1653         public static function getStatistics()
1654         {
1655                 $statistics = [
1656                         'total_users'           => 0,
1657                         'active_users_halfyear' => 0,
1658                         'active_users_monthly'  => 0,
1659                         'active_users_weekly'   => 0,
1660                 ];
1661
1662                 $userStmt = DBA::select('owner-view', ['uid', 'login_date', 'last-item'],
1663                         ["`verified` AND `login_date` > ? AND NOT `blocked`
1664                         AND NOT `account_removed` AND NOT `account_expired`",
1665                         DBA::NULL_DATETIME]);
1666                 if (!DBA::isResult($userStmt)) {
1667                         return $statistics;
1668                 }
1669
1670                 $halfyear = time() - (180 * 24 * 60 * 60);
1671                 $month = time() - (30 * 24 * 60 * 60);
1672                 $week = time() - (7 * 24 * 60 * 60);
1673
1674                 while ($user = DBA::fetch($userStmt)) {
1675                         $statistics['total_users']++;
1676
1677                         if ((strtotime($user['login_date']) > $halfyear) || (strtotime($user['last-item']) > $halfyear)
1678                         ) {
1679                                 $statistics['active_users_halfyear']++;
1680                         }
1681
1682                         if ((strtotime($user['login_date']) > $month) || (strtotime($user['last-item']) > $month)
1683                         ) {
1684                                 $statistics['active_users_monthly']++;
1685                         }
1686
1687                         if ((strtotime($user['login_date']) > $week) || (strtotime($user['last-item']) > $week)
1688                         ) {
1689                                 $statistics['active_users_weekly']++;
1690                         }
1691                 }
1692                 DBA::close($userStmt);
1693
1694                 return $statistics;
1695         }
1696
1697         /**
1698          * Get all users of the current node
1699          *
1700          * @param int    $start Start count (Default is 0)
1701          * @param int    $count Count of the items per page (Default is @see Pager::ITEMS_PER_PAGE)
1702          * @param string $type  The type of users, which should get (all, bocked, removed)
1703          * @param string $order Order of the user list (Default is 'contact.name')
1704          * @param bool   $descending Order direction (Default is ascending)
1705          *
1706          * @return array The list of the users
1707          * @throws Exception
1708          */
1709         public static function getList($start = 0, $count = Pager::ITEMS_PER_PAGE, $type = 'all', $order = 'name', bool $descending = false)
1710         {
1711                 $param = ['limit' => [$start, $count], 'order' => [$order => $descending]];
1712                 $condition = [];
1713                 switch ($type) {
1714                         case 'active':
1715                                 $condition['account_removed'] = false;
1716                                 $condition['blocked'] = false;
1717                                 break;
1718                         case 'blocked':
1719                                 $condition['account_removed'] = false;
1720                                 $condition['blocked'] = true;
1721                                 $condition['verified'] = true;
1722                                 break;
1723                         case 'removed':
1724                                 $condition['account_removed'] = true;
1725                                 break;
1726                 }
1727
1728                 return DBA::selectToArray('owner-view', [], $condition, $param);
1729         }
1730 }