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