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