]> git.mxchange.org Git - friendica.git/blob - src/Model/User.php
2a954e6e609c6853ca63efe455aaaccb10fdaecb
[friendica.git] / src / Model / User.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2020, Friendica
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Model;
23
24 use DivineOmega\PasswordExposed;
25 use Exception;
26 use Friendica\Content\Pager;
27 use Friendica\Core\Hook;
28 use Friendica\Core\L10n;
29 use Friendica\Core\Logger;
30 use Friendica\Core\Protocol;
31 use Friendica\Core\System;
32 use Friendica\Core\Worker;
33 use Friendica\Database\DBA;
34 use Friendica\DI;
35 use Friendica\Model\TwoFactor\AppSpecificPassword;
36 use Friendica\Network\HTTPException\InternalServerErrorException;
37 use Friendica\Object\Image;
38 use Friendica\Util\Crypto;
39 use Friendica\Util\DateTimeFormat;
40 use Friendica\Util\Images;
41 use Friendica\Util\Network;
42 use Friendica\Util\Strings;
43 use Friendica\Worker\Delivery;
44 use LightOpenID;
45
46 /**
47  * This class handles User related functions
48  */
49 class User
50 {
51         /**
52          * Page/profile types
53          *
54          * PAGE_FLAGS_NORMAL is a typical personal profile account
55          * PAGE_FLAGS_SOAPBOX automatically approves all friend requests as Contact::SHARING, (readonly)
56          * PAGE_FLAGS_COMMUNITY automatically approves all friend requests as Contact::SHARING, but with
57          *      write access to wall and comments (no email and not included in page owner's ACL lists)
58          * PAGE_FLAGS_FREELOVE automatically approves all friend requests as full friends (Contact::FRIEND).
59          *
60          * @{
61          */
62         const PAGE_FLAGS_NORMAL    = 0;
63         const PAGE_FLAGS_SOAPBOX   = 1;
64         const PAGE_FLAGS_COMMUNITY = 2;
65         const PAGE_FLAGS_FREELOVE  = 3;
66         const PAGE_FLAGS_BLOG      = 4;
67         const PAGE_FLAGS_PRVGROUP  = 5;
68         /**
69          * @}
70          */
71
72         /**
73          * Account types
74          *
75          * ACCOUNT_TYPE_PERSON - the account belongs to a person
76          *      Associated page types: PAGE_FLAGS_NORMAL, PAGE_FLAGS_SOAPBOX, PAGE_FLAGS_FREELOVE
77          *
78          * ACCOUNT_TYPE_ORGANISATION - the account belongs to an organisation
79          *      Associated page type: PAGE_FLAGS_SOAPBOX
80          *
81          * ACCOUNT_TYPE_NEWS - the account is a news reflector
82          *      Associated page type: PAGE_FLAGS_SOAPBOX
83          *
84          * ACCOUNT_TYPE_COMMUNITY - the account is community forum
85          *      Associated page types: PAGE_COMMUNITY, PAGE_FLAGS_PRVGROUP
86          *
87          * ACCOUNT_TYPE_RELAY - the account is a relay
88          *      This will only be assigned to contacts, not to user accounts
89          * @{
90          */
91         const ACCOUNT_TYPE_PERSON =       0;
92         const ACCOUNT_TYPE_ORGANISATION = 1;
93         const ACCOUNT_TYPE_NEWS =         2;
94         const ACCOUNT_TYPE_COMMUNITY =    3;
95         const ACCOUNT_TYPE_RELAY =        4;
96         /**
97          * @}
98          */
99
100         private static $owner;
101
102         /**
103          * Fetch the system account
104          *
105          * @return array system account
106          */
107         public static function getSystemAccount()
108         {
109                 $system = Contact::selectFirst([], ['self' => true, 'uid' => 0]);
110                 if (!DBA::isResult($system)) {
111                         self::createSystemAccount();
112                         $system = Contact::selectFirst([], ['self' => true, 'uid' => 0]);
113                         if (!DBA::isResult($system)) {
114                                 return [];
115                         }
116                 }
117
118                 $system['spubkey'] = $system['uprvkey'] = $system['prvkey'];
119                 $system['username'] = $system['name'];
120                 $system['nickname'] = $system['nick'];
121                 return $system;
122         }
123
124         /**
125          * Create the system account
126          *
127          * @return void
128          */
129         private static function createSystemAccount()
130         {
131                 $system_actor_name = self::getActorName();
132                 if (empty($system_actor_name)) {
133                         return;
134                 }
135
136                 $keys = Crypto::newKeypair(4096);
137                 if ($keys === false) {
138                         throw new Exception(DI::l10n()->t('SERIOUS ERROR: Generation of security keys failed.'));
139                 }
140
141                 $system = [];
142                 $system['uid'] = 0;
143                 $system['created'] = DateTimeFormat::utcNow();
144                 $system['self'] = true;
145                 $system['network'] = Protocol::ACTIVITYPUB;
146                 $system['name'] = 'System Account';
147                 $system['addr'] = $system_actor_name . '@' . DI::baseUrl()->getHostname();
148                 $system['nick'] = $system_actor_name;
149                 $system['avatar'] = DI::baseUrl() . Contact::DEFAULT_AVATAR_PHOTO;
150                 $system['photo'] = DI::baseUrl() . Contact::DEFAULT_AVATAR_PHOTO;
151                 $system['thumb'] = DI::baseUrl() . Contact::DEFAULT_AVATAR_THUMB;
152                 $system['micro'] = DI::baseUrl() . Contact::DEFAULT_AVATAR_MICRO;
153                 $system['url'] = DI::baseUrl() . '/friendica';
154                 $system['nurl'] = Strings::normaliseLink($system['url']);
155                 $system['pubkey'] = $keys['pubkey'];
156                 $system['prvkey'] = $keys['prvkey'];
157                 $system['blocked'] = 0;
158                 $system['pending'] = 0;
159                 $system['contact-type'] = Contact::TYPE_RELAY; // In AP this is translated to 'Application'
160                 $system['name-date'] = DateTimeFormat::utcNow();
161                 $system['uri-date'] = DateTimeFormat::utcNow();
162                 $system['avatar-date'] = DateTimeFormat::utcNow();
163                 $system['closeness'] = 0;
164                 $system['baseurl'] = DI::baseUrl();
165                 $system['gsid'] = GServer::getID($system['baseurl']);
166                 DBA::insert('contact', $system);
167         }
168
169         /**
170          * Detect a usable actor name
171          *
172          * @return string actor account name
173          */
174         public static function getActorName()
175         {
176                 $system_actor_name = DI::config()->get('system', 'actor_name');
177                 if (!empty($system_actor_name)) {
178                         return $system_actor_name;
179                 }
180
181                 // List of possible actor names
182                 $possible_accounts = ['friendica', 'actor', 'system', 'internal'];
183                 foreach ($possible_accounts as $name) {
184                         if (!DBA::exists('user', ['nickname' => $name, 'account_removed' => false, 'expire']) &&
185                                 !DBA::exists('userd', ['username' => $name])) {
186                                 DI::config()->set('system', 'actor_name', $name);
187                                 return $name;
188                         }
189                 }
190                 return '';
191         }
192
193         /**
194          * Returns true if a user record exists with the provided id
195          *
196          * @param  integer $uid
197          * @return boolean
198          * @throws Exception
199          */
200         public static function exists($uid)
201         {
202                 return DBA::exists('user', ['uid' => $uid]);
203         }
204
205         /**
206          * @param  integer       $uid
207          * @param array          $fields
208          * @return array|boolean User record if it exists, false otherwise
209          * @throws Exception
210          */
211         public static function getById($uid, array $fields = [])
212         {
213                 return DBA::selectFirst('user', $fields, ['uid' => $uid]);
214         }
215
216         /**
217          * Returns a user record based on it's GUID
218          *
219          * @param string $guid   The guid of the user
220          * @param array  $fields The fields to retrieve
221          * @param bool   $active True, if only active records are searched
222          *
223          * @return array|boolean User record if it exists, false otherwise
224          * @throws Exception
225          */
226         public static function getByGuid(string $guid, array $fields = [], bool $active = true)
227         {
228                 if ($active) {
229                         $cond = ['guid' => $guid, 'account_expired' => false, 'account_removed' => false];
230                 } else {
231                         $cond = ['guid' => $guid];
232                 }
233
234                 return DBA::selectFirst('user', $fields, $cond);
235         }
236
237         /**
238          * @param  string        $nickname
239          * @param array          $fields
240          * @return array|boolean User record if it exists, false otherwise
241          * @throws Exception
242          */
243         public static function getByNickname($nickname, array $fields = [])
244         {
245                 return DBA::selectFirst('user', $fields, ['nickname' => $nickname]);
246         }
247
248         /**
249          * Returns the user id of a given profile URL
250          *
251          * @param string $url
252          *
253          * @return integer user id
254          * @throws Exception
255          */
256         public static function getIdForURL(string $url)
257         {
258                 // Avoid any database requests when the hostname isn't even part of the url.
259                 if (!strpos($url, DI::baseUrl()->getHostname())) {
260                         return 0;
261                 }
262
263                 $self = Contact::selectFirst(['uid'], ['self' => true, 'nurl' => Strings::normaliseLink($url)]);
264                 if (!empty($self['uid'])) {
265                         return $self['uid'];
266                 }
267
268                 $self = Contact::selectFirst(['uid'], ['self' => true, 'addr' => $url]);
269                 if (!empty($self['uid'])) {
270                         return $self['uid'];
271                 }
272
273                 $self = Contact::selectFirst(['uid'], ['self' => true, 'alias' => [$url, Strings::normaliseLink($url)]]);
274                 if (!empty($self['uid'])) {
275                         return $self['uid'];
276                 }
277
278                 return 0;
279         }
280
281         /**
282          * Get a user based on its email
283          *
284          * @param string        $email
285          * @param array          $fields
286          *
287          * @return array|boolean User record if it exists, false otherwise
288          *
289          * @throws Exception
290          */
291         public static function getByEmail($email, array $fields = [])
292         {
293                 return DBA::selectFirst('user', $fields, ['email' => $email]);
294         }
295
296         /**
297          * Fetch the user array of the administrator. The first one if there are several.
298          *
299          * @param array $fields
300          * @return array user
301          */
302         public static function getFirstAdmin(array $fields = [])
303         {
304                 if (!empty(DI::config()->get('config', 'admin_nickname'))) {
305                         return self::getByNickname(DI::config()->get('config', 'admin_nickname'), $fields);
306                 } elseif (!empty(DI::config()->get('config', 'admin_email'))) {
307                         $adminList = explode(',', str_replace(' ', '', DI::config()->get('config', 'admin_email')));
308                         return self::getByEmail($adminList[0], $fields);
309                 } else {
310                         return [];
311                 }
312         }
313
314         /**
315          * Get owner data by user id
316          *
317          * @param int $uid
318          * @param boolean $check_valid Test if data is invalid and correct it
319          * @return boolean|array
320          * @throws Exception
321          */
322         public static function getOwnerDataById(int $uid, bool $check_valid = true)
323         {
324                 if (!empty(self::$owner[$uid])) {
325                         return self::$owner[$uid];
326                 }
327
328                 $owner = DBA::selectFirst('owner-view', [], ['uid' => $uid]);
329                 if (!DBA::isResult($owner)) {
330                         if (!DBA::exists('user', ['uid' => $uid]) || !$check_valid) {
331                                 return false;
332                         }
333                         Contact::createSelfFromUserId($uid);
334                         $owner = self::getOwnerDataById($uid, false);
335                 }
336
337                 if (empty($owner['nickname'])) {
338                         return false;
339                 }
340
341                 if (!$check_valid) {
342                         return $owner;
343                 }
344
345                 // Check if the returned data is valid, otherwise fix it. See issue #6122
346
347                 // Check for correct url and normalised nurl
348                 $url = DI::baseUrl() . '/profile/' . $owner['nickname'];
349                 $repair = ($owner['url'] != $url) || ($owner['nurl'] != Strings::normaliseLink($owner['url']));
350
351                 if (!$repair) {
352                         // Check if "addr" is present and correct
353                         $addr = $owner['nickname'] . '@' . substr(DI::baseUrl(), strpos(DI::baseUrl(), '://') + 3);
354                         $repair = ($addr != $owner['addr']);
355                 }
356
357                 if (!$repair) {
358                         // Check if the avatar field is filled and the photo directs to the correct path
359                         $avatar = Photo::selectFirst(['resource-id'], ['uid' => $uid, 'profile' => true]);
360                         if (DBA::isResult($avatar)) {
361                                 $repair = empty($owner['avatar']) || !strpos($owner['photo'], $avatar['resource-id']);
362                         }
363                 }
364
365                 if ($repair) {
366                         Contact::updateSelfFromUserID($uid);
367                         // Return the corrected data and avoid a loop
368                         $owner = self::getOwnerDataById($uid, false);
369                 }
370
371                 self::$owner[$uid] = $owner;
372                 return $owner;
373         }
374
375         /**
376          * Get owner data by nick name
377          *
378          * @param int $nick
379          * @return boolean|array
380          * @throws Exception
381          */
382         public static function getOwnerDataByNick($nick)
383         {
384                 $user = DBA::selectFirst('user', ['uid'], ['nickname' => $nick]);
385
386                 if (!DBA::isResult($user)) {
387                         return false;
388                 }
389
390                 return self::getOwnerDataById($user['uid']);
391         }
392
393         /**
394          * Returns the default group for a given user and network
395          *
396          * @param int $uid User id
397          * @param string $network network name
398          *
399          * @return int group id
400          * @throws InternalServerErrorException
401          */
402         public static function getDefaultGroup($uid, $network = '')
403         {
404                 $default_group = 0;
405
406                 if ($network == Protocol::OSTATUS) {
407                         $default_group = DI::pConfig()->get($uid, "ostatus", "default_group");
408                 }
409
410                 if ($default_group != 0) {
411                         return $default_group;
412                 }
413
414                 $user = DBA::selectFirst('user', ['def_gid'], ['uid' => $uid]);
415
416                 if (DBA::isResult($user)) {
417                         $default_group = $user["def_gid"];
418                 }
419
420                 return $default_group;
421         }
422
423
424         /**
425          * Authenticate a user with a clear text password
426          *
427          * @param mixed  $user_info
428          * @param string $password
429          * @param bool   $third_party
430          * @return int|boolean
431          * @deprecated since version 3.6
432          * @see        User::getIdFromPasswordAuthentication()
433          */
434         public static function authenticate($user_info, $password, $third_party = false)
435         {
436                 try {
437                         return self::getIdFromPasswordAuthentication($user_info, $password, $third_party);
438                 } catch (Exception $ex) {
439                         return false;
440                 }
441         }
442
443         /**
444          * Authenticate a user with a clear text password
445          *
446          * Returns the user id associated with a successful password authentication
447          *
448          * @param mixed  $user_info
449          * @param string $password
450          * @param bool   $third_party
451          * @return int User Id if authentication is successful
452          * @throws Exception
453          */
454         public static function getIdFromPasswordAuthentication($user_info, $password, $third_party = false)
455         {
456                 $user = self::getAuthenticationInfo($user_info);
457
458                 if ($third_party && DI::pConfig()->get($user['uid'], '2fa', 'verified')) {
459                         // Third-party apps can't verify two-factor authentication, we use app-specific passwords instead
460                         if (AppSpecificPassword::authenticateUser($user['uid'], $password)) {
461                                 return $user['uid'];
462                         }
463                 } elseif (strpos($user['password'], '$') === false) {
464                         //Legacy hash that has not been replaced by a new hash yet
465                         if (self::hashPasswordLegacy($password) === $user['password']) {
466                                 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
467
468                                 return $user['uid'];
469                         }
470                 } elseif (!empty($user['legacy_password'])) {
471                         //Legacy hash that has been double-hashed and not replaced by a new hash yet
472                         //Warning: `legacy_password` is not necessary in sync with the content of `password`
473                         if (password_verify(self::hashPasswordLegacy($password), $user['password'])) {
474                                 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
475
476                                 return $user['uid'];
477                         }
478                 } elseif (password_verify($password, $user['password'])) {
479                         //New password hash
480                         if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
481                                 self::updatePasswordHashed($user['uid'], self::hashPassword($password));
482                         }
483
484                         return $user['uid'];
485                 }
486
487                 throw new Exception(DI::l10n()->t('Login failed'));
488         }
489
490         /**
491          * Returns authentication info from various parameters types
492          *
493          * User info can be any of the following:
494          * - User DB object
495          * - User Id
496          * - User email or username or nickname
497          * - User array with at least the uid and the hashed password
498          *
499          * @param mixed $user_info
500          * @return array
501          * @throws Exception
502          */
503         private static function getAuthenticationInfo($user_info)
504         {
505                 $user = null;
506
507                 if (is_object($user_info) || is_array($user_info)) {
508                         if (is_object($user_info)) {
509                                 $user = (array) $user_info;
510                         } else {
511                                 $user = $user_info;
512                         }
513
514                         if (
515                                 !isset($user['uid'])
516                                 || !isset($user['password'])
517                                 || !isset($user['legacy_password'])
518                         ) {
519                                 throw new Exception(DI::l10n()->t('Not enough information to authenticate'));
520                         }
521                 } elseif (is_int($user_info) || is_string($user_info)) {
522                         if (is_int($user_info)) {
523                                 $user = DBA::selectFirst(
524                                         'user',
525                                         ['uid', 'password', 'legacy_password'],
526                                         [
527                                                 'uid' => $user_info,
528                                                 'blocked' => 0,
529                                                 'account_expired' => 0,
530                                                 'account_removed' => 0,
531                                                 'verified' => 1
532                                         ]
533                                 );
534                         } else {
535                                 $fields = ['uid', 'password', 'legacy_password'];
536                                 $condition = [
537                                         "(`email` = ? OR `username` = ? OR `nickname` = ?)
538                                         AND NOT `blocked` AND NOT `account_expired` AND NOT `account_removed` AND `verified`",
539                                         $user_info, $user_info, $user_info
540                                 ];
541                                 $user = DBA::selectFirst('user', $fields, $condition);
542                         }
543
544                         if (!DBA::isResult($user)) {
545                                 throw new Exception(DI::l10n()->t('User not found'));
546                         }
547                 }
548
549                 return $user;
550         }
551
552         /**
553          * Generates a human-readable random password
554          *
555          * @return string
556          */
557         public static function generateNewPassword()
558         {
559                 return ucfirst(Strings::getRandomName(8)) . random_int(1000, 9999);
560         }
561
562         /**
563          * Checks if the provided plaintext password has been exposed or not
564          *
565          * @param string $password
566          * @return bool
567          * @throws Exception
568          */
569         public static function isPasswordExposed($password)
570         {
571                 $cache = new \DivineOmega\DOFileCachePSR6\CacheItemPool();
572                 $cache->changeConfig([
573                         'cacheDirectory' => get_temppath() . '/password-exposed-cache/',
574                 ]);
575
576                 try {
577                         $passwordExposedChecker = new PasswordExposed\PasswordExposedChecker(null, $cache);
578
579                         return $passwordExposedChecker->passwordExposed($password) === PasswordExposed\PasswordStatus::EXPOSED;
580                 } catch (\Exception $e) {
581                         Logger::error('Password Exposed Exception: ' . $e->getMessage(), [
582                                 'code' => $e->getCode(),
583                                 'file' => $e->getFile(),
584                                 'line' => $e->getLine(),
585                                 'trace' => $e->getTraceAsString()
586                         ]);
587
588                         return false;
589                 }
590         }
591
592         /**
593          * Legacy hashing function, kept for password migration purposes
594          *
595          * @param string $password
596          * @return string
597          */
598         private static function hashPasswordLegacy($password)
599         {
600                 return hash('whirlpool', $password);
601         }
602
603         /**
604          * Global user password hashing function
605          *
606          * @param string $password
607          * @return string
608          * @throws Exception
609          */
610         public static function hashPassword($password)
611         {
612                 if (!trim($password)) {
613                         throw new Exception(DI::l10n()->t('Password can\'t be empty'));
614                 }
615
616                 return password_hash($password, PASSWORD_DEFAULT);
617         }
618
619         /**
620          * Updates a user row with a new plaintext password
621          *
622          * @param int    $uid
623          * @param string $password
624          * @return bool
625          * @throws Exception
626          */
627         public static function updatePassword($uid, $password)
628         {
629                 $password = trim($password);
630
631                 if (empty($password)) {
632                         throw new Exception(DI::l10n()->t('Empty passwords are not allowed.'));
633                 }
634
635                 if (!DI::config()->get('system', 'disable_password_exposed', false) && self::isPasswordExposed($password)) {
636                         throw new Exception(DI::l10n()->t('The new password has been exposed in a public data dump, please choose another.'));
637                 }
638
639                 $allowed_characters = '!"#$%&\'()*+,-./;<=>?@[\]^_`{|}~';
640
641                 if (!preg_match('/^[a-z0-9' . preg_quote($allowed_characters, '/') . ']+$/i', $password)) {
642                         throw new Exception(DI::l10n()->t('The password can\'t contain accentuated letters, white spaces or colons (:)'));
643                 }
644
645                 return self::updatePasswordHashed($uid, self::hashPassword($password));
646         }
647
648         /**
649          * Updates a user row with a new hashed password.
650          * Empties the password reset token field just in case.
651          *
652          * @param int    $uid
653          * @param string $pasword_hashed
654          * @return bool
655          * @throws Exception
656          */
657         private static function updatePasswordHashed($uid, $pasword_hashed)
658         {
659                 $fields = [
660                         'password' => $pasword_hashed,
661                         'pwdreset' => null,
662                         'pwdreset_time' => null,
663                         'legacy_password' => false
664                 ];
665                 return DBA::update('user', $fields, ['uid' => $uid]);
666         }
667
668         /**
669          * Checks if a nickname is in the list of the forbidden nicknames
670          *
671          * Check if a nickname is forbidden from registration on the node by the
672          * admin. Forbidden nicknames (e.g. role namess) can be configured in the
673          * admin panel.
674          *
675          * @param string $nickname The nickname that should be checked
676          * @return boolean True is the nickname is blocked on the node
677          * @throws InternalServerErrorException
678          */
679         public static function isNicknameBlocked($nickname)
680         {
681                 $forbidden_nicknames = DI::config()->get('system', 'forbidden_nicknames', '');
682                 if (!empty($forbidden_nicknames)) {
683                         // check if the nickname is in the list of blocked nicknames
684                         $forbidden = explode(',', $forbidden_nicknames);
685                         $forbidden = array_map('trim', $forbidden);
686                 } else {
687                         $forbidden = [];
688                 }
689
690                 // Add the name of the internal actor to the "forbidden" list
691                 $actor_name = self::getActorName();
692                 if (!empty($actor_name)) {
693                         $forbidden[] = $actor_name;
694                 }
695
696                 if (empty($forbidden)) {
697                         return false;
698                 }
699
700                 if (in_array(strtolower($nickname), $forbidden)) {
701                         return true;
702                 }
703
704                 // else return false
705                 return false;
706         }
707
708         /**
709          * Catch-all user creation function
710          *
711          * Creates a user from the provided data array, either form fields or OpenID.
712          * Required: { username, nickname, email } or { openid_url }
713          *
714          * Performs the following:
715          * - Sends to the OpenId auth URL (if relevant)
716          * - Creates new key pairs for crypto
717          * - Create self-contact
718          * - Create profile image
719          *
720          * @param  array $data
721          * @return array
722          * @throws \ErrorException
723          * @throws InternalServerErrorException
724          * @throws \ImagickException
725          * @throws Exception
726          */
727         public static function create(array $data)
728         {
729                 $return = ['user' => null, 'password' => ''];
730
731                 $using_invites = DI::config()->get('system', 'invitation_only');
732
733                 $invite_id  = !empty($data['invite_id'])  ? Strings::escapeTags(trim($data['invite_id']))  : '';
734                 $username   = !empty($data['username'])   ? Strings::escapeTags(trim($data['username']))   : '';
735                 $nickname   = !empty($data['nickname'])   ? Strings::escapeTags(trim($data['nickname']))   : '';
736                 $email      = !empty($data['email'])      ? Strings::escapeTags(trim($data['email']))      : '';
737                 $openid_url = !empty($data['openid_url']) ? Strings::escapeTags(trim($data['openid_url'])) : '';
738                 $photo      = !empty($data['photo'])      ? Strings::escapeTags(trim($data['photo']))      : '';
739                 $password   = !empty($data['password'])   ? trim($data['password'])           : '';
740                 $password1  = !empty($data['password1'])  ? trim($data['password1'])          : '';
741                 $confirm    = !empty($data['confirm'])    ? trim($data['confirm'])            : '';
742                 $blocked    = !empty($data['blocked']);
743                 $verified   = !empty($data['verified']);
744                 $language   = !empty($data['language'])   ? Strings::escapeTags(trim($data['language']))   : 'en';
745
746                 $netpublish = $publish = !empty($data['profile_publish_reg']);
747
748                 if ($password1 != $confirm) {
749                         throw new Exception(DI::l10n()->t('Passwords do not match. Password unchanged.'));
750                 } elseif ($password1 != '') {
751                         $password = $password1;
752                 }
753
754                 if ($using_invites) {
755                         if (!$invite_id) {
756                                 throw new Exception(DI::l10n()->t('An invitation is required.'));
757                         }
758
759                         if (!Register::existsByHash($invite_id)) {
760                                 throw new Exception(DI::l10n()->t('Invitation could not be verified.'));
761                         }
762                 }
763
764                 /// @todo Check if this part is really needed. We should have fetched all this data in advance
765                 if (empty($username) || empty($email) || empty($nickname)) {
766                         if ($openid_url) {
767                                 if (!Network::isUrlValid($openid_url)) {
768                                         throw new Exception(DI::l10n()->t('Invalid OpenID url'));
769                                 }
770                                 $_SESSION['register'] = 1;
771                                 $_SESSION['openid'] = $openid_url;
772
773                                 $openid = new LightOpenID(DI::baseUrl()->getHostname());
774                                 $openid->identity = $openid_url;
775                                 $openid->returnUrl = DI::baseUrl() . '/openid';
776                                 $openid->required = ['namePerson/friendly', 'contact/email', 'namePerson'];
777                                 $openid->optional = ['namePerson/first', 'media/image/aspect11', 'media/image/default'];
778                                 try {
779                                         $authurl = $openid->authUrl();
780                                 } catch (Exception $e) {
781                                         throw new Exception(DI::l10n()->t('We encountered a problem while logging in with the OpenID you provided. Please check the correct spelling of the ID.') . EOL . EOL . DI::l10n()->t('The error message was:') . $e->getMessage(), 0, $e);
782                                 }
783                                 System::externalRedirect($authurl);
784                                 // NOTREACHED
785                         }
786
787                         throw new Exception(DI::l10n()->t('Please enter the required information.'));
788                 }
789
790                 if (!Network::isUrlValid($openid_url)) {
791                         $openid_url = '';
792                 }
793
794                 // collapse multiple spaces in name
795                 $username = preg_replace('/ +/', ' ', $username);
796
797                 $username_min_length = max(1, min(64, intval(DI::config()->get('system', 'username_min_length', 3))));
798                 $username_max_length = max(1, min(64, intval(DI::config()->get('system', 'username_max_length', 48))));
799
800                 if ($username_min_length > $username_max_length) {
801                         Logger::log(DI::l10n()->t('system.username_min_length (%s) and system.username_max_length (%s) are excluding each other, swapping values.', $username_min_length, $username_max_length), Logger::WARNING);
802                         $tmp = $username_min_length;
803                         $username_min_length = $username_max_length;
804                         $username_max_length = $tmp;
805                 }
806
807                 if (mb_strlen($username) < $username_min_length) {
808                         throw new Exception(DI::l10n()->tt('Username should be at least %s character.', 'Username should be at least %s characters.', $username_min_length));
809                 }
810
811                 if (mb_strlen($username) > $username_max_length) {
812                         throw new Exception(DI::l10n()->tt('Username should be at most %s character.', 'Username should be at most %s characters.', $username_max_length));
813                 }
814
815                 // So now we are just looking for a space in the full name.
816                 $loose_reg = DI::config()->get('system', 'no_regfullname');
817                 if (!$loose_reg) {
818                         $username = mb_convert_case($username, MB_CASE_TITLE, 'UTF-8');
819                         if (strpos($username, ' ') === false) {
820                                 throw new Exception(DI::l10n()->t("That doesn't appear to be your full (First Last) name."));
821                         }
822                 }
823
824                 if (!Network::isEmailDomainAllowed($email)) {
825                         throw new Exception(DI::l10n()->t('Your email domain is not among those allowed on this site.'));
826                 }
827
828                 if (!filter_var($email, FILTER_VALIDATE_EMAIL) || !Network::isEmailDomainValid($email)) {
829                         throw new Exception(DI::l10n()->t('Not a valid email address.'));
830                 }
831                 if (self::isNicknameBlocked($nickname)) {
832                         throw new Exception(DI::l10n()->t('The nickname was blocked from registration by the nodes admin.'));
833                 }
834
835                 if (DI::config()->get('system', 'block_extended_register', false) && DBA::exists('user', ['email' => $email])) {
836                         throw new Exception(DI::l10n()->t('Cannot use that email.'));
837                 }
838
839                 // Disallow somebody creating an account using openid that uses the admin email address,
840                 // since openid bypasses email verification. We'll allow it if there is not yet an admin account.
841                 if (DI::config()->get('config', 'admin_email') && strlen($openid_url)) {
842                         $adminlist = explode(',', str_replace(' ', '', strtolower(DI::config()->get('config', 'admin_email'))));
843                         if (in_array(strtolower($email), $adminlist)) {
844                                 throw new Exception(DI::l10n()->t('Cannot use that email.'));
845                         }
846                 }
847
848                 $nickname = $data['nickname'] = strtolower($nickname);
849
850                 if (!preg_match('/^[a-z0-9][a-z0-9\_]*$/', $nickname)) {
851                         throw new Exception(DI::l10n()->t('Your nickname can only contain a-z, 0-9 and _.'));
852                 }
853
854                 // Check existing and deleted accounts for this nickname.
855                 if (
856                         DBA::exists('user', ['nickname' => $nickname])
857                         || DBA::exists('userd', ['username' => $nickname])
858                 ) {
859                         throw new Exception(DI::l10n()->t('Nickname is already registered. Please choose another.'));
860                 }
861
862                 $new_password = strlen($password) ? $password : User::generateNewPassword();
863                 $new_password_encoded = self::hashPassword($new_password);
864
865                 $return['password'] = $new_password;
866
867                 $keys = Crypto::newKeypair(4096);
868                 if ($keys === false) {
869                         throw new Exception(DI::l10n()->t('SERIOUS ERROR: Generation of security keys failed.'));
870                 }
871
872                 $prvkey = $keys['prvkey'];
873                 $pubkey = $keys['pubkey'];
874
875                 // Create another keypair for signing/verifying salmon protocol messages.
876                 $sres = Crypto::newKeypair(512);
877                 $sprvkey = $sres['prvkey'];
878                 $spubkey = $sres['pubkey'];
879
880                 $insert_result = DBA::insert('user', [
881                         'guid'     => System::createUUID(),
882                         'username' => $username,
883                         'password' => $new_password_encoded,
884                         'email'    => $email,
885                         'openid'   => $openid_url,
886                         'nickname' => $nickname,
887                         'pubkey'   => $pubkey,
888                         'prvkey'   => $prvkey,
889                         'spubkey'  => $spubkey,
890                         'sprvkey'  => $sprvkey,
891                         'verified' => $verified,
892                         'blocked'  => $blocked,
893                         'language' => $language,
894                         'timezone' => 'UTC',
895                         'register_date' => DateTimeFormat::utcNow(),
896                         'default-location' => ''
897                 ]);
898
899                 if ($insert_result) {
900                         $uid = DBA::lastInsertId();
901                         $user = DBA::selectFirst('user', [], ['uid' => $uid]);
902                 } else {
903                         throw new Exception(DI::l10n()->t('An error occurred during registration. Please try again.'));
904                 }
905
906                 if (!$uid) {
907                         throw new Exception(DI::l10n()->t('An error occurred during registration. Please try again.'));
908                 }
909
910                 // if somebody clicked submit twice very quickly, they could end up with two accounts
911                 // due to race condition. Remove this one.
912                 $user_count = DBA::count('user', ['nickname' => $nickname]);
913                 if ($user_count > 1) {
914                         DBA::delete('user', ['uid' => $uid]);
915
916                         throw new Exception(DI::l10n()->t('Nickname is already registered. Please choose another.'));
917                 }
918
919                 $insert_result = DBA::insert('profile', [
920                         'uid' => $uid,
921                         'name' => $username,
922                         'photo' => DI::baseUrl() . "/photo/profile/{$uid}.jpg",
923                         'thumb' => DI::baseUrl() . "/photo/avatar/{$uid}.jpg",
924                         'publish' => $publish,
925                         'net-publish' => $netpublish,
926                 ]);
927                 if (!$insert_result) {
928                         DBA::delete('user', ['uid' => $uid]);
929
930                         throw new Exception(DI::l10n()->t('An error occurred creating your default profile. Please try again.'));
931                 }
932
933                 // Create the self contact
934                 if (!Contact::createSelfFromUserId($uid)) {
935                         DBA::delete('user', ['uid' => $uid]);
936
937                         throw new Exception(DI::l10n()->t('An error occurred creating your self contact. Please try again.'));
938                 }
939
940                 // Create a group with no members. This allows somebody to use it
941                 // right away as a default group for new contacts.
942                 $def_gid = Group::create($uid, DI::l10n()->t('Friends'));
943                 if (!$def_gid) {
944                         DBA::delete('user', ['uid' => $uid]);
945
946                         throw new Exception(DI::l10n()->t('An error occurred creating your default contact group. Please try again.'));
947                 }
948
949                 $fields = ['def_gid' => $def_gid];
950                 if (DI::config()->get('system', 'newuser_private') && $def_gid) {
951                         $fields['allow_gid'] = '<' . $def_gid . '>';
952                 }
953
954                 DBA::update('user', $fields, ['uid' => $uid]);
955
956                 // if we have no OpenID photo try to look up an avatar
957                 if (!strlen($photo)) {
958                         $photo = Network::lookupAvatarByEmail($email);
959                 }
960
961                 // unless there is no avatar-addon loaded
962                 if (strlen($photo)) {
963                         $photo_failure = false;
964
965                         $filename = basename($photo);
966                         $curlResult = DI::httpRequest()->get($photo, true);
967                         if ($curlResult->isSuccess()) {
968                                 $img_str = $curlResult->getBody();
969                                 $type = $curlResult->getContentType();
970                         } else {
971                                 $img_str = '';
972                                 $type = '';
973                         }
974
975                         $type = Images::getMimeTypeByData($img_str, $photo, $type);
976
977                         $Image = new Image($img_str, $type);
978                         if ($Image->isValid()) {
979                                 $Image->scaleToSquare(300);
980
981                                 $resource_id = Photo::newResource();
982
983                                 $r = Photo::store($Image, $uid, 0, $resource_id, $filename, DI::l10n()->t('Profile Photos'), 4);
984
985                                 if ($r === false) {
986                                         $photo_failure = true;
987                                 }
988
989                                 $Image->scaleDown(80);
990
991                                 $r = Photo::store($Image, $uid, 0, $resource_id, $filename, DI::l10n()->t('Profile Photos'), 5);
992
993                                 if ($r === false) {
994                                         $photo_failure = true;
995                                 }
996
997                                 $Image->scaleDown(48);
998
999                                 $r = Photo::store($Image, $uid, 0, $resource_id, $filename, DI::l10n()->t('Profile Photos'), 6);
1000
1001                                 if ($r === false) {
1002                                         $photo_failure = true;
1003                                 }
1004
1005                                 if (!$photo_failure) {
1006                                         Photo::update(['profile' => 1], ['resource-id' => $resource_id]);
1007                                 }
1008                         }
1009                 }
1010
1011                 Hook::callAll('register_account', $uid);
1012
1013                 $return['user'] = $user;
1014                 return $return;
1015         }
1016
1017         /**
1018          * Sets block state for a given user
1019          *
1020          * @param int  $uid   The user id
1021          * @param bool $block Block state (default is true)
1022          *
1023          * @return bool True, if successfully blocked
1024
1025          * @throws Exception
1026          */
1027         public static function block(int $uid, bool $block = true)
1028         {
1029                 return DBA::update('user', ['blocked' => $block], ['uid' => $uid]);
1030         }
1031
1032         /**
1033          * Allows a registration based on a hash
1034          *
1035          * @param string $hash
1036          *
1037          * @return bool True, if the allow was successful
1038          *
1039          * @throws InternalServerErrorException
1040          * @throws Exception
1041          */
1042         public static function allow(string $hash)
1043         {
1044                 $register = Register::getByHash($hash);
1045                 if (!DBA::isResult($register)) {
1046                         return false;
1047                 }
1048
1049                 $user = User::getById($register['uid']);
1050                 if (!DBA::isResult($user)) {
1051                         return false;
1052                 }
1053
1054                 Register::deleteByHash($hash);
1055
1056                 DBA::update('user', ['blocked' => false, 'verified' => true], ['uid' => $register['uid']]);
1057
1058                 $profile = DBA::selectFirst('profile', ['net-publish'], ['uid' => $register['uid']]);
1059
1060                 if (DBA::isResult($profile) && $profile['net-publish'] && DI::config()->get('system', 'directory')) {
1061                         $url = DI::baseUrl() . '/profile/' . $user['nickname'];
1062                         Worker::add(PRIORITY_LOW, "Directory", $url);
1063                 }
1064
1065                 $l10n = DI::l10n()->withLang($register['language']);
1066
1067                 return User::sendRegisterOpenEmail(
1068                         $l10n,
1069                         $user,
1070                         DI::config()->get('config', 'sitename'),
1071                         DI::baseUrl()->get(),
1072                         ($register['password'] ?? '') ?: 'Sent in a previous email'
1073                 );
1074         }
1075
1076         /**
1077          * Denys a pending registration
1078          *
1079          * @param string $hash The hash of the pending user
1080          *
1081          * This does not have to go through user_remove() and save the nickname
1082          * permanently against re-registration, as the person was not yet
1083          * allowed to have friends on this system
1084          *
1085          * @return bool True, if the deny was successfull
1086          * @throws Exception
1087          */
1088         public static function deny(string $hash)
1089         {
1090                 $register = Register::getByHash($hash);
1091                 if (!DBA::isResult($register)) {
1092                         return false;
1093                 }
1094
1095                 $user = User::getById($register['uid']);
1096                 if (!DBA::isResult($user)) {
1097                         return false;
1098                 }
1099
1100                 return DBA::delete('user', ['uid' => $register['uid']]) &&
1101                        Register::deleteByHash($register['hash']);
1102         }
1103
1104         /**
1105          * Creates a new user based on a minimal set and sends an email to this user
1106          *
1107          * @param string $name  The user's name
1108          * @param string $email The user's email address
1109          * @param string $nick  The user's nick name
1110          * @param string $lang  The user's language (default is english)
1111          *
1112          * @return bool True, if the user was created successfully
1113          * @throws InternalServerErrorException
1114          * @throws \ErrorException
1115          * @throws \ImagickException
1116          */
1117         public static function createMinimal(string $name, string $email, string $nick, string $lang = L10n::DEFAULT)
1118         {
1119                 if (empty($name) ||
1120                     empty($email) ||
1121                     empty($nick)) {
1122                         throw new InternalServerErrorException('Invalid arguments.');
1123                 }
1124
1125                 $result = self::create([
1126                         'username' => $name,
1127                         'email' => $email,
1128                         'nickname' => $nick,
1129                         'verified' => 1,
1130                         'language' => $lang
1131                 ]);
1132
1133                 $user = $result['user'];
1134                 $preamble = Strings::deindent(DI::l10n()->t('
1135                 Dear %1$s,
1136                         the administrator of %2$s has set up an account for you.'));
1137                 $body = Strings::deindent(DI::l10n()->t('
1138                 The login details are as follows:
1139
1140                 Site Location:  %1$s
1141                 Login Name:             %2$s
1142                 Password:               %3$s
1143
1144                 You may change your password from your account "Settings" page after logging
1145                 in.
1146
1147                 Please take a few moments to review the other account settings on that page.
1148
1149                 You may also wish to add some basic information to your default profile
1150                 (on the "Profiles" page) so that other people can easily find you.
1151
1152                 We recommend setting your full name, adding a profile photo,
1153                 adding some profile "keywords" (very useful in making new friends) - and
1154                 perhaps what country you live in; if you do not wish to be more specific
1155                 than that.
1156
1157                 We fully respect your right to privacy, and none of these items are necessary.
1158                 If you are new and do not know anybody here, they may help
1159                 you to make some new and interesting friends.
1160
1161                 If you ever want to delete your account, you can do so at %1$s/removeme
1162
1163                 Thank you and welcome to %4$s.'));
1164
1165                 $preamble = sprintf($preamble, $user['username'], DI::config()->get('config', 'sitename'));
1166                 $body = sprintf($body, DI::baseUrl()->get(), $user['nickname'], $result['password'], DI::config()->get('config', 'sitename'));
1167
1168                 $email = DI::emailer()
1169                         ->newSystemMail()
1170                         ->withMessage(DI::l10n()->t('Registration details for %s', DI::config()->get('config', 'sitename')), $preamble, $body)
1171                         ->forUser($user)
1172                         ->withRecipient($user['email'])
1173                         ->build();
1174                 return DI::emailer()->send($email);
1175         }
1176
1177         /**
1178          * Sends pending registration confirmation email
1179          *
1180          * @param array  $user     User record array
1181          * @param string $sitename
1182          * @param string $siteurl
1183          * @param string $password Plaintext password
1184          * @return NULL|boolean from notification() and email() inherited
1185          * @throws InternalServerErrorException
1186          */
1187         public static function sendRegisterPendingEmail($user, $sitename, $siteurl, $password)
1188         {
1189                 $body = Strings::deindent(DI::l10n()->t(
1190                         '
1191                         Dear %1$s,
1192                                 Thank you for registering at %2$s. Your account is pending for approval by the administrator.
1193
1194                         Your login details are as follows:
1195
1196                         Site Location:  %3$s
1197                         Login Name:             %4$s
1198                         Password:               %5$s
1199                 ',
1200                         $user['username'],
1201                         $sitename,
1202                         $siteurl,
1203                         $user['nickname'],
1204                         $password
1205                 ));
1206
1207                 $email = DI::emailer()
1208                         ->newSystemMail()
1209                         ->withMessage(DI::l10n()->t('Registration at %s', $sitename), $body)
1210                         ->forUser($user)
1211                         ->withRecipient($user['email'])
1212                         ->build();
1213                 return DI::emailer()->send($email);
1214         }
1215
1216         /**
1217          * Sends registration confirmation
1218          *
1219          * It's here as a function because the mail is sent from different parts
1220          *
1221          * @param \Friendica\Core\L10n $l10n     The used language
1222          * @param array                $user     User record array
1223          * @param string               $sitename
1224          * @param string               $siteurl
1225          * @param string               $password Plaintext password
1226          *
1227          * @return NULL|boolean from notification() and email() inherited
1228          * @throws InternalServerErrorException
1229          */
1230         public static function sendRegisterOpenEmail(\Friendica\Core\L10n $l10n, $user, $sitename, $siteurl, $password)
1231         {
1232                 $preamble = Strings::deindent($l10n->t(
1233                         '
1234                                 Dear %1$s,
1235                                 Thank you for registering at %2$s. Your account has been created.
1236                         ',
1237                         $user['username'],
1238                         $sitename
1239                 ));
1240                 $body = Strings::deindent($l10n->t(
1241                         '
1242                         The login details are as follows:
1243
1244                         Site Location:  %3$s
1245                         Login Name:             %1$s
1246                         Password:               %5$s
1247
1248                         You may change your password from your account "Settings" page after logging
1249                         in.
1250
1251                         Please take a few moments to review the other account settings on that page.
1252
1253                         You may also wish to add some basic information to your default profile
1254                         ' . "\x28" . 'on the "Profiles" page' . "\x29" . ' so that other people can easily find you.
1255
1256                         We recommend setting your full name, adding a profile photo,
1257                         adding some profile "keywords" ' . "\x28" . 'very useful in making new friends' . "\x29" . ' - and
1258                         perhaps what country you live in; if you do not wish to be more specific
1259                         than that.
1260
1261                         We fully respect your right to privacy, and none of these items are necessary.
1262                         If you are new and do not know anybody here, they may help
1263                         you to make some new and interesting friends.
1264
1265                         If you ever want to delete your account, you can do so at %3$s/removeme
1266
1267                         Thank you and welcome to %2$s.',
1268                         $user['nickname'],
1269                         $sitename,
1270                         $siteurl,
1271                         $user['username'],
1272                         $password
1273                 ));
1274
1275                 $email = DI::emailer()
1276                         ->newSystemMail()
1277                         ->withMessage(DI::l10n()->t('Registration details for %s', $sitename), $preamble, $body)
1278                         ->forUser($user)
1279                         ->withRecipient($user['email'])
1280                         ->build();
1281                 return DI::emailer()->send($email);
1282         }
1283
1284         /**
1285          * @param int $uid user to remove
1286          * @return bool
1287          * @throws InternalServerErrorException
1288          */
1289         public static function remove(int $uid)
1290         {
1291                 if (!$uid) {
1292                         return false;
1293                 }
1294
1295                 Logger::log('Removing user: ' . $uid);
1296
1297                 $user = DBA::selectFirst('user', [], ['uid' => $uid]);
1298
1299                 Hook::callAll('remove_user', $user);
1300
1301                 // save username (actually the nickname as it is guaranteed
1302                 // unique), so it cannot be re-registered in the future.
1303                 DBA::insert('userd', ['username' => $user['nickname']]);
1304
1305                 // The user and related data will be deleted in Friendica\Worker\ExpireAndRemoveUsers
1306                 DBA::update('user', ['account_removed' => true, 'account_expires_on' => DateTimeFormat::utc('now + 7 day')], ['uid' => $uid]);
1307                 Worker::add(PRIORITY_HIGH, 'Notifier', Delivery::REMOVAL, $uid);
1308
1309                 // Send an update to the directory
1310                 $self = DBA::selectFirst('contact', ['url'], ['uid' => $uid, 'self' => true]);
1311                 Worker::add(PRIORITY_LOW, 'Directory', $self['url']);
1312
1313                 // Remove the user relevant data
1314                 Worker::add(PRIORITY_NEGLIGIBLE, 'RemoveUser', $uid);
1315
1316                 return true;
1317         }
1318
1319         /**
1320          * Return all identities to a user
1321          *
1322          * @param int $uid The user id
1323          * @return array All identities for this user
1324          *
1325          * Example for a return:
1326          *    [
1327          *        [
1328          *            'uid' => 1,
1329          *            'username' => 'maxmuster',
1330          *            'nickname' => 'Max Mustermann'
1331          *        ],
1332          *        [
1333          *            'uid' => 2,
1334          *            'username' => 'johndoe',
1335          *            'nickname' => 'John Doe'
1336          *        ]
1337          *    ]
1338          * @throws Exception
1339          */
1340         public static function identities($uid)
1341         {
1342                 $identities = [];
1343
1344                 $user = DBA::selectFirst('user', ['uid', 'nickname', 'username', 'parent-uid'], ['uid' => $uid]);
1345                 if (!DBA::isResult($user)) {
1346                         return $identities;
1347                 }
1348
1349                 if ($user['parent-uid'] == 0) {
1350                         // First add our own entry
1351                         $identities = [[
1352                                 'uid' => $user['uid'],
1353                                 'username' => $user['username'],
1354                                 'nickname' => $user['nickname']
1355                         ]];
1356
1357                         // Then add all the children
1358                         $r = DBA::select(
1359                                 'user',
1360                                 ['uid', 'username', 'nickname'],
1361                                 ['parent-uid' => $user['uid'], 'account_removed' => false]
1362                         );
1363                         if (DBA::isResult($r)) {
1364                                 $identities = array_merge($identities, DBA::toArray($r));
1365                         }
1366                 } else {
1367                         // First entry is our parent
1368                         $r = DBA::select(
1369                                 'user',
1370                                 ['uid', 'username', 'nickname'],
1371                                 ['uid' => $user['parent-uid'], 'account_removed' => false]
1372                         );
1373                         if (DBA::isResult($r)) {
1374                                 $identities = DBA::toArray($r);
1375                         }
1376
1377                         // Then add all siblings
1378                         $r = DBA::select(
1379                                 'user',
1380                                 ['uid', 'username', 'nickname'],
1381                                 ['parent-uid' => $user['parent-uid'], 'account_removed' => false]
1382                         );
1383                         if (DBA::isResult($r)) {
1384                                 $identities = array_merge($identities, DBA::toArray($r));
1385                         }
1386                 }
1387
1388                 $r = DBA::p(
1389                         "SELECT `user`.`uid`, `user`.`username`, `user`.`nickname`
1390                         FROM `manage`
1391                         INNER JOIN `user` ON `manage`.`mid` = `user`.`uid`
1392                         WHERE `user`.`account_removed` = 0 AND `manage`.`uid` = ?",
1393                         $user['uid']
1394                 );
1395                 if (DBA::isResult($r)) {
1396                         $identities = array_merge($identities, DBA::toArray($r));
1397                 }
1398
1399                 return $identities;
1400         }
1401
1402         /**
1403          * Returns statistical information about the current users of this node
1404          *
1405          * @return array
1406          *
1407          * @throws Exception
1408          */
1409         public static function getStatistics()
1410         {
1411                 $statistics = [
1412                         'total_users'           => 0,
1413                         'active_users_halfyear' => 0,
1414                         'active_users_monthly'  => 0,
1415                         'active_users_weekly'   => 0,
1416                 ];
1417
1418                 $userStmt = DBA::select('owner-view', ['uid', 'login_date', 'last-item'],
1419                         ["`verified` AND `login_date` > ? AND NOT `blocked`
1420                         AND NOT `account_removed` AND NOT `account_expired`",
1421                         DBA::NULL_DATETIME]);
1422                 if (!DBA::isResult($userStmt)) {
1423                         return $statistics;
1424                 }
1425
1426                 $halfyear = time() - (180 * 24 * 60 * 60);
1427                 $month = time() - (30 * 24 * 60 * 60);
1428                 $week = time() - (7 * 24 * 60 * 60);
1429
1430                 while ($user = DBA::fetch($userStmt)) {
1431                         $statistics['total_users']++;
1432
1433                         if ((strtotime($user['login_date']) > $halfyear) || (strtotime($user['last-item']) > $halfyear)
1434                         ) {
1435                                 $statistics['active_users_halfyear']++;
1436                         }
1437
1438                         if ((strtotime($user['login_date']) > $month) || (strtotime($user['last-item']) > $month)
1439                         ) {
1440                                 $statistics['active_users_monthly']++;
1441                         }
1442
1443                         if ((strtotime($user['login_date']) > $week) || (strtotime($user['last-item']) > $week)
1444                         ) {
1445                                 $statistics['active_users_weekly']++;
1446                         }
1447                 }
1448                 DBA::close($userStmt);
1449
1450                 return $statistics;
1451         }
1452
1453         /**
1454          * Get all users of the current node
1455          *
1456          * @param int    $start Start count (Default is 0)
1457          * @param int    $count Count of the items per page (Default is @see Pager::ITEMS_PER_PAGE)
1458          * @param string $type  The type of users, which should get (all, bocked, removed)
1459          * @param string $order Order of the user list (Default is 'contact.name')
1460          * @param bool   $descending Order direction (Default is ascending)
1461          *
1462          * @return array The list of the users
1463          * @throws Exception
1464          */
1465         public static function getList($start = 0, $count = Pager::ITEMS_PER_PAGE, $type = 'all', $order = 'name', bool $descending = false)
1466         {
1467                 $param = ['limit' => [$start, $count], 'order' => [$order => $descending]];
1468                 $condition = [];
1469                 switch ($type) {
1470                         case 'active':
1471                                 $condition['account_removed'] = false;
1472                                 $condition['blocked'] = false;
1473                                 break;
1474                         case 'blocked':
1475                                 $condition['blocked'] = true;
1476                                 break;
1477                         case 'removed':
1478                                 $condition['account_removed'] = true;
1479                                 break;
1480                 }
1481
1482                 return DBA::selectToArray('owner-view', [], $condition, $param);
1483         }
1484 }