diff --git a/typo3/sysext/extbase/Classes/Mvc/Controller/ActionController.php b/typo3/sysext/extbase/Classes/Mvc/Controller/ActionController.php index 0604c902c7c6a5d3b52f9f706634d59b8b32b7ba..edac0499f21207af0a169d79c81a56e36e2ccf20 100644 --- a/typo3/sysext/extbase/Classes/Mvc/Controller/ActionController.php +++ b/typo3/sysext/extbase/Classes/Mvc/Controller/ActionController.php @@ -599,13 +599,18 @@ class ActionController implements ControllerInterface protected function forwardToReferringRequest() { $referringRequest = null; - $referringRequestArguments = $this->request->getInternalArguments()['__referrer']['@request'] ?? null; - if (is_string($referringRequestArguments)) { + $referringRequestArguments = $this->request->getInternalArguments()['__referrer'] ?? null; + if (is_string($referringRequestArguments['@request'] ?? null)) { $referrerArray = json_decode( - $this->hashService->validateAndStripHmac($referringRequestArguments), + $this->hashService->validateAndStripHmac($referringRequestArguments['@request']), true ); $arguments = []; + if (is_string($referringRequestArguments['arguments'] ?? null)) { + $arguments = unserialize( + base64_decode($this->hashService->validateAndStripHmac($referringRequestArguments['arguments'])) + ); + } $referringRequest = new ReferringRequest(); $referringRequest->setArguments(array_replace_recursive($arguments, $referrerArray)); } diff --git a/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/ActionControllerArgumentTest.php b/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/ActionControllerArgumentTest.php new file mode 100644 index 0000000000000000000000000000000000000000..cc949140a13170fd630ebcb6e0254942848f3bfb --- /dev/null +++ b/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/ActionControllerArgumentTest.php @@ -0,0 +1,244 @@ +<?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\Extbase\Tests\Functional\Mvc\Controller; + +use TYPO3\CMS\Core\Utility\ArrayUtility; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Mvc\Exception\StopActionException; +use TYPO3\CMS\Extbase\Mvc\RequestInterface; +use TYPO3\CMS\Extbase\Mvc\ResponseInterface; +use TYPO3\CMS\Extbase\Mvc\Web\Request; +use TYPO3\CMS\Extbase\Mvc\Web\Response; +use TYPO3\CMS\Extbase\Object\Container\Container; +use TYPO3\CMS\Extbase\Object\ObjectManager; +use TYPO3\CMS\Extbase\Tests\Functional\Mvc\Controller\Fixture\Controller\ArgumentTestController; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +/** + * Test case + */ +class ActionControllerArgumentTest extends FunctionalTestCase +{ + private const ENCRYPTION_KEY = '4408d27a916d51e624b69af3554f516dbab61037a9f7b9fd6f81b4d3bedeccb6'; + + /** + * @var ObjectManager + */ + protected $objectManager; + + /** + * @var Container + */ + protected $objectContainer; + + private $pluginName; + private $extensionName; + private $pluginNamespacePrefix; + + protected function setUp(): void + { + parent::setUp(); + + $this->pluginName = 'Pi1'; + $this->extensionName = 'Extbase\\Tests\\Functional\\Mvc\\Controller\\Fixture'; + $this->pluginNamespacePrefix = strtolower('tx_' . $this->extensionName . '_' . $this->pluginName); + $this->objectManager = GeneralUtility::makeInstance(ObjectManager::class); + $this->objectContainer = $this->objectManager->get(Container::class); + $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] = self::ENCRYPTION_KEY; + } + + protected function tearDown(): void + { + unset($this->objectManager, $this->objectContainer); + unset($this->extensionName, $this->pluginName, $this->pluginNamespacePrefix); + unset($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']); + parent::tearDown(); + } + + public function validationErrorReturnsToForwardedPreviousActionDataProvider(): array + { + return [ + // regular models + 'preset model' => [ + 'inputPresetModel', + ['preset' => (new Fixture\Domain\Model\Model())->setValue('preset')], + 'validateModel', + [ + 'form/model/value' => 'preset', + 'validationResults/model' => [[]], + ], + ], + 'preset DTO' => [ + 'inputPresetDto', + ['preset' => (new Fixture\Domain\Model\ModelDto())->setValue('preset')], + 'validateDto', + [ + 'form/dto/value' => 'preset', + 'validationResults/dto' => [[]], + ], + ], + ]; + } + + /** + * @param string $forwardTargetAction + * @param array $forwardTargetArguments + * @param string $validateAction + * @param array $expectations + * + * @test + * @dataProvider validationErrorReturnsToForwardedPreviousActionDataProvider + */ + public function validationErrorReturnsToForwardedPreviousAction(string $forwardTargetAction, array $forwardTargetArguments, string $validateAction, array $expectations) + { + // trigger action to forward to some `input*` action + $controller = $this->buildController(); + $controller->declareForwardTargetAction($forwardTargetAction); + $controller->declareForwardTargetArguments($forwardTargetArguments); + + $inputRequest = $this->buildRequest('forward'); + $inputResponse = $this->buildResponse(); + $this->dispatch($controller, $inputRequest, $inputResponse); + + $inputDocument = $this->createDocument($inputResponse->getContent()); + $parsedInputData = $this->parseDataFromResponseDocument($inputDocument); + self::assertNotEmpty($parsedInputData['form'] ?? null); + unset($inputRequest, $controller); + + // trigger `validate*` action with generated arguments from FormViewHelper (see template) + $controller = $this->buildController(); + $validateRequest = $this->buildRequest($validateAction, $parsedInputData['form']); + $validateResponse = $this->buildResponse(); + + // dispatch request to `validate*` action + $this->dispatch($controller, $validateRequest, $validateResponse); + + $validateDocument = $this->createDocument($validateResponse->getContent()); + $parsedValidateData = $this->parseDataFromResponseDocument($validateDocument); + foreach ($expectations ?? [] as $bodyPath => $bodyValue) { + self::assertSame($bodyValue, ArrayUtility::getValueByPath($parsedValidateData, $bodyPath)); + } + } + + private function dispatch(ArgumentTestController $controller, RequestInterface $request, ResponseInterface $response): void + { + while (!$request->isDispatched()) { + try { + $controller->processRequest($request, $response); + } catch (StopActionException $exception) { + // simulate Dispatcher::resolveController() using a new controller instance + $controller = $this->buildController(); + } + } + } + + /** + * Parses result HTML, extracts inflated name/value pairs of `<form>` and validation errors, e.g. + * `['validationResults' => ..., 'form' => ['value' => ..., '__referrer' => [...]]]` + * + * @param \DOMDocument $document + * @return array + */ + private function parseDataFromResponseDocument(\DOMDocument $document): array + { + $results = []; + $xpath = new \DOMXPath($document); + + $elements = $xpath->query('//div[@id="validationResults"]'); + if ($elements->count() !== 0) { + $results['validationResults'] = json_decode( + trim($elements->item(0)->textContent), + true + ); + } + + $elements = $xpath->query('//input[@type="text" or @type="hidden"]'); + foreach ($elements as $element) { + if (!$element instanceof \DOMElement) { + continue; + } + $results['form'][$element->getAttribute('name')] = $element->getAttribute('value'); + } + if (!empty($results['form'])) { + $results['form'] = $this->inflateFormValues($results['form']); + } + return $results; + } + + /** + * Inflates form values for plugin arguments. + * `['tx_ext_pi1[aaa][bbb]' => 'value'] --> ['aaa' => ['bbb' => 'value']]` + * + * @param array $formValues + * @return array + */ + private function inflateFormValues(array $formValues): array + { + $inflatedFormValues = []; + $normalizedFormPaths = array_map( + function (string $formName) { + $formName = substr($formName, strlen($this->pluginNamespacePrefix)); + $formName = str_replace('][', '/', trim($formName, '[]')); + return $formName; + }, + array_keys($formValues) + ); + $normalizedFormValues = array_combine($normalizedFormPaths, $formValues); + foreach ($normalizedFormValues as $formPath => $formValue) { + $inflatedFormValues = ArrayUtility::setValueByPath($inflatedFormValues, $formPath, $formValue, '/'); + } + return $inflatedFormValues; + } + + private function createDocument(string $content): \DOMDocument + { + $document = new \DOMDocument(); + $document->loadHTML( + $content, + LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD + | LIBXML_NOBLANKS | LIBXML_NOERROR | LIBXML_NONET | LIBXML_NOWARNING + ); + $document->preserveWhiteSpace = false; + return $document; + } + + private function buildRequest(string $actionName, array $arguments = null): Request + { + $request = $this->objectManager->get(Request::class); + $request->setPluginName($this->pluginName); + $request->setControllerExtensionName($this->extensionName); + $request->setControllerName('ArgumentTest'); + $request->setMethod('GET'); + $request->setFormat('html'); + $request->setControllerActionName($actionName); + if ($arguments !== null) { + $request->setArguments($arguments); + } + return $request; + } + + private function buildResponse(): Response + { + return $this->objectManager->get(Response::class); + } + + private function buildController(): ArgumentTestController + { + return $this->objectManager->get(ArgumentTestController::class); + } +} diff --git a/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/Fixture/Controller/ArgumentTestController.php b/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/Fixture/Controller/ArgumentTestController.php new file mode 100644 index 0000000000000000000000000000000000000000..ba6b7e25f88676102abf9eb738fa2670dc0e774b --- /dev/null +++ b/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/Fixture/Controller/ArgumentTestController.php @@ -0,0 +1,120 @@ +<?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\Extbase\Tests\Functional\Mvc\Controller\Fixture\Controller; + +use TYPO3\CMS\Extbase\Annotation as Extbase; +use TYPO3\CMS\Extbase\Mvc\Controller\ActionController; +use TYPO3\CMS\Extbase\Mvc\View\ViewInterface; +use TYPO3\CMS\Extbase\Tests\Functional\Mvc\Controller\Fixture\Domain\Model\Model; +use TYPO3\CMS\Extbase\Tests\Functional\Mvc\Controller\Fixture\Domain\Model\ModelDto; +use TYPO3\CMS\Fluid\View\TemplateView; + +/** + * Fixture controller + */ +class ArgumentTestController extends ActionController +{ + /** + * Action to be used in `forwardAction`. + * + * @var string + */ + protected $forwardTargetAction; + + /** + * Arguments to be used in `forwardAction`. + * + * @var array + */ + protected $forwardTargetArguments; + + public function declareForwardTargetAction(string $forwardTargetAction): void + { + $this->forwardTargetAction = $forwardTargetAction; + } + + public function declareForwardTargetArguments(array $forwardTargetArguments): void + { + $this->forwardTargetArguments = $forwardTargetArguments; + } + + protected function setViewConfiguration(ViewInterface $view) + { + if ($view instanceof TemplateView) { + // assign template path directly without forging external configuration for that... + $view->getTemplatePaths()->setTemplateRootPaths([dirname(__DIR__) . '/Templates']); + } + } + + protected function addErrorFlashMessage() + { + // ignore flash messages + } + + public function forwardAction(): void + { + $this->forward( + $this->forwardTargetAction, + null, + null, + $this->forwardTargetArguments + ); + } + + /** + * @param \TYPO3\CMS\Extbase\Tests\Functional\Mvc\Controller\Fixture\Domain\Model\Model $preset + */ + public function inputPresetModelAction(Model $preset): void + { + $model = new Model(); + $model->setValue($preset->getValue()); + $this->view->assignMultiple([ + 'model' => $model, + ]); + } + + /** + * @param \TYPO3\CMS\Extbase\Tests\Functional\Mvc\Controller\Fixture\Domain\Model\ModelDto $preset + */ + public function inputPresetDtoAction(ModelDto $preset): void + { + $dto = new ModelDto(); + $dto->setValue($preset->getValue()); + $this->view->assignMultiple([ + 'dto' => $dto, + ]); + } + + /** + * @param \TYPO3\CMS\Extbase\Tests\Functional\Mvc\Controller\Fixture\Domain\Model\Model $model + * @Extbase\Validate("TYPO3.CMS.Extbase.Tests.Functional.Mvc.Controller.Fixture:FailingValidator", param="model") + */ + public function validateModelAction($model): void + { + // rendered in template `InputPresetModel.html` + } + + /** + * @param \TYPO3\CMS\Extbase\Tests\Functional\Mvc\Controller\Fixture\Domain\Model\ModelDto $dto + * @Extbase\Validate("TYPO3.CMS.Extbase.Tests.Functional.Mvc.Controller.Fixture:FailingValidator", param="dto") + */ + public function validateDtoAction($dto): void + { + // rendered in template `InputPresetDto.html` + } +} diff --git a/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/Fixture/Domain/Model/Model.php b/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/Fixture/Domain/Model/Model.php index 9b23078e46924a24071c967c0f05438ab6e8fbe4..fbdc2df6a174890e6f933df83d4edc73476959ca 100644 --- a/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/Fixture/Domain/Model/Model.php +++ b/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/Fixture/Domain/Model/Model.php @@ -24,4 +24,49 @@ use TYPO3\CMS\Extbase\DomainObject\AbstractEntity; */ class Model extends AbstractEntity { + /** + * @var string + */ + protected $value; + + /** + * @var Model + */ + protected $model; + + /** + * @return string + */ + public function getValue(): string + { + return $this->value; + } + + /** + * @param string $value + * @return self + */ + public function setValue(string $value): self + { + $this->value = $value; + return $this; + } + + /** + * @return Model + */ + public function getModel(): Model + { + return $this->model; + } + + /** + * @param Model $model + * @return self + */ + public function setModel(Model $model): self + { + $this->model = $model; + return $this; + } } diff --git a/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/Fixture/Domain/Model/ModelDto.php b/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/Fixture/Domain/Model/ModelDto.php new file mode 100644 index 0000000000000000000000000000000000000000..fd8ed59b884ebf584c54c0c0e69d70c95e53bb80 --- /dev/null +++ b/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/Fixture/Domain/Model/ModelDto.php @@ -0,0 +1,70 @@ +<?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\Extbase\Tests\Functional\Mvc\Controller\Fixture\Domain\Model; + +/** + * Fixture model data transfer object + */ +class ModelDto +{ + /** + * @var string + */ + protected $value; + + /** + * @var ModelDto + */ + protected $model; + + /** + * @return string + */ + public function getValue(): string + { + return $this->value; + } + + /** + * @param string $value + * @return self + */ + public function setValue(string $value): self + { + $this->value = $value; + return $this; + } + + /** + * @return ModelDto + */ + public function getModel(): ModelDto + { + return $this->model; + } + + /** + * @param ModelDto $model + * @return self + */ + public function setModel(ModelDto $model): self + { + $this->model = $model; + return $this; + } +} diff --git a/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/Fixture/Templates/ArgumentTest/InputPresetDto.html b/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/Fixture/Templates/ArgumentTest/InputPresetDto.html new file mode 100644 index 0000000000000000000000000000000000000000..b1e0892fe2ddc798b1f3ad534091039b0a20bfb9 --- /dev/null +++ b/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/Fixture/Templates/ArgumentTest/InputPresetDto.html @@ -0,0 +1,19 @@ +<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true"> +<main> + <f:form.validationResults> + <f:if condition="{validationResults.flattenedErrors}"> + <div id="validationResults"> + <f:format.json value="{validationResults.flattenedErrors}" /> + </div> + </f:if> + </f:form.validationResults> + + <f:form object="{dto}" name="dto" action="validate"> + <div class="form-group"> + <label>Name</label> + <f:form.textfield property="value" class="form-control" /> + </div> + <f:form.button class="btn btn-primary">Send</f:form.button> + </f:form> +</main> +</html> diff --git a/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/Fixture/Templates/ArgumentTest/InputPresetModel.html b/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/Fixture/Templates/ArgumentTest/InputPresetModel.html new file mode 100644 index 0000000000000000000000000000000000000000..e781e361869b16ab305bf38648f60e056d74002c --- /dev/null +++ b/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/Fixture/Templates/ArgumentTest/InputPresetModel.html @@ -0,0 +1,19 @@ +<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true"> +<main> + <f:form.validationResults> + <f:if condition="{validationResults.flattenedErrors}"> + <div id="validationResults"> + <f:format.json value="{validationResults.flattenedErrors}" /> + </div> + </f:if> + </f:form.validationResults> + + <f:form object="{model}" name="model" action="validate"> + <div class="form-group"> + <label>Name</label> + <f:form.textfield property="value" class="form-control" /> + </div> + <f:form.button class="btn btn-primary">Send</f:form.button> + </f:form> +</main> +</html> diff --git a/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/Fixture/Validation/Validator/FailingValidator.php b/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/Fixture/Validation/Validator/FailingValidator.php new file mode 100644 index 0000000000000000000000000000000000000000..e514d2d952f54116a8924164a1b9f12e4018376c --- /dev/null +++ b/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/Fixture/Validation/Validator/FailingValidator.php @@ -0,0 +1,34 @@ +<?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\Extbase\Tests\Functional\Mvc\Controller\Fixture\Validation\Validator; + +use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator; + +/** + * Validator which always fails + */ +class FailingValidator extends AbstractValidator +{ + /** + * @param mixed $value + */ + protected function isValid($value) + { + $this->addError('This will always fail', 157882910); + } +} diff --git a/typo3/sysext/fluid/Classes/ViewHelpers/FormViewHelper.php b/typo3/sysext/fluid/Classes/ViewHelpers/FormViewHelper.php index 35cd15a70c1cee14fe3caeaac8226462227c6e3d..962b42aee5852edc4f04a295b2342ea75a049f24 100644 --- a/typo3/sysext/fluid/Classes/ViewHelpers/FormViewHelper.php +++ b/typo3/sysext/fluid/Classes/ViewHelpers/FormViewHelper.php @@ -15,6 +15,7 @@ namespace TYPO3\CMS\Fluid\ViewHelpers; +use TYPO3\CMS\Extbase\Mvc\RequestInterface; use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder; /** @@ -267,6 +268,7 @@ class FormViewHelper extends \TYPO3\CMS\Fluid\ViewHelpers\Form\AbstractFormViewH */ protected function renderHiddenReferrerFields() { + /** @var RequestInterface $request */ $request = $this->renderingContext->getControllerContext()->getRequest(); $extensionName = $request->getControllerExtensionName(); $controllerName = $request->getControllerName(); @@ -281,6 +283,7 @@ class FormViewHelper extends \TYPO3\CMS\Fluid\ViewHelpers\Form\AbstractFormViewH $result .= '<input type="hidden" name="' . $this->prefixFieldName('__referrer[@extension]') . '" value="' . $extensionName . '" />' . LF; $result .= '<input type="hidden" name="' . $this->prefixFieldName('__referrer[@controller]') . '" value="' . $controllerName . '" />' . LF; $result .= '<input type="hidden" name="' . $this->prefixFieldName('__referrer[@action]') . '" value="' . $actionName . '" />' . LF; + $result .= '<input type="hidden" name="' . $this->prefixFieldName('__referrer[arguments]') . '" value="' . htmlspecialchars($this->hashService->appendHmac(base64_encode(serialize($request->getArguments())))) . '" />' . LF; $result .= '<input type="hidden" name="' . $this->prefixFieldName('__referrer[@request]') . '" value="' . htmlspecialchars($this->hashService->appendHmac(json_encode($actionRequest))) . '" />' . LF; return $result; diff --git a/typo3/sysext/fluid/Tests/Unit/ViewHelpers/FormViewHelperTest.php b/typo3/sysext/fluid/Tests/Unit/ViewHelpers/FormViewHelperTest.php index 0a0c0556efde5be255d59b1960421a5f6967245c..4f26ada5f10ea4edd31f8ae4d2c4e5d949d57c8d 100644 --- a/typo3/sysext/fluid/Tests/Unit/ViewHelpers/FormViewHelperTest.php +++ b/typo3/sysext/fluid/Tests/Unit/ViewHelpers/FormViewHelperTest.php @@ -253,6 +253,7 @@ class FormViewHelperTest extends ViewHelperBaseTestcase $expectedResult = \chr(10) . '<input type="hidden" name="__referrer[@extension]" value="extensionName" />' . \chr(10) . '<input type="hidden" name="__referrer[@controller]" value="controllerName" />' . \chr(10) . '<input type="hidden" name="__referrer[@action]" value="controllerActionName" />' + . \chr(10) . '<input type="hidden" name="__referrer[arguments]" value="" />' . \chr(10) . '<input type="hidden" name="__referrer[@request]" value="" />' . \chr(10); $viewHelper->setTagBuilder($this->tagBuilder); self::assertEquals($expectedResult, $hiddenFields);