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