[Form] Simplified CSRF mechanism and removed "csrf" type

CSRF fields are now only added when the view is built. For this reason we already know if
the form is the root form and avoid to create unnecessary CSRF fields for nested fields.
This commit is contained in:
Bernhard Schussek 2012-04-13 14:11:09 +02:00
parent e7470ffebb
commit 2a49449862
22 changed files with 404 additions and 654 deletions

View File

@ -269,6 +269,7 @@ To get the diff between two versions, go to https://github.com/symfony/symfony/c
don't receive an options array anymore.
* Deprecated FormValidatorInterface and substituted its implementations
by event subscribers
* simplified CSRF protection and removed the csrf type
### HttpFoundation

View File

@ -14,12 +14,9 @@
<argument>%kernel.secret%</argument>
</service>
<service id="form.type.csrf" class="Symfony\Component\Form\Extension\Csrf\Type\CsrfType">
<tag name="form.type" alias="csrf" />
<argument type="service" id="form.csrf_provider" />
</service>
<service id="form.type_extension.csrf" class="Symfony\Component\Form\Extension\Csrf\Type\FormTypeCsrfExtension">
<tag name="form.type_extension" alias="form" />
<argument type="service" id="form.csrf_provider" />
<argument>%form.type_extension.csrf.enabled%</argument>
<argument>%form.type_extension.csrf.field_name%</argument>
</service>

View File

@ -27,9 +27,9 @@ abstract class FrameworkExtensionTest extends TestCase
$def = $container->getDefinition('form.type_extension.csrf');
$this->assertTrue($container->getParameter('form.type_extension.csrf.enabled'));
$this->assertEquals('%form.type_extension.csrf.enabled%', $def->getArgument(0));
$this->assertEquals('%form.type_extension.csrf.enabled%', $def->getArgument(1));
$this->assertEquals('_csrf', $container->getParameter('form.type_extension.csrf.field_name'));
$this->assertEquals('%form.type_extension.csrf.field_name%', $def->getArgument(1));
$this->assertEquals('%form.type_extension.csrf.field_name%', $def->getArgument(2));
$this->assertEquals('s3cr3t', $container->getParameterBag()->resolveValue($container->findDefinition('form.csrf_provider')->getArgument(1)));
}

View File

@ -32,27 +32,13 @@ class CsrfExtension extends AbstractExtension
$this->csrfProvider = $csrfProvider;
}
/**
* {@inheritDoc}
*/
protected function loadTypes()
{
return array(
new Type\CsrfType($this->csrfProvider),
);
}
/**
* {@inheritDoc}
*/
protected function loadTypeExtensions()
{
return array(
new Type\ChoiceTypeCsrfExtension(),
new Type\DateTypeCsrfExtension(),
new Type\FormTypeCsrfExtension(),
new Type\RepeatedTypeCsrfExtension(),
new Type\TimeTypeCsrfExtension(),
new Type\FormTypeCsrfExtension($this->csrfProvider),
);
}
}

View File

