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