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