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