瀏覽代碼

[Form] Moved CSRF protection into separate field

Bernhard Schussek 14 年之前
父節點
當前提交
3e17b26105

+ 4 - 1
src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml

@@ -75,7 +75,6 @@
         <service id="form.type.form" class="Symfony\Component\Form\Type\FormType">
             <tag name="form.type" alias="form" />
             <argument type="service" id="form.theme" />
-            <argument type="service" id="form.csrf_provider" />
         </service>
         <service id="form.type.birthday" class="Symfony\Component\Form\Type\BirthdayFieldType">
             <tag name="form.type" alias="birthday" />
@@ -92,6 +91,10 @@
         <service id="form.type.country" class="Symfony\Component\Form\Type\CountryFieldType">
             <tag name="form.type" alias="country" />
         </service>
+        <service id="form.type.csrf" class="Symfony\Component\Form\Type\CsrfFieldType">
+            <tag name="form.type" alias="csrf" />
+            <argument type="service" id="form.csrf_provider" />
+        </service>
         <service id="form.type.date" class="Symfony\Component\Form\Type\DateFieldType">
             <tag name="form.type" alias="date" />
         </service>

+ 1 - 1
src/Symfony/Bundle/TwigBundle/Resources/views/widgets.html.twig

@@ -69,8 +69,8 @@
 {% endspaceless %}
 {% endblock hidden__widget %}
 
