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