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