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