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