From 3a38e988177b3f52152c575405ec8b7e98fe5ef6 Mon Sep 17 00:00:00 2001 From: Vincent Quatrevieux Date: Mon, 31 Jul 2023 21:56:09 +0200 Subject: [PATCH] feat(csrf): Allows to disable CSRF on form (#FRAM-123) (#10) --- src/Aggregate/RootForm.php | 3 +++ src/Csrf/CsrfValueValidator.php | 20 +++++++++++++++++ src/Custom/CustomForm.php | 20 +++++++++++++++++ src/Leaf/LeafRootElement.php | 3 +++ src/RootElementInterface.php | 25 ++++++++++++++++++++- src/Util/RootFlagsTrait.php | 36 ++++++++++++++++++++++++++++++ tests/Aggregate/RootFormTest.php | 19 ++++++++++++++++ tests/Csrf/CsrfElementTest.php | 22 ++++++++++++++++++ tests/Custom/CustomFormTest.php | 21 +++++++++++++++++ tests/Leaf/LeafRootElementTest.php | 20 +++++++++++++++++ 10 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 src/Util/RootFlagsTrait.php diff --git a/src/Aggregate/RootForm.php b/src/Aggregate/RootForm.php index 9072154..6678e3e 100644 --- a/src/Aggregate/RootForm.php +++ b/src/Aggregate/RootForm.php @@ -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; @@ -51,6 +52,8 @@ */ final class RootForm implements RootElementInterface, ChildAggregateInterface { + use RootFlagsTrait; + /** * @var WeakReference
*/ diff --git a/src/Csrf/CsrfValueValidator.php b/src/Csrf/CsrfValueValidator.php index 1cbafbd..a9f0cdb 100644 --- a/src/Csrf/CsrfValueValidator.php +++ b/src/Csrf/CsrfValueValidator.php @@ -4,10 +4,13 @@ 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 * @@ -15,6 +18,17 @@ */ 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 ? * @@ -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 { diff --git a/src/Custom/CustomForm.php b/src/Custom/CustomForm.php index af7e7da..39169d7 100644 --- a/src/Custom/CustomForm.php +++ b/src/Custom/CustomForm.php @@ -8,6 +8,7 @@ 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; @@ -15,6 +16,8 @@ use Iterator; use WeakReference; +use function method_exists; + /** * Utility class for simply create a custom form element * @@ -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 * diff --git a/src/Leaf/LeafRootElement.php b/src/Leaf/LeafRootElement.php index c2c8393..5b60f41 100644 --- a/src/Leaf/LeafRootElement.php +++ b/src/Leaf/LeafRootElement.php @@ -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; @@ -23,6 +24,8 @@ */ final class LeafRootElement implements RootElementInterface { + use RootFlagsTrait; + /** * @var ElementInterface */ diff --git a/src/RootElementInterface.php b/src/RootElementInterface.php index c7c4892..2682592 100644 --- a/src/RootElementInterface.php +++ b/src/RootElementInterface.php @@ -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 { @@ -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; } diff --git a/src/Util/RootFlagsTrait.php b/src/Util/RootFlagsTrait.php new file mode 100644 index 0000000..87919df --- /dev/null +++ b/src/Util/RootFlagsTrait.php @@ -0,0 +1,36 @@ + + */ + 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]); + } +} diff --git a/tests/Aggregate/RootFormTest.php b/tests/Aggregate/RootFormTest.php index 97a5287..9117585 100644 --- a/tests/Aggregate/RootFormTest.php +++ b/tests/Aggregate/RootFormTest.php @@ -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')); + } } diff --git a/tests/Csrf/CsrfElementTest.php b/tests/Csrf/CsrfElementTest.php index 81d88e2..5743b35 100644 --- a/tests/Csrf/CsrfElementTest.php +++ b/tests/Csrf/CsrfElementTest.php @@ -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; @@ -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()); + } } diff --git a/tests/Custom/CustomFormTest.php b/tests/Custom/CustomFormTest.php index eef5565..15d58f4 100644 --- a/tests/Custom/CustomFormTest.php +++ b/tests/Custom/CustomFormTest.php @@ -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; @@ -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()); + } } /** diff --git a/tests/Leaf/LeafRootElementTest.php b/tests/Leaf/LeafRootElementTest.php index c06c639..69b2d6d 100644 --- a/tests/Leaf/LeafRootElementTest.php +++ b/tests/Leaf/LeafRootElementTest.php @@ -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')); + } }