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