Skip to content

Commit

Permalink
[TASK] Define explicit routes for Extbase Backend Modules
Browse files Browse the repository at this point in the history
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 <bo@cedev.de>
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Stefan Bürk <stefan@buerk.tech>
Reviewed-by: Stefan Bürk <stefan@buerk.tech>
Tested-by: core-ci <typo3@b13.com>
Tested-by: Oliver Bartsch <bo@cedev.de>
Tested-by: Benni Mack <benni@typo3.org>
  • Loading branch information
bmack authored and o-ba committed Jan 25, 2023
1 parent e9c190b commit 8360074
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 10 deletions.
36 changes: 27 additions & 9 deletions typo3/sysext/backend/Classes/Module/ExtbaseModule.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,33 @@ public function getControllerActions(): array

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
15 changes: 14 additions & 1 deletion typo3/sysext/extbase/Classes/Mvc/Web/RequestBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,15 +147,24 @@ protected function loadDefaultValues(array $configuration = []): void
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);
Expand All @@ -168,6 +177,10 @@ public function build(ServerRequestInterface $mainRequest)
$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();
Expand Down
15 changes: 15 additions & 0 deletions typo3/sysext/extbase/Classes/Mvc/Web/Routing/UriBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,21 @@ public function buildBackendUri(): string
$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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
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;
Expand Down Expand Up @@ -833,6 +834,55 @@ public function silentlyIgnoreInvalidParameterAndUseDefaultAction(): void
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))
Expand Down
34 changes: 34 additions & 0 deletions typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ protected function setUp(): void
$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);
Expand Down Expand Up @@ -417,6 +419,38 @@ public function buildBackendUriCreatesAbsoluteUrisIfSpecified(): void
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
*/
Expand Down

0 comments on commit 8360074

Please sign in to comment.