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