diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-92562-FrontendGroupsResolvedDirectlyAfterTheFrontendUserItself.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-92562-FrontendGroupsResolvedDirectlyAfterTheFrontendUserItself.rst new file mode 100644 index 0000000000000000000000000000000000000000..ab1c8d95762990162eb6b7c2d1d264c430b9765b --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-92562-FrontendGroupsResolvedDirectlyAfterTheFrontendUserItself.rst @@ -0,0 +1,44 @@ +.. include:: ../../Includes.txt + +================================================================================== +Feature: #92562 - Frontend groups resolved directly after the Frontend User itself +================================================================================== + +See :issue:`92562` + +Description +=========== + +For legacy purposes, the valid frontend user groups were added while resolving +the root line in TypoScriptFrontendController. This is much later in the frontend +request process than the preparation of the Frontend User, which is resolved by +the session or form credentials. + +There are several reasons for the historic behavior: +* Special functionality like "pages.fe_login_mode" which can override groups + based on the current root line +* Previewing frontend user groups via the Admin Panel for backend users + +However, this historic behavior led to inconsistencies, especially with the +Context API to retrieve the correct usergroups in PSR-15 middlewares to build custom APIs. + +In addition, this functionality is now extracted from TSFE and into the middleware, +further decoupling the User authentication from the TSFE object. + + +Impact +====== + +When the PSR-15 middleware is setting up the FrontendUserAuthentication object +at a very early stage of the frontend request, the groups are resolved directly +afterwards, leaving the FrontendUserAuthentication object in a consistent state +for further middlewares to work with the appropriate groups. + +Please note that any existing custom AuthenticationService for resolving frontend +user groups, which might rely on a valid TSFE object will have to be evaluated +if it still works in TYPO3 v11. + +Also: Further middlewares and TSFE itself can still override the assigned groups +due to previewing behavior. + +.. index:: ext:frontend diff --git a/typo3/sysext/frontend/Classes/Authentication/FrontendUserAuthentication.php b/typo3/sysext/frontend/Classes/Authentication/FrontendUserAuthentication.php index b17fb2b7672a54d3d18fc10bb083dbab19eae6f1..eddd0c339cf233eef34d56fcec434720e8511f97 100644 --- a/typo3/sysext/frontend/Classes/Authentication/FrontendUserAuthentication.php +++ b/typo3/sysext/frontend/Classes/Authentication/FrontendUserAuthentication.php @@ -18,6 +18,7 @@ namespace TYPO3\CMS\Frontend\Authentication; use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication; use TYPO3\CMS\Core\Authentication\AuthenticationService; use TYPO3\CMS\Core\Configuration\Features; +use TYPO3\CMS\Core\Context\UserAspect; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Session\Backend\Exception\SessionNotFoundException; use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser; @@ -382,6 +383,49 @@ class FrontendUserAuthentication extends AbstractUserAuthentication return !empty($this->groupData['uid']) ? count($this->groupData['uid']) : 0; } + /** + * Initializes the front-end user groups for the context API, + * based on the user groups and the logged-in state. + * + * @param bool $respectUserGroups used with the $TSFE->loginAllowedInBranch flag to disable the inclusion of the users' groups + * @return UserAspect + */ + public function createUserAspect(bool $respectUserGroups = true): UserAspect + { + $userGroups = [0]; + $isUserAndGroupSet = is_array($this->user) && !empty($this->groupData['uid']); + if ($isUserAndGroupSet) { + // group -2 is not an existing group, but denotes a 'default' group when a user IS logged in. + // This is used to let elements be shown for all logged in users! + $userGroups[] = -2; + $groupsFromUserRecord = $this->groupData['uid']; + } else { + // group -1 is not an existing group, but denotes a 'default' group when not logged in. + // This is used to let elements be hidden, when a user is logged in! + $userGroups[] = -1; + if ($respectUserGroups) { + // For cases where logins are not banned from a branch usergroups can be set based on IP masks so we should add the usergroups uids. + $groupsFromUserRecord = $this->groupData['uid']; + } else { + // Set to blank since we will NOT risk any groups being set when no logins are allowed! + $groupsFromUserRecord = []; + } + } + // Make unique and sort the groups + $groupsFromUserRecord = array_unique($groupsFromUserRecord); + if ($respectUserGroups && !empty($groupsFromUserRecord)) { + sort($groupsFromUserRecord); + $userGroups = array_merge($userGroups, array_map('intval', $groupsFromUserRecord)); + } + + // For every 60 seconds the is_online timestamp for a logged-in user is updated + if ($isUserAndGroupSet) { + $this->updateOnlineTimestamp(); + } + + $this->logger->debug('Valid frontend usergroups: ' . implode(',', $userGroups)); + return GeneralUtility::makeInstance(UserAspect::class, $this, $userGroups); + } /** * Returns the parsed TSconfig for the fe_user * The TSconfig will be cached in $this->userTS. diff --git a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php index 4cf890f556330965b338276568d7bcabcb4af7a1..1cb3770e1eb4aef510b8a90322d07a4c2aa7fc75 100644 --- a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php +++ b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php @@ -658,43 +658,8 @@ class TypoScriptFrontendController implements LoggerAwareInterface */ public function initUserGroups() { - $userGroups = [0]; - // no matter if we have an active user we try to fetch matching groups which can be set without an user (simulation for instance!) - $this->fe_user->fetchGroupData(); - $isUserAndGroupSet = is_array($this->fe_user->user) && !empty($this->fe_user->groupData['uid']); - if ($isUserAndGroupSet) { - // group -2 is not an existing group, but denotes a 'default' group when a user IS logged in. - // This is used to let elements be shown for all logged in users! - $userGroups[] = -2; - $groupsFromUserRecord = $this->fe_user->groupData['uid']; - } else { - // group -1 is not an existing group, but denotes a 'default' group when not logged in. - // This is used to let elements be hidden, when a user is logged in! - $userGroups[] = -1; - if ($this->loginAllowedInBranch) { - // For cases where logins are not banned from a branch usergroups can be set based on IP masks so we should add the usergroups uids. - $groupsFromUserRecord = $this->fe_user->groupData['uid']; - } else { - // Set to blank since we will NOT risk any groups being set when no logins are allowed! - $groupsFromUserRecord = []; - } - } - // Clean up. - // Make unique and sort the groups - $groupsFromUserRecord = array_unique($groupsFromUserRecord); - if (!empty($groupsFromUserRecord) && !$this->loginAllowedInBranch_mode) { - sort($groupsFromUserRecord); - $userGroups = array_merge($userGroups, array_map('intval', $groupsFromUserRecord)); - } - - $this->context->setAspect('frontend.user', GeneralUtility::makeInstance(UserAspect::class, $this->fe_user, $userGroups)); - - // For every 60 seconds the is_online timestamp for a logged-in user is updated - if ($isUserAndGroupSet) { - $this->fe_user->updateOnlineTimestamp(); - } - - $this->logger->debug('Valid usergroups for TSFE: ' . implode(',', $userGroups)); + $userAspect = $this->fe_user->createUserAspect((bool)$this->loginAllowedInBranch); + $this->context->setAspect('frontend.user', $userAspect); } /** @@ -777,15 +742,12 @@ class TypoScriptFrontendController implements LoggerAwareInterface $this->loginAllowedInBranch = $this->checkIfLoginAllowedInBranch(); // Logins are not allowed, but there is a login, so will we run this. if (!$this->loginAllowedInBranch && $this->isUserOrGroupSet()) { + // Clear out user, and the group will be re-set in >initUserGroups() due to + // $this->loginAllowedInBranch = false if ($this->loginAllowedInBranch_mode === 'all') { - // Clear out user and group: $this->fe_user->hideActiveLogin(); - $userGroups = [0, -1]; - } else { - $userGroups = [0, -2]; } - $this->context->setAspect('frontend.user', GeneralUtility::makeInstance(UserAspect::class, $this->fe_user, $userGroups)); - // Fetching the id again, now with the preview settings reset. + // Fetching the id again, now with the preview settings reset and respecting $this->loginAllowedInBranch = false $this->fetch_the_id($request); } // Final cleaning. diff --git a/typo3/sysext/frontend/Classes/Middleware/FrontendUserAuthenticator.php b/typo3/sysext/frontend/Classes/Middleware/FrontendUserAuthenticator.php index 99e92e9efdfad6d5ac65617bbf2f4beb8a2d4057..63025b4f93121242316ef4d6e0a5c8d21862b169 100644 --- a/typo3/sysext/frontend/Classes/Middleware/FrontendUserAuthenticator.php +++ b/typo3/sysext/frontend/Classes/Middleware/FrontendUserAuthenticator.php @@ -21,9 +21,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication; use TYPO3\CMS\Core\Context\Context; -use TYPO3\CMS\Core\Context\UserAspect; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication; @@ -69,9 +67,13 @@ class FrontendUserAuthenticator implements MiddlewareInterface // Authenticate now $frontendUser->start(); $frontendUser->unpack_uc(); + // no matter if we have an active user we try to fetch matching groups which can + // be set without an user (simulation for instance!) + $frontendUser->fetchGroupData(); - // Register the frontend user as aspect and within the session - $this->setFrontendUserAspect($frontendUser); + // Register the frontend user as aspect and within the request + $userAspect = $frontendUser->createUserAspect(); + $this->context->setAspect('frontend.user', $userAspect); $request = $request->withAttribute('frontend.user', $frontendUser); $response = $handler->handle($request); @@ -120,14 +122,4 @@ class FrontendUserAuthenticator implements MiddlewareInterface } return $request; } - - /** - * Register the frontend user as aspect - * - * @param AbstractUserAuthentication $user - */ - protected function setFrontendUserAspect(AbstractUserAuthentication $user) - { - $this->context->setAspect('frontend.user', GeneralUtility::makeInstance(UserAspect::class, $user)); - } }