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