]> git.mxchange.org Git - friendica.git/blob - src/Model/User.php
Remove deprecated method to find duplicates (issue from 2013)
[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                         `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 = System::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(System::baseUrl(), strpos(System::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          * @brief 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          * @brief 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 = 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          * @brief      Authenticate a user with a clear text password
297          * @param mixed  $user_info
298          * @param string $password
299          * @param bool   $third_party
300          * @return int|boolean
301          * @deprecated since version 3.6
302          * @see        User::getIdFromPasswordAuthentication()
303          */
304         public static function authenticate($user_info, $password, $third_party = false)
305         {
306                 try {
307                         return self::getIdFromPasswordAuthentication($user_info, $password, $third_party);
308                 } catch (Exception $ex) {
309                         return false;
310                 }
311         }
312
313         /**
314          * Returns the user id associated with a successful password authentication
315          *
316          * @brief Authenticate a user with a clear text password
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 && 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(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(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(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(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(L10n::t('Empty passwords are not allowed.'));
502                 }
503
504                 if (!Config::get('system', 'disable_password_exposed', false) && self::isPasswordExposed($password)) {
505                         throw new Exception(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(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          * @brief 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 = 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          * @brief 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                 $a = \get_app();
590                 $return = ['user' => null, 'password' => ''];
591
592                 $using_invites = Config::get('system', 'invitation_only');
593
594                 $invite_id  = !empty($data['invite_id'])  ? Strings::escapeTags(trim($data['invite_id']))  : '';
595                 $username   = !empty($data['username'])   ? Strings::escapeTags(trim($data['username']))   : '';
596                 $nickname   = !empty($data['nickname'])   ? Strings::escapeTags(trim($data['nickname']))   : '';
597                 $email      = !empty($data['email'])      ? Strings::escapeTags(trim($data['email']))      : '';
598                 $openid_url = !empty($data['openid_url']) ? Strings::escapeTags(trim($data['openid_url'])) : '';
599                 $photo      = !empty($data['photo'])      ? Strings::escapeTags(trim($data['photo']))      : '';
600                 $password   = !empty($data['password'])   ? trim($data['password'])           : '';
601                 $password1  = !empty($data['password1'])  ? trim($data['password1'])          : '';
602                 $confirm    = !empty($data['confirm'])    ? trim($data['confirm'])            : '';
603                 $blocked    = !empty($data['blocked']);
604                 $verified   = !empty($data['verified']);
605                 $language   = !empty($data['language'])   ? Strings::escapeTags(trim($data['language']))   : 'en';
606
607                 $publish = !empty($data['profile_publish_reg']);
608                 $netpublish = $publish && Config::get('system', 'directory');
609
610                 if ($password1 != $confirm) {
611                         throw new Exception(L10n::t('Passwords do not match. Password unchanged.'));
612                 } elseif ($password1 != '') {
613                         $password = $password1;
614                 }
615
616                 if ($using_invites) {
617                         if (!$invite_id) {
618                                 throw new Exception(L10n::t('An invitation is required.'));
619                         }
620
621                         if (!Register::existsByHash($invite_id)) {
622                                 throw new Exception(L10n::t('Invitation could not be verified.'));
623                         }
624                 }
625
626                 /// @todo Check if this part is really needed. We should have fetched all this data in advance
627                 if (empty($username) || empty($email) || empty($nickname)) {
628                         if ($openid_url) {
629                                 if (!Network::isUrlValid($openid_url)) {
630                                         throw new Exception(L10n::t('Invalid OpenID url'));
631                                 }
632                                 $_SESSION['register'] = 1;
633                                 $_SESSION['openid'] = $openid_url;
634
635                                 $openid = new LightOpenID($a->getHostName());
636                                 $openid->identity = $openid_url;
637                                 $openid->returnUrl = System::baseUrl() . '/openid';
638                                 $openid->required = ['namePerson/friendly', 'contact/email', 'namePerson'];
639                                 $openid->optional = ['namePerson/first', 'media/image/aspect11', 'media/image/default'];
640                                 try {
641                                         $authurl = $openid->authUrl();
642                                 } catch (Exception $e) {
643                                         throw new Exception(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);
644                                 }
645                                 System::externalRedirect($authurl);
646                                 // NOTREACHED
647                         }
648
649                         throw new Exception(L10n::t('Please enter the required information.'));
650                 }
651
652                 if (!Network::isUrlValid($openid_url)) {
653                         $openid_url = '';
654                 }
655
656                 // collapse multiple spaces in name
657                 $username = preg_replace('/ +/', ' ', $username);
658
659                 $username_min_length = max(1, min(64, intval(Config::get('system', 'username_min_length', 3))));
660                 $username_max_length = max(1, min(64, intval(Config::get('system', 'username_max_length', 48))));
661
662                 if ($username_min_length > $username_max_length) {
663                         Logger::log(L10n::t('system.username_min_length (%s) and system.username_max_length (%s) are excluding each other, swapping values.', $username_min_length, $username_max_length), Logger::WARNING);
664                         $tmp = $username_min_length;
665                         $username_min_length = $username_max_length;
666                         $username_max_length = $tmp;
667                 }
668
669                 if (mb_strlen($username) < $username_min_length) {
670                         throw new Exception(L10n::tt('Username should be at least %s character.', 'Username should be at least %s characters.', $username_min_length));
671                 }
672
673                 if (mb_strlen($username) > $username_max_length) {
674                         throw new Exception(L10n::tt('Username should be at most %s character.', 'Username should be at most %s characters.', $username_max_length));
675                 }
676
677                 // So now we are just looking for a space in the full name.
678                 $loose_reg = Config::get('system', 'no_regfullname');
679                 if (!$loose_reg) {
680                         $username = mb_convert_case($username, MB_CASE_TITLE, 'UTF-8');
681                         if (strpos($username, ' ') === false) {
682                                 throw new Exception(L10n::t("That doesn't appear to be your full (First Last) name."));
683                         }
684                 }
685
686                 if (!Network::isEmailDomainAllowed($email)) {
687                         throw new Exception(L10n::t('Your email domain is not among those allowed on this site.'));
688                 }
689
690                 if (!filter_var($email, FILTER_VALIDATE_EMAIL) || !Network::isEmailDomainValid($email)) {
691                         throw new Exception(L10n::t('Not a valid email address.'));
692                 }
693                 if (self::isNicknameBlocked($nickname)) {
694                         throw new Exception(L10n::t('The nickname was blocked from registration by the nodes admin.'));
695                 }
696
697                 if (Config::get('system', 'block_extended_register', false) && DBA::exists('user', ['email' => $email])) {
698                         throw new Exception(L10n::t('Cannot use that email.'));
699                 }
700
701                 // Disallow somebody creating an account using openid that uses the admin email address,
702                 // since openid bypasses email verification. We'll allow it if there is not yet an admin account.
703                 if (Config::get('config', 'admin_email') && strlen($openid_url)) {
704                         $adminlist = explode(',', str_replace(' ', '', strtolower(Config::get('config', 'admin_email'))));
705                         if (in_array(strtolower($email), $adminlist)) {
706                                 throw new Exception(L10n::t('Cannot use that email.'));
707                         }
708                 }
709
710                 $nickname = $data['nickname'] = strtolower($nickname);
711
712                 if (!preg_match('/^[a-z0-9][a-z0-9\_]*$/', $nickname)) {
713                         throw new Exception(L10n::t('Your nickname can only contain a-z, 0-9 and _.'));
714                 }
715
716                 // Check existing and deleted accounts for this nickname.
717                 if (
718                         DBA::exists('user', ['nickname' => $nickname])
719                         || DBA::exists('userd', ['username' => $nickname])
720                 ) {
721                         throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
722                 }
723
724                 $new_password = strlen($password) ? $password : User::generateNewPassword();
725                 $new_password_encoded = self::hashPassword($new_password);
726
727                 $return['password'] = $new_password;
728
729                 $keys = Crypto::newKeypair(4096);
730                 if ($keys === false) {
731                         throw new Exception(L10n::t('SERIOUS ERROR: Generation of security keys failed.'));
732                 }
733
734                 $prvkey = $keys['prvkey'];
735                 $pubkey = $keys['pubkey'];
736
737                 // Create another keypair for signing/verifying salmon protocol messages.
738                 $sres = Crypto::newKeypair(512);
739                 $sprvkey = $sres['prvkey'];
740                 $spubkey = $sres['pubkey'];
741
742                 $insert_result = DBA::insert('user', [
743                         'guid'     => System::createUUID(),
744                         'username' => $username,
745                         'password' => $new_password_encoded,
746                         'email'    => $email,
747                         'openid'   => $openid_url,
748                         'nickname' => $nickname,
749                         'pubkey'   => $pubkey,
750                         'prvkey'   => $prvkey,
751                         'spubkey'  => $spubkey,
752                         'sprvkey'  => $sprvkey,
753                         'verified' => $verified,
754                         'blocked'  => $blocked,
755                         'language' => $language,
756                         'timezone' => 'UTC',
757                         'register_date' => DateTimeFormat::utcNow(),
758                         'default-location' => ''
759                 ]);
760
761                 if ($insert_result) {
762                         $uid = DBA::lastInsertId();
763                         $user = DBA::selectFirst('user', [], ['uid' => $uid]);
764                 } else {
765                         throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
766                 }
767
768                 if (!$uid) {
769                         throw new Exception(L10n::t('An error occurred during registration. Please try again.'));
770                 }
771
772                 // if somebody clicked submit twice very quickly, they could end up with two accounts
773                 // due to race condition. Remove this one.
774                 $user_count = DBA::count('user', ['nickname' => $nickname]);
775                 if ($user_count > 1) {
776                         DBA::delete('user', ['uid' => $uid]);
777
778                         throw new Exception(L10n::t('Nickname is already registered. Please choose another.'));
779                 }
780
781                 $insert_result = DBA::insert('profile', [
782                         'uid' => $uid,
783                         'name' => $username,
784                         'photo' => System::baseUrl() . "/photo/profile/{$uid}.jpg",
785                         'thumb' => System::baseUrl() . "/photo/avatar/{$uid}.jpg",
786                         'publish' => $publish,
787                         'is-default' => 1,
788                         'net-publish' => $netpublish,
789                         'profile-name' => L10n::t('default')
790                 ]);
791                 if (!$insert_result) {
792                         DBA::delete('user', ['uid' => $uid]);
793
794                         throw new Exception(L10n::t('An error occurred creating your default profile. Please try again.'));
795                 }
796
797                 // Create the self contact
798                 if (!Contact::createSelfFromUserId($uid)) {
799                         DBA::delete('user', ['uid' => $uid]);
800
801                         throw new Exception(L10n::t('An error occurred creating your self contact. Please try again.'));
802                 }
803
804                 // Create a group with no members. This allows somebody to use it
805                 // right away as a default group for new contacts.
806                 $def_gid = Group::create($uid, L10n::t('Friends'));
807                 if (!$def_gid) {
808                         DBA::delete('user', ['uid' => $uid]);
809
810                         throw new Exception(L10n::t('An error occurred creating your default contact group. Please try again.'));
811                 }
812
813                 $fields = ['def_gid' => $def_gid];
814                 if (Config::get('system', 'newuser_private') && $def_gid) {
815                         $fields['allow_gid'] = '<' . $def_gid . '>';
816                 }
817
818                 DBA::update('user', $fields, ['uid' => $uid]);
819
820                 // if we have no OpenID photo try to look up an avatar
821                 if (!strlen($photo)) {
822                         $photo = Network::lookupAvatarByEmail($email);
823                 }
824
825                 // unless there is no avatar-addon loaded
826                 if (strlen($photo)) {
827                         $photo_failure = false;
828
829                         $filename = basename($photo);
830                         $img_str = Network::fetchUrl($photo, true);
831                         // guess mimetype from headers or filename
832                         $type = Image::guessType($photo, true);
833
834                         $Image = new Image($img_str, $type);
835                         if ($Image->isValid()) {
836                                 $Image->scaleToSquare(300);
837
838                                 $hash = Photo::newResource();
839
840                                 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 4);
841
842                                 if ($r === false) {
843                                         $photo_failure = true;
844                                 }
845
846                                 $Image->scaleDown(80);
847
848                                 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 5);
849
850                                 if ($r === false) {
851                                         $photo_failure = true;
852                                 }
853
854                                 $Image->scaleDown(48);
855
856                                 $r = Photo::store($Image, $uid, 0, $hash, $filename, L10n::t('Profile Photos'), 6);
857
858                                 if ($r === false) {
859                                         $photo_failure = true;
860                                 }
861
862                                 if (!$photo_failure) {
863                                         Photo::update(['profile' => 1], ['resource-id' => $hash]);
864                                 }
865                         }
866                 }
867
868                 Hook::callAll('register_account', $uid);
869
870                 $return['user'] = $user;
871                 return $return;
872         }
873
874         /**
875          * @brief Sends pending registration confirmation email
876          *
877          * @param array  $user     User record array
878          * @param string $sitename
879          * @param string $siteurl
880          * @param string $password Plaintext password
881          * @return NULL|boolean from notification() and email() inherited
882          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
883          */
884         public static function sendRegisterPendingEmail($user, $sitename, $siteurl, $password)
885         {
886                 $body = Strings::deindent(L10n::t(
887                         '
888                         Dear %1$s,
889                                 Thank you for registering at %2$s. Your account is pending for approval by the administrator.
890
891                         Your login details are as follows:
892
893                         Site Location:  %3$s
894                         Login Name:             %4$s
895                         Password:               %5$s
896                 ',
897                         $user['username'],
898                         $sitename,
899                         $siteurl,
900                         $user['nickname'],
901                         $password
902                 ));
903
904                 return notification([
905                         'type'     => SYSTEM_EMAIL,
906                         'uid'      => $user['uid'],
907                         'to_email' => $user['email'],
908                         'subject'  => L10n::t('Registration at %s', $sitename),
909                         'body'     => $body
910                 ]);
911         }
912
913         /**
914          * @brief Sends registration confirmation
915          *
916          * It's here as a function because the mail is sent from different parts
917          *
918          * @param array  $user     User record array
919          * @param string $sitename
920          * @param string $siteurl
921          * @param string $password Plaintext password
922          * @return NULL|boolean from notification() and email() inherited
923          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
924          */
925         public static function sendRegisterOpenEmail($user, $sitename, $siteurl, $password)
926         {
927                 $preamble = Strings::deindent(L10n::t(
928                         '
929                                 Dear %1$s,
930                                 Thank you for registering at %2$s. Your account has been created.
931                         ',
932                         $user['username'],
933                         $sitename
934                 ));
935                 $body = Strings::deindent(L10n::t(
936                         '
937                         The login details are as follows:
938
939                         Site Location:  %3$s
940                         Login Name:             %1$s
941                         Password:               %5$s
942
943                         You may change your password from your account "Settings" page after logging
944                         in.
945
946                         Please take a few moments to review the other account settings on that page.
947
948                         You may also wish to add some basic information to your default profile
949                         ' . "\x28" . 'on the "Profiles" page' . "\x29" . ' so that other people can easily find you.
950
951                         We recommend setting your full name, adding a profile photo,
952                         adding some profile "keywords" ' . "\x28" . 'very useful in making new friends' . "\x29" . ' - and
953                         perhaps what country you live in; if you do not wish to be more specific
954                         than that.
955
956                         We fully respect your right to privacy, and none of these items are necessary.
957                         If you are new and do not know anybody here, they may help
958                         you to make some new and interesting friends.
959
960                         If you ever want to delete your account, you can do so at %3$s/removeme
961
962                         Thank you and welcome to %2$s.',
963                         $user['nickname'],
964                         $sitename,
965                         $siteurl,
966                         $user['username'],
967                         $password
968                 ));
969
970                 return notification([
971                         'uid'      => $user['uid'],
972                         'language' => $user['language'],
973                         'type'     => SYSTEM_EMAIL,
974                         'to_email' => $user['email'],
975                         'subject'  => L10n::t('Registration details for %s', $sitename),
976                         'preamble' => $preamble,
977                         'body'     => $body
978                 ]);
979         }
980
981         /**
982          * @param object $uid user to remove
983          * @return bool
984          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
985          */
986         public static function remove($uid)
987         {
988                 if (!$uid) {
989                         return false;
990                 }
991
992                 Logger::log('Removing user: ' . $uid);
993
994                 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
995
996                 Hook::callAll('remove_user', $user);
997
998                 // save username (actually the nickname as it is guaranteed
999                 // unique), so it cannot be re-registered in the future.
1000                 DBA::insert('userd', ['username' => $user['nickname']]);
1001
1002                 // The user and related data will be deleted in "cron_expire_and_remove_users" (cronjobs.php)
1003                 DBA::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utc('now + 7 day')], ['uid' => $uid]);
1004                 Worker::add(PRIORITY_HIGH, 'Notifier', Delivery::REMOVAL, $uid);
1005
1006                 // Send an update to the directory
1007                 $self = DBA::selectFirst('contact', ['url'], ['uid' => $uid, 'self' => true]);
1008                 Worker::add(PRIORITY_LOW, 'Directory', $self['url']);
1009
1010                 // Remove the user relevant data
1011                 Worker::add(PRIORITY_NEGLIGIBLE, 'RemoveUser', $uid);
1012
1013                 return true;
1014         }
1015
1016         /**
1017          * Return all identities to a user
1018          *
1019          * @param int $uid The user id
1020          * @return array All identities for this user
1021          *
1022          * Example for a return:
1023          *    [
1024          *        [
1025          *            'uid' => 1,
1026          *            'username' => 'maxmuster',
1027          *            'nickname' => 'Max Mustermann'
1028          *        ],
1029          *        [
1030          *            'uid' => 2,
1031          *            'username' => 'johndoe',
1032          *            'nickname' => 'John Doe'
1033          *        ]
1034          *    ]
1035          * @throws Exception
1036          */
1037         public static function identities($uid)
1038         {
1039                 $identities = [];
1040
1041                 $user = DBA::selectFirst('user', ['uid', 'nickname', 'username', 'parent-uid'], ['uid' => $uid]);
1042                 if (!DBA::isResult($user)) {
1043                         return $identities;
1044                 }
1045
1046                 if ($user['parent-uid'] == 0) {
1047                         // First add our own entry
1048                         $identities = [[
1049                                 'uid' => $user['uid'],
1050                                 'username' => $user['username'],
1051                                 'nickname' => $user['nickname']
1052                         ]];
1053
1054                         // Then add all the children
1055                         $r = DBA::select(
1056                                 'user',
1057                                 ['uid', 'username', 'nickname'],
1058                                 ['parent-uid' => $user['uid'], 'account_removed' => false]
1059                         );
1060                         if (DBA::isResult($r)) {
1061                                 $identities = array_merge($identities, DBA::toArray($r));
1062                         }
1063                 } else {
1064                         // First entry is our parent
1065                         $r = DBA::select(
1066                                 'user',
1067                                 ['uid', 'username', 'nickname'],
1068                                 ['uid' => $user['parent-uid'], 'account_removed' => false]
1069                         );
1070                         if (DBA::isResult($r)) {
1071                                 $identities = DBA::toArray($r);
1072                         }
1073
1074                         // Then add all siblings
1075                         $r = DBA::select(
1076                                 'user',
1077                                 ['uid', 'username', 'nickname'],
1078                                 ['parent-uid' => $user['parent-uid'], 'account_removed' => false]
1079                         );
1080                         if (DBA::isResult($r)) {
1081                                 $identities = array_merge($identities, DBA::toArray($r));
1082                         }
1083                 }
1084
1085                 $r = DBA::p(
1086                         "SELECT `user`.`uid`, `user`.`username`, `user`.`nickname`
1087                         FROM `manage`
1088                         INNER JOIN `user` ON `manage`.`mid` = `user`.`uid`
1089                         WHERE `user`.`account_removed` = 0 AND `manage`.`uid` = ?",
1090                         $user['uid']
1091                 );
1092                 if (DBA::isResult($r)) {
1093                         $identities = array_merge($identities, DBA::toArray($r));
1094                 }
1095
1096                 return $identities;
1097         }
1098
1099         /**
1100          * Returns statistical information about the current users of this node
1101          *
1102          * @return array
1103          *
1104          * @throws Exception
1105          */
1106         public static function getStatistics()
1107         {
1108                 $statistics = [
1109                         'total_users'           => 0,
1110                         'active_users_halfyear' => 0,
1111                         'active_users_monthly'  => 0,
1112                 ];
1113
1114                 $userStmt = DBA::p("SELECT `user`.`uid`, `user`.`login_date`, `contact`.`last-item`
1115                         FROM `user`
1116                         INNER JOIN `profile` ON `profile`.`uid` = `user`.`uid` AND `profile`.`is-default`
1117                         INNER JOIN `contact` ON `contact`.`uid` = `user`.`uid` AND `contact`.`self`
1118                         WHERE (`profile`.`publish` OR `profile`.`net-publish`) AND `user`.`verified`
1119                                 AND NOT `user`.`blocked` AND NOT `user`.`account_removed`
1120                                 AND NOT `user`.`account_expired`");
1121
1122                 if (!DBA::isResult($userStmt)) {
1123                         return $statistics;
1124                 }
1125
1126                 $halfyear = time() - (180 * 24 * 60 * 60);
1127                 $month = time() - (30 * 24 * 60 * 60);
1128
1129                 while ($user = DBA::fetch($userStmt)) {
1130                         $statistics['total_users']++;
1131
1132                         if ((strtotime($user['login_date']) > $halfyear) || (strtotime($user['last-item']) > $halfyear)
1133                         ) {
1134                                 $statistics['active_users_halfyear']++;
1135                         }
1136
1137                         if ((strtotime($user['login_date']) > $month) || (strtotime($user['last-item']) > $month)
1138                         ) {
1139                                 $statistics['active_users_monthly']++;
1140                         }
1141                 }
1142
1143                 return $statistics;
1144         }
1145 }