]> git.mxchange.org Git - friendica.git/blob - src/Model/User.php
AP Bugfix: Following a soapbox now works / rapid follow/unfollow should work now
[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\PasswordStatus;
9 use Exception;
10 use Friendica\Core\Addon;
11 use Friendica\Core\Config;
12 use Friendica\Core\L10n;
13 use Friendica\Core\PConfig;
14 use Friendica\Core\Protocol;
15 use Friendica\Core\System;
16 use Friendica\Core\Worker;
17 use Friendica\Database\DBA;
18 use Friendica\Object\Image;
19 use Friendica\Util\Crypto;
20 use Friendica\Util\DateTimeFormat;
21 use Friendica\Util\Network;
22 use LightOpenID;
23 use function password_exposed;
24
25 require_once 'boot.php';
26 require_once 'include/dba.php';
27 require_once 'include/enotify.php';
28 require_once 'include/text.php';
29 /**
30  * @brief This class handles User related functions
31  */
32 class User
33 {
34         /**
35          * @brief Returns the user id of a given profile url
36          *
37          * @param string $profile
38          *
39          * @return integer user id
40          */
41         public static function getIdForURL($url)
42         {
43                 $self = DBA::selectFirst('contact', ['uid'], ['nurl' => normalise_link($url), 'self' => true]);
44                 if (!DBA::isResult($self)) {
45                         return false;
46                 } else {
47                         return $self['uid'];
48                 }
49         }
50
51         /**
52          * @brief Get owner data by user id
53          *
54          * @param int $uid
55          * @return boolean|array
56          */
57         public static function getOwnerDataById($uid) {
58                 $r = DBA::fetchFirst("SELECT
59                         `contact`.*,
60                         `user`.`prvkey` AS `uprvkey`,
61                         `user`.`timezone`,
62                         `user`.`nickname`,
63                         `user`.`sprvkey`,
64                         `user`.`spubkey`,
65                         `user`.`page-flags`,
66                         `user`.`account-type`,
67                         `user`.`prvnets`
68                         FROM `contact`
69                         INNER JOIN `user`
70                                 ON `user`.`uid` = `contact`.`uid`
71                         WHERE `contact`.`uid` = ?
72                         AND `contact`.`self`
73                         LIMIT 1",
74                         $uid
75                 );
76                 if (!DBA::isResult($r)) {
77                         return false;
78                 }
79                 return $r;
80         }
81
82         /**
83          * @brief Get owner data by nick name
84          *
85          * @param int $nick
86          * @return boolean|array
87          */
88         public static function getOwnerDataByNick($nick)
89         {
90                 $user = DBA::selectFirst('user', ['uid'], ['nickname' => $nick]);
91
92                 if (!DBA::isResult($user)) {
93                         return false;
94                 }
95
96                 return self::getOwnerDataById($user['uid']);
97         }
98
99         /**
100          * @brief Returns the default group for a given user and network
101          *
102          * @param int $uid User id
103          * @param string $network network name
104          *
105          * @return int group id
106          */
107         public static function getDefaultGroup($uid, $network = '')
108         {
109                 $default_group = 0;
110
111                 if ($network == Protocol::OSTATUS) {
112                         $default_group = PConfig::get($uid, "ostatus", "default_group");
113                 }
114
115                 if ($default_group != 0) {
116                         return $default_group;
117                 }
118
119                 $user = DBA::selectFirst('user', ['def_gid'], ['uid' => $uid]);
120
121                 if (DBA::isResult($user)) {
122                         $default_group = $user["def_gid"];
123                 }
124
125                 return $default_group;
126         }
127
128
129         /**
130          * Authenticate a user with a clear text password
131          *
132          * @brief Authenticate a user with a clear text password
133          * @param mixed $user_info
134          * @param string $password
135          * @return int|boolean
136          * @deprecated since version 3.6
137          * @see User::getIdFromPasswordAuthentication()
138          */
139         public static function authenticate($user_info, $password)
140         {
141                 try {
142                         return self::getIdFromPasswordAuthentication($user_info, $password);
143                 } catch (Exception $ex) {
144                         return false;
145                 }
146         }
147
148         /**
149          * Returns the user id associated with a successful password authentication
150          *
151          * @brief Authenticate a user with a clear text password
152          * @param mixed $user_info
153          * @param string $password
154          * @return int User Id if authentication is successful
155          * @throws Exception
156          */
157         public static function getIdFromPasswordAuthentication($user_info, $password)
158         {
159                 $user = self::getAuthenticationInfo($user_info);
160
161                 if (strpos($user['password'], '$') === false) {
162                         //Legacy hash that has not been replaced by a new hash yet
163                         if (self::hashPasswordLegacy($password) === $user['password']) {
164                                 self::updatePassword($user['uid'], $password);
165
166                                 return $user['uid'];
167                         }
168                 } elseif (!empty($user['legacy_password'])) {
169                         //Legacy hash that has been double-hashed and not replaced by a new hash yet
170                         //Warning: `legacy_password` is not necessary in sync with the content of `password`
171                         if (password_verify(self::hashPasswordLegacy($password), $user['password'])) {
172                                 self::updatePassword($user['uid'], $password);
173
174                                 return $user['uid'];
175                         }
176                 } elseif (password_verify($password, $user['password'])) {
177                         //New password hash
178                         if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
179                                 self::updatePassword($user['uid'], $password);
180                         }
181
182                         return $user['uid'];
183                 }
184
185                 throw new Exception(L10n::t('Login failed'));
186         }
187
188         /**
189          * Returns authentication info from various parameters types
190          *
191          * User info can be any of the following:
192          * - User DB object
193          * - User Id
194          * - User email or username or nickname
195          * - User array with at least the uid and the hashed password
196          *
197          * @param mixed $user_info
198          * @return array
199          * @throws Exception
200          */
201         private static function getAuthenticationInfo($user_info)
202         {
203                 $user = null;
204
205                 if (is_object($user_info) || is_array($user_info)) {
206                         if (is_object($user_info)) {
207                                 $user = (array) $user_info;
208                         } else {
209                                 $user = $user_info;
210                         }
211
212                         if (!isset($user['uid'])
213                                 || !isset($user['password'])
214                                 || !isset($user['legacy_password'])
215                         ) {
216                                 throw new Exception(L10n::t('Not enough information to authenticate'));
217                         }
218                 } elseif (is_int($user_info) || is_string($user_info)) {
219                         if (is_int($user_info)) {
220                                 $user = DBA::selectFirst('user', ['uid', 'password', 'legacy_password'],
221                                         [
222                                                 'uid' => $user_info,
223                                                 'blocked' => 0,
224                                                 'account_expired' => 0,
225                                                 'account_removed' => 0,
226                                                 'verified' => 1
227                                         ]
228                                 );
229                         } else {
230                                 $fields = ['uid', 'password', 'legacy_password'];
231                                 $condition = ["(`email` = ? OR `username` = ? OR `nickname` = ?)
232                                         AND NOT `blocked` AND NOT `account_expired` AND NOT `account_removed` AND `verified`",
233                                         $user_info, $user_info, $user_info];
234                                 $user = DBA::selectFirst('user', $fields, $condition);
235                         }
236
237                         if (!DBA::isResult($user)) {
238                                 throw new Exception(L10n::t('User not found'));
239                         }
240                 }
241
242                 return $user;
243         }
244
245         /**
246          * Generates a human-readable random password
247          *
248          * @return string
249          */
250         public static function generateNewPassword()
251         {
252                 return autoname(6) . mt_rand(100, 9999);
253         }
254
255         /**
256          * Checks if the provided plaintext password has been exposed or not
257          *
258          * @param string $password
259          * @return bool
260          */
261         public static function isPasswordExposed($password)
262         {
263                 return password_exposed($password) === PasswordStatus::EXPOSED;
264         }
265
266         /**
267          * Legacy hashing function, kept for password migration purposes
268          *
269          * @param string $password
270          * @return string
271          */
272         private static function hashPasswordLegacy($password)
273         {
274                 return hash('whirlpool', $password);
275         }
276
277         /**
278          * Global user password hashing function
279          *
280          * @param string $password
281          * @return string
282          */
283         public static function hashPassword($password)
284         {
285                 if (!trim($password)) {
286                         throw new Exception(L10n::t('Password can\'t be empty'));
287                 }
288
289                 return password_hash($password, PASSWORD_DEFAULT);
290         }
291
292         /**
293          * Updates a user row with a new plaintext password
294          *
295          * @param int    $uid
296          * @param string $password
297          * @return bool
298          */
299         public static function updatePassword($uid, $password)
300         {
301                 return self::updatePasswordHashed($uid, self::hashPassword($password));
302         }
303
304         /**
305          * Updates a user row with a new hashed password.
306          * Empties the password reset token field just in case.
307          *
308          * @param int    $uid
309          * @param string $pasword_hashed
310          * @return bool
311          */
312         private static function updatePasswordHashed($uid, $pasword_hashed)
313         {
314                 $fields = [
315                         'password' => $pasword_hashed,
316                         'pwdreset' => null,
317                         'pwdreset_time' => null,
318                         'legacy_password' => false
319                 ];
320                 return DBA::update('user', $fields, ['uid' => $uid]);
321         }
322
323         /**
324          * @brief Checks if a nickname is in the list of the forbidden nicknames
325          *
326          * Check if a nickname is forbidden from registration on the node by the
327          * admin. Forbidden nicknames (e.g. role namess) can be configured in the
328          * admin panel.
329          *
330          * @param string $nickname The nickname that should be checked
331          * @return boolean True is the nickname is blocked on the node
332          */
333         public static function isNicknameBlocked($nickname)
334         {
335                 $forbidden_nicknames = Config::get('system', 'forbidden_nicknames', '');
336
337                 // if the config variable is empty return false
338                 if (empty($forbidden_nicknames)) {
339                         return false;
340                 }
341
342                 // check if the nickname is in the list of blocked nicknames
343                 $forbidden = explode(',', $forbidden_nicknames);
344                 $forbidden = array_map('trim', $forbidden);
345                 if (in_array(strtolower($nickname), $forbidden)) {
346                         return true;
347                 }
348
349                 // else return false
350                 return false;
351         }
352
353         /**
354          * @brief Catch-all user creation function
355          *
356          * Creates a user from the provided data array, either form fields or OpenID.
357          * Required: { username, nickname, email } or { openid_url }
358          *
359          * Performs the following:
360          * - Sends to the OpenId auth URL (if relevant)
361          * - Creates new key pairs for crypto
362          * - Create self-contact
363          * - Create profile image
364          *
365          * @param array $data
366          * @return string
367          * @throw Exception
368          */
369         public static function create(array $data)
370         {
371                 $a = get_app();
372                 $return = ['user' => null, 'password' => ''];
373
374                 $using_invites = Config::get('system', 'invitation_only');
375                 $num_invites   = Config::get('system', 'number_invites');
376
377                 $invite_id  = !empty($data['invite_id'])  ? notags(trim($data['invite_id']))  : '';
378                 $username   = !empty($data['username'])   ? notags(trim($data['username']))   : '';
379                 $nickname   = !empty($data['nickname'])   ? notags(trim($data['nickname']))   : '';
380                 $email      = !empty($data['email'])      ? notags(trim($data['email']))      : '';
381                 $openid_url = !empty($data['openid_url']) ? notags(trim($data['openid_url'])) : '';
382                 $photo      = !empty($data['photo'])      ? notags(trim($data['photo']))      : '';
383                 $password   = !empty($data['password'])   ? trim($data['password'])           : '';
384                 $password1  = !empty($data['password1'])  ? trim($data['password1'])          : '';
385                 $confirm    = !empty($data['confirm'])    ? trim($data['confirm'])            : '';
386                 $blocked    = !empty($data['blocked'])    ? intval($data['blocked'])          : 0;
387                 $verified   = !empty($data['verified'])   ? intval($data['verified'])         : 0;
388                 $language   = !empty($data['language'])   ? notags(trim($data['language']))   : 'en';
389
390                 $publish = !empty($data['profile_publish_reg']) && intval($data['profile_publish_reg']) ? 1 : 0;
391                 $netpublish = strlen(Config::get('system', 'directory')) ? $publish : 0;
392
393                 if ($password1 != $confirm) {
394                         throw new Exception(L10n::t('Passwords do not match. Password unchanged.'));
395                 } elseif ($password1 != '') {
396                         $password = $password1;
397                 }
398
399                 if ($using_invites) {
400                         if (!$invite_id) {
401                                 throw new Exception(L10n::t('An invitation is required.'));
402                         }
403
404                         if (!DBA::exists('register', ['hash' => $invite_id])) {
405                                 throw new Exception(L10n::t('Invitation could not be verified.'));
406                         }
407                 }
408
409                 if (empty($username) || empty($email) || empty($nickname)) {
410                         if ($openid_url) {
411                                 if (!Network::isUrlValid($openid_url)) {
412                                         throw new Exception(L10n::t('Invalid OpenID url'));
413                                 }
414                                 $_SESSION['register'] = 1;
415                                 $_SESSION['openid'] = $openid_url;
416
417                                 $openid = new LightOpenID($a->get_hostname());
418                                 $openid->identity = $openid_url;
419                                 $openid->returnUrl = System::baseUrl() . '/openid';
420                                 $openid->required = ['namePerson/friendly', 'contact/email', 'namePerson'];
421                                 $openid->optional = ['namePerson/first', 'media/image/aspect11', 'media/image/default'];
422                                 try {
423                                         $authurl = $openid->authUrl();
424                                 } catch (Exception $e) {
425                                         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);
426                                 }
427                                 goaway($authurl);
428                                 // NOTREACHED
429                         }
430
431                         throw new Exception(L10n::t('Please enter the required information.'));
432                 }
433
434                 if (!Network::isUrlValid($openid_url)) {
435                         $openid_url = '';
436                 }
437
438                 $err = '';
439
440                 // collapse multiple spaces in name
441                 $username = preg_replace('/ +/', ' ', $username);
442
443                 if (mb_strlen($username) > 48) {
444                         throw new Exception(L10n::t('Please use a shorter name.'));
445                 }
446                 if (mb_strlen($username) < 3) {
447                         throw new Exception(L10n::t('Name too short.'));
448                 }
449
450                 // So now we are just looking for a space in the full name.
451                 $loose_reg = Config::get('system', 'no_regfullname');
452                 if (!$loose_reg) {
453                         $username = mb_convert_case($username, MB_CASE_TITLE, 'UTF-8');
454                         if (!strpos($username, ' ')) {
455                                 throw new Exception(L10n::t("That doesn't appear to be your full \x28First Last\x29 name."));
456                         }
457                 }
458
459                 if (!Network::isEmailDomainAllowed($email)) {
460                         throw new Exception(L10n::t('Your email domain is not among those allowed on this site.'));
461                 }
462
463                 if (!valid_email($email) || !Network::isEmailDomainValid($email)) {
464                         throw new Exception(L10n::t('Not a valid email address.'));
465                 }
466                 if (self::isNicknameBlocked($nickname)) {
467                         throw new Exception(L10n::t('The nickname was blocked from registration by the nodes admin.'));
468                 }
469
470                 if (Config::get('system', 'block_extended_register', false) && DBA::exists('user', ['email' => $email])) {
471                         throw new Exception(L10n::t('Cannot use that email.'));
472                 }
473
474                 // Disallow somebody creating an account using openid that uses the admin email address,
475                 // since openid bypasses email verification. We'll allow it if there is not yet an admin account.
476                 if (Config::get('config', 'admin_email') && strlen($openid_url)) {
477                         $adminlist = explode(',', str_replace(' ', '', strtolower(Config::get('config', 'admin_email'))));
478                         if (in_array(strtolower($email), $adminlist)) {
479                                 throw new Exception(L10n::t('Cannot use that email.'));
480                         }
481                 }
482
483                 $nickname = $data['nickname'] = strtolower($nickname);
484
485                 if (!preg_match('/^[a-z0-9][a-z0-9\_]*$/', $nickname)) {
486                         throw new Exception(L10n::t('Your nickname can only contain a-z, 0-9 and _.'));
487                 }
488
489                 // Check existing and deleted accounts for this nickname.
490                 if (DBA::exists('user', ['nickname' => $nickname])
491                         || DBA::exists('userd', ['username' => $nickname])
492                 ) {
493                         throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
494                 }
495
496                 $new_password = strlen($password) ? $password : User::generateNewPassword();
497                 $new_password_encoded = self::hashPassword($new_password);
498
499                 $return['password'] = $new_password;
500
501                 $keys = Crypto::newKeypair(4096);
502                 if ($keys === false) {
503                         throw new Exception(L10n::t('SERIOUS ERROR: Generation of security keys failed.'));
504                 }
505
506                 $prvkey = $keys['prvkey'];
507                 $pubkey = $keys['pubkey'];
508
509                 // Create another keypair for signing/verifying salmon protocol messages.
510                 $sres = Crypto::newKeypair(512);
511                 $sprvkey = $sres['prvkey'];
512                 $spubkey = $sres['pubkey'];
513
514                 $insert_result = DBA::insert('user', [
515                         'guid'     => System::createUUID(),
516                         'username' => $username,
517                         'password' => $new_password_encoded,
518                         'email'    => $email,
519                         'openid'   => $openid_url,
520                         'nickname' => $nickname,
521                         'pubkey'   => $pubkey,
522                         'prvkey'   => $prvkey,
523                         'spubkey'  => $spubkey,
524                         'sprvkey'  => $sprvkey,
525                         'verified' => $verified,
526                         'blocked'  => $blocked,
527                         'language' => $language,
528                         'timezone' => 'UTC',
529                         'register_date' => DateTimeFormat::utcNow(),
530                         'default-location' => ''
531                 ]);
532
533                 if ($insert_result) {
534                         $uid = DBA::lastInsertId();
535                         $user = DBA::selectFirst('user', [], ['uid' => $uid]);
536                 } else {
537                         throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
538                 }
539
540                 if (!$uid) {
541                         throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
542                 }
543
544                 // if somebody clicked submit twice very quickly, they could end up with two accounts
545                 // due to race condition. Remove this one.
546                 $user_count = DBA::count('user', ['nickname' => $nickname]);
547                 if ($user_count > 1) {
548                         DBA::delete('user', ['uid' => $uid]);
549
550                         throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
551                 }
552
553                 $insert_result = DBA::insert('profile', [
554                         'uid' => $uid,
555                         'name' => $username,
556                         'photo' => System::baseUrl() . "/photo/profile/{$uid}.jpg",
557                         'thumb' => System::baseUrl() . "/photo/avatar/{$uid}.jpg",
558                         'publish' => $publish,
559                         'is-default' => 1,
560                         'net-publish' => $netpublish,
561                         'profile-name' => L10n::t('default')
562                 ]);
563                 if (!$insert_result) {
564                         DBA::delete('user', ['uid' => $uid]);
565
566                         throw new Exception(L10n::t('An error occurred creating your default profile. Please try again.'));
567                 }
568
569                 // Create the self contact
570                 if (!Contact::createSelfFromUserId($uid)) {
571                         DBA::delete('user', ['uid' => $uid]);
572
573                         throw new Exception(L10n::t('An error occurred creating your self contact. Please try again.'));
574                 }
575
576                 // Create a group with no members. This allows somebody to use it
577                 // right away as a default group for new contacts.
578                 $def_gid = Group::create($uid, L10n::t('Friends'));
579                 if (!$def_gid) {
580                         DBA::delete('user', ['uid' => $uid]);
581
582                         throw new Exception(L10n::t('An error occurred creating your default contact group. Please try again.'));
583                 }
584
585                 $fields = ['def_gid' => $def_gid];
586                 if (Config::get('system', 'newuser_private') && $def_gid) {
587                         $fields['allow_gid'] = '<' . $def_gid . '>';
588                 }
589
590                 DBA::update('user', $fields, ['uid' => $uid]);
591
592                 // if we have no OpenID photo try to look up an avatar
593                 if (!strlen($photo)) {
594                         $photo = Network::lookupAvatarByEmail($email);
595                 }
596
597                 // unless there is no avatar-addon loaded
598                 if (strlen($photo)) {
599                         $photo_failure = false;
600
601                         $filename = basename($photo);
602                         $img_str = Network::fetchUrl($photo, true);
603                         // guess mimetype from headers or filename
604                         $type = Image::guessType($photo, true);
605
606                         $Image = new Image($img_str, $type);
607                         if ($Image->isValid()) {
608                                 $Image->scaleToSquare(175);
609
610                                 $hash = Photo::newResource();
611
612                                 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 4);
613
614                                 if ($r === false) {
615                                         $photo_failure = true;
616                                 }
617
618                                 $Image->scaleDown(80);
619
620                                 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 5);
621
622                                 if ($r === false) {
623                                         $photo_failure = true;
624                                 }
625
626                                 $Image->scaleDown(48);
627
628                                 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 6);
629
630                                 if ($r === false) {
631                                         $photo_failure = true;
632                                 }
633
634                                 if (!$photo_failure) {
635                                         DBA::update('photo', ['profile' => 1], ['resource-id' => $hash]);
636                                 }
637                         }
638                 }
639
640                 Addon::callHooks('register_account', $uid);
641
642                 $return['user'] = $user;
643                 return $return;
644         }
645
646         /**
647          * @brief Sends pending registration confiƕmation email
648          *
649          * @param string $email
650          * @param string $sitename
651          * @param string $username
652          * @return NULL|boolean from notification() and email() inherited
653          */
654         public static function sendRegisterPendingEmail($email, $sitename, $username)
655         {
656                 $body = deindent(L10n::t('
657                         Dear %1$s,
658                                 Thank you for registering at %2$s. Your account is pending for approval by the administrator.
659                 '));
660
661                 $body = sprintf($body, $username, $sitename);
662
663                 return notification([
664                         'type' => SYSTEM_EMAIL,
665                         'to_email' => $email,
666                         'subject'=> L10n::t('Registration at %s', $sitename),
667                         'body' => $body]);
668         }
669
670         /**
671          * @brief Sends registration confirmation
672          *
673          * It's here as a function because the mail is sent from different parts
674          *
675          * @param string $email
676          * @param string $sitename
677          * @param string $siteurl
678          * @param string $username
679          * @param string $password
680          * @return NULL|boolean from notification() and email() inherited
681          */
682         public static function sendRegisterOpenEmail($email, $sitename, $siteurl, $username, $password, $user)
683         {
684                 $preamble = deindent(L10n::t('
685                         Dear %1$s,
686                                 Thank you for registering at %2$s. Your account has been created.
687                 '));
688                 $body = deindent(L10n::t('
689                         The login details are as follows:
690
691                         Site Location:  %3$s
692                         Login Name:             %1$s
693                         Password:               %5$s
694
695                         You may change your password from your account "Settings" page after logging
696                         in.
697
698                         Please take a few moments to review the other account settings on that page.
699
700                         You may also wish to add some basic information to your default profile
701                         ' . "\x28" . 'on the "Profiles" page' . "\x29" . ' so that other people can easily find you.
702
703                         We recommend setting your full name, adding a profile photo,
704                         adding some profile "keywords" ' . "\x28" . 'very useful in making new friends' . "\x29" . ' - and
705                         perhaps what country you live in; if you do not wish to be more specific
706                         than that.
707
708                         We fully respect your right to privacy, and none of these items are necessary.
709                         If you are new and do not know anybody here, they may help
710                         you to make some new and interesting friends.
711
712                         If you ever want to delete your account, you can do so at %3$s/removeme
713
714                         Thank you and welcome to %2$s.'));
715
716                 $preamble = sprintf($preamble, $username, $sitename);
717                 $body = sprintf($body, $email, $sitename, $siteurl, $username, $password);
718
719                 return notification([
720                         'uid' => $user['uid'],
721                         'language' => $user['language'],
722                         'type' => SYSTEM_EMAIL,
723                         'to_email' => $email,
724                         'subject'=> L10n::t('Registration details for %s', $sitename),
725                         'preamble'=> $preamble,
726                         'body' => $body]);
727         }
728
729         /**
730          * @param object $uid user to remove
731          * @return void
732          */
733         public static function remove($uid)
734         {
735                 if (!$uid) {
736                         return;
737                 }
738
739                 logger('Removing user: ' . $uid);
740
741                 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
742
743                 Addon::callHooks('remove_user', $user);
744
745                 // save username (actually the nickname as it is guaranteed
746                 // unique), so it cannot be re-registered in the future.
747                 DBA::insert('userd', ['username' => $user['nickname']]);
748
749                 // The user and related data will be deleted in "cron_expire_and_remove_users" (cronjobs.php)
750                 DBA::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utc(DateTimeFormat::utcNow() . " + 7 day")], ['uid' => $uid]);
751                 Worker::add(PRIORITY_HIGH, "Notifier", "removeme", $uid);
752
753                 // Send an update to the directory
754                 $self = DBA::selectFirst('contact', ['url'], ['uid' => $uid, 'self' => true]);
755                 Worker::add(PRIORITY_LOW, "Directory", $self['url']);
756
757                 // Remove the user relevant data
758                 Worker::add(PRIORITY_LOW, "RemoveUser", $uid);
759
760                 if ($uid == local_user()) {
761                         unset($_SESSION['authenticated']);
762                         unset($_SESSION['uid']);
763                         goaway(System::baseUrl());
764                 }
765         }
766 }