Skip to content

Commit

Permalink
feat(csrf): Allows to disable CSRF on form (#FRAM-123) (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
vincent4vx committed Jul 31, 2023
1 parent c1882c3 commit 3a38e98
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 1 deletion.
3 changes: 3 additions & 0 deletions src/Aggregate/RootForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Bdf\Form\ElementInterface;
use Bdf\Form\Error\FormError;
use Bdf\Form\RootElementInterface;
use Bdf\Form\Util\RootFlagsTrait;
use Bdf\Form\View\ElementViewInterface;
use Iterator;
use OutOfBoundsException;
Expand Down Expand Up @@ -51,6 +52,8 @@
*/
final class RootForm implements RootElementInterface, ChildAggregateInterface
{
use RootFlagsTrait;

/**
* @var WeakReference<Form>
*/
Expand Down
20 changes: 20 additions & 0 deletions src/Csrf/CsrfValueValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,31 @@

use Bdf\Form\ElementInterface;
use Bdf\Form\Error\FormError;
use Bdf\Form\RootElementInterface;
use Bdf\Form\Validator\ConstraintValueValidator;
use Bdf\Form\Validator\ValueValidatorInterface;
use Exception;

use function method_exists;

/**
* Class CsrfValueValidator
*
* @implements ValueValidatorInterface<\Symfony\Component\Security\Csrf\CsrfToken>
*/
final class CsrfValueValidator implements ValueValidatorInterface
{
/**
* Flag for disable the CSRF validation
*
* Use this flag on the root form to disable the CSRF validation
* Note: The CSRF token will be still generated, and the element will be still present on the form
*
* @see RootElementInterface::set() For define the flag
* @see RootElementInterface::is() For check the flag
*/
public const FLAG_DISABLE_CSRF_VALIDATION = 'disable_csrf_validation';

/**
* Invalidate the token after verification ?
*
Expand Down Expand Up @@ -49,6 +63,12 @@ public function __construct(bool $invalidate = false, array $options = [])
*/
public function validate($value, ElementInterface $element): FormError
{
$root = $element->root();

if (method_exists($root, 'is') && $root->is(self::FLAG_DISABLE_CSRF_VALIDATION)) {
return FormError::null();
}

try {
return (new ConstraintValueValidator([new CsrfConstraint($this->options + ['manager' => $element->getTokenManager()])]))->validate($value, $element);
} finally {
Expand Down
20 changes: 20 additions & 0 deletions src/Custom/CustomForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@
use Bdf\Form\Aggregate\View\FormView;
use Bdf\Form\Child\ChildInterface;
use Bdf\Form\Child\Http\HttpFieldPath;
use Bdf\Form\Csrf\CsrfValueValidator;
use Bdf\Form\ElementInterface;
use Bdf\Form\Error\FormError;
use Bdf\Form\RootElementInterface;
use Bdf\Form\View\ElementViewInterface;
use Iterator;
use WeakReference;

use function method_exists;

/**
* Utility class for simply create a custom form element
*
Expand Down Expand Up @@ -300,6 +303,23 @@ final public function setPostConfigureHooks(array $hooks): void
$this->postConfigureHooks = $hooks;
}

/**
* Disable the CSRF validation
* The CSRF token will be still generated, and the element will be still present on the form
*
* Note: the CSRF will be disabled on the root form, so all the sub-forms will be affected
*
* @return void
*/
final public function disableCsrfValidation(): void
{
$root = $this->root();

if (method_exists($root, 'set')) {
$root->set(CsrfValueValidator::FLAG_DISABLE_CSRF_VALIDATION, true);
}
}

/**
* Get (or build) the inner form
*
Expand Down
3 changes: 3 additions & 0 deletions src/Leaf/LeafRootElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Bdf\Form\ElementInterface;
use Bdf\Form\Error\FormError;
use Bdf\Form\RootElementInterface;
use Bdf\Form\Util\RootFlagsTrait;
use Bdf\Form\View\ElementViewInterface;
use OutOfBoundsException;
use Symfony\Component\PropertyAccess\PropertyAccessor;
Expand All @@ -23,6 +24,8 @@
*/
final class LeafRootElement implements RootElementInterface
{
use RootFlagsTrait;

/**
* @var ElementInterface
*/
Expand Down
25 changes: 24 additions & 1 deletion src/RootElementInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
* This element contains all the HTTP fields
*
* @see ElementInterface::root() To get the root instance
*
* @method void set(string $flag, mixed $value) Define a flag value
* @method bool is(string $flag) Check if a flag is defined
*/
interface RootElementInterface extends ElementInterface
{
Expand Down Expand Up @@ -75,9 +78,29 @@ public function getValidator(): ValidatorInterface;
public function getPropertyAccessor(): PropertyAccessorInterface;

/**
* Get the constraint groups related the the button
* Get the constraint groups related to the button
*
* @return string[]
*/
public function constraintGroups(): array;

/**
* Set a flag value
*
* @param string $flag Flag name
* @param bool $value Flag value
*
* @return void
*/
//public function set(string $flag, bool $value): void;

/**
* Check a flag value
* If a flag is not defined, it returns false
*
* @param string $flag Flag name
*
* @return bool Flag value
*/
//public function is(string $flag): bool;
}
36 changes: 36 additions & 0 deletions src/Util/RootFlagsTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace Bdf\Form\Util;

use Bdf\Form\RootElementInterface;

/**
* Implements flags for root elements
*
* @psalm-require-implements RootElementInterface
*/
trait RootFlagsTrait
{
/**
* Map of flags
*
* @var array<string, bool>
*/
private $flags = [];

/**
* {@inheritdoc}
*/
public function set(string $flag, bool $value): void
{
$this->flags[$flag] = $value;
}

/**
* {@inheritdoc}
*/
public function is(string $flag): bool
{
return !empty($this->flags[$flag]);
}
}
19 changes: 19 additions & 0 deletions tests/Aggregate/RootFormTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -300,4 +300,23 @@ public function test_container()
$this->assertNull($root->container());
$root->setContainer(new Child('foo', new StringElement()));
}

/**
*
*/
public function test_flags()
{
$form = new RootForm(new Form(new ChildrenCollection()));

$this->assertFalse($form->is('foo'));
$this->assertFalse($form->is('bar'));

$form->set('foo', true);
$this->assertTrue($form->is('foo'));
$this->assertFalse($form->is('bar'));

$form->set('foo', false);
$this->assertFalse($form->is('foo'));
$this->assertFalse($form->is('bar'));
}
}
22 changes: 22 additions & 0 deletions tests/Csrf/CsrfElementTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

use Bdf\Form\Aggregate\Collection\ChildrenCollection;
use Bdf\Form\Aggregate\Form;
use Bdf\Form\Aggregate\FormBuilderInterface;
use Bdf\Form\Child\Child;
use Bdf\Form\Child\Http\HttpFieldPath;
use Bdf\Form\Custom\CustomForm;
use Bdf\Form\Leaf\LeafRootElement;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Csrf\CsrfToken;
Expand Down Expand Up @@ -233,4 +235,24 @@ public function test_error()
$this->assertEquals('INVALID_TOKEN_ERROR', $error->code());
$this->assertEmpty($error->children());
}

public function test_disable_validation_flag()
{
$form = new class extends CustomForm {
public function configure(FormBuilderInterface $builder): void
{
$builder->string('foo')->getter()->setter();
$builder->csrf();
}
};

$form->submit(['foo' => 'bar']);
$this->assertFalse($form->valid());
$this->assertEquals(['_token' => 'The CSRF token is invalid.'], $form->error()->toArray());

$form->root()->set(CsrfValueValidator::FLAG_DISABLE_CSRF_VALIDATION, true);

$form->submit(['foo' => 'bar']);
$this->assertTrue($form->valid());
}
}
21 changes: 21 additions & 0 deletions tests/Custom/CustomFormTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Bdf\Form\Child\Child;
use Bdf\Form\Child\ChildInterface;
use Bdf\Form\Child\Http\HttpFieldPath;
use Bdf\Form\Csrf\CsrfValueValidator;
use Bdf\Form\ElementInterface;
use Bdf\Form\Error\FormError;
use Bdf\Form\Error\FormErrorPrinterInterface;
Expand Down Expand Up @@ -534,6 +535,26 @@ protected function configure(FormBuilderInterface $builder): void

$this->assertFalse($form->submit(['person' => ['firstName' => 'John']])->valid());
}

public function test_disableCsrfValidation()
{
$form = new class extends CustomForm {
public function configure(FormBuilderInterface $builder): void
{
$builder->string('foo')->getter()->setter();
$builder->csrf();
}
};

$form->submit(['foo' => 'bar']);
$this->assertFalse($form->valid());
$this->assertEquals(['_token' => 'The CSRF token is invalid.'], $form->error()->toArray());

$form->disableCsrfValidation();

$form->submit(['foo' => 'bar']);
$this->assertTrue($form->valid());
}
}

/**
Expand Down
20 changes: 20 additions & 0 deletions tests/Leaf/LeafRootElementTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,24 @@ public function test_constraintGroups()

$this->assertEquals([Constraint::DEFAULT_GROUP], $root->constraintGroups());
}

/**
*
*/
public function test_flags()
{
$element = $this->createMock(ElementInterface::class);
$root = new LeafRootElement($element);

$this->assertFalse($root->is('foo'));
$this->assertFalse($root->is('bar'));

$root->set('foo', true);
$this->assertTrue($root->is('foo'));
$this->assertFalse($root->is('bar'));

$root->set('foo', false);
$this->assertFalse($root->is('foo'));
$this->assertFalse($root->is('bar'));
}
}

0 comments on commit 3a38e98

Please sign in to comment.