[Form] Throwing an AlreadyBoundException in `add`, `remove`, `setParent`, `bind` and `setData` if called on a bound form
This commit is contained in:
parent
92cb685ebc
commit
cde34fd8ce
|
@ -210,6 +210,8 @@ To get the diff between two versions, go to https://github.com/symfony/symfony/c
|
||||||
* added constant Guess::VERY_HIGH_CONFIDENCE
|
* added constant Guess::VERY_HIGH_CONFIDENCE
|
||||||
* FormType::getDefaultOptions() now sees default options defined by parent types
|
* FormType::getDefaultOptions() now sees default options defined by parent types
|
||||||
* [BC BREAK] FormType::getParent() does not see default options anymore
|
* [BC BREAK] FormType::getParent() does not see default options anymore
|
||||||
|
* [BC BREAK] The methods `add`, `remove`, `setParent`, `bind` and `setData`
|
||||||
|
in class Form now throw an exception if the form is already bound
|
||||||
|
|
||||||
### HttpFoundation
|
### HttpFoundation
|
||||||
|
|
||||||
|
|
|
@ -248,3 +248,11 @@ UPGRADE FROM 2.0 to 2.1
|
||||||
{
|
{
|
||||||
return isset($options['widget']) && 'single_text' === $options['widget'] ? 'text' : 'choice';
|
return isset($options['widget']) && 'single_text' === $options['widget'] ? 'text' : 'choice';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
* The methods `add`, `remove`, `setParent`, `bind` and `setData` in class Form
|
||||||
|
now throw an exception if the form is already bound
|
||||||
|
|
||||||
|
If you used these methods on bound forms, you should consider moving your
|
||||||
|
logic to an event listener listening to either of the events
|
||||||
|
FormEvents::PRE_BIND, FormEvents::BIND_CLIENT_DATA or
|
||||||
|
FormEvents::BIND_NORM_DATA instead.
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Form\Extension\Csrf\EventListener;
|
||||||
|
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
use Symfony\Component\Form\FormEvents;
|
||||||
|
use Symfony\Component\Form\FormError;
|
||||||
|
use Symfony\Component\Form\Event\DataEvent;
|
||||||
|
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Bernhard Schussek <bschussek@gmail.com>
|
||||||
|
*/
|
||||||
|
class CsrfValidationListener implements EventSubscriberInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The provider for generating and validating CSRF tokens
|
||||||
|
* @var CsrfProviderInterface
|
||||||
|
*/
|
||||||
|
private $csrfProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A text mentioning the intention of the CSRF token
|
||||||
|
*
|
||||||
|
* Validation of the token will only succeed if it was generated in the
|
||||||
|
* same session and with the same intention.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $intention;
|
||||||
|
|
||||||
|
static public function getSubscribedEvents()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
FormEvents::BIND_CLIENT_DATA => 'onBindClientData',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __construct(CsrfProviderInterface $csrfProvider, $intention)
|
||||||
|
{
|
||||||
|
$this->csrfProvider = $csrfProvider;
|
||||||
|
$this->intention = $intention;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onBindClientData(DataEvent $event)
|
||||||
|
{
|
||||||
|
$form = $event->getForm();
|
||||||
|
$data = $event->getData();
|
||||||
|
|
||||||
|
if ((!$form->hasParent() || $form->getParent()->isRoot())
|
||||||
|
&& !$this->csrfProvider->isCsrfTokenValid($this->intention, $data)) {
|
||||||
|
$form->addError(new FormError('The CSRF token is invalid. Please try to resubmit the form'));
|
||||||
|
|
||||||
|
// If the session timed out, the token is invalid now.
|
||||||
|
// Regenerate the token so that a resubmission is possible.
|
||||||
|
$event->setData($this->csrfProvider->generateCsrfToken($this->intention));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,10 +11,12 @@
|
||||||
|
|
||||||
namespace Symfony\Component\Form\Extension\Csrf\Type;
|
namespace Symfony\Component\Form\Extension\Csrf\Type;
|
||||||
|
|
||||||
|
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
use Symfony\Component\Form\FormBuilder;
|
use Symfony\Component\Form\FormBuilder;
|
||||||
use Symfony\Component\Form\FormError;
|
use Symfony\Component\Form\FormError;
|
||||||
|
use Symfony\Component\Form\Extension\Csrf\EventListener\CsrfValidationListener;
|
||||||
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
|
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
|
||||||
use Symfony\Component\Form\CallbackValidator;
|
use Symfony\Component\Form\CallbackValidator;
|
||||||
|
|
||||||
|
@ -46,18 +48,9 @@ class CsrfType extends AbstractType
|
||||||
$csrfProvider = $options['csrf_provider'];
|
$csrfProvider = $options['csrf_provider'];
|
||||||
$intention = $options['intention'];
|
$intention = $options['intention'];
|
||||||
|
|
||||||
$validator = function (FormInterface $form) use ($csrfProvider, $intention)
|
|
||||||
{
|
|
||||||
if ((!$form->hasParent() || $form->getParent()->isRoot())
|
|
||||||
&& !$csrfProvider->isCsrfTokenValid($intention, $form->getData())) {
|
|
||||||
$form->addError(new FormError('The CSRF token is invalid. Please try to resubmit the form'));
|
|
||||||
$form->setData($csrfProvider->generateCsrfToken($intention));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$builder
|
$builder
|
||||||
->setData($csrfProvider->generateCsrfToken($intention))
|
->setData($csrfProvider->generateCsrfToken($intention))
|
||||||
->addValidator(new CallbackValidator($validator))
|
->addEventSubscriber(new CsrfValidationListener($csrfProvider, $intention))
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ namespace Symfony\Component\Form;
|
||||||
use Symfony\Component\Form\Event\DataEvent;
|
use Symfony\Component\Form\Event\DataEvent;
|
||||||
use Symfony\Component\Form\Event\FilterDataEvent;
|
use Symfony\Component\Form\Event\FilterDataEvent;
|
||||||
use Symfony\Component\Form\Exception\FormException;
|
use Symfony\Component\Form\Exception\FormException;
|
||||||
|
use Symfony\Component\Form\Exception\AlreadyBoundException;
|
||||||
use Symfony\Component\Form\Exception\UnexpectedTypeException;
|
use Symfony\Component\Form\Exception\UnexpectedTypeException;
|
||||||
use Symfony\Component\Form\Exception\TransformationFailedException;
|
use Symfony\Component\Form\Exception\TransformationFailedException;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
@ -297,6 +298,10 @@ class Form implements \IteratorAggregate, FormInterface
|
||||||
*/
|
*/
|
||||||
public function setParent(FormInterface $parent = null)
|
public function setParent(FormInterface $parent = null)
|
||||||
{
|
{
|
||||||
|
if ($this->bound) {
|
||||||
|
throw new AlreadyBoundException('You cannot set the parent of a bound form');
|
||||||
|
}
|
||||||
|
|
||||||
if ('' === $this->getName()) {
|
if ('' === $this->getName()) {
|
||||||
throw new FormException('Form with empty name can not have parent form.');
|
throw new FormException('Form with empty name can not have parent form.');
|
||||||
}
|
}
|
||||||
|
@ -377,6 +382,10 @@ class Form implements \IteratorAggregate, FormInterface
|
||||||
*/
|
*/
|
||||||
public function setData($appData)
|
public function setData($appData)
|
||||||
{
|
{
|
||||||
|
if ($this->bound) {
|
||||||
|
throw new AlreadyBoundException('You cannot change the data of a bound form');
|
||||||
|
}
|
||||||
|
|
||||||
$event = new DataEvent($this, $appData);
|
$event = new DataEvent($this, $appData);
|
||||||
$this->dispatcher->dispatch(FormEvents::PRE_SET_DATA, $event);
|
$this->dispatcher->dispatch(FormEvents::PRE_SET_DATA, $event);
|
||||||
|
|
||||||
|
@ -451,6 +460,10 @@ class Form implements \IteratorAggregate, FormInterface
|
||||||
*/
|
*/
|
||||||
public function bind($clientData)
|
public function bind($clientData)
|
||||||
{
|
{
|
||||||
|
if ($this->bound) {
|
||||||
|
throw new AlreadyBoundException('A form can only be bound once');
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->isDisabled()) {
|
if ($this->isDisabled()) {
|
||||||
$this->bound = true;
|
$this->bound = true;
|
||||||
|
|
||||||
|
@ -689,7 +702,7 @@ class Form implements \IteratorAggregate, FormInterface
|
||||||
*/
|
*/
|
||||||
public function isValid()
|
public function isValid()
|
||||||
{
|
{
|
||||||
if (!$this->isBound()) {
|
if (!$this->bound) {
|
||||||
throw new \LogicException('You cannot call isValid() on a form that is not bound.');
|
throw new \LogicException('You cannot call isValid() on a form that is not bound.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -821,6 +834,10 @@ class Form implements \IteratorAggregate, FormInterface
|
||||||
*/
|
*/
|
||||||
public function add(FormInterface $child)
|
public function add(FormInterface $child)
|
||||||
{
|
{
|
||||||
|
if ($this->bound) {
|
||||||
|
throw new AlreadyBoundException('You cannot add children to a bound form');
|
||||||
|
}
|
||||||
|
|
||||||
$this->children[$child->getName()] = $child;
|
$this->children[$child->getName()] = $child;
|
||||||
|
|
||||||
$child->setParent($this);
|
$child->setParent($this);
|
||||||
|
@ -841,6 +858,10 @@ class Form implements \IteratorAggregate, FormInterface
|
||||||
*/
|
*/
|
||||||
public function remove($name)
|
public function remove($name)
|
||||||
{
|
{
|
||||||
|
if ($this->bound) {
|
||||||
|
throw new AlreadyBoundException('You cannot remove children from a bound form');
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($this->children[$name])) {
|
if (isset($this->children[$name])) {
|
||||||
$this->children[$name]->setParent(null);
|
$this->children[$name]->setParent(null);
|
||||||
|
|
||||||
|
|
|
@ -199,6 +199,15 @@ class FormTest extends \PHPUnit_Framework_TestCase
|
||||||
$this->assertEquals(array('firstName' => 'Bernhard'), $this->form->getData());
|
$this->assertEquals(array('firstName' => 'Bernhard'), $this->form->getData());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException Symfony\Component\Form\Exception\AlreadyBoundException
|
||||||
|
*/
|
||||||
|
public function testBindThrowsExceptionIfAlreadyBound()
|
||||||
|
{
|
||||||
|
$this->form->bind(array());
|
||||||
|
$this->form->bind(array());
|
||||||
|
}
|
||||||
|
|
||||||
public function testBindForwardsNullIfValueIsMissing()
|
public function testBindForwardsNullIfValueIsMissing()
|
||||||
{
|
{
|
||||||
$child = $this->getMockForm('firstName');
|
$child = $this->getMockForm('firstName');
|
||||||
|
@ -408,8 +417,8 @@ class FormTest extends \PHPUnit_Framework_TestCase
|
||||||
->method('isValid')
|
->method('isValid')
|
||||||
->will($this->returnValue(false));
|
->will($this->returnValue(false));
|
||||||
|
|
||||||
$this->form->bind('foobar');
|
|
||||||
$this->form->add($child);
|
$this->form->add($child);
|
||||||
|
$this->form->bind(array());
|
||||||
|
|
||||||
$this->assertFalse($this->form->isValid());
|
$this->assertFalse($this->form->isValid());
|
||||||
}
|
}
|
||||||
|
@ -438,6 +447,15 @@ class FormTest extends \PHPUnit_Framework_TestCase
|
||||||
$this->assertFalse($this->form->hasChildren());
|
$this->assertFalse($this->form->hasChildren());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException Symfony\Component\Form\Exception\AlreadyBoundException
|
||||||
|
*/
|
||||||
|
public function testSetParentThrowsExceptionIfAlreadyBound()
|
||||||
|
{
|
||||||
|
$this->form->bind(array());
|
||||||
|
$this->form->setParent($this->getBuilder('parent')->getForm());
|
||||||
|
}
|
||||||
|
|
||||||
public function testAdd()
|
public function testAdd()
|
||||||
{
|
{
|
||||||
$child = $this->getBuilder('foo')->getForm();
|
$child = $this->getBuilder('foo')->getForm();
|
||||||
|
@ -447,6 +465,15 @@ class FormTest extends \PHPUnit_Framework_TestCase
|
||||||
$this->assertSame(array('foo' => $child), $this->form->getChildren());
|
$this->assertSame(array('foo' => $child), $this->form->getChildren());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException Symfony\Component\Form\Exception\AlreadyBoundException
|
||||||
|
*/
|
||||||
|
public function testAddThrowsExceptionIfAlreadyBound()
|
||||||
|
{
|
||||||
|
$this->form->bind(array());
|
||||||
|
$this->form->add($this->getBuilder('foo')->getForm());
|
||||||
|
}
|
||||||
|
|
||||||
public function testRemove()
|
public function testRemove()
|
||||||
{
|
{
|
||||||
$child = $this->getBuilder('foo')->getForm();
|
$child = $this->getBuilder('foo')->getForm();
|
||||||
|
@ -457,6 +484,16 @@ class FormTest extends \PHPUnit_Framework_TestCase
|
||||||
$this->assertFalse($this->form->hasChildren());
|
$this->assertFalse($this->form->hasChildren());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException Symfony\Component\Form\Exception\AlreadyBoundException
|
||||||
|
*/
|
||||||
|
public function testRemoveThrowsExceptionIfAlreadyBound()
|
||||||
|
{
|
||||||
|
$this->form->add($this->getBuilder('foo')->getForm());
|
||||||
|
$this->form->bind(array('foo' => 'bar'));
|
||||||
|
$this->form->remove('foo');
|
||||||
|
}
|
||||||
|
|
||||||
public function testRemoveIgnoresUnknownName()
|
public function testRemoveIgnoresUnknownName()
|
||||||
{
|
{
|
||||||
$this->form->remove('notexisting');
|
$this->form->remove('notexisting');
|
||||||
|
@ -504,6 +541,15 @@ class FormTest extends \PHPUnit_Framework_TestCase
|
||||||
$this->assertFalse($this->form->isBound());
|
$this->assertFalse($this->form->isBound());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException Symfony\Component\Form\Exception\AlreadyBoundException
|
||||||
|
*/
|
||||||
|
public function testSetDataThrowsExceptionIfAlreadyBound()
|
||||||
|
{
|
||||||
|
$this->form->bind(array());
|
||||||
|
$this->form->setData(null);
|
||||||
|
}
|
||||||
|
|
||||||
public function testSetDataExecutesTransformationChain()
|
public function testSetDataExecutesTransformationChain()
|
||||||
{
|
{
|
||||||
// use real event dispatcher now
|
// use real event dispatcher now
|
||||||
|
|
Reference in New Issue