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