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