]> git.mxchange.org Git - friendica.git/blob - src/Model/User.php
Merge pull request #8265 from nupplaphil/task/add_license
[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                 $publish = !empty($data['profile_publish_reg']);
620                 $netpublish = $publish && DI::config()->get('system', 'directory');
621
622                 if ($password1 != $confirm) {
623                         throw new Exception(DI::l10n()->t('Passwords do not match. Password unchanged.'));
624                 } elseif ($password1 != '') {
625                         $password = $password1;
626                 }
627
628                 if ($using_invites) {
629                         if (!$invite_id) {
630                                 throw new Exception(DI::l10n()->t('An invitation is required.'));
631                         }
632
633                         if (!Register::existsByHash($invite_id)) {
634                                 throw new Exception(DI::l10n()->t('Invitation could not be verified.'));
635                         }
636                 }
637
638                 /// @todo Check if this part is really needed. We should have fetched all this data in advance
639                 if (empty($username) || empty($email) || empty($nickname)) {
640                         if ($openid_url) {
641                                 if (!Network::isUrlValid($openid_url)) {
642                                         throw new Exception(DI::l10n()->t('Invalid OpenID url'));
643                                 }
644                                 $_SESSION['register'] = 1;
645                                 $_SESSION['openid'] = $openid_url;
646
647                                 $openid = new LightOpenID(DI::baseUrl()->getHostname());
648                                 $openid->identity = $openid_url;
649                                 $openid->returnUrl = DI::baseUrl() . '/openid';
650                                 $openid->required = ['namePerson/friendly', 'contact/email', 'namePerson'];
651                                 $openid->optional = ['namePerson/first', 'media/image/aspect11', 'media/image/default'];
652                                 try {
653                                         $authurl = $openid->authUrl();
654                                 } catch (Exception $e) {
655                                         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);
656                                 }
657                                 System::externalRedirect($authurl);
658                                 // NOTREACHED
659                         }
660
661                         throw new Exception(DI::l10n()->t('Please enter the required information.'));
662                 }
663
664                 if (!Network::isUrlValid($openid_url)) {
665                         $openid_url = '';
666                 }
667
668                 // collapse multiple spaces in name
669                 $username = preg_replace('/ +/', ' ', $username);
670
671                 $username_min_length = max(1, min(64, intval(DI::config()->get('system', 'username_min_length', 3))));
672                 $username_max_length = max(1, min(64, intval(DI::config()->get('system', 'username_max_length', 48))));
673
674                 if ($username_min_length > $username_max_length) {
675                         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);
676                         $tmp = $username_min_length;
677                         $username_min_length = $username_max_length;
678                         $username_max_length = $tmp;
679                 }
680
681                 if (mb_strlen($username) < $username_min_length) {
682                         throw new Exception(DI::l10n()->tt('Username should be at least %s character.', 'Username should be at least %s characters.', $username_min_length));
683                 }
684
685                 if (mb_strlen($username) > $username_max_length) {
686                         throw new Exception(DI::l10n()->tt('Username should be at most %s character.', 'Username should be at most %s characters.', $username_max_length));
687                 }
688
689                 // So now we are just looking for a space in the full name.
690                 $loose_reg = DI::config()->get('system', 'no_regfullname');
691                 if (!$loose_reg) {
692                         $username = mb_convert_case($username, MB_CASE_TITLE, 'UTF-8');
693                         if (strpos($username, ' ') === false) {
694                                 throw new Exception(DI::l10n()->t("That doesn't appear to be your full (First Last) name."));
695                         }
696                 }
697
698                 if (!Network::isEmailDomainAllowed($email)) {
699                         throw new Exception(DI::l10n()->t('Your email domain is not among those allowed on this site.'));
700                 }
701
702                 if (!filter_var($email, FILTER_VALIDATE_EMAIL) || !Network::isEmailDomainValid($email)) {
703                         throw new Exception(DI::l10n()->t('Not a valid email address.'));
704                 }
705                 if (self::isNicknameBlocked($nickname)) {
706                         throw new Exception(DI::l10n()->t('The nickname was blocked from registration by the nodes admin.'));
707                 }
708
709                 if (DI::config()->get('system', 'block_extended_register', false) && DBA::exists('user', ['email' => $email])) {
710                         throw new Exception(DI::l10n()->t('Cannot use that email.'));
711                 }
712
713                 // Disallow somebody creating an account using openid that uses the admin email address,
714                 // since openid bypasses email verification. We'll allow it if there is not yet an admin account.
715                 if (DI::config()->get('config', 'admin_email') && strlen($openid_url)) {
716                         $adminlist = explode(',', str_replace(' ', '', strtolower(DI::config()->get('config', 'admin_email'))));
717                         if (in_array(strtolower($email), $adminlist)) {
718                                 throw new Exception(DI::l10n()->t('Cannot use that email.'));
719                         }
720                 }
721
722                 $nickname = $data['nickname'] = strtolower($nickname);
723
724                 if (!preg_match('/^[a-z0-9][a-z0-9\_]*$/', $nickname)) {
725                         throw new Exception(DI::l10n()->t('Your nickname can only contain a-z, 0-9 and _.'));
726                 }
727
728                 // Check existing and deleted accounts for this nickname.
729                 if (
730                         DBA::exists('user', ['nickname' => $nickname])
731                         || DBA::exists('userd', ['username' => $nickname])
732                 ) {
733                         throw new Exception(DI::l10n()->t('Nickname is already registered. Please choose another.'));
734                 }
735
736                 $new_password = strlen($password) ? $password : User::generateNewPassword();
737                 $new_password_encoded = self::hashPassword($new_password);
738
739                 $return['password'] = $new_password;
740
741                 $keys = Crypto::newKeypair(4096);
742                 if ($keys === false) {
743                         throw new Exception(DI::l10n()->t('SERIOUS ERROR: Generation of security keys failed.'));
744                 }
745
746                 $prvkey = $keys['prvkey'];
747                 $pubkey = $keys['pubkey'];
748
749                 // Create another keypair for signing/verifying salmon protocol messages.
750                 $sres = Crypto::newKeypair(512);
751                 $sprvkey = $sres['prvkey'];
752                 $spubkey = $sres['pubkey'];
753
754                 $insert_result = DBA::insert('user', [
755                         'guid'     => System::createUUID(),
756                         'username' => $username,
757                         'password' => $new_password_encoded,
758                         'email'    => $email,
759                         'openid'   => $openid_url,
760                         'nickname' => $nickname,
761                         'pubkey'   => $pubkey,
762                         'prvkey'   => $prvkey,
763                         'spubkey'  => $spubkey,
764                         'sprvkey'  => $sprvkey,
765                         'verified' => $verified,
766                         'blocked'  => $blocked,
767                         'language' => $language,
768                         'timezone' => 'UTC',
769                         'register_date' => DateTimeFormat::utcNow(),
770                         'default-location' => ''
771                 ]);
772
773                 if ($insert_result) {
774                         $uid = DBA::lastInsertId();
775                         $user = DBA::selectFirst('user', [], ['uid' => $uid]);
776                 } else {
777                         throw new Exception(DI::l10n()->t('An error occurred during registration. Please try again.'));
778                 }
779
780                 if (!$uid) {
781                         throw new Exception(DI::l10n()->t('An error occurred during registration. Please try again.'));
782                 }
783
784                 // if somebody clicked submit twice very quickly, they could end up with two accounts
785                 // due to race condition. Remove this one.
786                 $user_count = DBA::count('user', ['nickname' => $nickname]);
787                 if ($user_count > 1) {
788                         DBA::delete('user', ['uid' => $uid]);
789
790                         throw new Exception(DI::l10n()->t('Nickname is already registered. Please choose another.'));
791                 }
792
793                 $insert_result = DBA::insert('profile', [
794                         'uid' => $uid,
795                         'name' => $username,
796                         'photo' => DI::baseUrl() . "/photo/profile/{$uid}.jpg",
797                         'thumb' => DI::baseUrl() . "/photo/avatar/{$uid}.jpg",
798                         'publish' => $publish,
799                         'net-publish' => $netpublish,
800                 ]);
801                 if (!$insert_result) {
802                         DBA::delete('user', ['uid' => $uid]);
803
804                         throw new Exception(DI::l10n()->t('An error occurred creating your default profile. Please try again.'));
805                 }
806
807                 // Create the self contact
808                 if (!Contact::createSelfFromUserId($uid)) {
809                         DBA::delete('user', ['uid' => $uid]);
810
811                         throw new Exception(DI::l10n()->t('An error occurred creating your self contact. Please try again.'));
812                 }
813
814                 // Create a group with no members. This allows somebody to use it
815                 // right away as a default group for new contacts.
816                 $def_gid = Group::create($uid, DI::l10n()->t('Friends'));
817                 if (!$def_gid) {
818                         DBA::delete('user', ['uid' => $uid]);
819
820                         throw new Exception(DI::l10n()->t('An error occurred creating your default contact group. Please try again.'));
821                 }
822
823                 $fields = ['def_gid' => $def_gid];
824                 if (DI::config()->get('system', 'newuser_private') && $def_gid) {
825                         $fields['allow_gid'] = '<' . $def_gid . '>';
826                 }
827
828                 DBA::update('user', $fields, ['uid' => $uid]);
829
830                 // if we have no OpenID photo try to look up an avatar
831                 if (!strlen($photo)) {
832                         $photo = Network::lookupAvatarByEmail($email);
833                 }
834
835                 // unless there is no avatar-addon loaded
836                 if (strlen($photo)) {
837                         $photo_failure = false;
838
839                         $filename = basename($photo);
840                         $img_str = Network::fetchUrl($photo, true);
841                         // guess mimetype from headers or filename
842                         $type = Images::guessType($photo, true);
843
844                         $Image = new Image($img_str, $type);
845                         if ($Image->isValid()) {
846                                 $Image->scaleToSquare(300);
847
848                                 $resource_id = Photo::newResource();
849
850                                 $r = Photo::store($Image, $uid, 0, $resource_id, $filename, DI::l10n()->t('Profile Photos'), 4);
851
852                                 if ($r === false) {
853                                         $photo_failure = true;
854                                 }
855
856                                 $Image->scaleDown(80);
857
858                                 $r = Photo::store($Image, $uid, 0, $resource_id, $filename, DI::l10n()->t('Profile Photos'), 5);
859
860                                 if ($r === false) {
861                                         $photo_failure = true;
862                                 }
863
864                                 $Image->scaleDown(48);
865
866                                 $r = Photo::store($Image, $uid, 0, $resource_id, $filename, DI::l10n()->t('Profile Photos'), 6);
867
868                                 if ($r === false) {
869                                         $photo_failure = true;
870                                 }
871
872                                 if (!$photo_failure) {
873                                         Photo::update(['profile' => 1], ['resource-id' => $resource_id]);
874                                 }
875                         }
876                 }
877
878                 Hook::callAll('register_account', $uid);
879
880                 $return['user'] = $user;
881                 return $return;
882         }
883
884         /**
885          * Sends pending registration confirmation email
886          *
887          * @param array  $user     User record array
888          * @param string $sitename
889          * @param string $siteurl
890          * @param string $password Plaintext password
891          * @return NULL|boolean from notification() and email() inherited
892          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
893          */
894         public static function sendRegisterPendingEmail($user, $sitename, $siteurl, $password)
895         {
896                 $body = Strings::deindent(DI::l10n()->t(
897                         '
898                         Dear %1$s,
899                                 Thank you for registering at %2$s. Your account is pending for approval by the administrator.
900
901                         Your login details are as follows:
902
903                         Site Location:  %3$s
904                         Login Name:             %4$s
905                         Password:               %5$s
906                 ',
907                         $user['username'],
908                         $sitename,
909                         $siteurl,
910                         $user['nickname'],
911                         $password
912                 ));
913
914                 $email = DI::emailer()
915                         ->newSystemMail()
916                         ->withMessage(DI::l10n()->t('Registration at %s', $sitename), $body)
917                         ->forUser($user)
918                         ->withRecipient($user['email'])
919                         ->build();
920                 return DI::emailer()->send($email);
921         }
922
923         /**
924          * Sends registration confirmation
925          *
926          * It's here as a function because the mail is sent from different parts
927          *
928          * @param \Friendica\Core\L10n $l10n     The used language
929          * @param array                $user     User record array
930          * @param string               $sitename
931          * @param string               $siteurl
932          * @param string               $password Plaintext password
933          *
934          * @return NULL|boolean from notification() and email() inherited
935          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
936          */
937         public static function sendRegisterOpenEmail(\Friendica\Core\L10n $l10n, $user, $sitename, $siteurl, $password)
938         {
939                 $preamble = Strings::deindent($l10n->t(
940                         '
941                                 Dear %1$s,
942                                 Thank you for registering at %2$s. Your account has been created.
943                         ',
944                         $user['username'],
945                         $sitename
946                 ));
947                 $body = Strings::deindent($l10n->t(
948                         '
949                         The login details are as follows:
950
951                         Site Location:  %3$s
952                         Login Name:             %1$s
953                         Password:               %5$s
954
955                         You may change your password from your account "Settings" page after logging
956                         in.
957
958                         Please take a few moments to review the other account settings on that page.
959
960                         You may also wish to add some basic information to your default profile
961                         ' . "\x28" . 'on the "Profiles" page' . "\x29" . ' so that other people can easily find you.
962
963                         We recommend setting your full name, adding a profile photo,
964                         adding some profile "keywords" ' . "\x28" . 'very useful in making new friends' . "\x29" . ' - and
965                         perhaps what country you live in; if you do not wish to be more specific
966                         than that.
967
968                         We fully respect your right to privacy, and none of these items are necessary.
969                         If you are new and do not know anybody here, they may help
970                         you to make some new and interesting friends.
971
972                         If you ever want to delete your account, you can do so at %3$s/removeme
973
974                         Thank you and welcome to %2$s.',
975                         $user['nickname'],
976                         $sitename,
977                         $siteurl,
978                         $user['username'],
979                         $password
980                 ));
981
982                 $email = DI::emailer()
983                         ->newSystemMail()
984                         ->withMessage(DI::l10n()->t('Registration details for %s', $sitename), $preamble, $body)
985                         ->forUser($user)
986                         ->withRecipient($user['email'])
987                         ->build();
988                 return DI::emailer()->send($email);
989         }
990
991         /**
992          * @param object $uid user to remove
993          * @return bool
994          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
995          */
996         public static function remove($uid)
997         {
998                 if (!$uid) {
999                         return false;
1000                 }
1001
1002                 Logger::log('Removing user: ' . $uid);
1003
1004                 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
1005
1006                 Hook::callAll('remove_user', $user);
1007
1008                 // save username (actually the nickname as it is guaranteed
1009                 // unique), so it cannot be re-registered in the future.
1010                 DBA::insert('userd', ['username' => $user['nickname']]);
1011
1012                 // The user and related data will be deleted in "cron_expire_and_remove_users" (cronjobs.php)
1013                 DBA::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utc('now + 7 day')], ['uid' => $uid]);
1014                 Worker::add(PRIORITY_HIGH, 'Notifier', Delivery::REMOVAL, $uid);
1015
1016                 // Send an update to the directory
1017                 $self = DBA::selectFirst('contact', ['url'], ['uid' => $uid, 'self' => true]);
1018                 Worker::add(PRIORITY_LOW, 'Directory', $self['url']);
1019
1020                 // Remove the user relevant data
1021                 Worker::add(PRIORITY_NEGLIGIBLE, 'RemoveUser', $uid);
1022
1023                 return true;
1024         }
1025
1026         /**
1027          * Return all identities to a user
1028          *
1029          * @param int $uid The user id
1030          * @return array All identities for this user
1031          *
1032          * Example for a return:
1033          *    [
1034          *        [
1035          *            'uid' => 1,
1036          *            'username' => 'maxmuster',
1037          *            'nickname' => 'Max Mustermann'
1038          *        ],
1039          *        [
1040          *            'uid' => 2,
1041          *            'username' => 'johndoe',
1042          *            'nickname' => 'John Doe'
1043          *        ]
1044          *    ]
1045          * @throws Exception
1046          */
1047         public static function identities($uid)
1048         {
1049                 $identities = [];
1050
1051                 $user = DBA::selectFirst('user', ['uid', 'nickname', 'username', 'parent-uid'], ['uid' => $uid]);
1052                 if (!DBA::isResult($user)) {
1053                         return $identities;
1054                 }
1055
1056                 if ($user['parent-uid'] == 0) {
1057                         // First add our own entry
1058                         $identities = [[
1059                                 'uid' => $user['uid'],
1060                                 'username' => $user['username'],
1061                                 'nickname' => $user['nickname']
1062                         ]];
1063
1064                         // Then add all the children
1065                         $r = DBA::select(
1066                                 'user',
1067                                 ['uid', 'username', 'nickname'],
1068                                 ['parent-uid' => $user['uid'], 'account_removed' => false]
1069                         );
1070                         if (DBA::isResult($r)) {
1071                                 $identities = array_merge($identities, DBA::toArray($r));
1072                         }
1073                 } else {
1074                         // First entry is our parent
1075                         $r = DBA::select(
1076                                 'user',
1077                                 ['uid', 'username', 'nickname'],
1078                                 ['uid' => $user['parent-uid'], 'account_removed' => false]
1079                         );
1080                         if (DBA::isResult($r)) {
1081                                 $identities = DBA::toArray($r);
1082                         }
1083
1084                         // Then add all siblings
1085                         $r = DBA::select(
1086                                 'user',
1087                                 ['uid', 'username', 'nickname'],
1088                                 ['parent-uid' => $user['parent-uid'], 'account_removed' => false]
1089                         );
1090                         if (DBA::isResult($r)) {
1091                                 $identities = array_merge($identities, DBA::toArray($r));
1092                         }
1093                 }
1094
1095                 $r = DBA::p(
1096                         "SELECT `user`.`uid`, `user`.`username`, `user`.`nickname`
1097                         FROM `manage`
1098                         INNER JOIN `user` ON `manage`.`mid` = `user`.`uid`
1099                         WHERE `user`.`account_removed` = 0 AND `manage`.`uid` = ?",
1100                         $user['uid']
1101                 );
1102                 if (DBA::isResult($r)) {
1103                         $identities = array_merge($identities, DBA::toArray($r));
1104                 }
1105
1106                 return $identities;
1107         }
1108
1109         /**
1110          * Returns statistical information about the current users of this node
1111          *
1112          * @return array
1113          *
1114          * @throws Exception
1115          */
1116         public static function getStatistics()
1117         {
1118                 $statistics = [
1119                         'total_users'           => 0,
1120                         'active_users_halfyear' => 0,
1121                         'active_users_monthly'  => 0,
1122                 ];
1123
1124                 $userStmt = DBA::p("SELECT `user`.`uid`, `user`.`login_date`, `contact`.`last-item`
1125                         FROM `user`
1126                         INNER JOIN `profile` ON `profile`.`uid` = `user`.`uid`
1127                         INNER JOIN `contact` ON `contact`.`uid` = `user`.`uid` AND `contact`.`self`
1128                         WHERE (`profile`.`publish` OR `profile`.`net-publish`) AND `user`.`verified`
1129                                 AND NOT `user`.`blocked` AND NOT `user`.`account_removed`
1130                                 AND NOT `user`.`account_expired`");
1131
1132                 if (!DBA::isResult($userStmt)) {
1133                         return $statistics;
1134                 }
1135
1136                 $halfyear = time() - (180 * 24 * 60 * 60);
1137                 $month = time() - (30 * 24 * 60 * 60);
1138
1139                 while ($user = DBA::fetch($userStmt)) {
1140                         $statistics['total_users']++;
1141
1142                         if ((strtotime($user['login_date']) > $halfyear) || (strtotime($user['last-item']) > $halfyear)
1143                         ) {
1144                                 $statistics['active_users_halfyear']++;
1145                         }
1146
1147                         if ((strtotime($user['login_date']) > $month) || (strtotime($user['last-item']) > $month)
1148                         ) {
1149                                 $statistics['active_users_monthly']++;
1150                         }
1151                 }
1152
1153                 return $statistics;
1154         }
1155 }