From 02198ea257b9f03f910b3b120392ab63fe792a8b Mon Sep 17 00:00:00 2001 From: Susanne Moog <look@susi.dev> Date: Thu, 3 Mar 2022 15:03:51 +0100 Subject: [PATCH] [FEATURE] Allow non-namespaced arguments in extbase backend modules Extbase modules traditionally use the plugin or module namespace to prefix get parameters and map arguments to plugins. In the frontend context, this makes sense, as multiple plugins may reside on a page. In the backend, however, an extbase module is responsible for rendering a complete view. Therefore the namespacing of arguments can be disabled, making URLs easier to read, more in line with non-extbase modules and allowing extbase modules to directly access outside information like the "id" parameter handed over by the page tree for example. To allow extbase modules to configure this behaviour, a feature flag can be set in the module configuration turning the namespacing off or on. Resolves: #97096 Releases: main Change-Id: Icffcab4c971a961d72749bee76ee6a3f2e079487 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/73798 Tested-by: Oliver Bartsch <bo@cedev.de> Tested-by: core-ci <typo3@b13.com> Tested-by: Benni Mack <benni@typo3.org> Reviewed-by: Oliver Bartsch <bo@cedev.de> Reviewed-by: Benni Mack <benni@typo3.org> --- .../TypeScript/extensionmanager/main.ts | 4 +- .../TypeScript/extensionmanager/repository.ts | 4 +- .../TypeScript/extensionmanager/update.ts | 2 +- .../SystemInformationController.php | 4 +- .../Controller/BackendUserController.php | 12 ++--- ...spacedArgumentsInExtbaseBackendModules.rst | 46 +++++++++++++++++++ .../Extensionmanager/GetExtensionsCest.php | 8 ++-- .../Widgets/Provider/SysLogButtonProvider.php | 2 +- .../Classes/Mvc/Web/RequestBuilder.php | 20 ++++++-- .../Classes/Mvc/Web/Routing/UriBuilder.php | 2 + .../ActionControllerArgumentTest.php | 2 +- .../Functional/Mvc/Web/RequestBuilderTest.php | 43 ++++++++--------- .../Unit/Mvc/Web/Routing/UriBuilderTest.php | 30 +++++++----- .../extbase/ext_typoscript_setup.typoscript | 2 + .../UploadExtensionFileController.php | 3 +- .../Report/ExtensionComposerStatus.php | 13 ++---- .../DownloadExtensionViewHelper.php | 10 +--- .../Resources/Public/JavaScript/main.js | 2 +- .../Resources/Public/JavaScript/repository.js | 2 +- .../Resources/Public/JavaScript/update.js | 2 +- .../Classes/ViewHelpers/FormViewHelper.php | 17 +++++++ .../ViewHelpers/FormViewHelperTest.php | 23 +++++++--- typo3/sysext/form/ext_localconf.php | 1 + .../Controller/InstallerController.php | 4 +- 24 files changed, 168 insertions(+), 90 deletions(-) create mode 100644 typo3/sysext/core/Documentation/Changelog/12.0/Feature-97096-Non-namespacedArgumentsInExtbaseBackendModules.rst diff --git a/Build/Sources/TypeScript/extensionmanager/main.ts b/Build/Sources/TypeScript/extensionmanager/main.ts index 30cf858c4ce4..a8f7dd4aa429 100644 --- a/Build/Sources/TypeScript/extensionmanager/main.ts +++ b/Build/Sources/TypeScript/extensionmanager/main.ts @@ -217,9 +217,7 @@ class ExtensionManager { trigger: (): void => { NProgress.start(); new AjaxRequest(data.url).withQueryArguments({ - tx_extensionmanager_tools_extensionmanagerextensionmanager: { - version: $('input:radio[name=version]:checked', Modal.currentModal).val(), - } + version: $('input:radio[name=version]:checked', Modal.currentModal).val(), }).get().finally((): void => { location.reload(); }); diff --git a/Build/Sources/TypeScript/extensionmanager/repository.ts b/Build/Sources/TypeScript/extensionmanager/repository.ts index 10c2319f8e6a..a9015af65d61 100644 --- a/Build/Sources/TypeScript/extensionmanager/repository.ts +++ b/Build/Sources/TypeScript/extensionmanager/repository.ts @@ -74,7 +74,7 @@ class Repository { btnClass: 'btn-info', trigger: (): void => { this.getResolveDependenciesAndInstallResult(data.url - + '&tx_extensionmanager_tools_extensionmanagerextensionmanager[downloadPath]=' + this.downloadPath); + + '&downloadPath=' + this.downloadPath); Modal.dismiss(); }, }, @@ -84,7 +84,7 @@ class Repository { Notification.error(data.title, data.message, 15); } else { this.getResolveDependenciesAndInstallResult(data.url - + '&tx_extensionmanager_tools_extensionmanagerextensionmanager[downloadPath]=' + this.downloadPath); + + '&downloadPath=' + this.downloadPath); } } } diff --git a/Build/Sources/TypeScript/extensionmanager/update.ts b/Build/Sources/TypeScript/extensionmanager/update.ts index 45b5d168b92b..40b28ebcf35f 100644 --- a/Build/Sources/TypeScript/extensionmanager/update.ts +++ b/Build/Sources/TypeScript/extensionmanager/update.ts @@ -53,7 +53,7 @@ class ExtensionManagerUpdate { private updateFromTer(url: string, forceUpdate: boolean): void { if (forceUpdate) { - url = url + '&tx_extensionmanager_tools_extensionmanagerextensionmanager%5BforceUpdateCheck%5D=1'; + url = url + '&forceUpdateCheck=1'; } // Hide triggers for TER update diff --git a/typo3/sysext/belog/Classes/Controller/SystemInformationController.php b/typo3/sysext/belog/Classes/Controller/SystemInformationController.php index df3d970ee846..4abad0a300ee 100644 --- a/typo3/sysext/belog/Classes/Controller/SystemInformationController.php +++ b/typo3/sysext/belog/Classes/Controller/SystemInformationController.php @@ -75,13 +75,13 @@ final class SystemInformationController $count, (string)$uriBuilder->buildUriFromRoute( 'system_BelogLog', - ['tx_belog_system_beloglog' => ['constraint' => ['channel' => 'php']]] + ['constraint' => ['channel' => 'php']] ) ), InformationStatus::STATUS_ERROR, $count, 'system_BelogLog', - http_build_query(['tx_belog_system_beloglog' => ['constraint' => ['channel' => 'php']]]) + http_build_query(['constraint' => ['channel' => 'php']]) ); } } diff --git a/typo3/sysext/beuser/Classes/Controller/BackendUserController.php b/typo3/sysext/beuser/Classes/Controller/BackendUserController.php index 3b39f248aaeb..270ec4f5ed6c 100644 --- a/typo3/sysext/beuser/Classes/Controller/BackendUserController.php +++ b/typo3/sysext/beuser/Classes/Controller/BackendUserController.php @@ -175,7 +175,7 @@ class BackendUserController extends ActionController $buttonBar->addButton($addUserButton); $shortcutButton = $buttonBar->makeShortcutButton() ->setRouteIdentifier('system_BeuserTxBeuser') - ->setArguments(['tx_beuser_system_beusertxbeuser' => ['action' => 'index']]) + ->setArguments(['action' => 'index']) ->setDisplayName(LocalizationUtility::translate('backendUsers', 'beuser')); $buttonBar->addButton($shortcutButton, ButtonBar::BUTTON_POSITION_RIGHT); @@ -209,7 +209,7 @@ class BackendUserController extends ActionController $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar(); $shortcutButton = $buttonBar->makeShortcutButton() ->setRouteIdentifier('system_BeuserTxBeuser') - ->setArguments(['tx_beuser_system_beusertxbeuser' => ['action' => 'online']]) + ->setArguments(['action' => 'online']) ->setDisplayName(LocalizationUtility::translate('onlineUsers', 'beuser')); $buttonBar->addButton($shortcutButton, ButtonBar::BUTTON_POSITION_RIGHT); @@ -246,7 +246,7 @@ class BackendUserController extends ActionController $buttonBar->addButton($addUserButton); $shortcutButton = $buttonBar->makeShortcutButton() ->setRouteIdentifier('system_BeuserTxBeuser') - ->setArguments(['tx_beuser_system_beusertxbeuser' => ['action' => 'show', 'uid' => $uid]]) + ->setArguments(['action' => 'show', 'uid' => $uid]) ->setDisplayName(LocalizationUtility::translate('backendUser', 'beuser') . ': ' . (string)$data['user']['username']); $buttonBar->addButton($shortcutButton, ButtonBar::BUTTON_POSITION_RIGHT); @@ -284,7 +284,7 @@ class BackendUserController extends ActionController $buttonBar->addButton($backButton); $shortcutButton = $buttonBar->makeShortcutButton() ->setRouteIdentifier('system_BeuserTxBeuser') - ->setArguments(['tx_beuser_system_beusertxbeuser' => ['action' => 'compare']]) + ->setArguments(['action' => 'compare']) ->setDisplayName(LocalizationUtility::translate('compareUsers', 'beuser')); $buttonBar->addButton($shortcutButton, ButtonBar::BUTTON_POSITION_RIGHT); @@ -406,7 +406,7 @@ class BackendUserController extends ActionController $buttonBar->addButton($addGroupButton); $shortcutButton = $buttonBar->makeShortcutButton() ->setRouteIdentifier('system_BeuserTxBeuser') - ->setArguments(['tx_beuser_system_beusertxbeuser' => ['action' => 'groups']]) + ->setArguments(['action' => 'groups']) ->setDisplayName(LocalizationUtility::translate('backendUserGroupsMenu', 'beuser')); $buttonBar->addButton($shortcutButton, ButtonBar::BUTTON_POSITION_RIGHT); @@ -438,7 +438,7 @@ class BackendUserController extends ActionController $buttonBar->addButton($backButton); $shortcutButton = $buttonBar->makeShortcutButton() ->setRouteIdentifier('system_BeuserTxBeuser') - ->setArguments(['tx_beuser_system_beusertxbeuser' => ['action' => 'compareGroups']]) + ->setArguments(['action' => 'compareGroups']) ->setDisplayName(LocalizationUtility::translate('compareBackendUsersGroups', 'beuser')); $buttonBar->addButton($shortcutButton, ButtonBar::BUTTON_POSITION_RIGHT); diff --git a/typo3/sysext/core/Documentation/Changelog/12.0/Feature-97096-Non-namespacedArgumentsInExtbaseBackendModules.rst b/typo3/sysext/core/Documentation/Changelog/12.0/Feature-97096-Non-namespacedArgumentsInExtbaseBackendModules.rst new file mode 100644 index 000000000000..fed7e26ac037 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/12.0/Feature-97096-Non-namespacedArgumentsInExtbaseBackendModules.rst @@ -0,0 +1,46 @@ +.. include:: ../../Includes.txt + +===================================================================== +Feature: #97096 - Non-namespaced arguments in extbase backend modules +===================================================================== + +See :issue:`97096` + +Description +=========== + +Extbase plugins and backend modules traditionally use the plugin / module +namespace to prefix their get parameters and form data. In the frontend context, +this makes sense, as multiple plugins may reside on a page. In the backend, +however, an extbase module is responsible for rendering a complete view. +Therefore, the namespacing of arguments has been disabled, making URLs easier +to read, more in line with non-extbase modules and allowing extbase modules +to directly access outside information like the `id` parameter handed over +by the page tree for example. + +To allow extbase modules to configure this behaviour, the extbase feature +flag :typoscript:`enableNamespacedArgumentsForBackend` can be set in the module +configuration, turning the namespacing off or on. + +Impact +====== + +Extbase will by default build and react to backend module links without paying +attention to the namespace of the parameters. + +A link may look like this: + +`https://example.org/typo3/module/web/BeuserTxBeuser?action=groups&controller=BackendUser` + +If a module explicitly wants to keep using the namespaced version of the arguments, +the feature flag can be set: + +.. code-block:: typoscript + + module.tx_extensionmanager { + features { + enableNamespacedArgumentsForBackend = 1 + } + } + +.. index:: Backend, PHP-API, ext:extbase diff --git a/typo3/sysext/core/Tests/Acceptance/Application/Extensionmanager/GetExtensionsCest.php b/typo3/sysext/core/Tests/Acceptance/Application/Extensionmanager/GetExtensionsCest.php index 0e94d1756d19..095b4afcfde9 100644 --- a/typo3/sysext/core/Tests/Acceptance/Application/Extensionmanager/GetExtensionsCest.php +++ b/typo3/sysext/core/Tests/Acceptance/Application/Extensionmanager/GetExtensionsCest.php @@ -66,7 +66,7 @@ class GetExtensionsCest */ public function checkSearchFilterListFindsExtensionKey(ApplicationTester $I): void { - $I->fillField('input[name="tx_extensionmanager_tools_extensionmanagerextensionmanager[search]"]', 'superext'); + $I->fillField('input[name="search"]', 'superext'); $I->click('Go'); // @todo do something about the double loading of the table, it is rendered twice (not double, but once, then retrieve extension list loader, then second time) $I->waitForElementVisible('#terSearchTable'); @@ -77,8 +77,8 @@ class GetExtensionsCest $I->amGoingTo('search extension needed ext and submit with enter'); - $I->fillField('input[name="tx_extensionmanager_tools_extensionmanagerextensionmanager[search]"]', 'neededext'); - $I->pressKey('input[name="tx_extensionmanager_tools_extensionmanagerextensionmanager[search]"]', WebDriverKeys::ENTER); + $I->fillField('input[name="search"]', 'neededext'); + $I->pressKey('input[name="search"]', WebDriverKeys::ENTER); $I->waitForElementVisible('#terSearchTable'); $I->wait(3); $I->waitForElementNotVisible('div#nprogess'); @@ -91,7 +91,7 @@ class GetExtensionsCest */ public function checkSearchFilterListFindsPartOfExtensionKey(ApplicationTester $I): void { - $I->fillField('input[name="tx_extensionmanager_tools_extensionmanagerextensionmanager[search]"]', 'ext'); + $I->fillField('input[name="search"]', 'ext'); $I->click('Go'); $I->waitForElementVisible('#terSearchTable'); $I->seeNumberOfElements('#terSearchTable tbody tr', 2); diff --git a/typo3/sysext/dashboard/Classes/Widgets/Provider/SysLogButtonProvider.php b/typo3/sysext/dashboard/Classes/Widgets/Provider/SysLogButtonProvider.php index ef3d509bf2fc..66f7594cb5ee 100644 --- a/typo3/sysext/dashboard/Classes/Widgets/Provider/SysLogButtonProvider.php +++ b/typo3/sysext/dashboard/Classes/Widgets/Provider/SysLogButtonProvider.php @@ -67,7 +67,7 @@ class SysLogButtonProvider implements ButtonProviderInterface, ElementAttributes return [ 'data-dispatch-action' => 'TYPO3.ModuleMenu.showModule', 'data-dispatch-args-list' => 'system_BelogLog,&' - . http_build_query(['tx_belog_system_beloglog' => ['constraint' => ['level' => 'notice']]]), + . http_build_query(['constraint' => ['channel' => 'php']]), ]; } } diff --git a/typo3/sysext/extbase/Classes/Mvc/Web/RequestBuilder.php b/typo3/sysext/extbase/Classes/Mvc/Web/RequestBuilder.php index 1dd321433b7d..9e3aebfa5a7a 100644 --- a/typo3/sysext/extbase/Classes/Mvc/Web/RequestBuilder.php +++ b/typo3/sysext/extbase/Classes/Mvc/Web/RequestBuilder.php @@ -17,7 +17,6 @@ namespace TYPO3\CMS\Extbase\Mvc\Web; use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Backend\Module\ExtbaseModule; -use TYPO3\CMS\Backend\Routing\Route; use TYPO3\CMS\Core\Error\Http\PageNotFoundException; use TYPO3\CMS\Core\Routing\PageArguments; use TYPO3\CMS\Core\SingletonInterface; @@ -161,30 +160,41 @@ class RequestBuilder implements SingletonInterface public function build(ServerRequestInterface $mainRequest) { $configuration = []; + // 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 $module = $mainRequest->getAttribute('module'); if ($module instanceof ExtbaseModule) { $configuration = [ 'controllerConfiguration' => $module->getControllerActions(), ]; + $useArgumentsWithoutNamespace = !$this->configurationManager->isFeatureEnabled('enableNamespacedArgumentsForBackend'); } $this->loadDefaultValues($configuration); $pluginNamespace = $this->extensionService->getPluginNamespace($this->extensionName, $this->pluginName); $queryArguments = $mainRequest->getAttribute('routing'); - if ($queryArguments instanceof PageArguments) { + if ($useArgumentsWithoutNamespace) { + $parameters = $mainRequest->getQueryParams(); + } elseif ($queryArguments instanceof PageArguments) { $parameters = $queryArguments->get($pluginNamespace) ?? []; } else { $parameters = $mainRequest->getQueryParams()[$pluginNamespace] ?? []; } $parameters = is_array($parameters) ? $parameters : []; if ($mainRequest->getMethod() === 'POST') { - $postParameters = $mainRequest->getParsedBody()[$pluginNamespace] ?? []; + if ($useArgumentsWithoutNamespace) { + $postParameters = $mainRequest->getParsedBody(); + } else { + $postParameters = $mainRequest->getParsedBody()[$pluginNamespace] ?? []; + } $postParameters = is_array($postParameters) ? $postParameters : []; - ArrayUtility::mergeRecursiveWithOverrule($parameters, $postParameters); + $parameters = array_replace_recursive($parameters, $postParameters); } $files = $this->untangleFilesArray($_FILES); - if (is_array($files[$pluginNamespace] ?? null)) { + if ($useArgumentsWithoutNamespace) { + $parameters = array_replace_recursive($parameters, $files); + } elseif (is_array($files[$pluginNamespace] ?? null)) { $parameters = array_replace_recursive($parameters, $files[$pluginNamespace]); } diff --git a/typo3/sysext/extbase/Classes/Mvc/Web/Routing/UriBuilder.php b/typo3/sysext/extbase/Classes/Mvc/Web/Routing/UriBuilder.php index 5d7f3f39a1d2..e656be6e2349 100644 --- a/typo3/sysext/extbase/Classes/Mvc/Web/Routing/UriBuilder.php +++ b/typo3/sysext/extbase/Classes/Mvc/Web/Routing/UriBuilder.php @@ -550,6 +550,8 @@ class UriBuilder } if ($this->argumentPrefix !== null) { $prefixedControllerArguments = [$this->argumentPrefix => $controllerArguments]; + } elseif (!$isFrontend && !$this->configurationManager->isFeatureEnabled('enableNamespacedArgumentsForBackend')) { + $prefixedControllerArguments = $controllerArguments; } else { $pluginNamespace = $this->extensionService->getPluginNamespace($extensionName, $pluginName); $prefixedControllerArguments = [$pluginNamespace => $controllerArguments]; diff --git a/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/ActionControllerArgumentTest.php b/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/ActionControllerArgumentTest.php index 6e5e381385ad..c7f97aac8544 100644 --- a/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/ActionControllerArgumentTest.php +++ b/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/ActionControllerArgumentTest.php @@ -48,7 +48,7 @@ class ActionControllerArgumentTest extends FunctionalTestCase parent::setUp(); $this->pluginNamespacePrefix = strtolower('tx_' . $this->extensionName . '_' . $this->pluginName); $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest()) - ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE); } public function validationErrorReturnsToForwardedPreviousActionDataProvider(): array diff --git a/typo3/sysext/extbase/Tests/Functional/Mvc/Web/RequestBuilderTest.php b/typo3/sysext/extbase/Tests/Functional/Mvc/Web/RequestBuilderTest.php index c274b8e53bba..2a5b4a0c4c12 100644 --- a/typo3/sysext/extbase/Tests/Functional/Mvc/Web/RequestBuilderTest.php +++ b/typo3/sysext/extbase/Tests/Functional/Mvc/Web/RequestBuilderTest.php @@ -127,7 +127,7 @@ class RequestBuilderTest extends FunctionalTestCase $configurationManager->setConfiguration($configuration); $mainRequest = $this->prepareServerRequest('https://example.com/'); - $mainRequest = $mainRequest->withQueryParams(['tx_blog_example_blog' => ['format' => 'json']]); + $mainRequest = $mainRequest->withQueryParams(['format' => 'json']); $mainRequest = $mainRequest->withAttribute('module', $module); $requestBuilder = $this->get(RequestBuilder::class); $request = $requestBuilder->build($mainRequest); @@ -195,6 +195,7 @@ class RequestBuilderTest extends FunctionalTestCase $configuration = []; $configuration['extensionName'] = $extensionName; $configuration['pluginName'] = $pluginName; + $configuration['features']['enableNamespacedArgumentsForBackend'] = 1; $configurationManager = $this->get(ConfigurationManager::class); $configurationManager->setConfiguration($configuration); @@ -254,6 +255,7 @@ class RequestBuilderTest extends FunctionalTestCase $configuration = []; $configuration['extensionName'] = $extensionName; $configuration['pluginName'] = $pluginName; + $configuration['features']['enableNamespacedArgumentsForBackend'] = 1; $configurationManager = $this->get(ConfigurationManager::class); $configurationManager->setConfiguration($configuration); @@ -314,7 +316,7 @@ class RequestBuilderTest extends FunctionalTestCase $mainRequest = $this->prepareServerRequest('https://example.com/'); $mainRequest = $mainRequest->withAttribute('module', $module); - $mainRequest = $mainRequest->withQueryParams(['tx_blog_example_blog' => ['controller' => 'NonExistentController']]); + $mainRequest = $mainRequest->withQueryParams(['controller' => 'NonExistentController']); $requestBuilder = $this->get(RequestBuilder::class); $requestBuilder->build($mainRequest); } @@ -349,7 +351,7 @@ class RequestBuilderTest extends FunctionalTestCase $configurationManager->setConfiguration($configuration); $mainRequest = $this->prepareServerRequest('https://example.com/'); - $mainRequest = $mainRequest->withQueryParams(['tx_blog_example_blog' => ['controller' => 'NonExistentController']]); + $mainRequest = $mainRequest->withQueryParams(['controller' => 'NonExistentController']); $mainRequest = $mainRequest->withAttribute('module', $module); $requestBuilder = $this->get(RequestBuilder::class); $requestBuilder->build($mainRequest); @@ -406,7 +408,7 @@ class RequestBuilderTest extends FunctionalTestCase $mainRequest = $this->prepareServerRequest('https://example.com/'); $mainRequest = $mainRequest->withAttribute('module', $module); - $mainRequest = $mainRequest->withQueryParams(['tx_blog_example_blog' => ['controller' => 'NonExistentController']]); + $mainRequest = $mainRequest->withQueryParams(['controller' => 'NonExistentController']); $requestBuilder = $this->get(RequestBuilder::class); $request = $requestBuilder->build($mainRequest); @@ -441,7 +443,7 @@ class RequestBuilderTest extends FunctionalTestCase $mainRequest = $this->prepareServerRequest('https://example.com/'); $mainRequest = $mainRequest->withAttribute('module', $module); - $mainRequest = $mainRequest->withQueryParams(['tx_blog_example_blog' => ['controller' => 'User']]); + $mainRequest = $mainRequest->withQueryParams(['controller' => 'User']); $requestBuilder = $this->get(RequestBuilder::class); $request = $requestBuilder->build($mainRequest); @@ -479,7 +481,7 @@ class RequestBuilderTest extends FunctionalTestCase $mainRequest = $this->prepareServerRequest('https://example.com/'); $mainRequest = $mainRequest->withAttribute('module', $module); - $mainRequest = $mainRequest->withQueryParams(['tx_blog_example_blog' => ['action' => 'NonExistentAction']]); + $mainRequest = $mainRequest->withQueryParams(['action' => 'NonExistentAction']); $requestBuilder = $this->get(RequestBuilder::class); $requestBuilder->build($mainRequest); } @@ -515,7 +517,7 @@ class RequestBuilderTest extends FunctionalTestCase $mainRequest = $this->prepareServerRequest('https://example.com/'); $mainRequest = $mainRequest->withAttribute('module', $module); - $mainRequest = $mainRequest->withQueryParams(['tx_blog_example_blog' => ['action' => 'NonExistentAction']]); + $mainRequest = $mainRequest->withQueryParams(['action' => 'NonExistentAction']); $requestBuilder = $this->get(RequestBuilder::class); $requestBuilder->build($mainRequest); } @@ -581,7 +583,7 @@ class RequestBuilderTest extends FunctionalTestCase $mainRequest = $this->prepareServerRequest('https://example.com/'); $mainRequest = $mainRequest->withAttribute('module', $module); - $mainRequest = $mainRequest->withQueryParams(['tx_blog_example_blog' => ['action' => 'show']]); + $mainRequest = $mainRequest->withQueryParams(['action' => 'show']); $requestBuilder = $this->get(RequestBuilder::class); $request = $requestBuilder->build($mainRequest); @@ -630,7 +632,7 @@ class RequestBuilderTest extends FunctionalTestCase { $mainRequest = $this->prepareServerRequest('https://example.com/'); $mainRequest = $mainRequest - ->withQueryParams(['tx_blog_example_blog' => ['action' => 'show']]) + ->withQueryParams(['action' => 'show']) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); $extensionName = 'blog_example'; @@ -670,24 +672,23 @@ class RequestBuilderTest extends FunctionalTestCase $mainRequest = $this->prepareServerRequest('https://example.com/'); $mainRequest = $mainRequest ->withAttribute('routing', $pageArguments) - ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE); + + $GLOBALS['TYPO3_REQUEST'] = $mainRequest; $extensionName = 'blog_example'; $pluginName = 'blog'; - $module = ExtbaseModule::createFromConfiguration($pluginName, [ - 'packageName' => 'typo3/cms-blog-example', - 'path' => '/blog-example', - 'extensionName' => $extensionName, - 'controllerActions' => [ - BlogController::class => ['list', 'show'], - ], - ]); - $mainRequest = $mainRequest->withAttribute('module', $module); - $configuration = []; $configuration['extensionName'] = $extensionName; $configuration['pluginName'] = $pluginName; + $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$extensionName]['plugins'][$pluginName]['controllers'] = [ + BlogController::class => [ + 'className' => BlogController::class, + 'alias' => 'blog', + 'actions' => ['list', 'show'], + ], + ]; $configurationManager = $this->get(ConfigurationManager::class); $configurationManager->setConfiguration($configuration); @@ -706,7 +707,7 @@ class RequestBuilderTest extends FunctionalTestCase { $mainRequest = $this->prepareServerRequest('https://example.com/', 'POST'); $mainRequest = $mainRequest - ->withParsedBody(['tx_blog_example_blog' => ['action' => 'show']]) + ->withParsedBody(['action' => 'show']) ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); $extensionName = 'blog_example'; diff --git a/typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php b/typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php index 4e82348c200c..bd392b0167ed 100644 --- a/typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php +++ b/typo3/sysext/extbase/Tests/Unit/Mvc/Web/Routing/UriBuilderTest.php @@ -136,8 +136,7 @@ class UriBuilderTest extends UnitTestCase */ public function uriForPrefixesArgumentsWithExtensionAndPluginNameAndSetsControllerArgument(): void { - $this->mockExtensionService->expects(self::once())->method('getPluginNamespace')->willReturn('tx_someextension_someplugin'); - $expectedArguments = ['tx_someextension_someplugin' => ['foo' => 'bar', 'baz' => ['extbase' => 'fluid'], 'controller' => 'SomeController']]; + $expectedArguments =['foo' => 'bar', 'baz' => ['extbase' => 'fluid'], 'controller' => 'SomeController']; $GLOBALS['TSFE'] = null; $this->uriBuilder->uriFor(null, ['foo' => 'bar', 'baz' => ['extbase' => 'fluid']], 'SomeController', 'SomeExtension', 'SomePlugin'); self::assertEquals($expectedArguments, $this->uriBuilder->getArguments()); @@ -148,10 +147,9 @@ class UriBuilderTest extends UnitTestCase */ public function uriForRecursivelyMergesAndOverrulesControllerArgumentsWithArguments(): void { - $this->mockExtensionService->expects(self::once())->method('getPluginNamespace')->willReturn('tx_someextension_someplugin'); - $arguments = ['tx_someextension_someplugin' => ['foo' => 'bar'], 'additionalParam' => 'additionalValue']; + $arguments = ['foo' => 'bar', 'additionalParam' => 'additionalValue']; $controllerArguments = ['foo' => 'overruled', 'baz' => ['extbase' => 'fluid']]; - $expectedArguments = ['tx_someextension_someplugin' => ['foo' => 'overruled', 'baz' => ['extbase' => 'fluid'], 'controller' => 'SomeController'], 'additionalParam' => 'additionalValue']; + $expectedArguments = ['foo' => 'overruled', 'baz' => ['extbase' => 'fluid'], 'controller' => 'SomeController', 'additionalParam' => 'additionalValue']; $this->uriBuilder->setArguments($arguments); $this->uriBuilder->uriFor(null, $controllerArguments, 'SomeController', 'SomeExtension', 'SomePlugin'); self::assertEquals($expectedArguments, $this->uriBuilder->getArguments()); @@ -162,8 +160,7 @@ class UriBuilderTest extends UnitTestCase */ public function uriForOnlySetsActionArgumentIfSpecified(): void { - $this->mockExtensionService->expects(self::once())->method('getPluginNamespace')->willReturn('tx_someextension_someplugin'); - $expectedArguments = ['tx_someextension_someplugin' => ['controller' => 'SomeController']]; + $expectedArguments = ['controller' => 'SomeController']; $this->uriBuilder->uriFor(null, [], 'SomeController', 'SomeExtension', 'SomePlugin'); self::assertEquals($expectedArguments, $this->uriBuilder->getArguments()); } @@ -173,9 +170,8 @@ class UriBuilderTest extends UnitTestCase */ public function uriForSetsControllerFromRequestIfControllerIsNotSet(): void { - $this->mockExtensionService->expects(self::once())->method('getPluginNamespace')->willReturn('tx_someextension_someplugin'); $this->mockRequest->expects(self::once())->method('getControllerName')->willReturn('SomeControllerFromRequest'); - $expectedArguments = ['tx_someextension_someplugin' => ['controller' => 'SomeControllerFromRequest']]; + $expectedArguments = ['controller' => 'SomeControllerFromRequest']; $this->uriBuilder->uriFor(null, [], null, 'SomeExtension', 'SomePlugin'); self::assertEquals($expectedArguments, $this->uriBuilder->getArguments()); } @@ -185,9 +181,8 @@ class UriBuilderTest extends UnitTestCase */ public function uriForSetsExtensionNameFromRequestIfExtensionNameIsNotSet(): void { - $this->mockExtensionService->method('getPluginNamespace')->willReturn('tx_someextensionnamefromrequest_someplugin'); $this->mockRequest->expects(self::once())->method('getControllerExtensionName')->willReturn('SomeExtensionNameFromRequest'); - $expectedArguments = ['tx_someextensionnamefromrequest_someplugin' => ['controller' => 'SomeController']]; + $expectedArguments = ['controller' => 'SomeController']; $this->uriBuilder->uriFor(null, [], 'SomeController', null, 'SomePlugin'); self::assertEquals($expectedArguments, $this->uriBuilder->getArguments()); } @@ -195,14 +190,25 @@ class UriBuilderTest extends UnitTestCase /** * @test */ - public function uriForSetsPluginNameFromRequestIfPluginNameIsNotSet(): void + public function uriForSetsPluginNameFromRequestIfPluginNameIsNotSetInFrontend(): void { + $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest(new Uri('')))->withAttribute('applicationType', 1); $this->mockExtensionService->expects(self::once())->method('getPluginNamespace')->willReturn('tx_someextension_somepluginnamefromrequest'); $this->mockRequest->expects(self::once())->method('getPluginName')->willReturn('SomePluginNameFromRequest'); $expectedArguments = ['tx_someextension_somepluginnamefromrequest' => ['controller' => 'SomeController']]; $this->uriBuilder->uriFor(null, [], 'SomeController', 'SomeExtension'); self::assertEquals($expectedArguments, $this->uriBuilder->getArguments()); } + /** + * @test + */ + public function uriForSetsPluginNameFromRequestIfPluginNameIsNotSet(): void + { + $this->mockRequest->expects(self::once())->method('getPluginName')->willReturn('SomePluginNameFromRequest'); + $expectedArguments = ['controller' => 'SomeController']; + $this->uriBuilder->uriFor(null, [], 'SomeController', 'SomeExtension'); + self::assertEquals($expectedArguments, $this->uriBuilder->getArguments()); + } /** * @test diff --git a/typo3/sysext/extbase/ext_typoscript_setup.typoscript b/typo3/sysext/extbase/ext_typoscript_setup.typoscript index 85f52c01d52b..5a500d526676 100644 --- a/typo3/sysext/extbase/ext_typoscript_setup.typoscript +++ b/typo3/sysext/extbase/ext_typoscript_setup.typoscript @@ -11,5 +11,7 @@ config.tx_extbase { skipDefaultArguments = 0 # if set to 1, the enable fields are ignored in BE context ignoreAllEnableFieldsInBe = 0 + # if set to 1, the arguments of backend links will be prefixed with the backend module namespace + enableNamespacedArgumentsForBackend = 0 } } diff --git a/typo3/sysext/extensionmanager/Classes/Controller/UploadExtensionFileController.php b/typo3/sysext/extensionmanager/Classes/Controller/UploadExtensionFileController.php index 9f19ab3a20e4..bea8015bdc8d 100644 --- a/typo3/sysext/extensionmanager/Classes/Controller/UploadExtensionFileController.php +++ b/typo3/sysext/extensionmanager/Classes/Controller/UploadExtensionFileController.php @@ -89,7 +89,8 @@ class UploadExtensionFileController extends AbstractController 1444725853 ); } - $file = $_FILES['tx_extensionmanager_tools_extensionmanagerextensionmanager']; + // @todo: this is ugly! + $file = $_FILES; $fileName = pathinfo($file['name']['extensionFile'], PATHINFO_BASENAME); try { // If the file name isn't valid an error will be thrown diff --git a/typo3/sysext/extensionmanager/Classes/Report/ExtensionComposerStatus.php b/typo3/sysext/extensionmanager/Classes/Report/ExtensionComposerStatus.php index 60e0d75caa03..a60851a025c5 100644 --- a/typo3/sysext/extensionmanager/Classes/Report/ExtensionComposerStatus.php +++ b/typo3/sysext/extensionmanager/Classes/Report/ExtensionComposerStatus.php @@ -61,22 +61,17 @@ class ExtensionComposerStatus implements RequestAwareStatusProviderInterface ]; $queryParameters = [ - 'tx_extensionmanager_tools_extensionmanagerextensionmanager' => [ - 'action' => 'list', - 'controller' => 'ExtensionComposerStatus', - ], + 'action' => 'list', + 'controller' => 'ExtensionComposerStatus', ]; if ($request !== null) { - $queryParameters['tx_extensionmanager_tools_extensionmanagerextensionmanager']['returnUrl'] = + $queryParameters['returnUrl'] = $request->getAttribute('normalizedParams')->getRequestUri(); } $dispatchAction = 'TYPO3.ModuleMenu.showModule'; - $dispatchArgs = [ - 'tools_ExtensionmanagerExtensionmanager', - '&' . http_build_query($queryParameters), - ]; + $dispatchArgs = [http_build_query($queryParameters)]; foreach ($deficits as $key => $deficit) { $message = ''; diff --git a/typo3/sysext/extensionmanager/Classes/ViewHelpers/DownloadExtensionViewHelper.php b/typo3/sysext/extensionmanager/Classes/ViewHelpers/DownloadExtensionViewHelper.php index 4a23005aeaab..11ad172d89bd 100644 --- a/typo3/sysext/extensionmanager/Classes/ViewHelpers/DownloadExtensionViewHelper.php +++ b/typo3/sysext/extensionmanager/Classes/ViewHelpers/DownloadExtensionViewHelper.php @@ -60,7 +60,7 @@ final class DownloadExtensionViewHelper extends AbstractFormViewHelper foreach ($installPaths as $installPathType => $installPath) { /** @var string $installPathType */ $pathSelector .= '<li> - <input type="radio" id="' . htmlspecialchars($extension->getExtensionKey()) . '-downloadPath-' . htmlspecialchars($installPathType) . '" name="' . htmlspecialchars($this->getDefaultFieldNamePrefix()) . '[downloadPath]" class="downloadPath" value="' . htmlspecialchars($installPathType) . '" ' . ($installPathType === 'Local' ? 'checked="checked"' : '') . ' /> + <input type="radio" id="' . htmlspecialchars($extension->getExtensionKey()) . '-downloadPath-' . htmlspecialchars($installPathType) . '" name="downloadPath" class="downloadPath" value="' . htmlspecialchars($installPathType) . '" ' . ($installPathType === 'Local' ? 'checked="checked"' : '') . ' /> <label for="' . htmlspecialchars($extension->getExtensionKey()) . '-downloadPath-' . htmlspecialchars($installPathType) . '">' . htmlspecialchars($installPathType) . '</label> </li>'; } @@ -97,14 +97,6 @@ final class DownloadExtensionViewHelper extends AbstractFormViewHelper return '<div id="' . htmlspecialchars($extension->getExtensionKey()) . '-downloadFromTer" class="downloadFromTer">' . $this->tag->render() . '</div>'; } - /** - * Retrieves the field name prefix for this form - */ - protected function getDefaultFieldNamePrefix(): string - { - return $this->extensionService->getPluginNamespace('Extensionmanager', 'tools_ExtensionmanagerExtensionmanager'); - } - protected function getLanguageService(): LanguageService { return $GLOBALS['LANG']; diff --git a/typo3/sysext/extensionmanager/Resources/Public/JavaScript/main.js b/typo3/sysext/extensionmanager/Resources/Public/JavaScript/main.js index 6d7390adee51..5525e72659ff 100644 --- a/typo3/sysext/extensionmanager/Resources/Public/JavaScript/main.js +++ b/typo3/sysext/extensionmanager/Resources/Public/JavaScript/main.js @@ -10,4 +10,4 @@ * * The TYPO3 project - inspiring people to share! */ -import $ from"jquery";import NProgress from"nprogress";import Modal from"@typo3/backend/modal.js";import Tooltip from"@typo3/backend/tooltip.js";import Severity from"@typo3/backend/severity.js";import SecurityUtility from"@typo3/core/security-utility.js";import ExtensionManagerRepository from"@typo3/extensionmanager/repository.js";import ExtensionManagerUpdate from"@typo3/extensionmanager/update.js";import ExtensionManagerUploadForm from"@typo3/extensionmanager/upload-form.js";import Tablesort from"tablesort";import"tablesort.dotsep.js";import"@typo3/backend/input/clearable.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import DebounceEvent from"@typo3/core/event/debounce-event.js";import RegularEvent from"@typo3/core/event/regular-event.js";const securityUtility=new SecurityUtility;var ExtensionManagerIdentifier;!function(e){e.extensionlist="typo3-extension-list",e.searchField="#Tx_Extensionmanager_extensionkey"}(ExtensionManagerIdentifier||(ExtensionManagerIdentifier={}));class ExtensionManager{constructor(){const e=this;$(()=>{this.Update=new ExtensionManagerUpdate,this.UploadForm=new ExtensionManagerUploadForm,this.Repository=new ExtensionManagerRepository;const t=document.getElementById(ExtensionManagerIdentifier.extensionlist);let n;null!==t&&(new Tablesort(t),new RegularEvent("click",(function(t){t.preventDefault(),Modal.confirm(TYPO3.lang["extensionList.removalConfirmation.title"],TYPO3.lang["extensionList.removalConfirmation.question"],Severity.error,[{text:TYPO3.lang["button.cancel"],active:!0,btnClass:"btn-default",trigger:()=>{Modal.dismiss()}},{text:TYPO3.lang["button.remove"],btnClass:"btn-danger",trigger:()=>{e.removeExtensionFromDisk(this),Modal.dismiss()}}])})).delegateTo(t,".removeExtension")),$(document).on("click",".onClickMaskExtensionManager",()=>{NProgress.start()}).on("click","a[data-action=update-extension]",e=>{e.preventDefault(),NProgress.start(),new AjaxRequest($(e.currentTarget).attr("href")).get().then(this.updateExtension)}).on("change","input[name=unlockDependencyIgnoreButton]",e=>{$(".t3js-dependencies").toggleClass("disabled",!$(e.currentTarget).prop("checked"))}),null!==(n=document.querySelector(ExtensionManagerIdentifier.searchField))&&(new RegularEvent("submit",e=>{e.preventDefault()}).bindTo(n.closest("form")),new DebounceEvent("keyup",e=>{this.filterExtensions(e.target)},100).bindTo(n),n.clearable({onClear:e=>{this.filterExtensions(e)}})),$(document).on("click",".t3-button-action-installdistribution",()=>{NProgress.start()}),this.Repository.initDom(),this.Update.initializeEvents(),this.UploadForm.initializeEvents(),Tooltip.initialize("#typo3-extension-list [title]")})}static getUrlVars(){let e=[],t=window.location.href.slice(window.location.href.indexOf("?")+1).split("&");for(let n of t){const[t,o]=n.split("=");e.push(t),e[t]=o}return e}filterExtensions(e){const t=document.querySelectorAll("[data-filterable]"),n=[];t.forEach(e=>{const t=Array.from(e.parentElement.children);n.push(t.indexOf(e))});const o=n.map(e=>`td:nth-child(${e+1})`).join(",");document.querySelectorAll("#typo3-extension-list tbody tr").forEach(t=>{const n=t.querySelectorAll(o),i=[];n.forEach(e=>{i.push(e.textContent.trim().replace(/\s+/g," "))}),t.classList.toggle("hidden",""!==e.value&&!RegExp(e.value,"i").test(i.join(":")))})}removeExtensionFromDisk(e){NProgress.start(),new AjaxRequest(e.href).get().then(()=>{location.reload()}).finally(()=>{NProgress.done()})}async updateExtension(e){let t=0;const n=await e.resolve(),o=$("<form>");$.each(n.updateComments,(e,n)=>{const i=$("<input>").attr({type:"radio",name:"version"}).val(e);0===t&&i.attr("checked","checked"),o.append([$("<h3>").append([i," "+securityUtility.encodeHtml(e)]),$("<div>").append(n.replace(/(\r\n|\n\r|\r|\n)/g,"\n").split(/\n/).map(e=>securityUtility.encodeHtml(e)).join("<br>"))]),t++});const i=$("<div>").append([$("<h1>").text(TYPO3.lang["extensionList.updateConfirmation.title"]),$("<h2>").text(TYPO3.lang["extensionList.updateConfirmation.message"]),o]);NProgress.done(),Modal.confirm(TYPO3.lang["extensionList.updateConfirmation.questionVersionComments"],i,Severity.warning,[{text:TYPO3.lang["button.cancel"],active:!0,btnClass:"btn-default",trigger:()=>{Modal.dismiss()}},{text:TYPO3.lang["button.updateExtension"],btnClass:"btn-warning",trigger:()=>{NProgress.start(),new AjaxRequest(n.url).withQueryArguments({tx_extensionmanager_tools_extensionmanagerextensionmanager:{version:$("input:radio[name=version]:checked",Modal.currentModal).val()}}).get().finally(()=>{location.reload()}),Modal.dismiss()}}])}}let extensionManagerObject=new ExtensionManager;void 0===TYPO3.ExtensionManager&&(TYPO3.ExtensionManager=extensionManagerObject);export default extensionManagerObject; \ No newline at end of file +import $ from"jquery";import NProgress from"nprogress";import Modal from"@typo3/backend/modal.js";import Tooltip from"@typo3/backend/tooltip.js";import Severity from"@typo3/backend/severity.js";import SecurityUtility from"@typo3/core/security-utility.js";import ExtensionManagerRepository from"@typo3/extensionmanager/repository.js";import ExtensionManagerUpdate from"@typo3/extensionmanager/update.js";import ExtensionManagerUploadForm from"@typo3/extensionmanager/upload-form.js";import Tablesort from"tablesort";import"tablesort.dotsep.js";import"@typo3/backend/input/clearable.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import DebounceEvent from"@typo3/core/event/debounce-event.js";import RegularEvent from"@typo3/core/event/regular-event.js";const securityUtility=new SecurityUtility;var ExtensionManagerIdentifier;!function(e){e.extensionlist="typo3-extension-list",e.searchField="#Tx_Extensionmanager_extensionkey"}(ExtensionManagerIdentifier||(ExtensionManagerIdentifier={}));class ExtensionManager{constructor(){const e=this;$(()=>{this.Update=new ExtensionManagerUpdate,this.UploadForm=new ExtensionManagerUploadForm,this.Repository=new ExtensionManagerRepository;const t=document.getElementById(ExtensionManagerIdentifier.extensionlist);let n;null!==t&&(new Tablesort(t),new RegularEvent("click",(function(t){t.preventDefault(),Modal.confirm(TYPO3.lang["extensionList.removalConfirmation.title"],TYPO3.lang["extensionList.removalConfirmation.question"],Severity.error,[{text:TYPO3.lang["button.cancel"],active:!0,btnClass:"btn-default",trigger:()=>{Modal.dismiss()}},{text:TYPO3.lang["button.remove"],btnClass:"btn-danger",trigger:()=>{e.removeExtensionFromDisk(this),Modal.dismiss()}}])})).delegateTo(t,".removeExtension")),$(document).on("click",".onClickMaskExtensionManager",()=>{NProgress.start()}).on("click","a[data-action=update-extension]",e=>{e.preventDefault(),NProgress.start(),new AjaxRequest($(e.currentTarget).attr("href")).get().then(this.updateExtension)}).on("change","input[name=unlockDependencyIgnoreButton]",e=>{$(".t3js-dependencies").toggleClass("disabled",!$(e.currentTarget).prop("checked"))}),null!==(n=document.querySelector(ExtensionManagerIdentifier.searchField))&&(new RegularEvent("submit",e=>{e.preventDefault()}).bindTo(n.closest("form")),new DebounceEvent("keyup",e=>{this.filterExtensions(e.target)},100).bindTo(n),n.clearable({onClear:e=>{this.filterExtensions(e)}})),$(document).on("click",".t3-button-action-installdistribution",()=>{NProgress.start()}),this.Repository.initDom(),this.Update.initializeEvents(),this.UploadForm.initializeEvents(),Tooltip.initialize("#typo3-extension-list [title]")})}static getUrlVars(){let e=[],t=window.location.href.slice(window.location.href.indexOf("?")+1).split("&");for(let n of t){const[t,o]=n.split("=");e.push(t),e[t]=o}return e}filterExtensions(e){const t=document.querySelectorAll("[data-filterable]"),n=[];t.forEach(e=>{const t=Array.from(e.parentElement.children);n.push(t.indexOf(e))});const o=n.map(e=>`td:nth-child(${e+1})`).join(",");document.querySelectorAll("#typo3-extension-list tbody tr").forEach(t=>{const n=t.querySelectorAll(o),i=[];n.forEach(e=>{i.push(e.textContent.trim().replace(/\s+/g," "))}),t.classList.toggle("hidden",""!==e.value&&!RegExp(e.value,"i").test(i.join(":")))})}removeExtensionFromDisk(e){NProgress.start(),new AjaxRequest(e.href).get().then(()=>{location.reload()}).finally(()=>{NProgress.done()})}async updateExtension(e){let t=0;const n=await e.resolve(),o=$("<form>");$.each(n.updateComments,(e,n)=>{const i=$("<input>").attr({type:"radio",name:"version"}).val(e);0===t&&i.attr("checked","checked"),o.append([$("<h3>").append([i," "+securityUtility.encodeHtml(e)]),$("<div>").append(n.replace(/(\r\n|\n\r|\r|\n)/g,"\n").split(/\n/).map(e=>securityUtility.encodeHtml(e)).join("<br>"))]),t++});const i=$("<div>").append([$("<h1>").text(TYPO3.lang["extensionList.updateConfirmation.title"]),$("<h2>").text(TYPO3.lang["extensionList.updateConfirmation.message"]),o]);NProgress.done(),Modal.confirm(TYPO3.lang["extensionList.updateConfirmation.questionVersionComments"],i,Severity.warning,[{text:TYPO3.lang["button.cancel"],active:!0,btnClass:"btn-default",trigger:()=>{Modal.dismiss()}},{text:TYPO3.lang["button.updateExtension"],btnClass:"btn-warning",trigger:()=>{NProgress.start(),new AjaxRequest(n.url).withQueryArguments({version:$("input:radio[name=version]:checked",Modal.currentModal).val()}).get().finally(()=>{location.reload()}),Modal.dismiss()}}])}}let extensionManagerObject=new ExtensionManager;void 0===TYPO3.ExtensionManager&&(TYPO3.ExtensionManager=extensionManagerObject);export default extensionManagerObject; \ No newline at end of file diff --git a/typo3/sysext/extensionmanager/Resources/Public/JavaScript/repository.js b/typo3/sysext/extensionmanager/Resources/Public/JavaScript/repository.js index adc3e0f7a45e..6459e97ff5a3 100644 --- a/typo3/sysext/extensionmanager/Resources/Public/JavaScript/repository.js +++ b/typo3/sysext/extensionmanager/Resources/Public/JavaScript/repository.js @@ -10,4 +10,4 @@ * * The TYPO3 project - inspiring people to share! */ -import $ from"jquery";import NProgress from"nprogress";import Modal from"@typo3/backend/modal.js";import Notification from"@typo3/backend/notification.js";import Severity from"@typo3/backend/severity.js";import Tablesort from"tablesort";import"@typo3/backend/input/clearable.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import RegularEvent from"@typo3/core/event/regular-event.js";class Repository{constructor(){this.downloadPath="",this.getDependencies=async e=>{const t=await e.resolve();NProgress.done(),t.hasDependencies?Modal.confirm(t.title,$(t.message),Severity.info,[{text:TYPO3.lang["button.cancel"],active:!0,btnClass:"btn-default",trigger:()=>{Modal.dismiss()}},{text:TYPO3.lang["button.resolveDependencies"],btnClass:"btn-info",trigger:()=>{this.getResolveDependenciesAndInstallResult(t.url+"&tx_extensionmanager_tools_extensionmanagerextensionmanager[downloadPath]="+this.downloadPath),Modal.dismiss()}}]):t.hasErrors?Notification.error(t.title,t.message,15):this.getResolveDependenciesAndInstallResult(t.url+"&tx_extensionmanager_tools_extensionmanagerextensionmanager[downloadPath]="+this.downloadPath)}}initDom(){NProgress.configure({parent:".module-loading-indicator",showSpinner:!1});const e=document.getElementById("terVersionTable"),t=document.getElementById("terSearchTable");null!==e&&new Tablesort(e),null!==t&&new Tablesort(t),this.bindDownload(),this.bindSearchFieldResetter()}bindDownload(){const e=this;new RegularEvent("click",(function(t){t.preventDefault();const n=this.closest("form"),o=n.dataset.href;e.downloadPath=n.querySelector("input.downloadPath:checked").value,NProgress.start(),new AjaxRequest(o).get().then(e.getDependencies)})).delegateTo(document,".downloadFromTer form.download button[type=submit]")}getResolveDependenciesAndInstallResult(e){NProgress.start(),new AjaxRequest(e).get().then(async e=>{const t=await e.raw().json();if(t.errorCount>0)Modal.confirm(t.errorTitle,$(t.errorMessage),Severity.error,[{text:TYPO3.lang["button.cancel"],active:!0,btnClass:"btn-default",trigger:()=>{Modal.dismiss()}},{text:TYPO3.lang["button.resolveDependenciesIgnore"],btnClass:"btn-danger disabled t3js-dependencies",trigger:e=>{$(e.currentTarget).hasClass("disabled")||(this.getResolveDependenciesAndInstallResult(t.skipDependencyUri),Modal.dismiss())}}]),Modal.currentModal.on("shown.bs.modal",()=>{const e=Modal.currentModal.find(".t3js-dependencies");$('input[name="unlockDependencyIgnoreButton"]',Modal.currentModal).on("change",t=>{e.toggleClass("disabled",!$(t.currentTarget).prop("checked"))})});else{let e=TYPO3.lang["extensionList.dependenciesResolveDownloadSuccess.message"+t.installationTypeLanguageKey].replace(/\{0\}/g,t.extension);e+="\n"+TYPO3.lang["extensionList.dependenciesResolveDownloadSuccess.header"]+": ",$.each(t.result,(t,n)=>{e+="\n\n"+TYPO3.lang["extensionList.dependenciesResolveDownloadSuccess.item"]+" "+t+": ",$.each(n,t=>{e+="\n* "+t})}),Notification.info(TYPO3.lang["extensionList.dependenciesResolveFlashMessage.title"+t.installationTypeLanguageKey].replace(/\{0\}/g,t.extension),e,15),top.TYPO3.ModuleMenu.App.refreshMenu()}}).finally(()=>{NProgress.done()})}bindSearchFieldResetter(){let e;if(null!==(e=document.querySelector('.typo3-extensionmanager-searchTerForm input[type="text"]'))){const t=""!==e.value;e.clearable({onClear:e=>{t&&e.closest("form").submit()}})}}}export default Repository; \ No newline at end of file +import $ from"jquery";import NProgress from"nprogress";import Modal from"@typo3/backend/modal.js";import Notification from"@typo3/backend/notification.js";import Severity from"@typo3/backend/severity.js";import Tablesort from"tablesort";import"@typo3/backend/input/clearable.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import RegularEvent from"@typo3/core/event/regular-event.js";class Repository{constructor(){this.downloadPath="",this.getDependencies=async e=>{const t=await e.resolve();NProgress.done(),t.hasDependencies?Modal.confirm(t.title,$(t.message),Severity.info,[{text:TYPO3.lang["button.cancel"],active:!0,btnClass:"btn-default",trigger:()=>{Modal.dismiss()}},{text:TYPO3.lang["button.resolveDependencies"],btnClass:"btn-info",trigger:()=>{this.getResolveDependenciesAndInstallResult(t.url+"&downloadPath="+this.downloadPath),Modal.dismiss()}}]):t.hasErrors?Notification.error(t.title,t.message,15):this.getResolveDependenciesAndInstallResult(t.url+"&downloadPath="+this.downloadPath)}}initDom(){NProgress.configure({parent:".module-loading-indicator",showSpinner:!1});const e=document.getElementById("terVersionTable"),t=document.getElementById("terSearchTable");null!==e&&new Tablesort(e),null!==t&&new Tablesort(t),this.bindDownload(),this.bindSearchFieldResetter()}bindDownload(){const e=this;new RegularEvent("click",(function(t){t.preventDefault();const n=this.closest("form"),o=n.dataset.href;e.downloadPath=n.querySelector("input.downloadPath:checked").value,NProgress.start(),new AjaxRequest(o).get().then(e.getDependencies)})).delegateTo(document,".downloadFromTer form.download button[type=submit]")}getResolveDependenciesAndInstallResult(e){NProgress.start(),new AjaxRequest(e).get().then(async e=>{const t=await e.raw().json();if(t.errorCount>0)Modal.confirm(t.errorTitle,$(t.errorMessage),Severity.error,[{text:TYPO3.lang["button.cancel"],active:!0,btnClass:"btn-default",trigger:()=>{Modal.dismiss()}},{text:TYPO3.lang["button.resolveDependenciesIgnore"],btnClass:"btn-danger disabled t3js-dependencies",trigger:e=>{$(e.currentTarget).hasClass("disabled")||(this.getResolveDependenciesAndInstallResult(t.skipDependencyUri),Modal.dismiss())}}]),Modal.currentModal.on("shown.bs.modal",()=>{const e=Modal.currentModal.find(".t3js-dependencies");$('input[name="unlockDependencyIgnoreButton"]',Modal.currentModal).on("change",t=>{e.toggleClass("disabled",!$(t.currentTarget).prop("checked"))})});else{let e=TYPO3.lang["extensionList.dependenciesResolveDownloadSuccess.message"+t.installationTypeLanguageKey].replace(/\{0\}/g,t.extension);e+="\n"+TYPO3.lang["extensionList.dependenciesResolveDownloadSuccess.header"]+": ",$.each(t.result,(t,n)=>{e+="\n\n"+TYPO3.lang["extensionList.dependenciesResolveDownloadSuccess.item"]+" "+t+": ",$.each(n,t=>{e+="\n* "+t})}),Notification.info(TYPO3.lang["extensionList.dependenciesResolveFlashMessage.title"+t.installationTypeLanguageKey].replace(/\{0\}/g,t.extension),e,15),top.TYPO3.ModuleMenu.App.refreshMenu()}}).finally(()=>{NProgress.done()})}bindSearchFieldResetter(){let e;if(null!==(e=document.querySelector('.typo3-extensionmanager-searchTerForm input[type="text"]'))){const t=""!==e.value;e.clearable({onClear:e=>{t&&e.closest("form").submit()}})}}}export default Repository; \ No newline at end of file diff --git a/typo3/sysext/extensionmanager/Resources/Public/JavaScript/update.js b/typo3/sysext/extensionmanager/Resources/Public/JavaScript/update.js index 902b1d8c6698..9b5e36c57185 100644 --- a/typo3/sysext/extensionmanager/Resources/Public/JavaScript/update.js +++ b/typo3/sysext/extensionmanager/Resources/Public/JavaScript/update.js @@ -10,4 +10,4 @@ * * The TYPO3 project - inspiring people to share! */ -import $ from"jquery";import NProgress from"nprogress";import Notification from"@typo3/backend/notification.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";var ExtensionManagerUpdateIdentifier;!function(e){e.extensionTable="#terTable",e.terUpdateAction=".update-from-ter",e.pagination=".pagination-wrap",e.splashscreen=".splash-receivedata",e.terTableWrapper="#terTableWrapper .table"}(ExtensionManagerUpdateIdentifier||(ExtensionManagerUpdateIdentifier={}));class ExtensionManagerUpdate{initializeEvents(){$(ExtensionManagerUpdateIdentifier.terUpdateAction).each((e,a)=>{const t=$(a),n=t.attr("action");t.attr("action","#"),t.on("submit",()=>(this.updateFromTer(n,!0),!1)),this.updateFromTer(n,!1)})}updateFromTer(e,a){a&&(e+="&tx_extensionmanager_tools_extensionmanagerextensionmanager%5BforceUpdateCheck%5D=1"),$(ExtensionManagerUpdateIdentifier.terUpdateAction).addClass("extensionmanager-is-hidden"),$(ExtensionManagerUpdateIdentifier.extensionTable).hide(),$(ExtensionManagerUpdateIdentifier.splashscreen).addClass("extensionmanager-is-shown"),$(ExtensionManagerUpdateIdentifier.terTableWrapper).addClass("extensionmanager-is-loading"),$(ExtensionManagerUpdateIdentifier.pagination).addClass("extensionmanager-is-loading");let t=!1;NProgress.start(),new AjaxRequest(e).get().then(async e=>{const a=await e.resolve();a.errorMessage.length&&Notification.error(TYPO3.lang["extensionList.updateFromTerFlashMessage.title"],a.errorMessage,10);const n=$(ExtensionManagerUpdateIdentifier.terUpdateAction+" .extension-list-last-updated");n.text(a.timeSinceLastUpdate),n.attr("title",TYPO3.lang["extensionList.updateFromTer.lastUpdate.timeOfLastUpdate"]+a.lastUpdateTime),a.updated&&(t=!0,window.location.replace(window.location.href))},async e=>{const a=e.response.statusText+"("+e.response.status+"): "+await e.response.text();Notification.warning(TYPO3.lang["extensionList.updateFromTerFlashMessage.title"],a,10)}).finally(()=>{NProgress.done(),t||($(ExtensionManagerUpdateIdentifier.splashscreen).removeClass("extensionmanager-is-shown"),$(ExtensionManagerUpdateIdentifier.terTableWrapper).removeClass("extensionmanager-is-loading"),$(ExtensionManagerUpdateIdentifier.pagination).removeClass("extensionmanager-is-loading"),$(ExtensionManagerUpdateIdentifier.terUpdateAction).removeClass("extensionmanager-is-hidden"),$(ExtensionManagerUpdateIdentifier.extensionTable).show())})}}export default ExtensionManagerUpdate; \ No newline at end of file +import $ from"jquery";import NProgress from"nprogress";import Notification from"@typo3/backend/notification.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";var ExtensionManagerUpdateIdentifier;!function(e){e.extensionTable="#terTable",e.terUpdateAction=".update-from-ter",e.pagination=".pagination-wrap",e.splashscreen=".splash-receivedata",e.terTableWrapper="#terTableWrapper .table"}(ExtensionManagerUpdateIdentifier||(ExtensionManagerUpdateIdentifier={}));class ExtensionManagerUpdate{initializeEvents(){$(ExtensionManagerUpdateIdentifier.terUpdateAction).each((e,a)=>{const t=$(a),n=t.attr("action");t.attr("action","#"),t.on("submit",()=>(this.updateFromTer(n,!0),!1)),this.updateFromTer(n,!1)})}updateFromTer(e,a){a&&(e+="&forceUpdateCheck=1"),$(ExtensionManagerUpdateIdentifier.terUpdateAction).addClass("extensionmanager-is-hidden"),$(ExtensionManagerUpdateIdentifier.extensionTable).hide(),$(ExtensionManagerUpdateIdentifier.splashscreen).addClass("extensionmanager-is-shown"),$(ExtensionManagerUpdateIdentifier.terTableWrapper).addClass("extensionmanager-is-loading"),$(ExtensionManagerUpdateIdentifier.pagination).addClass("extensionmanager-is-loading");let t=!1;NProgress.start(),new AjaxRequest(e).get().then(async e=>{const a=await e.resolve();a.errorMessage.length&&Notification.error(TYPO3.lang["extensionList.updateFromTerFlashMessage.title"],a.errorMessage,10);const n=$(ExtensionManagerUpdateIdentifier.terUpdateAction+" .extension-list-last-updated");n.text(a.timeSinceLastUpdate),n.attr("title",TYPO3.lang["extensionList.updateFromTer.lastUpdate.timeOfLastUpdate"]+a.lastUpdateTime),a.updated&&(t=!0,window.location.replace(window.location.href))},async e=>{const a=e.response.statusText+"("+e.response.status+"): "+await e.response.text();Notification.warning(TYPO3.lang["extensionList.updateFromTerFlashMessage.title"],a,10)}).finally(()=>{NProgress.done(),t||($(ExtensionManagerUpdateIdentifier.splashscreen).removeClass("extensionmanager-is-shown"),$(ExtensionManagerUpdateIdentifier.terTableWrapper).removeClass("extensionmanager-is-loading"),$(ExtensionManagerUpdateIdentifier.pagination).removeClass("extensionmanager-is-loading"),$(ExtensionManagerUpdateIdentifier.terUpdateAction).removeClass("extensionmanager-is-hidden"),$(ExtensionManagerUpdateIdentifier.extensionTable).show())})}}export default ExtensionManagerUpdate; \ No newline at end of file diff --git a/typo3/sysext/fluid/Classes/ViewHelpers/FormViewHelper.php b/typo3/sysext/fluid/Classes/ViewHelpers/FormViewHelper.php index 1bfd376e0dd6..ff14d4d63295 100644 --- a/typo3/sysext/fluid/Classes/ViewHelpers/FormViewHelper.php +++ b/typo3/sysext/fluid/Classes/ViewHelpers/FormViewHelper.php @@ -17,7 +17,10 @@ declare(strict_types=1); namespace TYPO3\CMS\Fluid\ViewHelpers; +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Core\Http\ApplicationType; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface; use TYPO3\CMS\Extbase\Mvc\Controller\MvcPropertyMappingConfigurationService; use TYPO3\CMS\Extbase\Mvc\RequestInterface; use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder; @@ -70,6 +73,7 @@ class FormViewHelper extends AbstractFormViewHelper protected HashService $hashService; protected MvcPropertyMappingConfigurationService $mvcPropertyMappingConfigurationService; protected ExtensionService $extensionService; + protected ConfigurationManagerInterface $configurationManager; /** * We need the arguments of the formActionUri on request hash calculation @@ -92,6 +96,11 @@ class FormViewHelper extends AbstractFormViewHelper $this->extensionService = $extensionService; } + public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager): void + { + $this->configurationManager = $configurationManager; + } + public function initializeArguments(): void { parent::initializeArguments(); @@ -379,6 +388,14 @@ class FormViewHelper extends AbstractFormViewHelper protected function getDefaultFieldNamePrefix(): string { $request = $this->renderingContext->getRequest(); + // New Backend URLs doe not have a prefix anymore + if (!$this->configurationManager->isFeatureEnabled('enableNamespacedArgumentsForBackend') + && $request instanceof ServerRequestInterface + && $request->getAttribute('applicationType') + && ApplicationType::fromRequest($request)->isBackend() + ) { + return ''; + } if ($this->hasArgument('extensionName')) { $extensionName = $this->arguments['extensionName']; } else { diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/FormViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/FormViewHelperTest.php index 304c0fcbccc8..93f5a4f33e1b 100644 --- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/FormViewHelperTest.php +++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/FormViewHelperTest.php @@ -17,6 +17,8 @@ declare(strict_types=1); namespace TYPO3\CMS\Fluid\Tests\Functional\ViewHelpers; +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder; use TYPO3\CMS\Core\Http\ServerRequest; use TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters; use TYPO3\CMS\Extbase\Mvc\Request; @@ -61,7 +63,7 @@ class FormViewHelperTest extends FunctionalTestCase { $context = $this->get(RenderingContextFactory::class)->create(); $context->getTemplatePaths()->setTemplateSource($source); - $context->setRequest(new Request()); + $context->setRequest($this->createRequest()); $view = new TemplateView($context); $view->assignMultiple($variables); $body = $view->render(); @@ -81,7 +83,7 @@ class FormViewHelperTest extends FunctionalTestCase $extendsAbstractEntity->_setProperty('uid', 123); $context = $this->get(RenderingContextFactory::class)->create(); $context->getTemplatePaths()->setTemplateSource('<f:form fieldNamePrefix="prefix" objectName="myObjectName" object="{object}" />'); - $context->setRequest(new Request()); + $context->setRequest($this->createRequest()); $view = new TemplateView($context); $view->assign('object', $extendsAbstractEntity); $expected = '<input type="hidden" name="prefix[myObjectName][__identity]" value="123" />'; @@ -95,7 +97,7 @@ class FormViewHelperTest extends FunctionalTestCase { $context = $this->get(RenderingContextFactory::class)->create(); $context->getTemplatePaths()->setTemplateSource('<f:form actionUri="foobar" />'); - $context->setRequest(new Request()); + $context->setRequest($this->createRequest()); $expected = '<form action="foobar" method="post">'; self::assertStringContainsString($expected, (new TemplateView($context))->render()); } @@ -109,7 +111,7 @@ class FormViewHelperTest extends FunctionalTestCase $extendsAbstractEntity->_setProperty('uid', 123); $context = $this->get(RenderingContextFactory::class)->create(); $context->getTemplatePaths()->setTemplateSource('<f:form name="formName" fieldNamePrefix="prefix" object="{object}" />'); - $context->setRequest(new Request()); + $context->setRequest($this->createRequest()); $view = new TemplateView($context); $view->assign('object', $extendsAbstractEntity); $expected = '<input type="hidden" name="prefix[formName][__identity]" value="123" />'; @@ -125,7 +127,7 @@ class FormViewHelperTest extends FunctionalTestCase $extendsAbstractEntity->_setProperty('uid', 123); $context = $this->get(RenderingContextFactory::class)->create(); $context->getTemplatePaths()->setTemplateSource('<f:form name="formName" fieldNamePrefix="prefix" objectName="myObjectName" object="{object}" />'); - $context->setRequest(new Request()); + $context->setRequest($this->createRequest()); $view = new TemplateView($context); $view->assign('object', $extendsAbstractEntity); $expected = '<input type="hidden" name="prefix[myObjectName][__identity]" value="123" />'; @@ -141,7 +143,7 @@ class FormViewHelperTest extends FunctionalTestCase $extendsAbstractEntity->_setProperty('uid', 123); $context = $this->get(RenderingContextFactory::class)->create(); $context->getTemplatePaths()->setTemplateSource('<f:form fieldNamePrefix="prefix" objectName="myObjectName" object="{object}" />'); - $context->setRequest(new Request()); + $context->setRequest($this->createRequest()); $view = new TemplateView($context); $view->assign('object', $extendsAbstractEntity); $expected = '<form action="" method="post">' . chr(10) . '<div>'; @@ -157,7 +159,7 @@ class FormViewHelperTest extends FunctionalTestCase $extendsAbstractEntity->_setProperty('uid', 123); $context = $this->get(RenderingContextFactory::class)->create(); $context->getTemplatePaths()->setTemplateSource('<f:form hiddenFieldClassName="hidden" fieldNamePrefix="prefix" objectName="myObjectName" object="{object}" />'); - $context->setRequest(new Request()); + $context->setRequest($this->createRequest()); $view = new TemplateView($context); $view->assign('object', $extendsAbstractEntity); $expected = '<form action="" method="post">' . chr(10) . '<div class="hidden">'; @@ -174,7 +176,9 @@ class FormViewHelperTest extends FunctionalTestCase $extbaseRequestParameters->setControllerName('controllerName'); $extbaseRequestParameters->setControllerExtensionName('extensionName'); $psr7Request = (new ServerRequest())->withAttribute('extbase', $extbaseRequestParameters); + $psr7Request = $psr7Request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE); $extbaseRequest = new Request($psr7Request); + $extbaseRequest = $extbaseRequest->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE); $extendsAbstractEntity = new ExtendsAbstractEntity(); $extendsAbstractEntity->_setProperty('uid', 123); @@ -197,4 +201,9 @@ class FormViewHelperTest extends FunctionalTestCase </form>'; self::assertSame($expected, $view->render()); } + + protected function createRequest(): ServerRequestInterface + { + return (new Request())->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE); + } } diff --git a/typo3/sysext/form/ext_localconf.php b/typo3/sysext/form/ext_localconf.php index ceebad7ba0fd..55e9216262d7 100644 --- a/typo3/sysext/form/ext_localconf.php +++ b/typo3/sysext/form/ext_localconf.php @@ -27,6 +27,7 @@ call_user_func(static function () { // Add module configuration ExtensionManagementUtility::addTypoScriptSetup( 'module.tx_form { + features.enableNamespacedArgumentsForBackend = 1 settings { yamlConfigurations { 10 = EXT:form/Configuration/Yaml/FormSetup.yaml diff --git a/typo3/sysext/install/Classes/Controller/InstallerController.php b/typo3/sysext/install/Classes/Controller/InstallerController.php index d3ab33e09116..6838e54ab4e8 100644 --- a/typo3/sysext/install/Classes/Controller/InstallerController.php +++ b/typo3/sysext/install/Classes/Controller/InstallerController.php @@ -1038,9 +1038,7 @@ class InstallerController RouteRedirect::create( 'tools_ExtensionmanagerExtensionmanager', [ - 'tx_extensionmanager_tools_extensionmanagerextensionmanager' => [ - 'action' => 'distributions', - ], + 'action' => 'distributions', ] ) ); -- GitLab