Skip to content

Commit

Permalink
Merge pull request #64 from mficzel/feature/makeReferrerFieldsOptional
Browse files Browse the repository at this point in the history
FEATURE: Add `enableReferrer` and `enableTrustedProperties` to the Form Definition
  • Loading branch information
mficzel committed May 2, 2024
2 parents 90c960f + 6ac57cb commit 75e0800
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 8 deletions.
25 changes: 21 additions & 4 deletions Classes/Domain/Form.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ class Form extends AbstractFormObject
*/
protected $encoding;

/**
* @var bool
*/
protected $enableReferrer;

/**
* @var bool
*/
protected $enableTrustedProperties;

/**
* @var string|null
*/
Expand All @@ -89,15 +99,19 @@ class Form extends AbstractFormObject
* @param string|null $target
* @param string|null $method
* @param string|null $encoding
* @param bool $enableReferrer
* @param bool $enableTrustedProperties
*/
public function __construct(ActionRequest $request = null, $data = null, ?string $namespace = null, ?string $target = null, ?string $method = "get", ?string $encoding = null)
public function __construct(ActionRequest $request = null, $data = null, ?string $namespace = null, ?string $target = null, ?string $method = "get", ?string $encoding = null, bool $enableReferrer = true, bool $enableTrustedProperties = true)
{
$this->request = $request;
$this->data = $data;
$this->namespace = $namespace;
$this->target = $target;
$this->method = $method;
$this->encoding = $encoding;
$this->enableReferrer = $enableReferrer;
$this->enableTrustedProperties = $enableTrustedProperties;

// determine submitted values and result from request
/** @phpstan-ignore-next-line the return type of $request->getInternalArgument is misleading */
Expand Down Expand Up @@ -245,7 +259,7 @@ public function calculateHiddenFields(string $content = null): array
// forwarded to the previous request where the __submittedArguments and
// __submittedArgumentValidationResults can be handled from Form.createField or custom logic.
//
if ($request) {
if ($request && ($this->enableReferrer === true)) {
$childRequestArgumentNamespace = null;
while ($request instanceof ActionRequest) {
$requestArgumentNamespace = $request->getArgumentNamespace();
Expand Down Expand Up @@ -335,7 +349,8 @@ public function calculateHiddenFields(string $content = null): array
foreach ($formFieldNames as $name) {
$path = $this->fieldNameToPath(substr($name, strlen($fieldNamePrefix)));
$pathSegments = explode('.', $path);
for ($i = 1; $i < count($pathSegments); $i++) {
$pathSegmentCount = count($pathSegments);
for ($i = 1; $i < $pathSegmentCount; $i++) {
$possiblePathes[] = implode('.', array_slice($pathSegments, 0, $i));
}
}
Expand All @@ -357,7 +372,9 @@ public function calculateHiddenFields(string $content = null): array
// A signed array of all properties the property mapper is allowed to convert from string to the target type
// so no property mapping configuration is needed on the target controller
//
$hiddenFields[ $this->prefixFieldName('__trustedProperties', $fieldNamePrefix) ] = $this->mvcPropertyMappingConfigurationService->generateTrustedPropertiesToken($formFieldNames, $fieldNamePrefix);
if ($this->enableTrustedProperties === true) {
$hiddenFields[$this->prefixFieldName('__trustedProperties', $fieldNamePrefix)] = $this->mvcPropertyMappingConfigurationService->generateTrustedPropertiesToken($formFieldNames, $fieldNamePrefix);
}

return $hiddenFields;
}
Expand Down
23 changes: 22 additions & 1 deletion Classes/FusionObjects/FormDefinitionImplementation.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,23 @@ protected function getEncoding(): ?string
{
return $this->fusionValue('encoding');
}

/**
* @return bool
*/
protected function getEnableReferrer(): bool
{
return (bool)$this->fusionValue('enableReferrer');
}

/**
* @return bool
*/
protected function getEnableTrustedProperties(): bool
{
return (bool)$this->fusionValue('enableTrustedProperties');
}

/**
* @return Form
*/
Expand All @@ -78,14 +95,18 @@ public function evaluate(): Form
$target = $this->getTarget();
$method = $this->getMethod();
$encoding = $this->getEncoding();
$enableReferrer = $this->getEnableReferrer();
$enableTrustedProperties = $this->getEnableTrustedProperties();

return new Form(
$request,
$data,
$namespace,
$target,
$method,
$encoding
$encoding,
$enableReferrer,
$enableTrustedProperties
);
}
}
7 changes: 6 additions & 1 deletion Classes/Runtime/FusionObjects/RuntimeFormImplementation.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,18 @@ public function evaluate(): string
protected function renderForm(ProcessInterface $process, ActionRequest $formRequest, array $attributes)
{
$data = $process->getData();

// @todo adjust after raising min php version to 8+
// new Form(request:$formRequest, data: $data, method: 'post', encoding:'multipart/form-data', enableReferrer: false);
$form = new Form(
$formRequest,
$data,
null,
null,
'post',
'multipart/form-data'
'multipart/form-data',
false,
true
);

$context = $this->runtime->getCurrentContext();
Expand Down
6 changes: 6 additions & 0 deletions Documentation/FusionReference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ In addition the form component will also:
:form.target: (string, default to `Neos.Fusion:UriBuilder`) The target uri the form will be sent to.
:form.method: (string, default to `post`) The form method.
:form.encoding: (string, default to `multipart/form-data` when `form.method` == `post`) The form enctype `multipart/form.data` is required for file-uploads.:attributes: (string), all props are rendered as attributes to the form tag
:form.enableReferrer: (bool, defaults to true) Enable the generation of hidden `__referrer` fields. Can be disabled when the `method` is `get` or no flow validation is used
:form.enableTrustedProperties: (bool, defaults to true) Enable the generation of hidden `__trustedProperties` fields. Can be disabled when flow property mapping is not in use
:attributes: (`Neos.Fusion:DataStructure`_) form attributes, will override all automatically rendered ones
:content: (string, defaults to '') afx content with the form controls

Expand Down Expand Up @@ -287,6 +289,8 @@ The Form component is a base prototype for rendering forms in afx. The prototype
:form.target: (string, default to `Neos.Fusion:UriBuilder`) The target uri the form will be sent to.
:form.method: (string, default to `post`) The form method.
:form.encoding: (string, default to `multipart/form-data` when `form.method` == `post`) The form enctype `multipart/form.data` is required for file-uploads.
:form.enableReferrer: (bool, defaults to true) Enable the generation of hidden `__referrer` fields. Can be disabled when the `method` is `get` or no flow validation is used
:form.enableTrustedProperties: (bool, defaults to true) Enable the generation of hidden `__trustedProperties` fields. Can be disabled when flow property mapping is not in use
:attributes: (`Neos.Fusion:DataStructure`_) form attributes, will override all automatically rendered ones
:content: (string) form content, supported where needed

Expand Down Expand Up @@ -336,6 +340,8 @@ by the `Neos.Fusion.Form:Component.Form`_ prototype.
:target: (string, default to `Neos.Fusion:UriBuilder`) The target uri the form will be sent to.
:method: (string, default to `post`) The form method.
:encoding: (string, default to `multipart/form-data` when `form.method` == `post`) The form enctype `multipart/form.data` is required for file-uploads.
:enableReferrer: (bool, defaults to true) Enable the generation of hidden `__referrer` fields. Can be disabled when the `method` is `get` or no flow validation is used
:enableTrustedProperties: (bool, defaults to true) Enable the generation of hidden `__trustedProperties` fields. Can be disabled when flow property mapping is not in use

Neos.Fusion.Form:Definition.Field
---------------------------------
Expand Down
4 changes: 4 additions & 0 deletions Resources/Private/Fusion/Prototypes/Definition/Form.fusion
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ prototype(Neos.Fusion.Form:Definition.Form) {
target = ${PropTypes.string}
method = ${PropTypes.string}
encoding = ${PropTypes.string}
enableReferrer = ${PropTypes.boolean}
enableTrustedProperties = ${PropTypes.boolean}
}

request = ${request}
Expand All @@ -17,4 +19,6 @@ prototype(Neos.Fusion.Form:Definition.Form) {
target = Neos.Fusion:UriBuilder
method = 'post'
encoding = ${(String.toLowerCase(this.method) == 'post') ? 'multipart/form-data' : null}
enableReferrer = true
enableTrustedProperties = true
}
128 changes: 126 additions & 2 deletions Tests/Functional/FormTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public function setUp(): void
/**
* @return Form
*/
protected function createForm(): Form
protected function createForm(ActionRequest $request = null, $data = null, ?string $namespace = null, ?string $target = null, ?string $method = "get", ?string $encoding = null, bool $enableReferrer = true, bool $enableTrustedProperties = true): Form
{
$reflector = new \ReflectionClass(Form::class);
$form = $reflector->newInstanceArgs(func_get_args());
Expand Down Expand Up @@ -81,6 +81,19 @@ public function calculateHiddenFieldsReturnsOnlyTrustedPropertiesTokenIfNoFormOr
$this->assertEquals($expectation, $hiddenFields);
}

/**
* @test
*/
public function calculateHiddenFieldsWillSkipTrustedPropertiesTokenIfDisabled()
{
// @todo once php 8 is min version adjust to `$this->createForm(disableTrustedProperties: true);`
$form = $this->createForm(null, null, null, null, null, null, true, false);
$this->mvcPropertyMappingConfigurationService->expects($this->never())->method('generateTrustedPropertiesToken');

$hiddenFields = $form->calculateHiddenFields(null);
$this->assertArrayNotHasKey('__trustedProperties', $hiddenFields);
}

/**
* @test
*/
Expand Down Expand Up @@ -238,7 +251,72 @@ public function calculateHiddenFieldsAddsReferrerFieldsIfFormWithActionRequestIs
/**
* @test
*/
public function calculateHiddenFieldsAddsReferrerFieldsIfFormWithNestedActionRequestIsGiven()
public function calculateHiddenFieldsDoesNotAddsReferrerFieldsIfFormWithActionRequestWhenDisabled()
{
$request = $this->getMockBuilder(ActionRequest::class)->disableOriginalConstructor()->getMock();
$request->method('getControllerPackageKey')->willReturn('Vendor.Example');
$request->method('getControllerSubpackageKey')->willReturn('Application');
$request->method('getControllerName')->willReturn('Main');
$request->method('getControllerActionName')->willReturn('List');
$request->method('isMainRequest')->willReturn(true);
$request->method('getArguments')->willReturn([]);
$request->method('getArgumentNamespace')->willReturn('');

// @todo adjust to $this->createForm(request: $request, disableReferrer: true); once php 8 is min version
$form = $this->createForm($request, null, null, null, null, null, false);

$hiddenFields = $form->calculateHiddenFields(null);

$this->assertArrayNotHasKey('__referrer[@package]', $hiddenFields);
$this->assertArrayNotHasKey('__referrer[@subpackage]', $hiddenFields);
$this->assertArrayNotHasKey('__referrer[@controller]', $hiddenFields);
$this->assertArrayNotHasKey('__referrer[@action]', $hiddenFields);
$this->assertArrayNotHasKey('__referrer[arguments]', $hiddenFields);
}

/**
* @test
*/
public function calculateHiddenFieldsDoesNotAddReferrerFieldsIfFormWithNestedActionRequestWhenDisabled()
{
$parentRequest = $this->getMockBuilder(ActionRequest::class)->disableOriginalConstructor()->getMock();
$parentRequest->method('getControllerPackageKey')->willReturn('Vendor.Foo');
$parentRequest->method('getControllerSubpackageKey')->willReturn('Application');
$parentRequest->method('getControllerName')->willReturn('Parent');
$parentRequest->method('getControllerActionName')->willReturn('Something');
$parentRequest->method('isMainRequest')->willReturn(true);

$request = $this->getMockBuilder(ActionRequest::class)->disableOriginalConstructor()->getMock();
$request->method('getControllerPackageKey')->willReturn('Vendor.Bar');
$request->method('getControllerSubpackageKey')->willReturn('');
$request->method('getControllerName')->willReturn('Child');
$request->method('getControllerActionName')->willReturn('SomethingElse');
$request->method('getArgumentNamespace')->willReturn('childNamespace');
$request->method('isMainRequest')->willReturn(false);
$request->method('getParentRequest')->willReturn($parentRequest);

$form = $this->createForm($request, null, null, null, null, null, false);

$hiddenFields = $form->calculateHiddenFields(null);

$this->assertArrayNotHasKey('__referrer[@package]', $hiddenFields);
$this->assertArrayNotHasKey('__referrer[@subpackage]', $hiddenFields);
$this->assertArrayNotHasKey('__referrer[@controller]', $hiddenFields);
$this->assertArrayNotHasKey('__referrer[@action]', $hiddenFields);

$this->assertArrayNotHasKey('childNamespace[__referrer][@package]', $hiddenFields);
$this->assertArrayNotHasKey('childNamespace[__referrer][@subpackage]', $hiddenFields);
$this->assertArrayNotHasKey('childNamespace[__referrer][@controller]', $hiddenFields);
$this->assertArrayNotHasKey('childNamespace[__referrer][@action]', $hiddenFields);

$this->assertArrayNotHasKey('__referrer[arguments]', $hiddenFields);
$this->assertArrayNotHasKey('childNamespace[__referrer][arguments]', $hiddenFields);
}

/**
* @test
*/
public function calculateHiddenFieldsAddsReferrerFieldsIfFormWithNestedActionRequest()
{
$parentRequest = $this->getMockBuilder(ActionRequest::class)->disableOriginalConstructor()->getMock();
$parentRequest->method('getControllerPackageKey')->willReturn('Vendor.Foo');
Expand Down Expand Up @@ -319,6 +397,52 @@ public function calculateHiddenFieldsAddsReferrerFieldArgumentsIfFormWithNestedA
$this->assertEquals('--argumentsWithHmac--', $hiddenFields['childNamespace[__referrer][arguments]']);
}

/**
* @test
*/
public function calculateHiddenFieldsDoesNotAddReferrerFieldArgumentsIfFormWithNestedActionRequestWhenDisabled()
{
$childRequestArguments = ['foo' => 456, 'bar' => 'another string'];
$parentRequestArguments = ['foo' => 123, 'bar' => 'string'];
$parentWithChildRequestArguments = array_merge($parentRequestArguments, ['childNamespace' => $childRequestArguments]);

$parentRequest = $this->getMockBuilder(ActionRequest::class)->disableOriginalConstructor()->getMock();
$parentRequest->method('getControllerPackageKey')->willReturn('Vendor.Foo');
$parentRequest->method('getControllerSubpackageKey')->willReturn('Application');
$parentRequest->method('getControllerName')->willReturn('Parent');
$parentRequest->method('getControllerActionName')->willReturn('Something');
$parentRequest->method('getArguments')->willReturn($parentWithChildRequestArguments);
$parentRequest->method('getArgumentNamespace')->willReturn('');
$parentRequest->method('isMainRequest')->willReturn(true);

$request = $this->getMockBuilder(ActionRequest::class)->disableOriginalConstructor()->getMock();
$request->method('getControllerPackageKey')->willReturn('Vendor.Bar');
$request->method('getControllerSubpackageKey')->willReturn('');
$request->method('getControllerName')->willReturn('Child');
$request->method('getControllerActionName')->willReturn('SomethingElse');
$request->method('getArguments')->willReturn($childRequestArguments);
$request->method('getArgumentNamespace')->willReturn('childNamespace');
$request->method('isMainRequest')->willReturn(false);
$request->method('getParentRequest')->willReturn($parentRequest);

// only arguments in each requests namespace are passed to the hashing service
// so for the parent request the child request namespace is excluded
$this->hashService
->method('appendHmac')
->withConsecutive(
[base64_encode(serialize($childRequestArguments))],
[base64_encode(serialize($parentRequestArguments))]
)
->willReturn('--argumentsWithHmac--');

// @todo adjust to $this->createForm(request: $request, disableReferrer: true); once php 8 is min version
$form = $this->createForm($request, null, null, null, null, null, false);
$hiddenFields = $form->calculateHiddenFields(null);

$this->assertArrayNotHasKey('__referrer[arguments]', $hiddenFields);
$this->assertArrayNotHasKey('childNamespace[__referrer][arguments]', $hiddenFields);
}

/**
* @test
*/
Expand Down

0 comments on commit 75e0800

Please sign in to comment.