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