Commit 83600741 authored by Benni Mack's avatar Benni Mack Committed by Oliver Bartsch
Browse files

[TASK] Define explicit routes for Extbase Backend Modules

Previously, the controller / action pairs of an Extbase Backend
Module were defined via corresponding GET parameters. Thanks to
the new Module Registration API and the support for individual
sub routes, it's now also possible to define explicit routes
for each controller / action pair. This is done automatically,
as long as the "enableNamespacedArgumentsForBackend" feature
toggle is turned off, which is the default.

This therefore results in following change:

http://example.com/typo3/module/system/BeuserTxBeuser?controller=BackendUser&action=filemounts

becomes

http://example.com/typo3/module/system/BeuserTxBeuser/BackendUser/filemounts

Resolves: #99704
Related: #99647
Related: #96733
Releases: main
Change-Id: Ie7bbf70793f7e3da17db3ab1a322ba8ad7bcc5b8
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/77508


Reviewed-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Tested-by: Stefan Bürk's avatarStefan Bürk <stefan@buerk.tech>
Reviewed-by: Stefan Bürk's avatarStefan Bürk <stefan@buerk.tech>
Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
parent e9c190bf
......@@ -53,15 +53,33 @@ class ExtbaseModule extends BaseModule implements ModuleInterface
public function getDefaultRouteOptions(): array
{
return [
'_default' => [
'module' => $this,
'packageName' => $this->packageName,
'absolutePackagePath' => $this->absolutePackagePath,
'access' => $this->access,
'target' => Bootstrap::class . '::handleBackendRequest',
],
];
$allRoutes = [];
foreach ($this->controllerActions as $controllerConfiguration) {
foreach ($controllerConfiguration['actions'] as $actionName) {
if ($allRoutes === []) {
$allRoutes['_default'] = [
'module' => $this,
'packageName' => $this->packageName,
'absolutePackagePath' => $this->absolutePackagePath,
'access' => $this->access,
'target' => Bootstrap::class . '::handleBackendRequest',
'controller' => $controllerConfiguration['alias'],
'action' => $actionName,
];
}
$allRoutes[$controllerConfiguration['alias'] . '_' . $actionName] = [
'module' => $this,
'path' => $controllerConfiguration['alias'] . '/' . $actionName,
'packageName' => $this->packageName,
'absolutePackagePath' => $this->absolutePackagePath,
'access' => $this->access,
'target' => Bootstrap::class . '::handleBackendRequest',
'controller' => $controllerConfiguration['alias'],
'action' => $actionName,
];
}
}
return $allRoutes;
}
protected static function sanitizeExtensionName(string $extensionName): string
......
......@@ -62,10 +62,60 @@ sub route could therefore look like this:
UriBuilder->buildUriFromRoute('my_module.edit')
Extbase modules
^^^^^^^^^^^^^^^
Also Extbase Backend Modules are enhanced and do now automatically
define explicit routes for each controller / action combination,
as long as the :typoscript:`enableNamespacedArgumentsForBackend`
feature toggle is turned off, which is the default. This means,
the following module configuration
.. code-block:: php
return [
'web_ExtkeyExample' => [
'parent' => 'web',
'position' => ['after' => 'web_info'],
'access' => 'admin',
'workspaces' => 'live',
'iconIdentifier' => 'module-example',
'path' => '/module/web/ExtkeyExample',
'labels' => 'LLL:EXT:beuser/Resources/Private/Language/locallang_mod.xlf',
'extensionName' => 'Extkey',
'controllerActions' => [
MyModuleController::class => [
'list',
'detail'
],
],
],
];
now leads to following URLs:
- `https://example.com/typo3/module/web/ExtkeyExample`
- `https://example.com/typo3/module/web/ExtkeyExample/MyModuleController/list`
- `https://example.com/typo3/module/web/ExtkeyExample/MyModuleController/detail`
The route identifier of corresponding routes is registered with similar syntax
as standard backend modules: :php:`<module_identifier>.<controller>_<action>`.
Above configuration will therefore register the following routes:
- `web_ExtkeyExample`
- `web_ExtkeyExample.MyModuleController_list`
- `web_ExtkeyExample.MyModuleController_detail`
Impact
======
It's now possible to configure specific routes for a module, which all can
target any controller / action combination.
As long as :typoscript:`enableNamespacedArgumentsForBackend` is turned off
for Extbase Backend Modules, all controller / action combinations are explicitly
registered as individual routes. This effectively means human-readable URLs,
since the controller / action combinations are no longer defined via query
parameters but are now part of the path.
.. index:: Backend, PHP-API, ext:backend
......@@ -147,15 +147,24 @@ class RequestBuilder implements SingletonInterface
public function build(ServerRequestInterface $mainRequest)
{
$configuration = [];
// Parameters, which are not part of the request URL (e.g. due to "useArgumentsWithoutNamespace"), which however
// need to be taken into account on building the extbase request. Usually those are "controller" and "action".
$fallbackParameters = [];
// To be used in TYPO3 Backend for Extbase modules that do not need the "namespaces" GET and POST parameters anymore.
$useArgumentsWithoutNamespace = false;
// Load values from the route object, this is used for TYPO3 Backend Modules
// Fetch requested module from the main request. This is only used for TYPO3 Backend Modules.
$module = $mainRequest->getAttribute('module');
if ($module instanceof ExtbaseModule) {
$configuration = [
'controllerConfiguration' => $module->getControllerActions(),
];
$useArgumentsWithoutNamespace = !$this->configurationManager->isFeatureEnabled('enableNamespacedArgumentsForBackend');
// Ensure the "controller" and "action" information are added as fallback
// parameters in case "enableNamespacedArgumentsForBackend" is turned off.
if ($useArgumentsWithoutNamespace && ($routeOptions = $mainRequest->getAttribute('route')?->getOptions())) {
$fallbackParameters['controller'] = $routeOptions['controller'] ?? null;
$fallbackParameters['action'] = $routeOptions['action'];
}
}
$this->loadDefaultValues($configuration);
$pluginNamespace = $this->extensionService->getPluginNamespace($this->extensionName, $this->pluginName);
......@@ -168,6 +177,10 @@ class RequestBuilder implements SingletonInterface
$parameters = $mainRequest->getQueryParams()[$pluginNamespace] ?? [];
}
$parameters = is_array($parameters) ? $parameters : [];
if ($fallbackParameters !== []) {
// Enhance with fallback parameters, such as "controller" and "action"
$parameters = array_replace_recursive($fallbackParameters, $parameters);
}
if ($mainRequest->getMethod() === 'POST') {
if ($useArgumentsWithoutNamespace) {
$postParameters = $mainRequest->getParsedBody();
......
......@@ -613,6 +613,21 @@ class UriBuilder
$this->lastArguments = $arguments;
$routeIdentifier = $arguments['route'] ?? null;
unset($arguments['route'], $arguments['token']);
$useArgumentsWithoutNamespace = !$this->configurationManager->isFeatureEnabled('enableNamespacedArgumentsForBackend');
if ($useArgumentsWithoutNamespace) {
// In case the current route identifier is an identifier of a sub route, remove the sub route
// part to be able to add the actually requested sub route based on the current arguments.
if ($routeIdentifier && str_contains($routeIdentifier, '.')) {
[$routeIdentifier] = explode('.', $routeIdentifier);
}
// Build route identifier to the actually requested sub route (controller / action pair) - if any -
// and unset corresponding arguments, because "enableNamespacedArgumentsForBackend" is turned off.
if ($routeIdentifier && isset($arguments['controller'], $arguments['action'])) {
$routeIdentifier .= '.' . $arguments['controller'] . '_' . $arguments['action'];
unset($arguments['controller'], $arguments['action']);
}
}
$uri = '';
if ($routeIdentifier) {
$backendUriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
......
......@@ -20,6 +20,7 @@ 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;
......@@ -833,6 +834,55 @@ class RequestBuilderTest extends FunctionalTestCase
self::assertSame('list', $request->getControllerActionName());
}
/**
* @test
*/
public function controllerActionParametersAreAddedToRequest(): void
{
$mainRequest = $this->prepareServerRequest('https://example.com/typo3/module/blog-example/Blog/show');
$pluginName = 'blog';
$extensionName = 'blog_example';
$module = ExtbaseModule::createFromConfiguration($pluginName, [
'packageName' => 'typo3/cms-blog-example',
'path' => '/blog-example',
'extensionName' => $extensionName,
'controllerActions' => [
BlogController::class => ['list', 'show'],
],
]);
$mainRequest = $mainRequest
->withAttribute('module', $module)
->withAttribute('route', new Route(
'/module/blog-example/Blog/show',
[
'module' => $module,
'controller' => 'Blog',
'action' => 'show',
]
));
$configuration = [];
$configuration['extensionName'] = $extensionName;
$configuration['pluginName'] = $pluginName;
// Feature is turned off by default. We set it here explicitly to make the tests' intention clear
$configuration['features']['enableNamespacedArgumentsForBackend'] = '0';
$configurationManager = $this->get(ConfigurationManager::class);
$configurationManager->setConfiguration($configuration);
$requestBuilder = $this->get(RequestBuilder::class);
$request = $requestBuilder->build($mainRequest);
self::assertInstanceOf(RequestInterface::class, $request);
self::assertSame('show', $request->getControllerActionName());
self::assertSame('Blog', $request->getArgument('controller'));
self::assertSame('show', $request->getArgument('action'));
}
protected function prepareServerRequest(string $url, $method = 'GET'): ServerRequestInterface
{
$request = (new ServerRequest($url, $method))
......
......@@ -99,6 +99,8 @@ class UriBuilderTest extends UnitTestCase
$requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver());
$router = new Router($requestContextFactory);
$router->addRoute('module_key', new Route('/test/Path', []));
$router->addRoute('module_key.controller_action', new Route('/test/Path/Controller/action', []));
$router->addRoute('module_key.controller2_action2', new Route('/test/Path/Controller2/action2', []));
$router->addRoute('module_key2', new Route('/test/Path2', []));
$router->addRoute('', new Route('', []));
$formProtectionFactory = $this->createMock(FormProtectionFactory::class);
......@@ -417,6 +419,38 @@ class UriBuilderTest extends UnitTestCase
self::assertSame($expectedResult, $actualResult);
}
/**
* @test
*/
public function buildBackendRespectsGivenControllerActionArguments(): void
{
$serverRequest = $this
->getRequestWithRouteAttribute()
->withAttribute('extbase', new ExtbaseRequestParameters());
$request = new Request($serverRequest);
$this->uriBuilder->setRequest($request);
$this->uriBuilder->setArguments(['controller' => 'controller', 'action' => 'action']);
$expectedResult = '/typo3/test/Path/Controller/action?token=dummyToken';
$actualResult = $this->uriBuilder->buildBackendUri();
self::assertSame($expectedResult, $actualResult);
}
/**
* @test
*/
public function buildBackendOverwritesSubRouteIdentifierControllerActionArguments(): void
{
$serverRequest = $this
->getRequestWithRouteAttribute('module_key.controller_action')
->withAttribute('extbase', new ExtbaseRequestParameters());
$request = new Request($serverRequest);
$this->uriBuilder->setRequest($request);
$this->uriBuilder->setArguments(['controller' => 'controller2', 'action' => 'action2']);
$expectedResult = '/typo3/test/Path/Controller2/action2?token=dummyToken';
$actualResult = $this->uriBuilder->buildBackendUri();
self::assertSame($expectedResult, $actualResult);
}
/**
* @test
*/
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment