From 9da08034dc67951c6d304223ea197fcec77921a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20B=C3=BCrk?= <stefan@buerk.tech> Date: Sat, 15 Jul 2023 12:40:04 +0200 Subject: [PATCH] [TASK] Allow f:link.action and f:uri.action without Extbase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change uses the core LinkFactory (FE) for creating links in f:link.action and f:uri.action when not executed in an Extbase context in order to avoid booting up Extbase. To operate in without Extbase context all Extbase related arguments are required to be passed as there is no fallback for determination based on Extbase configuration. Note: This change uses the same boilerplate approach as #98474. The intention is to replace that with an universally usable Extbase URI builder in TYPO3 v13. Resolves: #101729 Related: #100758 Related: #98474 Releases: main, 12.4 Change-Id: Ib5c27c46e9420947c0347c05c299cd67badb52ce Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/80908 Tested-by: Stefan Bürk <stefan@buerk.tech> Reviewed-by: Stefan Bürk <stefan@buerk.tech> Tested-by: core-ci <typo3@b13.com> --- .../ViewHelpers/Link/ActionViewHelper.php | 128 +++++++++- .../ViewHelpers/Uri/ActionViewHelper.php | 129 +++++++++- .../ViewHelpers/Link/ActionViewHelperTest.php | 232 ++++++++++++++++++ .../ViewHelpers/Uri/ActionViewHelperTest.php | 228 +++++++++++++++++ 4 files changed, 703 insertions(+), 14 deletions(-) create mode 100644 typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/ActionViewHelperTest.php create mode 100644 typo3/sysext/fluid/Tests/Functional/ViewHelpers/Uri/ActionViewHelperTest.php diff --git a/typo3/sysext/fluid/Classes/ViewHelpers/Link/ActionViewHelper.php b/typo3/sysext/fluid/Classes/ViewHelpers/Link/ActionViewHelper.php index 090cecd44bb6..808bfabf4fdc 100644 --- a/typo3/sysext/fluid/Classes/ViewHelpers/Link/ActionViewHelper.php +++ b/typo3/sysext/fluid/Classes/ViewHelpers/Link/ActionViewHelper.php @@ -17,11 +17,17 @@ declare(strict_types=1); namespace TYPO3\CMS\Fluid\ViewHelpers\Link; +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Core\Http\ApplicationType; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Utility\HttpUtility; use TYPO3\CMS\Core\Utility\MathUtility; -use TYPO3\CMS\Extbase\Mvc\RequestInterface; -use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder; +use TYPO3\CMS\Extbase\Mvc\RequestInterface as ExtbaseRequestInterface; +use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder as ExtbaseUriBuilder; use TYPO3\CMS\Fluid\Core\Rendering\RenderingContext; +use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; +use TYPO3\CMS\Frontend\Typolink\LinkFactory; +use TYPO3\CMS\Frontend\Typolink\UnableToLinkException; use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper; /** @@ -78,13 +84,122 @@ final class ActionViewHelper extends AbstractTagBasedViewHelper /** @var RenderingContext $renderingContext */ $renderingContext = $this->renderingContext; $request = $renderingContext->getRequest(); - if (!$request instanceof RequestInterface) { + if ($request instanceof ExtbaseRequestInterface) { + return $this->renderWithExtbaseContext($request); + } + if ($request instanceof ServerRequestInterface && ApplicationType::fromRequest($request)->isFrontend()) { + return $this->renderFrontendLinkWithCoreContext($request); + } + throw new \RuntimeException( + 'The rendering context of ViewHelper f:link.action is missing a valid request object.', + 1690365240 + ); + } + + protected function renderFrontendLinkWithCoreContext(ServerRequestInterface $request): string + { + // No support for following arguments: + // * format + $pageUid = (int)($this->arguments['pageUid'] ?? 0); + $pageType = (int)($this->arguments['pageType'] ?? 0); + $noCache = (bool)($this->arguments['noCache'] ?? false); + /** @var string|null $language */ + $language = $this->arguments['language'] ?? null; + /** @var string|null $section */ + $section = $this->arguments['section'] ?? null; + $linkAccessRestrictedPages = (bool)($this->arguments['linkAccessRestrictedPages'] ?? false); + /** @var array|null $additionalParams */ + $additionalParams = $this->arguments['additionalParams'] ?? null; + $absolute = (bool)($this->arguments['absolute'] ?? false); + /** @var bool|string $addQueryString */ + $addQueryString = $this->arguments['addQueryString'] ?? false; + /** @var array|null $argumentsToBeExcludedFromQueryString */ + $argumentsToBeExcludedFromQueryString = $this->arguments['argumentsToBeExcludedFromQueryString'] ?? null; + /** @var string|null $action */ + $action = $this->arguments['action'] ?? null; + /** @var string|null $controller */ + $controller = $this->arguments['controller'] ?? null; + /** @var string|null $extensionName */ + $extensionName = $this->arguments['extensionName'] ?? null; + /** @var string|null $pluginName */ + $pluginName = $this->arguments['pluginName'] ?? null; + /** @var array|null $arguments */ + $arguments = $this->arguments['arguments'] ?? []; + + $allExtbaseArgumentsAreSet = ( + is_string($extensionName) && $extensionName !== '' + && is_string($pluginName) && $pluginName !== '' + && is_string($controller) && $controller !== '' + && is_string($action) && $action !== '' + ); + if (!$allExtbaseArgumentsAreSet) { throw new \RuntimeException( - 'ViewHelper f:link.action can be used only in extbase context and needs a request implementing extbase RequestInterface.', - 1639818540 + 'ViewHelper f:link.action needs either all extbase arguments set' + . ' ("extensionName", "pluginName", "controller", "action")' + . ' or needs a request implementing extbase RequestInterface.', + 1690370264 ); } + // Provide extbase default and custom arguments as prefixed additional params + $extbaseArgumentNamespace = 'tx_' + . str_replace('_', '', strtolower($extensionName)) + . '_' + . str_replace('_', '', strtolower($pluginName)); + $additionalParams ??= []; + $additionalParams[$extbaseArgumentNamespace] = array_replace( + [ + 'controller' => $controller, + 'action' => $action, + ], + $arguments + ); + + $typolinkConfiguration = [ + 'parameter' => $pageUid, + ]; + if ($pageType) { + $typolinkConfiguration['parameter'] .= ',' . $pageType; + } + if ($language !== null) { + $typolinkConfiguration['language'] = $language; + } + if ($noCache) { + $typolinkConfiguration['no_cache'] = 1; + } + if ($section) { + $typolinkConfiguration['section'] = $section; + } + if ($linkAccessRestrictedPages) { + $typolinkConfiguration['linkAccessRestrictedPages'] = 1; + } + $typolinkConfiguration['additionalParams'] = HttpUtility::buildQueryString($additionalParams, '&'); + if ($absolute) { + $typolinkConfiguration['forceAbsoluteUrl'] = true; + } + if ($addQueryString && $addQueryString !== 'false') { + $typolinkConfiguration['addQueryString'] = $addQueryString; + if ($argumentsToBeExcludedFromQueryString !== []) { + $typolinkConfiguration['addQueryString.']['exclude'] = implode(',', $argumentsToBeExcludedFromQueryString); + } + } + + try { + $cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class); + $cObj->setRequest($request); + $linkFactory = GeneralUtility::makeInstance(LinkFactory::class); + $linkResult = $linkFactory->create((string)$this->renderChildren(), $typolinkConfiguration, $cObj); + $this->tag->addAttributes($linkResult->getAttributes()); + $this->tag->setContent($this->renderChildren()); + $this->tag->forceClosingTag(true); + return $this->tag->render(); + } catch (UnableToLinkException) { + return (string)$this->renderChildren(); + } + } + + protected function renderWithExtbaseContext(ExtbaseRequestInterface $request): string + { $action = $this->arguments['action']; $controller = $this->arguments['controller']; $extensionName = $this->arguments['extensionName']; @@ -102,7 +217,7 @@ final class ActionViewHelper extends AbstractTagBasedViewHelper $argumentsToBeExcludedFromQueryString = (array)$this->arguments['argumentsToBeExcludedFromQueryString']; $parameters = $this->arguments['arguments']; - $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); + $uriBuilder = GeneralUtility::makeInstance(ExtbaseUriBuilder::class); $uriBuilder ->reset() ->setRequest($request) @@ -121,7 +236,6 @@ final class ActionViewHelper extends AbstractTagBasedViewHelper if (MathUtility::canBeInterpretedAsInteger($pageUid)) { $uriBuilder->setTargetPageUid((int)$pageUid); } - $uri = $uriBuilder->uriFor($action, $parameters, $controller, $extensionName, $pluginName); if ($uri === '') { return $this->renderChildren(); diff --git a/typo3/sysext/fluid/Classes/ViewHelpers/Uri/ActionViewHelper.php b/typo3/sysext/fluid/Classes/ViewHelpers/Uri/ActionViewHelper.php index 3fd0a9b3d968..42e042fc79d9 100644 --- a/typo3/sysext/fluid/Classes/ViewHelpers/Uri/ActionViewHelper.php +++ b/typo3/sysext/fluid/Classes/ViewHelpers/Uri/ActionViewHelper.php @@ -17,10 +17,16 @@ declare(strict_types=1); namespace TYPO3\CMS\Fluid\ViewHelpers\Uri; +use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Core\Http\ApplicationType; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Extbase\Mvc\RequestInterface; -use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder; +use TYPO3\CMS\Core\Utility\HttpUtility; +use TYPO3\CMS\Extbase\Mvc\RequestInterface as ExtbaseRequestInterface; +use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder as ExtbaseUriBuilder; use TYPO3\CMS\Fluid\Core\Rendering\RenderingContext; +use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; +use TYPO3\CMS\Frontend\Typolink\LinkFactory; +use TYPO3\CMS\Frontend\Typolink\UnableToLinkException; use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface; use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper; use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic; @@ -67,16 +73,124 @@ final class ActionViewHelper extends AbstractViewHelper { /** @var RenderingContext $renderingContext */ $request = $renderingContext->getRequest(); - if (!$request instanceof RequestInterface) { + if ($request instanceof ExtbaseRequestInterface) { + return self::renderWithExtbaseContext($request, $arguments); + } + + if ($request instanceof ServerRequestInterface && ApplicationType::fromRequest($request)->isFrontend()) { + return self::renderFrontendLinkWithCoreContext($request, $arguments, $renderChildrenClosure); + } + throw new \RuntimeException( + 'The rendering context of ViewHelper f:uri.action is missing a valid request object.', + 1690360598 + ); + } + + protected static function renderFrontendLinkWithCoreContext(ServerRequestInterface $request, array $arguments, \Closure $renderChildrenClosure): string + { + // No support for following arguments: + // * format + $pageUid = (int)($arguments['pageUid'] ?? 0); + $pageType = (int)($arguments['pageType'] ?? 0); + $noCache = (bool)($arguments['noCache'] ?? false); + /** @var string|null $language */ + $language = $arguments['language'] ?? null; + /** @var string|null $section */ + $section = $arguments['section'] ?? null; + $linkAccessRestrictedPages = (bool)($arguments['linkAccessRestrictedPages'] ?? false); + /** @var array|null $additionalParams */ + $additionalParams = $arguments['additionalParams'] ?? null; + $absolute = (bool)($arguments['absolute'] ?? false); + /** @var bool|string $addQueryString */ + $addQueryString = $arguments['addQueryString'] ?? false; + /** @var array|null $argumentsToBeExcludedFromQueryString */ + $argumentsToBeExcludedFromQueryString = $arguments['argumentsToBeExcludedFromQueryString'] ?? null; + /** @var string|null $action */ + $action = $arguments['action'] ?? null; + /** @var string|null $controller */ + $controller = $arguments['controller'] ?? null; + /** @var string|null $extensionName */ + $extensionName = $arguments['extensionName'] ?? null; + /** @var string|null $pluginName */ + $pluginName = $arguments['pluginName'] ?? null; + /** @var array|null $arguments */ + $arguments = $arguments['arguments'] ?? []; + + $allExtbaseArgumentsAreSet = ( + is_string($extensionName) && $extensionName !== '' + && is_string($pluginName) && $pluginName !== '' + && is_string($controller) && $controller !== '' + && is_string($action) && $action !== '' + ); + if (!$allExtbaseArgumentsAreSet) { throw new \RuntimeException( - 'ViewHelper f:uri.action can be used only in extbase context and needs a request implementing extbase RequestInterface.', + 'ViewHelper f:uri.action needs either all extbase arguments set' + . ' ("extensionName", "pluginName", "controller", "action")' + . ' or needs a request implementing extbase RequestInterface.', 1639819692 ); } + // Provide extbase default and custom arguments as prefixed additional params + $extbaseArgumentNamespace = 'tx_' + . str_replace('_', '', strtolower($extensionName)) + . '_' + . str_replace('_', '', strtolower($pluginName)); + $additionalParams ??= []; + $additionalParams[$extbaseArgumentNamespace] = array_replace( + [ + 'controller' => $controller, + 'action' => $action, + ], + $arguments + ); + + $typolinkConfiguration = [ + 'parameter' => $pageUid, + ]; + if ($pageType) { + $typolinkConfiguration['parameter'] .= ',' . $pageType; + } + if ($language !== null) { + $typolinkConfiguration['language'] = $language; + } + if ($noCache) { + $typolinkConfiguration['no_cache'] = 1; + } + if ($section) { + $typolinkConfiguration['section'] = $section; + } + if ($linkAccessRestrictedPages) { + $typolinkConfiguration['linkAccessRestrictedPages'] = 1; + } + $typolinkConfiguration['additionalParams'] = HttpUtility::buildQueryString($additionalParams, '&'); + if ($absolute) { + $typolinkConfiguration['forceAbsoluteUrl'] = true; + } + if ($addQueryString && $addQueryString !== 'false') { + $typolinkConfiguration['addQueryString'] = $addQueryString; + if ($argumentsToBeExcludedFromQueryString !== []) { + $typolinkConfiguration['addQueryString.']['exclude'] = implode(',', $argumentsToBeExcludedFromQueryString); + } + } + + try { + $cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class); + $cObj->setRequest($request); + $linkFactory = GeneralUtility::makeInstance(LinkFactory::class); + $linkResult = $linkFactory->create((string)$renderChildrenClosure(), $typolinkConfiguration, $cObj); + return $linkResult->getUrl(); + } catch (UnableToLinkException) { + return (string)$renderChildrenClosure(); + } + } + + protected static function renderWithExtbaseContext(ExtbaseRequestInterface $request, array $arguments): string + { $pageUid = (int)($arguments['pageUid'] ?? 0); $pageType = (int)($arguments['pageType'] ?? 0); $noCache = (bool)($arguments['noCache'] ?? false); + /** @var string|null $language */ $language = $arguments['language'] ?? null; /** @var string|null $section */ $section = $arguments['section'] ?? null; @@ -101,9 +215,10 @@ final class ActionViewHelper extends AbstractViewHelper /** @var array|null $arguments */ $arguments = $arguments['arguments'] ?? []; - /** @var UriBuilder $uriBuilder */ - $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); - $uriBuilder->reset()->setRequest($request); + /** @var ExtbaseUriBuilder $uriBuilder */ + $uriBuilder = GeneralUtility::makeInstance(ExtbaseUriBuilder::class); + $uriBuilder->reset(); + $uriBuilder->setRequest($request); if ($pageUid > 0) { $uriBuilder->setTargetPageUid($pageUid); diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/ActionViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/ActionViewHelperTest.php new file mode 100644 index 000000000000..52146f016ddd --- /dev/null +++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/ActionViewHelperTest.php @@ -0,0 +1,232 @@ +<?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\Fluid\Tests\Functional\ViewHelpers\Link; + +use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder; +use TYPO3\CMS\Core\Domain\Repository\PageRepository; +use TYPO3\CMS\Core\Http\ServerRequest; +use TYPO3\CMS\Core\Routing\PageArguments; +use TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait; +use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode; +use TYPO3\CMS\Core\TypoScript\FrontendTypoScript; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters; +use TYPO3\CMS\Extbase\Mvc\Request; +use TYPO3\CMS\Fluid\View\StandaloneView; +use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; +use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +final class ActionViewHelperTest extends FunctionalTestCase +{ + use SiteBasedTestTrait; + + protected const LANGUAGE_PRESETS = [ + 'EN' => ['id' => 0, 'title' => 'English', 'locale' => 'en_US.UTF8'], + ]; + + protected array $configurationToUseInTestInstance = [ + 'FE' => [ + 'cacheHash' => [ + 'excludedParameters' => [ + 'untrusted', + ], + ], + ], + ]; + + /** + * @test + */ + public function renderThrowsExceptionWithoutARequest(): void + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionCode(1690365240); + $view = new StandaloneView(); + $view->setRequest(); + $view->setTemplateSource('<f:link.action />'); + $view->render(); + } + + /** + * @test + */ + public function renderInFrontendCoreContextThrowsExceptionWithIncompleteArguments(): void + { + $request = new ServerRequest(); + $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE); + $request = $request->withAttribute('routing', new PageArguments(1, '0', [])); + $this->expectException(\RuntimeException::class); + $this->expectExceptionCode(1690370264); + $view = new StandaloneView(); + $view->setRequest($request); + $view->setTemplateSource('<f:link.action />'); + $view->render(); + } + + /** + * @test + */ + public function renderInBackendCoreContextThrowsExceptionWithIncompleteArguments(): void + { + $request = new ServerRequest('http://localhost/typo3/'); + $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $request = $request->withQueryParams(['route' => 'web_layout']); + $this->expectException(\RuntimeException::class); + $this->expectExceptionCode(1690365240); + $view = new StandaloneView(); + $view->setRequest($request); + $view->setTemplateSource('<f:link.action />'); + $view->render(); + } + + public static function renderInFrontendWithCoreContextAndAllNecessaryExtbaseArgumentsDataProvider(): \Generator + { + yield 'link to root page with plugin' => [ + '<f:link.action pageUid="1" extensionName="examples" pluginName="haiku" controller="Detail" action="show">link to root page with plugin</f:link.action>', + '<a href="/?tx_examples_haiku%5Baction%5D=show&tx_examples_haiku%5Bcontroller%5D=Detail&cHash=5c6aa07f6ceee30ae2ea8dbf574cf26c">link to root page with plugin</a>', + ]; + + yield 'link to root page with plugin and section' => [ + '<f:link.action pageUid="1" extensionName="examples" pluginName="haiku" controller="Detail" action="show" section="c13">link to root page with plugin and section</f:link.action>', + '<a href="/?tx_examples_haiku%5Baction%5D=show&tx_examples_haiku%5Bcontroller%5D=Detail&cHash=5c6aa07f6ceee30ae2ea8dbf574cf26c#c13">link to root page with plugin and section</a>', + ]; + + yield 'link to root page with page type' => [ + '<f:link.action pageUid="1" extensionName="examples" pluginName="haiku" controller="Detail" action="show" pageType="1234">link to root page with page type</f:link.action>', + '<a href="/?tx_examples_haiku%5Baction%5D=show&tx_examples_haiku%5Bcontroller%5D=Detail&type=1234&cHash=5c6aa07f6ceee30ae2ea8dbf574cf26c">link to root page with page type</a>', + ]; + } + + /** + * @test + * @dataProvider renderInFrontendWithCoreContextAndAllNecessaryExtbaseArgumentsDataProvider + */ + public function renderInFrontendWithCoreContextAndAllNecessaryExtbaseArguments(string $template, string $expected): void + { + $this->importCSVDataSet(__DIR__ . '/../../Fixtures/pages.csv'); + $this->writeSiteConfiguration( + 'test', + $this->buildSiteConfiguration(1, '/'), + ); + $request = new ServerRequest(); + $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE); + $request = $request->withAttribute('routing', new PageArguments(1, '0', ['untrusted' => 123])); + $GLOBALS['TYPO3_REQUEST'] = $request; + $GLOBALS['TSFE'] = $this->createMock(TypoScriptFrontendController::class); + $GLOBALS['TSFE']->id = 1; + $GLOBALS['TSFE']->sys_page = GeneralUtility::makeInstance(PageRepository::class); + $view = new StandaloneView(); + $view->setRequest($request); + $view->setTemplateSource($template); + $result = $view->render(); + self::assertSame($expected, $result); + } + + public static function renderInFrontendWithExtbaseContextDataProvider(): \Generator + { + // with all extbase arguments provided + yield 'link to root page with plugin' => [ + '<f:link.action pageUid="1" extensionName="examples" pluginName="haiku" controller="Detail" action="show">link to root page with plugin</f:link.action>', + '<a href="/?tx_examples_haiku%5Baction%5D=show&tx_examples_haiku%5Bcontroller%5D=Detail&cHash=5c6aa07f6ceee30ae2ea8dbf574cf26c">link to root page with plugin</a>', + ]; + + yield 'link to root page with plugin and section' => [ + '<f:link.action pageUid="1" extensionName="examples" pluginName="haiku" controller="Detail" action="show" section="c13">link to root page with plugin and section</f:link.action>', + '<a href="/?tx_examples_haiku%5Baction%5D=show&tx_examples_haiku%5Bcontroller%5D=Detail&cHash=5c6aa07f6ceee30ae2ea8dbf574cf26c#c13">link to root page with plugin and section</a>', + ]; + + yield 'link to root page with page type' => [ + '<f:link.action pageUid="1" extensionName="examples" pluginName="haiku" controller="Detail" action="show" pageType="1234">link to root page with page type</f:link.action>', + '<a href="/?tx_examples_haiku%5Baction%5D=show&tx_examples_haiku%5Bcontroller%5D=Detail&type=1234&cHash=5c6aa07f6ceee30ae2ea8dbf574cf26c">link to root page with page type</a>', + ]; + // without all extbase arguments provided + yield 'renderProvidesATagForValidLinkTarget' => [ + '<f:link.action>index.php</f:link.action>', + '<a href="/?tx_examples_haiku%5Bcontroller%5D=Detail&cHash=1d5a12de6bf2d5245b654deb866ee9c3">index.php</a>', + ]; + yield 'renderWillProvideEmptyATagForNonValidLinkTarget' => [ + '<f:link.action></f:link.action>', + '<a href="/?tx_examples_haiku%5Bcontroller%5D=Detail&cHash=1d5a12de6bf2d5245b654deb866ee9c3"></a>', + ]; + yield 'link to root page in extbase context' => [ + '<f:link.action pageUid="1">linkMe</f:link.action>', + '<a href="/?tx_examples_haiku%5Bcontroller%5D=Detail&cHash=1d5a12de6bf2d5245b654deb866ee9c3">linkMe</a>', + ]; + yield 'link to root page with section' => [ + '<f:link.action pageUid="1" section="c13">linkMe</f:link.action>', + '<a href="/?tx_examples_haiku%5Bcontroller%5D=Detail&cHash=1d5a12de6bf2d5245b654deb866ee9c3#c13">linkMe</a>', + ]; + yield 'link to root page with page type in extbase context' => [ + '<f:link.action pageUid="1" pageType="1234">linkMe</f:link.action>', + '<a href="/?tx_examples_haiku%5Bcontroller%5D=Detail&type=1234&cHash=1d5a12de6bf2d5245b654deb866ee9c3">linkMe</a>', + ]; + yield 'link to root page with untrusted query arguments' => [ + '<f:link.action addQueryString="untrusted"></f:link.action>', + '<a href="/?tx_examples_haiku%5Bcontroller%5D=Detail&untrusted=123&cHash=1d5a12de6bf2d5245b654deb866ee9c3"></a>', + ]; + yield 'link to page sub page' => [ + '<f:link.action pageUid="3">linkMe</f:link.action>', + '<a href="/dummy-1-2/dummy-1-2-3?tx_examples_haiku%5Bcontroller%5D=Detail&cHash=d9289022f99f8cbc8080832f61e46509">linkMe</a>', + ]; + yield 'arguments one level' => [ + '<f:link.action pageUid="3" arguments="{foo: \'bar\'}">haiku title</f:link.action>', + '<a href="/dummy-1-2/dummy-1-2-3?tx_examples_haiku%5Bcontroller%5D=Detail&tx_examples_haiku%5Bfoo%5D=bar&cHash=74dd4635cee85b19b67cd9b497ec99e9">haiku title</a>', + ]; + yield 'additional parameters two levels' => [ + '<f:link.action pageUid="3" additionalParams="{tx_examples_haiku: {action: \'show\', haiku: 42}}">haiku title</f:link.action>', + '<a href="/dummy-1-2/dummy-1-2-3?tx_examples_haiku%5Baction%5D=show&tx_examples_haiku%5Bcontroller%5D=Detail&tx_examples_haiku%5Bhaiku%5D=42&cHash=aefc37bc2323ebd8c8e39c222adb7413">haiku title</a>', + ]; + } + + /** + * @test + * @dataProvider renderInFrontendWithExtbaseContextDataProvider + */ + public function renderInFrontendWithExtbaseContext(string $template, string $expected): void + { + $this->importCSVDataSet(__DIR__ . '/../../Fixtures/pages.csv'); + $this->writeSiteConfiguration( + 'test', + $this->buildSiteConfiguration(1, '/'), + ); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript->setSetupArray([]); + $extbaseRequestParameters = new ExtbaseRequestParameters(); + $extbaseRequestParameters->setControllerExtensionName('Examples'); + $extbaseRequestParameters->setControllerName('Detail'); + $extbaseRequestParameters->setControllerActionName('show'); + $extbaseRequestParameters->setPluginName('Haiku'); + $request = new ServerRequest(); + $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE); + $request = $request->withAttribute('routing', new PageArguments(1, '0', ['untrusted' => 123])); + $request = $request->withAttribute('extbase', $extbaseRequestParameters); + $request = $request->withAttribute('currentContentObject', $this->get(ContentObjectRenderer::class)); + $request = $request->withAttribute('frontend.typoscript', $frontendTypoScript); + $request = new Request($request); + $GLOBALS['TYPO3_REQUEST'] = $request; + $GLOBALS['TSFE'] = $this->createMock(TypoScriptFrontendController::class); + $GLOBALS['TSFE']->id = 1; + $GLOBALS['TSFE']->sys_page = GeneralUtility::makeInstance(PageRepository::class); + $view = new StandaloneView(); + $view->setRequest($request); + $view->setTemplateSource($template); + $result = $view->render(); + self::assertSame($expected, $result); + } +} diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Uri/ActionViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Uri/ActionViewHelperTest.php new file mode 100644 index 000000000000..96a2d61d7351 --- /dev/null +++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Uri/ActionViewHelperTest.php @@ -0,0 +1,228 @@ +<?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\Fluid\Tests\Functional\ViewHelpers\Uri; + +use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder; +use TYPO3\CMS\Core\Domain\Repository\PageRepository; +use TYPO3\CMS\Core\Http\ServerRequest; +use TYPO3\CMS\Core\Routing\PageArguments; +use TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait; +use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode; +use TYPO3\CMS\Core\TypoScript\FrontendTypoScript; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters; +use TYPO3\CMS\Extbase\Mvc\Request; +use TYPO3\CMS\Fluid\View\StandaloneView; +use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; +use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +final class ActionViewHelperTest extends FunctionalTestCase +{ + use SiteBasedTestTrait; + + protected const LANGUAGE_PRESETS = [ + 'EN' => ['id' => 0, 'title' => 'English', 'locale' => 'en_US.UTF8'], + ]; + + protected array $configurationToUseInTestInstance = [ + 'FE' => [ + 'cacheHash' => [ + 'excludedParameters' => [ + 'untrusted', + ], + ], + ], + ]; + + /** + * @test + */ + public function renderThrowsExceptionWithoutARequest(): void + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionCode(1690360598); + $view = new StandaloneView(); + $view->setRequest(); + $view->setTemplateSource('<f:uri.action />'); + $view->render(); + } + + /** + * @test + */ + public function renderInFrontendCoreContextThrowsExceptionWithIncompleteArguments(): void + { + $request = new ServerRequest(); + $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE); + $request = $request->withAttribute('routing', new PageArguments(1, '0', [])); + $this->expectException(\RuntimeException::class); + $this->expectExceptionCode(1639819692); + $view = new StandaloneView(); + $view->setRequest($request); + $view->setTemplateSource('<f:uri.action />'); + $view->render(); + } + + /** + * @test + */ + public function renderInBackendCoreContextThrowsExceptionWithIncompleteArguments(): void + { + $request = new ServerRequest('http://localhost/typo3/'); + $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + $request = $request->withQueryParams(['route' => 'web_layout']); + $this->expectException(\RuntimeException::class); + $this->expectExceptionCode(1690360598); + $view = new StandaloneView(); + $view->setRequest($request); + $view->setTemplateSource('<f:uri.action />'); + $view->render(); + } + + public static function renderInFrontendWithCoreContextAndAllNecessaryExtbaseArgumentsDataProvider(): \Generator + { + yield 'link to root page with plugin' => [ + '<f:uri.action pageUid="1" extensionName="examples" pluginName="haiku" controller="Detail" action="show" />', + '/?tx_examples_haiku%5Baction%5D=show&tx_examples_haiku%5Bcontroller%5D=Detail&cHash=5c6aa07f6ceee30ae2ea8dbf574cf26c', + ]; + + yield 'link to root page with plugin and section' => [ + '<f:uri.action pageUid="1" extensionName="examples" pluginName="haiku" controller="Detail" action="show" section="c13" />', + '/?tx_examples_haiku%5Baction%5D=show&tx_examples_haiku%5Bcontroller%5D=Detail&cHash=5c6aa07f6ceee30ae2ea8dbf574cf26c#c13', + ]; + + yield 'link to root page with page type' => [ + '<f:uri.action pageUid="1" extensionName="examples" pluginName="haiku" controller="Detail" action="show" pageType="1234" />', + '/?tx_examples_haiku%5Baction%5D=show&tx_examples_haiku%5Bcontroller%5D=Detail&type=1234&cHash=5c6aa07f6ceee30ae2ea8dbf574cf26c', + ]; + } + + /** + * @test + * @dataProvider renderInFrontendWithCoreContextAndAllNecessaryExtbaseArgumentsDataProvider + */ + public function renderInFrontendWithCoreContextAndAllNecessaryExtbaseArguments(string $template, string $expected): void + { + $this->importCSVDataSet(__DIR__ . '/../../Fixtures/pages.csv'); + $this->writeSiteConfiguration( + 'test', + $this->buildSiteConfiguration(1, '/'), + ); + $request = new ServerRequest(); + $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE); + $request = $request->withAttribute('routing', new PageArguments(1, '0', ['untrusted' => 123])); + $GLOBALS['TYPO3_REQUEST'] = $request; + $GLOBALS['TSFE'] = $this->createMock(TypoScriptFrontendController::class); + $GLOBALS['TSFE']->id = 1; + $GLOBALS['TSFE']->sys_page = GeneralUtility::makeInstance(PageRepository::class); + $view = new StandaloneView(); + $view->setRequest($request); + $view->setTemplateSource($template); + $result = $view->render(); + self::assertSame($expected, $result); + } + + public static function renderInFrontendWithExtbaseContextDataProvider(): \Generator + { + // with all extbase arguments provided + yield 'link to root page with plugin' => [ + '<f:uri.action pageUid="1" extensionName="examples" pluginName="haiku" controller="Detail" action="show" />', + '/?tx_examples_haiku%5Baction%5D=show&tx_examples_haiku%5Bcontroller%5D=Detail&cHash=5c6aa07f6ceee30ae2ea8dbf574cf26c', + ]; + + yield 'link to root page with plugin and section' => [ + '<f:uri.action pageUid="1" extensionName="examples" pluginName="haiku" controller="Detail" action="show" section="c13" />', + '/?tx_examples_haiku%5Baction%5D=show&tx_examples_haiku%5Bcontroller%5D=Detail&cHash=5c6aa07f6ceee30ae2ea8dbf574cf26c#c13', + ]; + + yield 'link to root page with page type' => [ + '<f:uri.action pageUid="1" extensionName="examples" pluginName="haiku" controller="Detail" action="show" pageType="1234" />', + '/?tx_examples_haiku%5Baction%5D=show&tx_examples_haiku%5Bcontroller%5D=Detail&type=1234&cHash=5c6aa07f6ceee30ae2ea8dbf574cf26c', + ]; + // without all extbase arguments provided + yield 'renderWillProvideEmptyATagForNonValidLinkTarget' => [ + '<f:uri.action />', + '/?tx_examples_haiku%5Bcontroller%5D=Detail&cHash=1d5a12de6bf2d5245b654deb866ee9c3', + ]; + yield 'link to root page in extbase context' => [ + '<f:uri.action pageUid="1" />', + '/?tx_examples_haiku%5Bcontroller%5D=Detail&cHash=1d5a12de6bf2d5245b654deb866ee9c3', + ]; + yield 'link to root page with section' => [ + '<f:uri.action pageUid="1" section="c13" />', + '/?tx_examples_haiku%5Bcontroller%5D=Detail&cHash=1d5a12de6bf2d5245b654deb866ee9c3#c13', + ]; + yield 'link to root page with page type in extbase context' => [ + '<f:uri.action pageUid="1" pageType="1234" />', + '/?tx_examples_haiku%5Bcontroller%5D=Detail&type=1234&cHash=1d5a12de6bf2d5245b654deb866ee9c3', + ]; + yield 'link to root page with untrusted query arguments' => [ + '<f:uri.action addQueryString="untrusted" />', + '/?tx_examples_haiku%5Bcontroller%5D=Detail&untrusted=123&cHash=1d5a12de6bf2d5245b654deb866ee9c3', + ]; + yield 'link to page sub page' => [ + '<f:uri.action pageUid="3" />', + '/dummy-1-2/dummy-1-2-3?tx_examples_haiku%5Bcontroller%5D=Detail&cHash=d9289022f99f8cbc8080832f61e46509', + ]; + yield 'arguments one level' => [ + '<f:uri.action pageUid="3" arguments="{foo: \'bar\'}" />', + '/dummy-1-2/dummy-1-2-3?tx_examples_haiku%5Bcontroller%5D=Detail&tx_examples_haiku%5Bfoo%5D=bar&cHash=74dd4635cee85b19b67cd9b497ec99e9', + ]; + yield 'additional parameters two levels' => [ + '<f:uri.action pageUid="3" additionalParams="{tx_examples_haiku: {action: \'show\', haiku: 42}}" />', + '/dummy-1-2/dummy-1-2-3?tx_examples_haiku%5Baction%5D=show&tx_examples_haiku%5Bcontroller%5D=Detail&tx_examples_haiku%5Bhaiku%5D=42&cHash=aefc37bc2323ebd8c8e39c222adb7413', + ]; + } + + /** + * @test + * @dataProvider renderInFrontendWithExtbaseContextDataProvider + */ + public function renderInFrontendWithExtbaseContext(string $template, string $expected): void + { + $this->importCSVDataSet(__DIR__ . '/../../Fixtures/pages.csv'); + $this->writeSiteConfiguration( + 'test', + $this->buildSiteConfiguration(1, '/'), + ); + $frontendTypoScript = new FrontendTypoScript(new RootNode(), []); + $frontendTypoScript->setSetupArray([]); + $extbaseRequestParameters = new ExtbaseRequestParameters(); + $extbaseRequestParameters->setControllerExtensionName('Examples'); + $extbaseRequestParameters->setControllerName('Detail'); + $extbaseRequestParameters->setControllerActionName('show'); + $extbaseRequestParameters->setPluginName('Haiku'); + $request = new ServerRequest(); + $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE); + $request = $request->withAttribute('routing', new PageArguments(1, '0', ['untrusted' => 123])); + $request = $request->withAttribute('extbase', $extbaseRequestParameters); + $request = $request->withAttribute('currentContentObject', $this->get(ContentObjectRenderer::class)); + $request = $request->withAttribute('frontend.typoscript', $frontendTypoScript); + $request = new Request($request); + $GLOBALS['TYPO3_REQUEST'] = $request; + $GLOBALS['TSFE'] = $this->createMock(TypoScriptFrontendController::class); + $GLOBALS['TSFE']->id = 1; + $GLOBALS['TSFE']->sys_page = GeneralUtility::makeInstance(PageRepository::class); + $view = new StandaloneView(); + $view->setRequest($request); + $view->setTemplateSource($template); + $result = $view->render(); + self::assertSame($expected, $result); + } +} -- GitLab