-{# Don't render hidden fields - they will be rendered in the "rest" helper #}
 {% block hidden__row %}
+    {{ this.widget }}
 {% endblock hidden__row %}
 
 {% block textarea__widget %}

+ 31 - 0
src/Symfony/Component/Form/DataValidator/CallbackValidator.php

@@ -0,0 +1,31 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\DataValidator;
+
+use Symfony\Component\Form\FieldInterface;
+
+class CallbackValidator implements DataValidatorInterface
+{
+    private $callback;
+
+    public function __construct($callback)
+    {
+        // TODO validate callback
+
+        $this->callback = $callback;
+    }
+
+    public function validate(FieldInterface $field)
+    {
+        return call_user_func($this->callback, $field);
+    }
+}

+ 21 - 20
src/Symfony/Component/Form/DataValidator/DelegatingValidator.php

@@ -13,12 +13,11 @@ namespace Symfony\Component\Form\DataValidator;
 
 use Symfony\Component\Form\FieldInterface;
 use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\Error;
 use Symfony\Component\Form\DataError;
 use Symfony\Component\Form\FieldError;
+use Symfony\Component\Form\PropertyPath;
 use Symfony\Component\Form\PropertyPathIterator;
-use Symfony\Component\Form\Events;
-use Symfony\Component\Form\Event\DataEvent;
-use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 use Symfony\Component\Validator\ValidatorInterface;
 
 class DelegatingValidator implements DataValidatorInterface
@@ -35,24 +34,26 @@ class DelegatingValidator implements DataValidatorInterface
      */
     public function validate(FieldInterface $field)
     {
-        // Validate the field in group "Default"
-        // Validation of the data in the custom group is done by validateData(),
-        // which is constrained by the Execute constraint
-        if ($violations = $this->validator->validate($field)) {
-            foreach ($violations as $violation) {
-                $propertyPath = new PropertyPath($violation->getPropertyPath());
-                $iterator = $propertyPath->getIterator();
-                $template = $violation->getMessageTemplate();
-                $parameters = $violation->getMessageParameters();
-
-                if ($iterator->current() == 'data') {
-                    $iterator->next(); // point at the first data element
-                    $error = new DataError($template, $parameters);
-                } else {
-                    $error = new FieldError($template, $parameters);
-                }
+        if ($field->isRoot()) {
+            // Validate the field in group "Default"
+            // Validation of the data in the custom group is done by validateData(),
+            // which is constrained by the Execute constraint
+            if ($violations = $this->validator->validate($field)) {
+                foreach ($violations as $violation) {
+                    $propertyPath = new PropertyPath($violation->getPropertyPath());
+                    $iterator = $propertyPath->getIterator();
+                    $template = $violation->getMessageTemplate();
+                    $parameters = $violation->getMessageParameters();
+
+                    if ($iterator->current() == 'data') {
+                        $iterator->next(); // point at the first data element
+                        $error = new DataError($template, $parameters);
+                    } else {
+                        $error = new FieldError($template, $parameters);
+                    }
 
-                $this->mapError($error, $field, $iterator);
+                    $this->mapError($error, $field, $iterator);
+                }
             }
         }
     }

+ 1 - 1
src/Symfony/Component/Form/Field.php

@@ -276,7 +276,7 @@ class Field implements FieldInterface
         $event = new DataEvent($this, $clientData);
         $this->dispatcher->dispatch(Events::postBind, $event);
 
-        if ($this->isRoot() && $this->dataValidator) {
+        if ($this->dataValidator) {
             $this->dataValidator->validate($this);
         }
     }

+ 11 - 1
src/Symfony/Component/Form/Form.php

@@ -164,6 +164,10 @@ class Form extends Field implements \IteratorAggregate, FormInterface
     {
         $data = $event->getData();
 
+        if (empty($data)) {
+            $data = array();
+        }
+
         if (!is_array($data)) {
             throw new UnexpectedTypeException($data, 'array');
         }
@@ -189,9 +193,15 @@ class Form extends Field implements \IteratorAggregate, FormInterface
 
     public function preBind(DataEvent $event)
     {
+        $data = $event->getData();
+
+        if (empty($data)) {
+            $data = array();
+        }
+
         $this->extraFields = array();
 
-        foreach ((array)$event->getData() as $name => $value) {
+        foreach ($data as $name => $value) {
             if (!$this->has($name)) {
                 $this->extraFields[] = $name;
             }

+ 7 - 14
src/Symfony/Component/Form/FormBuilder.php

@@ -28,15 +28,6 @@ class FormBuilder extends FieldBuilder
 
     private $dataMapper;
 
-    public function __construct(ThemeInterface $theme,
-            EventDispatcherInterface $dispatcher,
-            CsrfProviderInterface $csrfProvider)
-    {
-        parent::__construct($theme, $dispatcher);
-
-        $this->csrfProvider = $csrfProvider;
-    }
-
     public function setDataMapper(DataMapperInterface $dataMapper)
     {
         $this->dataMapper = $dataMapper;
@@ -226,12 +217,14 @@ class FormBuilder extends FieldBuilder
     protected function buildCsrfProtection()
     {
         if ($this->hasCsrfProtection()) {
-            $token = $this->csrfProvider->generateCsrfToken(get_class($this));
+            // need a page ID here, maybe FormType class?
+            $options = array('page_id' => null);
+
+            if ($this->csrfProvider) {
+                $options['csrf_provider'] = $this->csrfProvider;
+            }
 
-            $this->add('hidden', $this->csrfFieldName, array(
-                'data' => $token,
-                'property_path' => null,
-            ));
+            $this->add('csrf', $this->csrfFieldName, $options);
         }
     }
 

+ 0 - 5
src/Symfony/Component/Form/Resources/config/validation.xml

@@ -27,11 +27,6 @@
         <option name="message">The uploaded file was too large. Please try to upload a smaller file</option>
       </constraint>
     </getter>
-    <getter property="csrfTokenValid">
-      <constraint name="AssertTrue">
-        <option name="message">The CSRF token is invalid. Please try to resubmit the form</option>
-      </constraint>
-    </getter>
   </class>
 
   <class name="Symfony\Component\Form\RepeatedField">

+ 65 - 0
src/Symfony/Component/Form/Type/CsrfFieldType.php

@@ -0,0 +1,65 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\Type;
+
+use Symfony\Component\Form\FieldInterface;
+use Symfony\Component\Form\FieldBuilder;
+use Symfony\Component\Form\FieldError;
+use Symfony\Component\Form\CsrfProvider\CsrfProviderInterface;
+use Symfony\Component\Form\DataValidator\CallbackValidator;
+
+class CsrfFieldType extends AbstractFieldType
+{
+    private $csrfProvider;
+
+    public function __construct(CsrfProviderInterface $csrfProvider)
+    {
+        $this->csrfProvider = $csrfProvider;
+    }
+
+    public function configure(FieldBuilder $builder, array $options)
+    {
+        $csrfProvider = $options['csrf_provider'];
+        $pageId = $options['page_id'];
+
+        $builder
+            ->setData($csrfProvider->generateCsrfToken($pageId))
+            ->setDataValidator(new CallbackValidator(
+                function (FieldInterface $field) use ($csrfProvider, $pageId) {
+                    if (!$csrfProvider->isCsrfTokenValid($pageId, $field->getData())) {
+                        // FIXME this error is currently not displayed
+                        // it needs to be passed up to the form
+                        $field->addError(new FieldError('The CSRF token is invalid. Please try to resubmit the form'));
+                    }
+                }
+            ));
+    }
+
+    public function getDefaultOptions(array $options)
+    {
+        return array(
+            'csrf_provider' => $this->csrfProvider,
+            'page_id' => null,
+            'property_path' => null,
+        );
+    }
+
+    public function getParent(array $options)
+    {
+        return 'hidden';
+    }
+
+    public function getName()
+    {
+        return 'csrf';
+    }
+}

+ 2 - 5
src/Symfony/Component/Form/Type/FormType.php

@@ -23,12 +23,9 @@ class FormType extends AbstractFieldType
 {
     private $theme;
 
-    private $csrfProvider;
-
-    public function __construct(ThemeInterface $theme, CsrfProviderInterface $csrfProvider)
+    public function __construct(ThemeInterface $theme)
     {
         $this->theme = $theme;
-        $this->csrfProvider = $csrfProvider;
     }
 
     public function configure(FieldBuilder $builder, array $options)
@@ -62,7 +59,7 @@ class FormType extends AbstractFieldType
 
     public function createBuilder(array $options)
     {
-        return new FormBuilder($this->theme, new EventDispatcher(), $this->csrfProvider);
+        return new FormBuilder($this->theme, new EventDispatcher());
     }
 
     public function getParent(array $options)

+ 2 - 1
src/Symfony/Component/Form/Type/Loader/DefaultTypeLoader.php

@@ -30,11 +30,12 @@ class DefaultTypeLoader implements TypeLoaderInterface
             EntityManager $em = null)
     {
         $this->addType(new Type\FieldType($theme, $validator));
-        $this->addType(new Type\FormType($theme, $csrfProvider));
+        $this->addType(new Type\FormType($theme));
         $this->addType(new Type\CheckboxFieldType());
         $this->addType(new Type\ChoiceFieldType());
         $this->addType(new Type\CollectionFieldType());
         $this->addType(new Type\CountryFieldType());
+        $this->addType(new Type\CsrfFieldType($csrfProvider));
         $this->addType(new Type\DateFieldType());
         $this->addType(new Type\DateTimeFieldType());
         $this->addType(new Type\FileFieldType($storage));