diff --git a/typo3/sysext/fluid/Classes/ViewHelpers/Link/ActionViewHelper.php b/typo3/sysext/fluid/Classes/ViewHelpers/Link/ActionViewHelper.php index 090cecd44bb6375b6c7a3daf4872584f86d74a9f..808bfabf4fdcd3588ccea53d5a28fb1145f77fc7 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 3fd0a9b3d96817f6774818710b23c19c368779ea..42e042fc79d979bf9ff3de869a2fc5ffc030e1d1 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 0000000000000000000000000000000000000000..52146f016ddd7125f0bd4356d45769425ebb035f --- /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 0000000000000000000000000000000000000000..96a2d61d7351375fb1010f31e0a17c07b3132057 --- /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); + } +}