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