@ -14,7 +14,7 @@ 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\Event\FilterDataEvent;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
/**
@ -22,6 +22,12 @@ use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
*/
class CsrfValidationListener implements EventSubscriberInterface
{
/**
* The name of the CSRF field
* @var string
*/
private $fieldName;
/**
* The provider for generating and validating CSRF tokens
* @var CsrfProviderInterface
@ -45,24 +51,26 @@ class CsrfValidationListener implements EventSubscriberInterface
);
}
public function __construct(CsrfProviderInterface $csrfProvider, $intention)
public function __construct($fieldName, CsrfProviderInterface $csrfProvider, $intention)
{
$this->fieldName = $fieldName;
$this->csrfProvider = $csrfProvider;
$this->intention = $intention;
}
public function onBindClientData(DataEvent $event)
public function onBindClientData(FilterDataEvent $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 ($form->isRoot() && $form->hasChildren() && isset($data[$this->fieldName])) {
if (!$this->csrfProvider->isCsrfTokenValid($this->intention, $data[$this->fieldName])) {
$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));
unset($data[$this->fieldName]);
}
$event->setData($data);
}
}

View File

@ -1,66 +0,0 @@
<?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\Form\Event\DataEvent;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
use Symfony\Component\Form\FormFactoryInterface;
/**
* Ensures the CSRF field.
*
* @author Bulat Shakirzyanov <mallluhuct@gmail.com>
* @author Kris Wallsmith <kris@symfony.com>
*/
class EnsureCsrfFieldListener
{
private $factory;
private $name;
private $intention;
private $provider;
/**
* Constructor.
*
* @param FormFactoryInterface $factory The form factory
* @param string $name A name for the CSRF field
* @param string $intention The intention string
* @param CsrfProviderInterface $provider The CSRF provider
*/
public function __construct(FormFactoryInterface $factory, $name, $intention = null, CsrfProviderInterface $provider = null)
{
$this->factory = $factory;
$this->name = $name;
$this->intention = $intention;
$this->provider = $provider;
}
/**
* Ensures a root form has a CSRF field.
*
* This method should be connected to both form.pre_set_data and form.pre_bind.
*/
public function ensureCsrfField(DataEvent $event)
{
$form = $event->getForm();
$options = array();
if ($this->intention) {
$options['intention'] = $this->intention;
}
if ($this->provider) {
$options['csrf_provider'] = $this->provider;
}
$form->add($this->factory->createNamed('csrf', $this->name, null, $options));
}
}

View File

@ -1,27 +0,0 @@
<?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\Type;
use Symfony\Component\Form\AbstractTypeExtension;
class ChoiceTypeCsrfExtension extends AbstractTypeExtension
{
public function getDefaultOptions()
{
return array('csrf_protection' => false);
}
public function getExtendedType()
{
return 'choice';
}
}

View File

@ -1,83 +0,0 @@
<?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\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\Extension\Csrf\EventListener\CsrfValidationListener;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
class CsrfType extends AbstractType
{
private $csrfProvider;
/**
* Constructor.
*
* @param CsrfProviderInterface $csrfProvider The provider to use to generate the token
*/
public function __construct(CsrfProviderInterface $csrfProvider)
{
$this->csrfProvider = $csrfProvider;
}
/**
* Builds the CSRF field.
*
* A validator is added to check the token value when the CSRF field is added to
* a root form
*
* @param FormBuilder $builder The form builder
* @param array $options The options
*/
public function buildForm(FormBuilder $builder, array $options)
{
$csrfProvider = $options['csrf_provider'];
$intention = $options['intention'];
$builder
->setData($csrfProvider->generateCsrfToken($intention))
->addEventSubscriber(new CsrfValidationListener($csrfProvider, $intention))
;
}
/**
* {@inheritDoc}
*/
public function getDefaultOptions()
{
return array(
'csrf_provider' => $this->csrfProvider,
'intention' => null,
'property_path' => false,
);
}
/**
* {@inheritDoc}
*/
public function getParent(array $options)
{
return 'hidden';
}
/**
* Returns the name of this form.
*
* @return string 'csrf'
*/
public function getName()
{
return 'csrf';
}
}

View File

@ -1,27 +0,0 @@
<?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\Type;
use Symfony\Component\Form\AbstractTypeExtension;
class DateTypeCsrfExtension extends AbstractTypeExtension
{
public function getDefaultOptions()
{
return array('csrf_protection' => false);
}
public function getExtendedType()
{
return 'date';
}
}

View File

@ -12,21 +12,27 @@
namespace Symfony\Component\Form\Extension\Csrf\Type;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Csrf\EventListener\EnsureCsrfFieldListener;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
use Symfony\Component\Form\Extension\Csrf\EventListener\CsrfValidationListener;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FormTypeCsrfExtension extends AbstractTypeExtension
{
private $enabled;
private $fieldName;
private $defaultCsrfProvider;
private $defaultEnabled;
private $defaultFieldName;
public function __construct($enabled = true, $fieldName = '_token')
public function __construct(CsrfProviderInterface $defaultCsrfProvider, $defaultEnabled = true, $defaultFieldName = '_token')
{
$this->enabled = $enabled;
$this->fieldName = $fieldName;
$this->defaultCsrfProvider = $defaultCsrfProvider;
$this->defaultEnabled = $defaultEnabled;
$this->defaultFieldName = $defaultFieldName;
}
/**
@ -41,35 +47,35 @@ class FormTypeCsrfExtension extends AbstractTypeExtension
return;
}
$listener = new EnsureCsrfFieldListener(
$builder->getFormFactory(),
$options['csrf_field_name'],
$options['intention'],
$options['csrf_provider']
);
// use a low priority so higher priority listeners don't remove the field
$builder
->setAttribute('csrf_field_name', $options['csrf_field_name'])
->addEventListener(FormEvents::PRE_SET_DATA, array($listener, 'ensureCsrfField'), -10)
->addEventListener(FormEvents::PRE_BIND, array($listener, 'ensureCsrfField'), -10)
->setAttribute('csrf_provider', $options['csrf_provider'])
->setAttribute('csrf_intention', $options['intention'])
->setAttribute('csrf_factory', $builder->getFormFactory())
->addEventSubscriber(new CsrfValidationListener($options['csrf_field_name'], $options['csrf_provider'], $options['intention']))
;
}
/**
* Removes CSRF fields from all the form views except the root one.
* Adds a CSRF field to the root form view.
*
* @param FormView $view The form view
* @param FormInterface $form The form
*/
public function buildViewBottomUp(FormView $view, FormInterface $form)
public function buildView(FormView $view, FormInterface $form)
{
if ($view->hasParent() && $form->hasAttribute('csrf_field_name')) {
if ($form->isRoot() && $form->hasChildren() && $form->hasAttribute('csrf_field_name')) {
$name = $form->getAttribute('csrf_field_name');
$csrfProvider = $form->getAttribute('csrf_provider');
$intention = $form->getAttribute('csrf_intention');
$factory = $form->getAttribute('csrf_factory');
$data = $csrfProvider->generateCsrfToken($intention);
$csrfForm = $factory->createNamed('hidden', $name, $data, array(
'property_path' => false,
));
if (isset($view[$name])) {
unset($view[$name]);
}
$view->addChild($csrfForm->createView($view));
}
}
@ -79,9 +85,9 @@ class FormTypeCsrfExtension extends AbstractTypeExtension
public function getDefaultOptions()
{
return array(
'csrf_protection' => $this->enabled,
'csrf_field_name' => $this->fieldName,
'csrf_provider' => null,
'csrf_protection' => $this->defaultEnabled,
'csrf_field_name' => $this->defaultFieldName,
'csrf_provider' => $this->defaultCsrfProvider,
'intention' => 'unknown',
);
}

View File

@ -1,27 +0,0 @@
<?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\Type;
use Symfony\Component\Form\AbstractTypeExtension;
class RepeatedTypeCsrfExtension extends AbstractTypeExtension
{
public function getDefaultOptions()
{
return array('csrf_protection' => false);
}
public function getExtendedType()
{
return 'repeated';
}
}

View File

@ -1,27 +0,0 @@
<?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\Type;
use Symfony\Component\Form\AbstractTypeExtension;
class TimeTypeCsrfExtension extends AbstractTypeExtension
{
public function getDefaultOptions()
{
return array('csrf_protection' => false);
}
public function getExtendedType()
{
return 'time';
}
}

View File

@ -975,7 +975,7 @@ class Form implements \IteratorAggregate, FormInterface
$parent = $this->parent->createView();
}
$view = new FormView();
$view = new FormView($this->name);
$view->setParent($parent);
@ -989,14 +989,10 @@ class Form implements \IteratorAggregate, FormInterface
}
}
$childViews = array();
foreach ($this->children as $key => $child) {
$childViews[$key] = $child->createView($view);
foreach ($this->children as $child) {
$view->addChild($child->createView($view));
}
$view->setChildren($childViews);
foreach ($types as $type) {
$type->buildViewBottomUp($view, $this);

View File

@ -11,8 +11,17 @@
namespace Symfony\Component\Form;
class FormView implements \ArrayAccess, \IteratorAggregate, \Countable
use ArrayAccess;
use IteratorAggregate;
use Countable;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FormView implements ArrayAccess, IteratorAggregate, Countable
{
private $name;
private $vars = array(
'value' => null,
'attr' => array(),
@ -33,6 +42,16 @@ class FormView implements \ArrayAccess, \IteratorAggregate, \Countable
*/
private $rendered = false;
public function __construct($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
/**
* @param string $name
* @param mixed $value
@ -177,15 +196,29 @@ class FormView implements \ArrayAccess, \IteratorAggregate, \Countable
}
/**
* Sets the children view.
* Adds a child view.
*
* @param array $children The children as instances of FormView
* @param FormView $child The child view to add.
*
* @return FormView The current view
*/
public function setChildren(array $children)
public function addChild(FormView $child)
{
$this->children = $children;
$this->children[$child->getName()] = $child;
return $this;
}
/**
* Removes a child view.
*
* @param string $name The name of the removed child view.
*
* @return FormView The current view
*/
public function removeChild($name)
{
unset($this->children[$name]);
return $this;
}
@ -222,6 +255,18 @@ class FormView implements \ArrayAccess, \IteratorAggregate, \Countable
return count($this->children) > 0;
}
/**
* Returns whether this view has a given child.
*
* @param string $name The name of the child
*
* @return Boolean Whether the child with the given name exists
*/
public function hasChild($name)
{
return isset($this->children[$name]);
}
/**
* Returns a child by name (implements \ArrayAccess).
*

View File

@ -363,6 +363,31 @@ abstract class AbstractDivLayoutTest extends AbstractLayoutTest
);
}
public function testCsrf()
{
$this->csrfProvider->expects($this->any())
->method('generateCsrfToken')
->will($this->returnValue('foo&bar'));
$form = $this->factory->createNamedBuilder('form', 'name')
->add($this->factory
// No CSRF protection on nested forms
->createNamedBuilder('form', 'child')
->add($this->factory->createNamedBuilder('text', 'grandchild'))
)
->getForm();
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/div
[
./input[@type="hidden"][@id="name__token"][@value="foo&bar"]
/following-sibling::div
]
[count(.//input[@type="hidden"])=1]
'
);
}
public function testRepeated()
{
$form = $this->factory->createNamed('repeated', 'name', 'foobar', array(
@ -372,7 +397,8 @@ abstract class AbstractDivLayoutTest extends AbstractLayoutTest
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/div
[
./div
./input[@type="hidden"][@id="name__token"]
/following-sibling::div
[
./label[@for="name_first"]
/following-sibling::input[@type="text"][@id="name_first"]
@ -383,7 +409,7 @@ abstract class AbstractDivLayoutTest extends AbstractLayoutTest
/following-sibling::input[@type="text"][@id="name_second"]
]
]
[count(.//input)=2]
[count(.//input)=3]
'
);
}
@ -399,7 +425,8 @@ abstract class AbstractDivLayoutTest extends AbstractLayoutTest
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/div
[
./div
./input[@type="hidden"][@id="name__token"]
/following-sibling::div
[
./label[@for="name_first"][.="[trans]Test[/trans]"]
/following-sibling::input[@type="text"][@id="name_first"][@required="required"]
@ -410,7 +437,7 @@ abstract class AbstractDivLayoutTest extends AbstractLayoutTest
/following-sibling::input[@type="text"][@id="name_second"][@required="required"]
]
]
[count(.//input)=2]
[count(.//input)=3]
'
);
}

View File

@ -646,12 +646,13 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/div
[
./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked]
./input[@type="hidden"][@id="name__token"]
/following-sibling::input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked]
/following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"]
/following-sibling::input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)]
/following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"]
]
[count(./input)=2]
[count(./input)=3]
'
);
}
@ -668,12 +669,13 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/div
[
./input[@type="radio"][@name="name"][@id="name_0"][@checked]
./input[@type="hidden"][@id="name__token"]
/following-sibling::input[@type="radio"][@name="name"][@id="name_0"][@checked]
/following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"]
/following-sibling::input[@type="radio"][@name="name"][@id="name_1"][not(@checked)]
/following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"]
]
[count(./input)=2]
[count(./input)=3]
'
);
}
@ -689,12 +691,13 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/div
[
./input[@type="radio"][@name="name"][@id="name_0"][@checked]
./input[@type="hidden"][@id="name__token"]
/following-sibling::input[@type="radio"][@name="name"][@id="name_0"][@checked]
/following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"]
/following-sibling::input[@type="radio"][@name="name"][@id="name_1"][not(@checked)]
/following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"]
]
[count(./input)=2]
[count(./input)=3]
'
);
}
@ -711,14 +714,15 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/div
[
./input[@type="checkbox"][@name="name[]"][@id="name_0"][@checked][not(@required)]
./input[@type="hidden"][@id="name__token"]
/following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_0"][@checked][not(@required)]
/following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"]
/following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_1"][not(@checked)][not(@required)]
/following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"]
/following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_2"][@checked][not(@required)]
/following-sibling::label[@for="name_2"][.="[trans]Choice&C[/trans]"]
]
[count(./input)=3]
[count(./input)=4]
'
);
}
@ -753,22 +757,6 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase
);
}
public function testCsrf()
{
$this->csrfProvider->expects($this->any())
->method('generateCsrfToken')
->will($this->returnValue('foo&bar'));
$form = $this->factory->createNamed('csrf', 'name');
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/input
[@type="hidden"]
[@value="foo&bar"]
'
);
}
public function testDateTime()
{
$form = $this->factory->createNamed('datetime', 'name', '2011-02-03 04:05:06', array(

View File

@ -45,36 +45,10 @@ abstract class AbstractTableLayoutTest extends AbstractLayoutTest
$html = $this->renderRow($form->createView());
$this->assertMatchesXpath($html,
'/tr
[
./td
[./label[@for="name_first"]]
/following-sibling::td
[./input[@id="name_first"]]
]
/following-sibling::tr
[
./td
[./label[@for="name_second"]]
/following-sibling::td
[./input[@id="name_second"]]
]
[count(../tr)=2]
'
);
}
public function testRepeatedRowWithErrors()
{
$form = $this->factory->createNamed('repeated', 'name');
$form->addError(new FormError('Error!'));
$view = $form->createView();
$html = $this->renderRow($view);
$this->assertMatchesXpath($html,
'/tr
[./td[@colspan="2"]/ul
[./li[.="[trans]Error![/trans]"]]
'/tr[@style="display: none"]
[./td[@colspan="2"]/input
[@type="hidden"]
[@id="name__token"]
]
/following-sibling::tr
[
@ -95,6 +69,42 @@ abstract class AbstractTableLayoutTest extends AbstractLayoutTest
);
}
public function testRepeatedRowWithErrors()
{
$form = $this->factory->createNamed('repeated', 'name');
$form->addError(new FormError('Error!'));
$view = $form->createView();
$html = $this->renderRow($view);
$this->assertMatchesXpath($html,
'/tr
[./td[@colspan="2"]/ul
[./li[.="[trans]Error![/trans]"]]
]
/following-sibling::tr[@style="display: none"]
[./td[@colspan="2"]/input
[@type="hidden"]
[@id="name__token"]
]
/following-sibling::tr
[
./td
[./label[@for="name_first"]]
/following-sibling::td
[./input[@id="name_first"]]
]
/following-sibling::tr
[
./td
[./label[@for="name_second"]]
/following-sibling::td
[./input[@id="name_second"]]
]
[count(../tr)=4]
'
);
}
public function testRest()
{
$view = $this->factory->createNamedBuilder('form', 'name')
@ -151,9 +161,9 @@ abstract class AbstractTableLayoutTest extends AbstractLayoutTest
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/table
[
./tr[./td/input[@type="text"][@value="a"]]
./tr[@style="display: none"][./td[@colspan="2"]/input[@type="hidden"][@id="name__token"]]
/following-sibling::tr[./td/input[@type="text"][@value="a"]]
/following-sibling::tr[./td/input[@type="text"][@value="b"]]
/following-sibling::tr[./td/input[@type="hidden"][@id="name__token"]]
]
[count(./tr[./td/input])=3]
'
@ -217,6 +227,34 @@ abstract class AbstractTableLayoutTest extends AbstractLayoutTest
);
}
public function testCsrf()
{
$this->csrfProvider->expects($this->any())
->method('generateCsrfToken')
->will($this->returnValue('foo&bar'));
$form = $this->factory->createNamedBuilder('form', 'name')
->add($this->factory
// No CSRF protection on nested forms
->createNamedBuilder('form', 'child')
->add($this->factory->createNamedBuilder('text', 'grandchild'))
)
->getForm();
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/table
[
./tr[@style="display: none"]
[./td[@colspan="2"]/input
[@type="hidden"]
[@id="name__token"]
]
]
[count(.//input[@type="hidden"])=1]
'
);
}
public function testRepeated()
{
$form = $this->factory->createNamed('repeated', 'name', 'foobar', array(
@ -226,7 +264,12 @@ abstract class AbstractTableLayoutTest extends AbstractLayoutTest
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/table
[
./tr
./tr[@style="display: none"]
[./td[@colspan="2"]/input
[@type="hidden"]
[@id="name__token"]
]
/following-sibling::tr
[
./td
[./label[@for="name_first"]]
@ -241,7 +284,7 @@ abstract class AbstractTableLayoutTest extends AbstractLayoutTest
[./input[@type="text"][@id="name_second"]]
]
]
[count(.//input)=2]
[count(.//input)=3]
'
);
}
@ -257,7 +300,12 @@ abstract class AbstractTableLayoutTest extends AbstractLayoutTest
$this->assertWidgetMatchesXpath($form->createView(), array(),
'/table
[
./tr
./tr[@style="display: none"]
[./td[@colspan="2"]/input
[@type="hidden"]
[@id="name__token"]
]
/following-sibling::tr
[
./td
[./label[@for="name_first"][.="[trans]Test[/trans]"]]
@ -272,7 +320,7 @@ abstract class AbstractTableLayoutTest extends AbstractLayoutTest
[./input[@type="password"][@id="name_second"][@required="required"]]
]
]
[count(.//input)=2]
[count(.//input)=3]
'
);
}

View File

@ -1,87 +0,0 @@
<?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\Tests\Extension\Csrf\EventListener;
use Symfony\Component\Form\Event\DataEvent;
use Symfony\Component\Form\Extension\Csrf\EventListener\EnsureCsrfFieldListener;
class EnsureCsrfFieldListenerTest extends \PHPUnit_Framework_TestCase
{
private $form;
private $formFactory;
private $field;
private $event;
protected function setUp()
{
if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) {
$this->markTestSkipped('The "EventDispatcher" component is not available');
}
$this->formFactory = $this->getMock('Symfony\\Component\\Form\\FormFactoryInterface');
$this->form = $this->getMock('Symfony\\Component\\Form\\Tests\\FormInterface');
$this->field = $this->getMock('Symfony\\Component\\Form\\Tests\\FormInterface');
$this->event = new DataEvent($this->form, array());
}
protected function tearDown()
{
$this->form = null;
$this->formFactory = null;
$this->field = null;
$this->event = null;
}
public function testAddField()
{
$this->formFactory->expects($this->once())
->method('createNamed')
->with('csrf', '_token', null, array())
->will($this->returnValue($this->field));
$this->form->expects($this->once())
->method('add')
->with($this->isInstanceOf('Symfony\\Component\\Form\\Tests\\FormInterface'));
$listener = new EnsureCsrfFieldListener($this->formFactory, '_token');
$listener->ensureCsrfField($this->event);
}
public function testIntention()
{
$this->formFactory->expects($this->once())
->method('createNamed')
->with('csrf', '_token', null, array('intention' => 'something'))
->will($this->returnValue($this->field));
$this->form->expects($this->once())
->method('add')
->with($this->isInstanceOf('Symfony\\Component\\Form\\Tests\\FormInterface'));
$listener = new EnsureCsrfFieldListener($this->formFactory, '_token', 'something');
$listener->ensureCsrfField($this->event);
}
public function testProvider()
{
$provider = $this->getMock('Symfony\\Component\\Form\\Extension\\Csrf\\CsrfProvider\\CsrfProviderInterface');
$this->formFactory->expects($this->once())
->method('createNamed')
->with('csrf', '_token', null, array('csrf_provider' => $provider))
->will($this->returnValue($this->field));
$this->form->expects($this->once())
->method('add')
->with($this->isInstanceOf('Symfony\\Component\\Form\\Tests\\FormInterface'));
$listener = new EnsureCsrfFieldListener($this->formFactory, '_token', null, $provider);
$listener->ensureCsrfField($this->event);
}
}

View File

@ -1,112 +0,0 @@
<?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\Tests\Extension\Csrf\Type;
class CsrfTypeTest extends TypeTestCase
{
protected $provider;
protected function setUp()
{
parent::setUp();
$this->provider = $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface');
}
protected function tearDown()
{
parent::tearDown();
$this->provider = null;
}
protected function getNonRootForm()
{
$form = $this->getMock('Symfony\Component\Form\Tests\FormInterface');
$form->expects($this->any())
->method('isRoot')
->will($this->returnValue(false));
return $form;
}
public function testGenerateCsrfToken()
{
$this->provider->expects($this->once())
->method('generateCsrfToken')
->with('%INTENTION%')
->will($this->returnValue('token'));
$form = $this->factory->create('csrf', null, array(
'csrf_provider' => $this->provider,
'intention' => '%INTENTION%'
));
$this->assertEquals('token', $form->getData());
}
public function testValidateTokenOnBind()
{
$this->provider->expects($this->once())
->method('isCsrfTokenValid')
->with('%INTENTION%', 'token')
->will($this->returnValue(true));
$form = $this->factory->create('csrf', null, array(
'csrf_provider' => $this->provider,
'intention' => '%INTENTION%'
));
$form->bind('token');
$this->assertEquals('token', $form->getData());
}
public function testDontValidateTokenIfParentIsNotRoot()
{
$this->provider->expects($this->never())
->method('isCsrfTokenValid');
$form = $this->factory->create('csrf', null, array(
'csrf_provider' => $this->provider,
'intention' => '%INTENTION%'
));
$form->setParent($this->getNonRootForm());
$form->bind('token');
}
public function testCsrfTokenIsRegeneratedIfValidationFails()
{
$this->provider->expects($this->at(0))
->method('generateCsrfToken')
->with('%INTENTION%')
->will($this->returnValue('token1'));
$this->provider->expects($this->at(1))
->method('isCsrfTokenValid')
->with('%INTENTION%', 'invalid')
->will($this->returnValue(false));
// The token is regenerated to avoid stalled tokens, for example when
// the session ID changed
$this->provider->expects($this->at(2))
->method('generateCsrfToken')
->with('%INTENTION%')
->will($this->returnValue('token2'));
$form = $this->factory->create('csrf', null, array(
'csrf_provider' => $this->provider,
'intention' => '%INTENTION%'
));
$form->bind('invalid');
$this->assertEquals('token2', $form->getData());
}
}

View File

@ -11,43 +11,188 @@
namespace Symfony\Component\Form\Tests\Extension\Csrf\Type;
use Symfony\Component\Form\Extension\Csrf\CsrfExtension;
use Symfony\Component\Form\Tests\Extension\Core\Type\TypeTestCase;
class FormTypeCsrfExtensionTest extends TypeTestCase
{
public function testCsrfProtectionByDefault()
{
$form = $this->factory->create('form', null, array(
'csrf_field_name' => 'csrf',
));
protected $csrfProvider;
$this->assertTrue($form->has('csrf'));
protected function setUp()
{
$this->csrfProvider = $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface');
parent::setUp();
}
protected function tearDown()
{
$this->csrfProvider = null;
parent::tearDown();
}
protected function getExtensions()
{
return array_merge(parent::getExtensions(), array(
new CsrfExtension($this->csrfProvider),
));
}
public function testCsrfProtectionByDefaultIfRootAndChildren()
{
$view = $this->factory
->createBuilder('form', null, array(
'csrf_field_name' => 'csrf',
))
->add($this->factory->createNamedBuilder('form', 'child'))
->getForm()
->createView();
$this->assertTrue($view->hasChild('csrf'));
}
public function testNoCsrfProtectionByDefaultIfChildrenButNotRoot()
{
$view = $this->factory
->createNamedBuilder('form', 'root')
->add($this->factory
->createNamedBuilder('form', 'form', null, array(
'csrf_field_name' => 'csrf',
))
->add($this->factory->createNamedBuilder('form', 'child'))
)
->getForm()
->get('form')
->createView();
$this->assertFalse($view->hasChild('csrf'));
}
public function testNoCsrfProtectionByDefaultIfRootButNoChildren()
{
$view = $this->factory
->createBuilder('form', null, array(
'csrf_field_name' => 'csrf',
))
->getForm()
->createView();
$this->assertFalse($view->hasChild('csrf'));
}
public function testCsrfProtectionCanBeDisabled()
{
$form = $this->factory->create('form', null, array(
'csrf_protection' => false,
));
$this->assertCount(0, $form);
}
public function testCsrfTokenIsOnlyIncludedInRootView()
{
$view =
$this->factory->createBuilder('form', null, array(
$view = $this->factory
->createBuilder('form', null, array(
'csrf_field_name' => 'csrf',
'csrf_protection' => false,
))
->add('notCsrf', 'text')
->add(
$this->factory->createNamedBuilder('form', 'child', null, array(
'csrf_field_name' => 'csrf',
))
->add('notCsrf', 'text')
)
->add($this->factory->createNamedBuilder('form', 'child'))
->getForm()
->createView();
$this->assertEquals(array('csrf', 'notCsrf', 'child'), array_keys(iterator_to_array($view)));
$this->assertEquals(array('notCsrf'), array_keys(iterator_to_array($view['child'])));
$this->assertFalse($view->hasChild('csrf'));
}
public function testGenerateCsrfToken()
{
$this->csrfProvider->expects($this->once())
->method('generateCsrfToken')
->with('%INTENTION%')
->will($this->returnValue('token'));
$view = $this->factory
->createBuilder('form', null, array(
'csrf_field_name' => 'csrf',
'csrf_provider' => $this->csrfProvider,
'intention' => '%INTENTION%'
))
->add($this->factory->createNamedBuilder('form', 'child'))
->getForm()
->createView();
$this->assertEquals('token', $view->getChild('csrf')->get('value'));
}
public function provideBoolean()
{
return array(
array(true),
array(false),
);
}
/**
* @dataProvider provideBoolean
*/
public function testValidateTokenOnBindIfRootAndChildren($valid)
{
$this->csrfProvider->expects($this->once())
->method('isCsrfTokenValid')
->with('%INTENTION%', 'token')
->will($this->returnValue($valid));
$form = $this->factory
->createBuilder('form', null, array(
'csrf_field_name' => 'csrf',
'csrf_provider' => $this->csrfProvider,
'intention' => '%INTENTION%'
))
->add($this->factory->createNamedBuilder('form', 'child'))
->getForm();
$form->bind(array(
'child' => 'foobar',
'csrf' => 'token',
));
// Remove token from data
$this->assertSame(array('child' => 'foobar'), $form->getData());
// Validate accordingly
$this->assertSame($valid, $form->isValid());
}
public function testDontValidateTokenIfChildrenButNoRoot()
{
$this->csrfProvider->expects($this->never())
->method('isCsrfTokenValid');
$form = $this->factory
->createNamedBuilder('form', 'root')
->add($this->factory
->createNamedBuilder('form', 'form', null, array(
'csrf_field_name' => 'csrf',
'csrf_provider' => $this->csrfProvider,
'intention' => '%INTENTION%'
))
->add($this->factory->createNamedBuilder('form', 'child'))
)
->getForm()
->get('form');
$form->bind(array(
'child' => 'foobar',
'csrf' => 'token',
));
}
public function testDontValidateTokenIfRootButNoChildren()
{
$this->csrfProvider->expects($this->never())
->method('isCsrfTokenValid');
$form = $this->factory
->createBuilder('form', null, array(
'csrf_field_name' => 'csrf',
'csrf_provider' => $this->csrfProvider,
'intention' => '%INTENTION%'
))
->getForm();
$form->bind(array(
'csrf' => 'token',
));
}
}

View File

@ -1,41 +0,0 @@
<?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\Tests\Extension\Csrf\Type;
use Symfony\Component\Form\Tests\Extension\Core\Type\TypeTestCase as BaseTestCase;
use Symfony\Component\Form\Extension\Csrf\CsrfExtension;
abstract class TypeTestCase extends BaseTestCase
{
protected $csrfProvider;
protected function setUp()
{
$this->csrfProvider = $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface');
parent::setUp();
}
protected function tearDown()
{
$this->csrfProvider = null;
parent::tearDown();
}
protected function getExtensions()
{
return array_merge(parent::getExtensions(), array(
new CsrfExtension($this->csrfProvider),
));
}
}

View File

@ -1247,7 +1247,7 @@ class FormTest extends \PHPUnit_Framework_TestCase
public function testCreateViewAcceptsParent()
{
$parent = new FormView();
$parent = new FormView('form');
$form = $this->getBuilder()->getForm();
$view = $form->createView($parent);