diff --git a/typo3/sysext/backend/Classes/Backend/Shortcut/ShortcutRepository.php b/typo3/sysext/backend/Classes/Backend/Shortcut/ShortcutRepository.php index 6b14bfe5cff09acde1b7e2e505dba31126260870..94b2cb4af8b7e3655032658f59f1eba28733d15d 100644 --- a/typo3/sysext/backend/Classes/Backend/Shortcut/ShortcutRepository.php +++ b/typo3/sysext/backend/Classes/Backend/Shortcut/ShortcutRepository.php @@ -565,8 +565,7 @@ class ShortcutRepository return $routeIdentifier; } - $route = $this->getRoute($routeIdentifier); - return $route !== null ? (string)($route->getOption('moduleName') ?? '') : ''; + return (string)($this->getRoute($routeIdentifier)?->getOption('module')?->getIdentifier() ?? ''); } /** diff --git a/typo3/sysext/backend/Classes/Http/RouteDispatcher.php b/typo3/sysext/backend/Classes/Http/RouteDispatcher.php index cc523eee31b118c3773a766774b39860412c943c..22498558d4ae93387b4cc6676fcc0d23507cac25 100644 --- a/typo3/sysext/backend/Classes/Http/RouteDispatcher.php +++ b/typo3/sysext/backend/Classes/Http/RouteDispatcher.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + /* * This file is part of the TYPO3 CMS project. * @@ -15,42 +17,24 @@ namespace TYPO3\CMS\Backend\Http; -use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use TYPO3\CMS\Backend\Module\ModuleInterface; -use TYPO3\CMS\Backend\Module\ModuleProvider; use TYPO3\CMS\Backend\Routing\Exception\InvalidRequestTokenException; use TYPO3\CMS\Backend\Routing\Exception\MissingRequestTokenException; use TYPO3\CMS\Backend\Routing\Route; -use TYPO3\CMS\Backend\Routing\RouteRedirect; use TYPO3\CMS\Backend\Routing\UriBuilder; -use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Core\Configuration\Features; use TYPO3\CMS\Core\FormProtection\AbstractFormProtection; use TYPO3\CMS\Core\FormProtection\FormProtectionFactory; use TYPO3\CMS\Core\Http\Dispatcher; -use TYPO3\CMS\Core\Http\RedirectResponse; use TYPO3\CMS\Core\Http\Security\ReferrerEnforcer; -use TYPO3\CMS\Core\Type\Bitmask\Permission; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Core\Utility\MathUtility; /** * Dispatcher which resolves a route to call a controller and method (but also a callable) */ class RouteDispatcher extends Dispatcher { - private UriBuilder $uriBuilder; - private ModuleProvider $moduleProvider; - - public function __construct(ContainerInterface $container, UriBuilder $uriBuilder, ModuleProvider $moduleProvider) - { - parent::__construct($container); - $this->uriBuilder = $uriBuilder; - $this->moduleProvider = $moduleProvider; - } - /** * Main method checks the target of the route, and tries to call it. * @@ -72,23 +56,6 @@ class RouteDispatcher extends Dispatcher // Ensure that a token exists, and the token is requested, if the route requires a valid token $this->assertRequestToken($request, $route); - if ($route->hasOption('module')) { - $this->validateModule($request, $route); - - // This module request (which is usually opened inside the list_frame) - // has been issued from a toplevel browser window (e.g. a link was opened in a new tab). - // Redirect to open the module as frame inside the TYPO3 backend layout. - // HEADS UP: This header will only be available in secure connections (https:// or .localhost TLD) - if ($request->getHeaderLine('Sec-Fetch-Dest') === 'document') { - return new RedirectResponse( - $this->uriBuilder->buildUriWithRedirect( - 'main', - [], - RouteRedirect::createFromRoute($route, $request->getQueryParams()) - ) - ); - } - } $targetIdentifier = $route->getOption('target'); $target = $this->getCallableFromTarget($targetIdentifier); $arguments = [$request]; @@ -100,7 +67,7 @@ class RouteDispatcher extends Dispatcher * * @return AbstractFormProtection */ - protected function getFormProtection() + protected function getFormProtection(): AbstractFormProtection { return FormProtectionFactory::get(); } @@ -161,40 +128,4 @@ class RouteDispatcher extends Dispatcher ); } } - - /** - * Fetches the module object from the resolved route and checks permissions - * for the current user. Furthermore, evaluates page access permissions, in - * case an "id" is given in the request. - * - * @param ServerRequestInterface $request - * @param Route $route - * @throws \RuntimeException - */ - protected function validateModule(ServerRequestInterface $request, Route $route): void - { - $backendUserAuthentication = $GLOBALS['BE_USER']; - $module = $route->getOption('module'); - - if (!($module instanceof ModuleInterface) - || !$this->moduleProvider->accessGranted($module->getIdentifier(), $backendUserAuthentication) - ) { - throw new \RuntimeException('You don\'t have access to this module.', 1642450334); - } - - // '' for "no value found at all" to guarantee that the following if condition fails. - $id = $request->getQueryParams()['id'] ?? $request->getParsedBody()['id'] ?? ''; - if (MathUtility::canBeInterpretedAsInteger($id) && $id > 0) { - $permClause = $backendUserAuthentication->getPagePermsClause(Permission::PAGE_SHOW); - // Check page access - if (!is_array(BackendUtility::readPageAccess($id, $permClause))) { - // Check if page has been deleted - $deleteField = $GLOBALS['TCA']['pages']['ctrl']['delete']; - $pageInfo = BackendUtility::getRecord('pages', $id, $deleteField, $permClause ? ' AND ' . $permClause : '', false); - if (!($pageInfo[$deleteField] ?? false)) { - throw new \RuntimeException('You don\'t have access to this page', 1289917924); - } - } - } - } } diff --git a/typo3/sysext/backend/Classes/Middleware/BackendModuleValidator.php b/typo3/sysext/backend/Classes/Middleware/BackendModuleValidator.php new file mode 100644 index 0000000000000000000000000000000000000000..ccbbc055e2e478dd2078148cf414295d34e9de7c --- /dev/null +++ b/typo3/sysext/backend/Classes/Middleware/BackendModuleValidator.php @@ -0,0 +1,115 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Backend\Middleware; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; +use TYPO3\CMS\Backend\Module\ModuleInterface; +use TYPO3\CMS\Backend\Module\ModuleProvider; +use TYPO3\CMS\Backend\Routing\Route; +use TYPO3\CMS\Backend\Routing\RouteRedirect; +use TYPO3\CMS\Backend\Routing\UriBuilder; +use TYPO3\CMS\Backend\Utility\BackendUtility; +use TYPO3\CMS\Core\Http\RedirectResponse; +use TYPO3\CMS\Core\Type\Bitmask\Permission; +use TYPO3\CMS\Core\Utility\MathUtility; + +/** + * Validates module access and extends the PSR-7 Request with the + * resolved module object for the use in further components. + * + * @internal + */ +class BackendModuleValidator implements MiddlewareInterface +{ + public function __construct( + protected readonly UriBuilder $uriBuilder, + protected readonly ModuleProvider $moduleProvider + ) { + } + + /** + * In case the current route targets a TYPO3 backend module and the user + * has necessary access permissions, add the module to the request. + * + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * @return ResponseInterface + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + /** @var Route $route */ + $route = $request->getAttribute('route'); + + if ($route->hasOption('module') + && ($module = $route->getOption('module')) instanceof ModuleInterface + ) { + $this->validateModuleAccess($request, $module); + + // This module request (which is usually opened inside the list_frame) + // has been issued from a toplevel browser window (e.g. a link was opened in a new tab). + // Redirect to open the module as frame inside the TYPO3 backend layout. + // HEADS UP: This header will only be available in secure connections (https:// or .localhost TLD) + if ($request->getHeaderLine('Sec-Fetch-Dest') === 'document') { + return new RedirectResponse( + $this->uriBuilder->buildUriWithRedirect( + 'main', + [], + RouteRedirect::createFromRoute($route, $request->getQueryParams()) + ) + ); + } + + // Add the validated module to the current request + $request = $request->withAttribute('module', $module); + } + + return $handler->handle($request); + } + + /** + * Checks whether the current user is allowed to access the requested module. Does + * also evaluate page access permissions, in case an "id" is given in the request. + * + * @throws \RuntimeException + */ + protected function validateModuleAccess(ServerRequestInterface $request, ModuleInterface $module): void + { + $backendUserAuthentication = $GLOBALS['BE_USER']; + if (!$this->moduleProvider->accessGranted($module->getIdentifier(), $backendUserAuthentication)) { + throw new \RuntimeException('You don\'t have access to this module.', 1642450334); + } + + $id = $request->getQueryParams()['id'] ?? $request->getParsedBody()['id'] ?? 0; + if (MathUtility::canBeInterpretedAsInteger($id) && $id > 0) { + $id = (int)$id; + $permClause = $backendUserAuthentication->getPagePermsClause(Permission::PAGE_SHOW); + // Check page access + if (!is_array(BackendUtility::readPageAccess($id, $permClause))) { + // Check if page has been deleted + $deleteField = $GLOBALS['TCA']['pages']['ctrl']['delete']; + $pageInfo = BackendUtility::getRecord('pages', $id, $deleteField, $permClause ? ' AND ' . $permClause : '', false); + if (!($pageInfo[$deleteField] ?? false)) { + throw new \RuntimeException('You don\'t have access to this page', 1289917924); + } + } + } + } +} diff --git a/typo3/sysext/backend/Classes/Module/ExtbaseModule.php b/typo3/sysext/backend/Classes/Module/ExtbaseModule.php index 4f7349d618bc74f7abec561eead986267b4ef461..6f3c5a46abd618c3965a2f78fc9c3aa885d0b2e4 100644 --- a/typo3/sysext/backend/Classes/Module/ExtbaseModule.php +++ b/typo3/sysext/backend/Classes/Module/ExtbaseModule.php @@ -55,7 +55,6 @@ class ExtbaseModule extends BaseModule implements ModuleInterface { return [ 'module' => $this, - 'moduleName' => $this->identifier, 'access' => $this->access, 'target' => Bootstrap::class . '::handleBackendRequest', ]; diff --git a/typo3/sysext/backend/Classes/Module/Module.php b/typo3/sysext/backend/Classes/Module/Module.php index 718814b271ecc4e16c3c19bcc58c732650b6cb70..0c9c1dab2b2b8155f11312d06e69b46985540573 100644 --- a/typo3/sysext/backend/Classes/Module/Module.php +++ b/typo3/sysext/backend/Classes/Module/Module.php @@ -39,7 +39,6 @@ class Module extends BaseModule implements ModuleInterface } return [ 'module' => $this, - 'moduleName' => $this->identifier, 'access' => $this->access, 'target' => $this->routes['_default']['target'], ]; diff --git a/typo3/sysext/backend/Classes/Template/ModuleTemplate.php b/typo3/sysext/backend/Classes/Template/ModuleTemplate.php index 1a4b6cadacf1a8e3f1b4d2155b6c65de9e6ba9e7..9b0aa49b064e3eb43e132d24f2ef862961412be7 100644 --- a/typo3/sysext/backend/Classes/Template/ModuleTemplate.php +++ b/typo3/sysext/backend/Classes/Template/ModuleTemplate.php @@ -21,7 +21,7 @@ use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamFactoryInterface; -use TYPO3\CMS\Backend\Routing\Route; +use TYPO3\CMS\Backend\Module\ModuleInterface; use TYPO3\CMS\Backend\Template\Components\DocHeaderComponent; use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; @@ -76,13 +76,11 @@ final class ModuleTemplate implements ViewInterface, ResponsableViewInterface protected readonly StreamFactoryInterface $streamFactory, protected readonly ServerRequestInterface $request, ) { - $currentRoute = $request->getAttribute('route'); - if ($currentRoute instanceof Route) { - if ($currentRoute->hasOption('module')) { - $this->setModuleName($currentRoute->getOption('module')?->getIdentifier() ?? ''); - } else { - $this->setModuleName($currentRoute->getOption('_identifier')); - } + $module = $request->getAttribute('module'); + if ($module instanceof ModuleInterface) { + $this->setModuleName($module->getIdentifier()); + } else { + $this->setModuleName($request->getAttribute('route')?->getOption('_identifier') ?? ''); } $this->flashMessageQueue = $flashMessageService->getMessageQueueByIdentifier(); $this->docHeaderComponent = GeneralUtility::makeInstance(DocHeaderComponent::class); diff --git a/typo3/sysext/backend/Configuration/RequestMiddlewares.php b/typo3/sysext/backend/Configuration/RequestMiddlewares.php index cc5506d1c060fa0dc2e1796056d2233e3fa6d60b..4e11b55e9b3b4f1bb6f9d5351d21efd0645a533e 100644 --- a/typo3/sysext/backend/Configuration/RequestMiddlewares.php +++ b/typo3/sysext/backend/Configuration/RequestMiddlewares.php @@ -47,10 +47,16 @@ return [ 'typo3/cms-backend/backend-routing', ], ], + 'typo3/cms-backend/backend-module-validator' => [ + 'target' => \TYPO3\CMS\Backend\Middleware\BackendModuleValidator::class, + 'after' => [ + 'typo3/cms-backend/authentication', + ], + ], 'typo3/cms-backend/site-resolver' => [ 'target' => \TYPO3\CMS\Backend\Middleware\SiteResolver::class, 'after' => [ - 'typo3/cms-backend/backend-routing', + 'typo3/cms-backend/backend-module-validator', ], ], /** internal: do not use or reference this middleware in your own code */ diff --git a/typo3/sysext/backend/Tests/Functional/Middleware/BackendModuleValidatorTest.php b/typo3/sysext/backend/Tests/Functional/Middleware/BackendModuleValidatorTest.php new file mode 100644 index 0000000000000000000000000000000000000000..619a77a8281a1fccd7b031e70972b8133e7b5294 --- /dev/null +++ b/typo3/sysext/backend/Tests/Functional/Middleware/BackendModuleValidatorTest.php @@ -0,0 +1,161 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Backend\Tests\Functional\Middleware; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; +use TYPO3\CMS\Backend\Middleware\BackendModuleValidator; +use TYPO3\CMS\Backend\Module\ModuleFactory; +use TYPO3\CMS\Backend\Module\ModuleProvider; +use TYPO3\CMS\Backend\Routing\Route; +use TYPO3\CMS\Backend\Routing\UriBuilder; +use TYPO3\CMS\Core\Core\Bootstrap; +use TYPO3\CMS\Core\Http\Response; +use TYPO3\CMS\Core\Http\ServerRequest; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +class BackendModuleValidatorTest extends FunctionalTestCase +{ + protected BackendModuleValidator $subject; + protected ServerRequestInterface $request; + protected RequestHandlerInterface $requestHandler; + + protected function setUp(): void + { + parent::setUp(); + + $this->setUpBackendUserFromFixture(1); + Bootstrap::initializeLanguageObject(); + + $this->subject = new BackendModuleValidator( + $this->getContainer()->get(UriBuilder::class), + $this->getContainer()->get(ModuleProvider::class), + ); + $this->request = new ServerRequest('/some/uri'); + $this->requestHandler = new class() implements RequestHandlerInterface { + public function handle(ServerRequestInterface $request): ResponseInterface + { + // In case the module is valid, it is added to the request. + // To test this, we add the possible module identifier as header to the response. + return (new Response())->withHeader('moduleIdentifier', $request->getAttribute('module')?->getIdentifier() ?? ''); + } + }; + } + + /** + * @test + */ + public function moduleIsAddedToRequest(): void + { + $module = $this->getContainer()->get(ModuleFactory::class)->createModule( + 'web_layout', + ['path' => '/module/web/layout'] + ); + + $response = $this->subject->process( + $this->request->withAttribute('route', new Route('/some/route', ['module' => $module])), + $this->requestHandler + ); + + self::assertEquals('web_layout', $response->getHeaderLine('moduleIdentifier')); + } + + /** + * @test + */ + public function invalidModuleThrowsException(): void + { + $module = $this->getContainer()->get(ModuleFactory::class)->createModule( + 'some_module', + ['path' => '/some/module'] + ); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionCode(1642450334); + + $this->subject->process( + $this->request->withAttribute('route', new Route('/some/route', ['module' => $module])), + $this->requestHandler + ); + } + + /** + * @test + */ + public function insufficientAccessPermissionsThrowsException(): void + { + $GLOBALS['BE_USER']->user['admin'] = 0; + + // site_configuration requires admin access + $module = $this->getContainer()->get(ModuleFactory::class)->createModule( + 'site_configuration', + ['path' => '/module/site/configuration'] + ); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionCode(1642450334); + + $this->subject->process( + $this->request->withAttribute('route', new Route('/some/route', ['module' => $module])), + $this->requestHandler + ); + } + + /** + * @test + */ + public function noPageAccessThrowsException(): void + { + $module = $this->getContainer()->get(ModuleFactory::class)->createModule( + 'web_layout', + ['path' => '/module/web/layout'] + ); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionCode(1289917924); + + $this->subject->process( + $this->request + ->withQueryParams(['id' => 123]) + ->withAttribute('route', new Route('/some/route', ['module' => $module])), + $this->requestHandler + ); + } + + /** + * @test + */ + public function redirectsToMainForSecFetchDestHeader(): void + { + $module = $this->getContainer()->get(ModuleFactory::class)->createModule( + 'web_layout', + ['path' => '/module/web/layout'] + ); + + $response = $this->subject->process( + $this->request + ->withHeader('Sec-Fetch-Dest', 'document') + ->withAttribute('route', new Route('/some/route', ['_identifier' => 'web_layout', 'module' => $module])), + $this->requestHandler + ); + + self::assertEquals(302, $response->getStatusCode()); + self::assertMatchesRegularExpression('/^\/typo3\/main.*redirect=web_layout$/', $response->getHeaderLine('location')); + } +} diff --git a/typo3/sysext/backend/Tests/Unit/Http/RouteDispatcherTest.php b/typo3/sysext/backend/Tests/Unit/Http/RouteDispatcherTest.php index 3e318348aa062c55a7d763ac9a0e7a4775b218aa..f1cac1b945eb5e702189cb977cd5725ff586727d 100644 --- a/typo3/sysext/backend/Tests/Unit/Http/RouteDispatcherTest.php +++ b/typo3/sysext/backend/Tests/Unit/Http/RouteDispatcherTest.php @@ -22,9 +22,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Psr\Container\ContainerInterface; use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Backend\Http\RouteDispatcher; -use TYPO3\CMS\Backend\Module\ModuleProvider; use TYPO3\CMS\Backend\Routing\Route; -use TYPO3\CMS\Backend\Routing\UriBuilder; use TYPO3\CMS\Backend\Tests\Unit\Http\Fixtures\RouteDispatcherClassFixture; use TYPO3\CMS\Backend\Tests\Unit\Http\Fixtures\RouteDispatcherClassInvokeFixture; use TYPO3\CMS\Backend\Tests\Unit\Http\Fixtures\RouteDispatcherClassWithoutInvokeFixture; @@ -62,13 +60,11 @@ class RouteDispatcherTest extends UnitTestCase $requestProphecy->getAttribute('route')->willReturn($route); $containerProphecy = $this->prophesize(ContainerInterface::class); $containerProphecy->has(Argument::any())->willReturn(false); - $uriBuilderProphecy = $this->prophesize(UriBuilder::class); - $moduleProviderProphecy = $this->prophesize(ModuleProvider::class); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionCode(1425381442); - $subject = new RouteDispatcher($containerProphecy->reveal(), $uriBuilderProphecy->reveal(), $moduleProviderProphecy->reveal()); + $subject = new RouteDispatcher($containerProphecy->reveal()); $subject->dispatch($requestProphecy->reveal()); } @@ -89,13 +85,11 @@ class RouteDispatcherTest extends UnitTestCase $requestProphecy->getAttribute('route')->willReturn($route); $containerProphecy = $this->prophesize(ContainerInterface::class); $containerProphecy->has(Argument::any())->willReturn(false); - $uriBuilderProphecy = $this->prophesize(UriBuilder::class); - $moduleProviderProphecy = $this->prophesize(ModuleProvider::class); $this->expectException(\RuntimeException::class); $this->expectExceptionCode(1520756142); - $subject = new RouteDispatcher($containerProphecy->reveal(), $uriBuilderProphecy->reveal(), $moduleProviderProphecy->reveal()); + $subject = new RouteDispatcher($containerProphecy->reveal()); $subject->dispatch($requestProphecy->reveal()); } @@ -116,13 +110,11 @@ class RouteDispatcherTest extends UnitTestCase $requestProphecy->getAttribute('route')->willReturn($route); $containerProphecy = $this->prophesize(ContainerInterface::class); $containerProphecy->has(Argument::any())->willReturn(false); - $uriBuilderProphecy = $this->prophesize(UriBuilder::class); - $moduleProviderProphecy = $this->prophesize(ModuleProvider::class); $this->expectException(\RuntimeException::class); $this->expectExceptionCode(1520756466); - $subject = new RouteDispatcher($containerProphecy->reveal(), $uriBuilderProphecy->reveal(), $moduleProviderProphecy->reveal()); + $subject = new RouteDispatcher($containerProphecy->reveal()); $subject->dispatch($requestProphecy->reveal()); } @@ -141,13 +133,11 @@ class RouteDispatcherTest extends UnitTestCase $requestProphecy->getAttribute('route')->willReturn($route); $containerProphecy = $this->prophesize(ContainerInterface::class); $containerProphecy->has(Argument::any())->willReturn(false); - $uriBuilderProphecy = $this->prophesize(UriBuilder::class); - $moduleProviderProphecy = $this->prophesize(ModuleProvider::class); $this->expectException(\RuntimeException::class); $this->expectExceptionCode(1520756623); - $subject = new RouteDispatcher($containerProphecy->reveal(), $uriBuilderProphecy->reveal(), $moduleProviderProphecy->reveal()); + $subject = new RouteDispatcher($containerProphecy->reveal()); $subject->dispatch($requestProphecy->reveal()); } @@ -167,13 +157,11 @@ class RouteDispatcherTest extends UnitTestCase $containerProphecy = $this->prophesize(ContainerInterface::class); $containerProphecy->has($target)->willReturn(true); $containerProphecy->get($target)->willReturn(new RouteDispatcherClassInvokeFixture()); - $uriBuilderProphecy = $this->prophesize(UriBuilder::class); - $moduleProviderProphecy = $this->prophesize(ModuleProvider::class); $this->expectException(\RuntimeException::class); $this->expectExceptionCode(1520756623); - $subject = new RouteDispatcher($containerProphecy->reveal(), $uriBuilderProphecy->reveal(), $moduleProviderProphecy->reveal()); + $subject = new RouteDispatcher($containerProphecy->reveal()); $subject->dispatch($requestProphecy->reveal()); } @@ -193,13 +181,11 @@ class RouteDispatcherTest extends UnitTestCase $route = new Route('not important', ['access' => 'public', 'target' => $target]); $requestProphecy = $this->prophesize(ServerRequestInterface::class); $requestProphecy->getAttribute('route')->willReturn($route); - $uriBuilderProphecy = $this->prophesize(UriBuilder::class); - $moduleProviderProphecy = $this->prophesize(ModuleProvider::class); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionCode(1442431631); - $subject = new RouteDispatcher($containerProphecy->reveal(), $uriBuilderProphecy->reveal(), $moduleProviderProphecy->reveal()); + $subject = new RouteDispatcher($containerProphecy->reveal()); $subject->dispatch($requestProphecy->reveal()); } @@ -218,13 +204,11 @@ class RouteDispatcherTest extends UnitTestCase $requestProphecy->getAttribute('route')->willReturn($route); $containerProphecy = $this->prophesize(ContainerInterface::class); $containerProphecy->has(Argument::any())->willReturn(false); - $uriBuilderProphecy = $this->prophesize(UriBuilder::class); - $moduleProviderProphecy = $this->prophesize(ModuleProvider::class); $this->expectException(\RuntimeException::class); $this->expectExceptionCode(1520756142); - $subject = new RouteDispatcher($containerProphecy->reveal(), $uriBuilderProphecy->reveal(), $moduleProviderProphecy->reveal()); + $subject = new RouteDispatcher($containerProphecy->reveal()); $subject->dispatch($requestProphecy->reveal()); } @@ -243,13 +227,11 @@ class RouteDispatcherTest extends UnitTestCase $requestProphecy->getAttribute('route')->willReturn($route); $containerProphecy = $this->prophesize(ContainerInterface::class); $containerProphecy->has(Argument::any())->willReturn(false); - $uriBuilderProphecy = $this->prophesize(UriBuilder::class); - $moduleProviderProphecy = $this->prophesize(ModuleProvider::class); $this->expectException(\RuntimeException::class); $this->expectExceptionCode(1520757000); - $subject = new RouteDispatcher($containerProphecy->reveal(), $uriBuilderProphecy->reveal(), $moduleProviderProphecy->reveal()); + $subject = new RouteDispatcher($containerProphecy->reveal()); $subject->dispatch($requestProphecy->reveal()); } } diff --git a/typo3/sysext/belog/Classes/Module/BackendLogModuleBootstrap.php b/typo3/sysext/belog/Classes/Module/BackendLogModuleBootstrap.php index 5ea78651b84e2b07dd42d316d0d447a9a47685f4..a8ff3184f7f86223756eb70a16d8cf45018ff010 100644 --- a/typo3/sysext/belog/Classes/Module/BackendLogModuleBootstrap.php +++ b/typo3/sysext/belog/Classes/Module/BackendLogModuleBootstrap.php @@ -18,7 +18,6 @@ namespace TYPO3\CMS\Belog\Module; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Backend\Module\ModuleProvider; -use TYPO3\CMS\Backend\Routing\Route; use TYPO3\CMS\Backend\Routing\UriBuilder; use TYPO3\CMS\Extbase\Core\Bootstrap as ExtbaseBootstrap; @@ -48,16 +47,11 @@ class BackendLogModuleBootstrap $queryParams = $request->getQueryParams(); $queryParams['tx_belog_system_beloglog']['pageId'] = $request->getQueryParams()['id'] ?? $request->getParsedBody()['id'] ?? 0; $queryParams['tx_belog_system_beloglog']['layout'] = 'Plain'; - $request = $request->withQueryParams($queryParams); - - $routePath = $this->uriBuilder->buildUriFromRoute('system_BelogLog')->getPath(); - $routeOptions = $this->moduleProvider->getModule('system_BelogLog')?->getDefaultRouteOptions() ?? []; return $this->extbaseBootstrap->handleBackendRequest( - $request->withAttribute( - 'route', - new Route($routePath, $routeOptions) - ) + $request + ->withQueryParams($queryParams) + ->withAttribute('module', $this->moduleProvider->getModule('system_BelogLog')) ); } } diff --git a/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php b/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php index d7e02aedd8666f1973c74762c85887ccd27794c9..8c62165e24fb39146c16922a5260335e57ceba5a 100644 --- a/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php +++ b/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php @@ -983,10 +983,10 @@ abstract class AbstractUserAuthentication implements LoggerAwareInterface /** * Stores data for a module. - * The data is stored with the session id so you can even check upon retrieval + * The data is stored with the session ID, so you can even check upon retrieval * if the module data is from a previous session or from the current session. * - * @param string $module Is the name of the module ($MCONF['name']) + * @param string $module Is the identifier of the module, e.g. "web_info" * @param mixed $data Is the data you want to store for that module (array, string, ...) * @param bool|int $noSave If $noSave is set, then the ->uc array (which carries all kinds of user data) is NOT written immediately, but must be written by some subsequent call. */ @@ -1006,7 +1006,7 @@ abstract class AbstractUserAuthentication implements LoggerAwareInterface /** * Gets module data for a module (from a loaded ->uc array) * - * @param string $module Is the name of the module ($MCONF['name']) + * @param string $module Is the identifier of the module, e.g. "web_info" * @param string $type If $type = 'ses' then module data is returned only if it was stored in the current session, otherwise data from a previous session will be returned (if available). * @return mixed The module data if available: $this->uc['moduleData'][$module]; */ diff --git a/typo3/sysext/core/Documentation/Changelog/12.0/Breaking-96733-RemovedSupportForModuleHandlingBasedOnTBE_MODULES.rst b/typo3/sysext/core/Documentation/Changelog/12.0/Breaking-96733-RemovedSupportForModuleHandlingBasedOnTBE_MODULES.rst index ffb33483433f8842977b61d6ca31224b893d2a47..40520dfcf701546a5900be35c1b259635164ac72 100644 --- a/typo3/sysext/core/Documentation/Changelog/12.0/Breaking-96733-RemovedSupportForModuleHandlingBasedOnTBE_MODULES.rst +++ b/typo3/sysext/core/Documentation/Changelog/12.0/Breaking-96733-RemovedSupportForModuleHandlingBasedOnTBE_MODULES.rst @@ -89,8 +89,19 @@ Migrate to the new Module Registration API, and use the :php:`ModuleProvider` class to get allowed modules and work with the objects. The current module information (an implementation of :php:`ModuleInterface`) is stored in a TYPO3 Backend request within the `module` option of a TYPO3 Backend route, -which can be accessed via -:php:`$request->getAttribute('route')->getOption('module')`. +which can be accessed via :php:`$request->getAttribute('route')->getOption('module')`. + +As soon as the new TYPO3 :php:`BackendModuleValidator` PSR-15 middleware +has validated the module for the current user, the :php:`ModuleInterface` +object is also added to the current request and can then be accessed +via :php:`$request->getAttribute('module')` in custom middlewares or +components. + +.. note:: + + With the new module registration, the module identifier is also used + as the route identifier. Therefore, the `moduleName` option is removed + form the TYPO3 backend route object. The registration has to be moved from :file:`ext_tables.php` to the :file:`Configuration/Backend/Modules.php` file. See the diff --git a/typo3/sysext/extbase/Classes/Core/Bootstrap.php b/typo3/sysext/extbase/Classes/Core/Bootstrap.php index 032d2d170da138a9b67830d18b2c7398c41c6ad0..ff75861f8f692b4d5868b1de614ceba160067de7 100644 --- a/typo3/sysext/extbase/Classes/Core/Bootstrap.php +++ b/typo3/sysext/extbase/Classes/Core/Bootstrap.php @@ -20,7 +20,6 @@ namespace TYPO3\CMS\Extbase\Core; use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use TYPO3\CMS\Backend\Routing\Route; use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface; use TYPO3\CMS\Extbase\Mvc\Dispatcher; @@ -216,12 +215,11 @@ class Bootstrap */ public function handleBackendRequest(ServerRequestInterface $request): ResponseInterface { - // build the configuration from the Server request / route - /** @var Route $route */ - $route = $request->getAttribute('route'); + // build the configuration from the module, included in the current request + $module = $request->getAttribute('module'); $configuration = [ - 'extensionName' => $route->getOption('module')?->getExtensionName(), - 'pluginName' => $route->getOption('module')?->getIdentifier(), + 'extensionName' => $module?->getExtensionName(), + 'pluginName' => $module?->getIdentifier(), ]; $this->initialize($configuration); diff --git a/typo3/sysext/extbase/Classes/Mvc/Web/RequestBuilder.php b/typo3/sysext/extbase/Classes/Mvc/Web/RequestBuilder.php index c1df23a587d27f943a4fc6dceb65044038a3c6f7..1dd321433b7ddef1da758489a5bc926352c3815d 100644 --- a/typo3/sysext/extbase/Classes/Mvc/Web/RequestBuilder.php +++ b/typo3/sysext/extbase/Classes/Mvc/Web/RequestBuilder.php @@ -162,7 +162,7 @@ class RequestBuilder implements SingletonInterface { $configuration = []; // Load values from the route object, this is used for TYPO3 Backend Modules - $module = $mainRequest->getAttribute('route')?->getOption('module'); + $module = $mainRequest->getAttribute('module'); if ($module instanceof ExtbaseModule) { $configuration = [ 'controllerConfiguration' => $module->getControllerActions(), diff --git a/typo3/sysext/extbase/Tests/Functional/Mvc/Web/RequestBuilderTest.php b/typo3/sysext/extbase/Tests/Functional/Mvc/Web/RequestBuilderTest.php index 2d9e8fee1050c10d2882bde4d9f7432f2b2b0987..eb8da4e50c9fa9039d413b7c4f767c4fc11bc49e 100644 --- a/typo3/sysext/extbase/Tests/Functional/Mvc/Web/RequestBuilderTest.php +++ b/typo3/sysext/extbase/Tests/Functional/Mvc/Web/RequestBuilderTest.php @@ -20,7 +20,6 @@ namespace TYPO3\CMS\Extbase\Tests\Functional\Mvc\Web; use ExtbaseTeam\BlogExample\Controller\BlogController; use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Backend\Module\ExtbaseModule; -use TYPO3\CMS\Backend\Routing\Route; use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder; use TYPO3\CMS\Core\Error\Http\PageNotFoundException; use TYPO3\CMS\Core\Http\NormalizedParams; @@ -60,7 +59,7 @@ class RequestBuilderTest extends FunctionalTestCase $configurationManager->setConfiguration($configuration); $mainRequest = $this->prepareServerRequest('https://example.com/'); - $mainRequest = $mainRequest->withAttribute('route', new Route($module->getPath(), ['module' => $module])); + $mainRequest = $mainRequest->withAttribute('module', $module); $requestBuilder = $this->getContainer()->get(RequestBuilder::class); $request = $requestBuilder->build($mainRequest); @@ -93,7 +92,7 @@ class RequestBuilderTest extends FunctionalTestCase $configurationManager->setConfiguration($configuration); $mainRequest = $this->prepareServerRequest('https://example.com/'); - $mainRequest = $mainRequest->withAttribute('route', new Route($module->getPath(), ['module' => $module])); + $mainRequest = $mainRequest->withAttribute('module', $module); $requestBuilder = $this->getContainer()->get(RequestBuilder::class); $request = $requestBuilder->build($mainRequest); @@ -126,7 +125,7 @@ class RequestBuilderTest extends FunctionalTestCase $mainRequest = $this->prepareServerRequest('https://example.com/'); $mainRequest = $mainRequest->withQueryParams(['tx_blog_example_blog' => ['format' => 'json']]); - $mainRequest = $mainRequest->withAttribute('route', new Route($module->getPath(), ['module' => $module])); + $mainRequest = $mainRequest->withAttribute('module', $module); $requestBuilder = $this->getContainer()->get(RequestBuilder::class); $request = $requestBuilder->build($mainRequest); @@ -197,7 +196,7 @@ class RequestBuilderTest extends FunctionalTestCase $configurationManager->setConfiguration($configuration); $mainRequest = $this->prepareServerRequest('https://example.com/', 'POST'); - $mainRequest = $mainRequest->withAttribute('route', new Route($module->getPath(), ['module' => $module])); + $mainRequest = $mainRequest->withAttribute('module', $module); $requestBuilder = $this->getContainer()->get(RequestBuilder::class); $request = $requestBuilder->build($mainRequest); @@ -255,7 +254,7 @@ class RequestBuilderTest extends FunctionalTestCase $configurationManager->setConfiguration($configuration); $mainRequest = $this->prepareServerRequest('https://example.com/', 'POST'); - $mainRequest = $mainRequest->withAttribute('route', new Route($module->getPath(), ['module' => $module])); + $mainRequest = $mainRequest->withAttribute('module', $module); $requestBuilder = $this->getContainer()->get(RequestBuilder::class); $request = $requestBuilder->build($mainRequest); @@ -308,7 +307,7 @@ class RequestBuilderTest extends FunctionalTestCase $configurationManager->setConfiguration($configuration); $mainRequest = $this->prepareServerRequest('https://example.com/'); - $mainRequest = $mainRequest->withAttribute('route', new Route($module->getPath(), ['module' => $module])); + $mainRequest = $mainRequest->withAttribute('module', $module); $mainRequest = $mainRequest->withQueryParams(['tx_blog_example_blog' => ['controller' => 'NonExistentController']]); $requestBuilder = $this->getContainer()->get(RequestBuilder::class); $requestBuilder->build($mainRequest); @@ -344,7 +343,7 @@ class RequestBuilderTest extends FunctionalTestCase $mainRequest = $this->prepareServerRequest('https://example.com/'); $mainRequest = $mainRequest->withQueryParams(['tx_blog_example_blog' => ['controller' => 'NonExistentController']]); - $mainRequest = $mainRequest->withAttribute('route', new Route($module->getPath(), ['module' => $module])); + $mainRequest = $mainRequest->withAttribute('module', $module); $requestBuilder = $this->getContainer()->get(RequestBuilder::class); $requestBuilder->build($mainRequest); } @@ -398,7 +397,7 @@ class RequestBuilderTest extends FunctionalTestCase $configurationManager->setConfiguration($configuration); $mainRequest = $this->prepareServerRequest('https://example.com/'); - $mainRequest = $mainRequest->withAttribute('route', new Route($module->getPath(), ['module' => $module])); + $mainRequest = $mainRequest->withAttribute('module', $module); $mainRequest = $mainRequest->withQueryParams(['tx_blog_example_blog' => ['controller' => 'NonExistentController']]); $requestBuilder = $this->getContainer()->get(RequestBuilder::class); $request = $requestBuilder->build($mainRequest); @@ -432,8 +431,8 @@ class RequestBuilderTest extends FunctionalTestCase $configurationManager->setConfiguration($configuration); $mainRequest = $this->prepareServerRequest('https://example.com/'); + $mainRequest = $mainRequest->withAttribute('module', $module); $mainRequest = $mainRequest->withQueryParams(['tx_blog_example_blog' => ['controller' => 'User']]); - $mainRequest = $mainRequest->withAttribute('route', new Route($module->getPath(), ['module' => $module])); $requestBuilder = $this->getContainer()->get(RequestBuilder::class); $request = $requestBuilder->build($mainRequest); @@ -469,7 +468,7 @@ class RequestBuilderTest extends FunctionalTestCase $configurationManager->setConfiguration($configuration); $mainRequest = $this->prepareServerRequest('https://example.com/'); - $mainRequest = $mainRequest->withAttribute('route', new Route($module->getPath(), ['module' => $module])); + $mainRequest = $mainRequest->withAttribute('module', $module); $mainRequest = $mainRequest->withQueryParams(['tx_blog_example_blog' => ['action' => 'NonExistentAction']]); $requestBuilder = $this->getContainer()->get(RequestBuilder::class); $requestBuilder->build($mainRequest); @@ -504,7 +503,7 @@ class RequestBuilderTest extends FunctionalTestCase $configurationManager->setConfiguration($configuration); $mainRequest = $this->prepareServerRequest('https://example.com/'); - $mainRequest = $mainRequest->withAttribute('route', new Route($module->getPath(), ['module' => $module])); + $mainRequest = $mainRequest->withAttribute('module', $module); $mainRequest = $mainRequest->withQueryParams(['tx_blog_example_blog' => ['action' => 'NonExistentAction']]); $requestBuilder = $this->getContainer()->get(RequestBuilder::class); $requestBuilder->build($mainRequest); @@ -535,7 +534,7 @@ class RequestBuilderTest extends FunctionalTestCase $configurationManager->setConfiguration($configuration); $mainRequest = $this->prepareServerRequest('https://example.com/'); - $mainRequest = $mainRequest->withAttribute('route', new Route($module->getPath(), ['module' => $module])); + $mainRequest = $mainRequest->withAttribute('module', $module); $mainRequest = $mainRequest->withQueryParams(['tx_blog_example_blog' => ['action' => 'NonExistentAction']]); $requestBuilder = $this->getContainer()->get(RequestBuilder::class); $request = $requestBuilder->build($mainRequest); @@ -568,7 +567,7 @@ class RequestBuilderTest extends FunctionalTestCase $configurationManager->setConfiguration($configuration); $mainRequest = $this->prepareServerRequest('https://example.com/'); - $mainRequest = $mainRequest->withAttribute('route', new Route($module->getPath(), ['module' => $module])); + $mainRequest = $mainRequest->withAttribute('module', $module); $mainRequest = $mainRequest->withQueryParams(['tx_blog_example_blog' => ['action' => 'show']]); $requestBuilder = $this->getContainer()->get(RequestBuilder::class); $request = $requestBuilder->build($mainRequest); @@ -605,7 +604,7 @@ class RequestBuilderTest extends FunctionalTestCase $configurationManager->setConfiguration($configuration); $mainRequest = $this->prepareServerRequest('https://example.com/'); - $mainRequest = $mainRequest->withAttribute('route', new Route($module->getPath(), ['module' => $module])); + $mainRequest = $mainRequest->withAttribute('module', $module); $requestBuilder = $this->getContainer()->get(RequestBuilder::class); $requestBuilder->build($mainRequest); } @@ -630,7 +629,7 @@ class RequestBuilderTest extends FunctionalTestCase BlogController::class => ['list', 'show'], ], ]); - $mainRequest = $mainRequest->withAttribute('route', new Route($module->getPath(), ['module' => $module])); + $mainRequest = $mainRequest->withAttribute('module', $module); $configuration = []; $configuration['extensionName'] = $extensionName; @@ -668,7 +667,7 @@ class RequestBuilderTest extends FunctionalTestCase BlogController::class => ['list', 'show'], ], ]); - $mainRequest = $mainRequest->withAttribute('route', new Route($module->getPath(), ['module' => $module])); + $mainRequest = $mainRequest->withAttribute('module', $module); $configuration = []; $configuration['extensionName'] = $extensionName; @@ -704,7 +703,7 @@ class RequestBuilderTest extends FunctionalTestCase BlogController::class => ['list', 'show'], ], ]); - $mainRequest = $mainRequest->withAttribute('route', new Route($module->getPath(), ['module' => $module])); + $mainRequest = $mainRequest->withAttribute('module', $module); $configuration = []; $configuration['extensionName'] = $extensionName; @@ -743,7 +742,7 @@ class RequestBuilderTest extends FunctionalTestCase BlogController::class => ['list', 'show'], ], ]); - $mainRequest = $mainRequest->withAttribute('route', new Route($module->getPath(), ['module' => $module])); + $mainRequest = $mainRequest->withAttribute('module', $module); $configuration = []; $configuration['extensionName'] = $extensionName; diff --git a/typo3/sysext/install/Classes/Updates/ShortcutRecordsMigration.php b/typo3/sysext/install/Classes/Updates/ShortcutRecordsMigration.php index 9a878ed919f45e99d40c3c75014e185c5db0fbf3..2997c4325b2c0f48a83c1fba41f59dad4fb487ef 100644 --- a/typo3/sysext/install/Classes/Updates/ShortcutRecordsMigration.php +++ b/typo3/sysext/install/Classes/Updates/ShortcutRecordsMigration.php @@ -166,15 +166,7 @@ class ShortcutRecordsMigration implements UpgradeWizardInterface } if ($this->moduleProvider->isModuleRegistered($moduleName)) { - return $this->moduleProvider->getModule($moduleName)->getIdentifier(); - } - - // If the defined route identifier can't be fetched, try from the other side - // by iterating over the routes to match a route by the defined module name - foreach ($this->routes as $identifier => $route) { - if ($route->hasOption('moduleName') && $route->getOption('moduleName') === $moduleName) { - return $identifier; - } + return $moduleName; } return ''; diff --git a/typo3/sysext/sys_note/Classes/Hook/ButtonBarHook.php b/typo3/sysext/sys_note/Classes/Hook/ButtonBarHook.php index ef63f67d6f905b609d65311fce7dc91337ae57a8..db2bbd62f82debc40e8eee3eef4023761be366d5 100644 --- a/typo3/sysext/sys_note/Classes/Hook/ButtonBarHook.php +++ b/typo3/sysext/sys_note/Classes/Hook/ButtonBarHook.php @@ -54,17 +54,17 @@ class ButtonBarHook $request = $this->getRequest(); $id = (int)($request->getParsedBody()['id'] ?? $request->getQueryParams()['id'] ?? 0); - $route = $request->getAttribute('route'); + $module = $request->getAttribute('module'); $normalizedParams = $request->getAttribute('normalizedParams'); $pageTSconfig = BackendUtility::getPagesTSconfig($id); if (!$id - || $route === null + || $module === null || $normalizedParams === null || !empty($pageTSconfig['mod.']['SHARED.']['disableSysNoteButton']) || !$this->canCreateNewRecord($id) - || !in_array($route->getOption('moduleName'), self::ALLOWED_MODULES, true) - || ($route->getOption('moduleName') === 'web_list' && !$this->isCreationAllowed($pageTSconfig['mod.']['web_list.'] ?? [])) + || !in_array($module->getIdentifier(), self::ALLOWED_MODULES, true) + || ($module->getIdentifier() === 'web_list' && !$this->isCreationAllowed($pageTSconfig['mod.']['web_list.'] ?? [])) ) { return $buttons; }