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