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