From 8f8f53d63103392ab2d7632df3caec5b9617a3db Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Wed, 19 Jan 2011 13:43:24 +0100 Subject: [PATCH] [Form][FrameworkBundle] Implemented FormFactory and added it to the DI container --- .../Compiler/AddFieldFactoryGuessersPass.php | 41 +++ .../FrameworkExtension.php | 27 +- .../FrameworkBundle/FrameworkBundle.php | 26 +- .../FrameworkBundle/Resources/config/form.xml | 62 +++++ src/Symfony/Component/Form/Field.php | 6 +- .../Form/FieldFactory/FieldFactory.php | 1 + src/Symfony/Component/Form/Form.php | 165 ++++++------ .../Component/Form/FormConfiguration.php | 128 ---------- src/Symfony/Component/Form/FormContext.php | 235 ++++++++++++++++++ .../Component/Form/FormContextInterface.php | 100 ++++++++ src/Symfony/Component/Form/FormFactory.php | 175 +++++++++++++ .../Tests/Component/Form/CountryFieldTest.php | 4 +- .../Tests/Component/Form/DateFieldTest.php | 4 +- .../Tests/Component/Form/FieldTest.php | 4 +- .../Symfony/Tests/Component/Form/FormTest.php | 136 +++++----- .../Component/Form/LanguageFieldTest.php | 4 +- .../Tests/Component/Form/LocaleFieldTest.php | 4 +- 17 files changed, 805 insertions(+), 317 deletions(-) create mode 100644 src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddFieldFactoryGuessersPass.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml delete mode 100644 src/Symfony/Component/Form/FormConfiguration.php create mode 100644 src/Symfony/Component/Form/FormContext.php create mode 100644 src/Symfony/Component/Form/FormContextInterface.php create mode 100644 src/Symfony/Component/Form/FormFactory.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddFieldFactoryGuessersPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddFieldFactoryGuessersPass.php new file mode 100644 index 0000000000..10dd975a01 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddFieldFactoryGuessersPass.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Adds all services with the tag "form.field_factory_guesser" as argument + * to the "form.field_factory" service + * + * @author Bernhard Schussek + */ +class AddFieldFactoryGuessersPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition('form.field_factory')) { + return; + } + + $guessers = array_map(function($id) { + return new Reference($id); + }, array_keys($container->findTaggedServiceIds('form.field_factory.guesser'))); + + $definition = $container->getDefinition('form.field_factory'); + $arguments = $definition->getArguments(); + $arguments[0] = $guessers; + $definition->setArguments($arguments); + } +} \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index df8305cec3..74362b1419 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -20,6 +20,7 @@ use Symfony\Component\DependencyInjection\Parameter; use Symfony\Component\Finder\Finder; use Symfony\Component\HttpFoundation\RequestMatcher; use Symfony\Component\HttpKernel\DependencyInjection\Extension; +use Symfony\Component\Form\FormContext; /** * FrameworkExtension. @@ -42,6 +43,23 @@ class FrameworkExtension extends Extension $loader->load('web.xml'); } + if (!$container->hasDefinition('form.factory')) { + $loader->load('form.xml'); + } + + if (isset($config['csrf_protection'])) { + foreach (array('enabled', 'field_name', 'field-name', 'secret') as $key) { + if (isset($config['csrf_protection'][$key])) { + $container->setParameter('form.csrf_protection.'.strtr($key, '-', '_'), + $config['csrf_protection'][$key]); + } + } + } + + if (isset($config['i18n']) && $config['i18n']) { + FormContext::setLocale(\Locale::get()); + } + if (isset($config['ide'])) { switch ($config['ide']) { case 'textmate': @@ -60,12 +78,6 @@ class FrameworkExtension extends Extension $container->setParameter('debug.file_link_format', $pattern); } - foreach (array('csrf_secret', 'csrf-secret') as $key) { - if (isset($config[$key])) { - $container->setParameter('csrf_secret', $config[$key]); - } - } - if (!$container->hasDefinition('event_dispatcher')) { $loader = new XmlFileLoader($container, array(__DIR__.'/../Resources/config', __DIR__.'/Resources/config')); $loader->load('services.xml'); @@ -149,7 +161,8 @@ class FrameworkExtension extends Extension 'Symfony\\Component\\EventDispatcher\\EventDispatcher', 'Symfony\\Bundle\\FrameworkBundle\\EventDispatcher', - 'Symfony\\Component\\Form\\FormConfiguration', + 'Symfony\\Component\\Form\\FormContext', + 'Symfony\\Component\\Form\\FormContextInterface', )); } diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index 318acc34d2..f59f7265fb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddConstraintValidatorsPass; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddFieldFactoryGuessersPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TemplatingPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\RegisterKernelListenersPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddSecurityVotersPass; @@ -23,7 +24,6 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslatorPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\HttpKernel\Bundle\Bundle; -use Symfony\Component\Form\FormConfiguration; /** * Bundle. @@ -37,28 +37,27 @@ class FrameworkBundle extends Bundle */ public function boot() { - if ($this->container->has('error_handler')) { - $this->container->get('error_handler'); - } + $container = $this->container; - FormConfiguration::clearDefaultCsrfSecrets(); - - if ($this->container->hasParameter('csrf_secret')) { - FormConfiguration::addDefaultCsrfSecret($this->container->getParameter('csrf_secret')); - FormConfiguration::enableDefaultCsrfProtection(); + if ($container->has('error_handler')) { + $container->get('error_handler'); } // the session ID should always be included in the CSRF token, even // if default CSRF protection is not enabled - if ($this->container->has('session')) { - $container = $this->container; - FormConfiguration::addDefaultCsrfSecret(function () use ($container) { + if ($container->has('form.default_context') && $container->has('session')) { + $addSessionId = function () use ($container) { // automatically starts the session when the CSRF token is // generated $container->get('session')->start(); return $container->get('session')->getId(); - }); + }; + +// $container->getDefinition('form.default_context') +// ->addMethodCall('addCsrfSecret', array($addSessionId)); +// +// var_dump($container->getDefinition('form.default_context')); } } @@ -73,6 +72,7 @@ class FrameworkBundle extends Bundle $container->addCompilerPass(new RegisterKernelListenersPass()); $container->addCompilerPass(new TemplatingPass()); $container->addCompilerPass(new AddConstraintValidatorsPass()); + $container->addCompilerPass(new AddFieldFactoryGuessersPass()); $container->addCompilerPass(new AddClassesToCachePass()); $container->addCompilerPass(new TranslatorPass()); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml new file mode 100644 index 0000000000..f5d4d023ed --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml @@ -0,0 +1,62 @@ + + + + + + Symfony\Component\Form\FormFactory + Symfony\Component\Form\FieldFactory\FieldFactory + Symfony\Component\Form\FieldFactory\ValidatorFieldFactoryGuesser + Symfony\Component\Form\FormContext + true + _token + secret + Default + + + + + + + + + + + + + + + + + + + + + + + + + + + + %form.validation_groups% + + + + + + %form.csrf_protection.enabled% + + + %form.csrf_protection.field_name% + + + + %form.csrf_protection.secret% + + + + + + diff --git a/src/Symfony/Component/Form/Field.php b/src/Symfony/Component/Form/Field.php index 47fcc56714..d88294beef 100644 --- a/src/Symfony/Component/Form/Field.php +++ b/src/Symfony/Component/Form/Field.php @@ -1,5 +1,7 @@ addOption('normalization_transformer'); $this->key = (string)$key; - $this->locale = FormConfiguration::getDefaultLocale(); + $this->locale = FormContext::getLocale(); parent::__construct($options); diff --git a/src/Symfony/Component/Form/FieldFactory/FieldFactory.php b/src/Symfony/Component/Form/FieldFactory/FieldFactory.php index e48534c886..bf921dbed7 100644 --- a/src/Symfony/Component/Form/FieldFactory/FieldFactory.php +++ b/src/Symfony/Component/Form/FieldFactory/FieldFactory.php @@ -40,6 +40,7 @@ class FieldFactory implements FieldFactoryInterface throw new UnexpectedTypeException($guesser, 'FieldFactoryGuesserInterface'); } } + $this->guessers = $guessers; } diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index c332c66e59..8fa1e9aa18 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -27,14 +27,11 @@ use Symfony\Component\Form\Exception\FormException; * is generated on the fly. * * @author Fabien Potencier + * @author Bernhard Schussek */ class Form extends FieldGroup { protected $validator = null; - protected $validationGroups = null; - - private $csrfSecret = null; - private $csrfFieldName = null; /** * Constructor. @@ -53,11 +50,15 @@ class Form extends FieldGroup $this->setData($data); } - if (FormConfiguration::isDefaultCsrfProtectionEnabled()) { - $this->enableCsrfProtection(); - } - + $this->addOption('csrf_protection'); + $this->addOption('csrf_field_name', '_token'); + $this->addOption('csrf_secrets', array(__FILE__.php_uname())); $this->addOption('field_factory'); + $this->addOption('validation_groups'); + + if (isset($options['validation_groups'])) { + $options['validation_groups'] = (array)$options['validation_groups']; + } parent::__construct($name, $options); @@ -66,26 +67,16 @@ class Form extends FieldGroup if (null !== $data) { $this->setPropertyPath(null); } - } - /** - * Sets the validation groups for this form. - * - * @param array|string $validationGroups - */ - public function setValidationGroups($validationGroups) - { - $this->validationGroups = null === $validationGroups ? $validationGroups : (array) $validationGroups; - } + // Enable CSRF protection, if necessary + if ($this->getOption('csrf_protection')) { + $field = new HiddenField($this->getOption('csrf_field_name'), array( + 'property_path' => null, + )); + $field->setData($this->generateCsrfToken($this->getOption('csrf_secrets'))); - /** - * Returns the validation groups for this form. - * - * @return array - */ - public function getValidationGroups() - { - return $this->validationGroups; + $this->add($field); + } } /** @@ -99,6 +90,46 @@ class Form extends FieldGroup return $this->getOption('field_factory'); } + /** + * Returns the validator used by the form + * + * @return ValidatorInterface The validator instance + */ + public function getValidator() + { + return $this->validator; + } + + /** + * Returns the validation groups validated by the form + * + * @return array A list of validation groups or null + */ + public function getValidationGroups() + { + return $this->getOption('validation_groups'); + } + + /** + * Returns the name used for the CSRF protection field + * + * @return string The field name + */ + public function getCsrfFieldName() + { + return $this->getOption('csrf_field_name'); + } + + /** + * Returns the secret values used for the CSRF protection + * + * @return array A list of string valuesf + */ + public function getCsrfSecrets() + { + return $this->getOption('csrf_secrets'); + } + /** * Binds the form with values and files. * @@ -132,7 +163,7 @@ class Form extends FieldGroup throw new FormException('A validator is required for binding. Forgot to pass it to the constructor of the form?'); } - if ($violations = $this->validator->validate($this, $this->getValidationGroups())) { + if ($violations = $this->validator->validate($this, $this->getOption('validation_groups'))) { // TODO: test me foreach ($violations as $violation) { $propertyPath = new PropertyPath($violation->getPropertyPath()); @@ -172,20 +203,19 @@ class Form extends FieldGroup * * @return string A token string */ - protected function generateCsrfToken($secret) + protected function generateCsrfToken(array $secrets) { - $secret .= get_class($this); - $defaultSecrets = FormConfiguration::getDefaultCsrfSecrets(); + $implodedSecrets = get_class($this); - foreach ($defaultSecrets as $defaultSecret) { - if ($defaultSecret instanceof \Closure) { - $defaultSecret = $defaultSecret(); + foreach ($secrets as $secret) { + if ($secret instanceof \Closure) { + $secret = $secret(); } - $secret .= $defaultSecret; + $implodedSecrets .= $secret; } - return md5($secret); + return md5($implodedSecrets); } /** @@ -193,65 +223,7 @@ class Form extends FieldGroup */ public function isCsrfProtected() { - return $this->has($this->getCsrfFieldName()); - } - - /** - * Enables CSRF protection for this form. - */ - public function enableCsrfProtection($csrfFieldName = null, $csrfSecret = null) - { - if (!$this->isCsrfProtected()) { - if (null === $csrfFieldName) { - $csrfFieldName = FormConfiguration::getDefaultCsrfFieldName(); - } - - if (null === $csrfSecret) { - $csrfSecret = md5(__FILE__.php_uname()); - } - - $field = new HiddenField($csrfFieldName, array( - 'property_path' => null, - )); - $field->setData($this->generateCsrfToken($csrfSecret)); - $this->add($field); - - $this->csrfFieldName = $csrfFieldName; - $this->csrfSecret = $csrfSecret; - } - } - - /** - * Disables CSRF protection for this form. - */ - public function disableCsrfProtection() - { - if ($this->isCsrfProtected()) { - $this->remove($this->getCsrfFieldName()); - - $this->csrfFieldName = null; - $this->csrfSecret = null; - } - } - - /** - * Returns the CSRF field name used in this form - * - * @return string The CSRF field name - */ - public function getCsrfFieldName() - { - return $this->csrfFieldName; - } - - /** - * Returns the CSRF secret used in this form - * - * @return string The CSRF secret - */ - public function getCsrfSecret() - { - return $this->csrfSecret; + return $this->has($this->getOption('csrf_field_name')); } /** @@ -264,7 +236,10 @@ class Form extends FieldGroup if (!$this->isCsrfProtected()) { return true; } else { - return $this->get($this->getCsrfFieldName())->getDisplayedData() === $this->generateCsrfToken($this->getCsrfSecret()); + $actual = $this->get($this->getOption('csrf_field_name'))->getDisplayedData(); + $expected = $this->generateCsrfToken($this->getOption('csrf_secrets')); + + return $actual === $expected; } } diff --git a/src/Symfony/Component/Form/FormConfiguration.php b/src/Symfony/Component/Form/FormConfiguration.php deleted file mode 100644 index 39aab3998f..0000000000 --- a/src/Symfony/Component/Form/FormConfiguration.php +++ /dev/null @@ -1,128 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Form; - -/** - * FormConfiguration holds the default configuration for forms (CSRF, locale, ...). - * - * @author Fabien Potencier - */ -class FormConfiguration -{ - protected static $defaultCsrfSecrets = array(); - protected static $defaultCsrfProtection = false; - protected static $defaultCsrfFieldName = '_token'; - - protected static $defaultLocale = null; - - /** - * Sets the default locale for newly created forms. - * - * @param string $defaultLocale - */ - static public function setDefaultLocale($defaultLocale) - { - self::$defaultLocale = $defaultLocale; - } - - /** - * Returns the default locale for newly created forms. - * - * @return string - */ - static public function getDefaultLocale() - { - return isset(self::$defaultLocale) ? self::$defaultLocale : \Locale::getDefault(); - } - - /** - * Enables CSRF protection for all new forms. - */ - static public function enableDefaultCsrfProtection() - { - self::$defaultCsrfProtection = true; - } - - /** - * Checks if Csrf protection for all forms is enabled. - */ - static public function isDefaultCsrfProtectionEnabled() - { - return self::$defaultCsrfProtection; - } - - /** - * Disables Csrf protection for all forms. - */ - static public function disableDefaultCsrfProtection() - { - self::$defaultCsrfProtection = false; - } - - /** - * Sets the CSRF field name used in all new CSRF protected forms. - * - * @param string $name The CSRF field name - */ - static public function setDefaultCsrfFieldName($name) - { - self::$defaultCsrfFieldName = $name; - } - - /** - * Returns the default CSRF field name. - * - * @return string The CSRF field name - */ - static public function getDefaultCsrfFieldName() - { - return self::$defaultCsrfFieldName; - } - - /** - * Sets the default CSRF secrets to be used in all new CSRF protected forms. - * - * @param array $secrets - */ - static public function setDefaultCsrfSecrets(array $secrets) - { - self::$defaultCsrfSecrets = $secrets; - } - - /** - * Adds CSRF secrets to be used in all new CSRF protected forms. - * - * @param string $secret - */ - static public function addDefaultCsrfSecret($secret) - { - self::$defaultCsrfSecrets[] = $secret; - } - - /** - * Clears the default CSRF secrets. - */ - static public function clearDefaultCsrfSecrets() - { - self::$defaultCsrfSecrets = array(); - } - - /** - * Returns the default CSRF secrets. - * - * @return array - */ - static public function getDefaultCsrfSecrets() - { - return self::$defaultCsrfSecrets; - } -} diff --git a/src/Symfony/Component/Form/FormContext.php b/src/Symfony/Component/Form/FormContext.php new file mode 100644 index 0000000000..5713c93ab2 --- /dev/null +++ b/src/Symfony/Component/Form/FormContext.php @@ -0,0 +1,235 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Component\Form\FieldFactory\FieldFactoryInterface; +use Symfony\Component\Validator\ValidatorInterface; + +/** + * Default implementaton of FormContextInterface + * + * @author Fabien Potencier + * @author Bernhard Schussek + */ +class FormContext implements FormContextInterface +{ + /** + * The locale used by new forms + * @var string + */ + protected static $locale = 'en'; + + /** + * The validator used in the new form + * @var ValidatorInterface + */ + protected $validator = null; + + /** + * The validation group(s) validated in the new form + * @var string|array + */ + protected $validationGroups = null; + + /** + * The field factory used for automatically creating fields in the form + * @var FieldFactoryInterface + */ + protected $fieldFactory = null; + + /** + * The secret strings used for CSRF protection + * @var array + */ + protected $csrfSecrets = null; + + /** + * Whether the new form should be CSRF protected + * @var boolean + */ + protected $csrfProtection = false; + + /** + * The field name used for the CSRF protection + * @var string + */ + protected $csrfFieldName = '_token'; + + /** + * Globally sets the locale for new forms and fields + * + * @param string $locale A valid locale, such as "en", "de_DE" etc. + */ + public static function setLocale($locale) + { + self::$locale = $locale; + } + + /** + * Returns the locale used for new forms and fields + * + * @return string A valid locale, such as "en", "de_DE" etc. + */ + public static function getLocale() + { + return self::$locale; + } + + /** + * @inheritDoc + */ + public function validator(ValidatorInterface $validator) + { + $this->validator = $validator; + + return $this; + } + + /** + * @inheritDoc + */ + public function validationGroups($validationGroups) + { + $this->validationGroups = null === $validationGroups ? $validationGroups : (array) $validationGroups; + + return $this; + } + + /** + * @inheritDoc + */ + public function fieldFactory(FieldFactoryInterface $fieldFactory) + { + $this->fieldFactory = $fieldFactory; + + return $this; + } + + /** + * @inheritDoc + */ + public function csrfProtection($enabled) + { + $this->csrfProtection = $enabled; + + return $this; + } + + /** + * @inheritDoc + */ + public function csrfFieldName($name) + { + $this->csrfFieldName = $name; + + return $this; + } + + /** + * @inheritDoc + */ + public function csrfSecrets(array $secrets) + { + $this->csrfSecrets = $secrets; + + return $this; + } + + /** + * @inheritDoc + */ + public function getForm($name, $data = null) + { + return new Form( + $name, + $data, + $this->validator, + array( + 'csrf_protection' => $this->csrfProtection, + 'csrf_field_name' => $this->csrfFieldName, + 'csrf_secrets' => $this->csrfSecrets, + 'validation_groups' => $this->validationGroups, + 'field_factory' => $this->fieldFactory, + ) + ); + } + + /** + * @inheritDoc + */ + public function addCsrfSecret($secret) + { + $this->csrfSecrets[] = $secret; + + return $this; + } + + /** + * Returns the validator used in the new form + * + * @return ValidatorInterface The validator instance + */ + public function getValidator() + { + return $this->validator; + } + + /** + * Returns the validation groups validated by the new form + * + * @return string|array One or more validation groups + */ + public function getValidationGroups() + { + return $this->validationGroups; + } + + /** + * Returns the field factory used by the new form + * + * @return FieldFactoryInterface The field factory instance + */ + public function getFieldFactory() + { + return $this->fieldFactory; + } + + /** + * Returns whether the new form will be CSRF protected + * + * @return boolean Whether the form will be CSRF protected + */ + public function isCsrfProtectionEnabled() + { + return $this->csrfProtection; + } + + /** + * Returns the field name used for CSRF protection in the new form + * + * @return string The CSRF field name + */ + public function getCsrfFieldName() + { + return $this->csrfFieldName; + } + + /** + * Returns the secret values used for CSRF protection in the new form + * + * @return array A list of secret string values + */ + public function getCsrfSecrets() + { + return $this->csrfSecrets; + } +} diff --git a/src/Symfony/Component/Form/FormContextInterface.php b/src/Symfony/Component/Form/FormContextInterface.php new file mode 100644 index 0000000000..992222d602 --- /dev/null +++ b/src/Symfony/Component/Form/FormContextInterface.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Component\Form\FieldFactory\FieldFactoryInterface; +use Symfony\Component\Validator\ValidatorInterface; + +/** + * Stores settings for creating a new form and creates forms + * + * The methods in this class are chainable, i.e. they return the form context + * object itself. When you have finished configuring the new form, call + * getForm() to create the form. + * + * + * $form = $context + * ->locale('en_US') + * ->validationGroups('Address') + * ->getForm('author'); + * + * + * @author Bernhard Schussek + */ +interface FormContextInterface +{ + /** + * Sets the validator used for validating the form + * + * @param ValidatorInterface $validator The validator instance + * @return FormContextInterface This object + */ + function validator(ValidatorInterface $validator); + + /** + * Sets the validation groups validated by the form + * + * @param string|array $validationGroups One or more validation groups + * @return FormContextInterface This object + */ + function validationGroups($validationGroups); + + /** + * Sets the field factory used for automatically creating fields in the form + * + * @param FieldFactoryInterface $fieldFactory The field factory instance + * @return FormContextInterface This object + */ + function fieldFactory(FieldFactoryInterface $fieldFactory); + + /** + * Enables or disables CSRF protection for the form + * + * @param boolean $enabled Whether the form should be CSRF protected + * @return FormContextInterface This object + */ + function csrfProtection($enabled); + + /** + * Sets the field name used for CSRF protection in the form + * + * @param string $name The CSRF field name + * @return FormContextInterface This object + */ + function csrfFieldName($name); + + /** + * Sets the CSRF secrets to be used in the form + * + * @param array $secrets A list of secret values + * @return FormContextInterface This object + */ + function csrfSecrets(array $secrets); + + /** + * Adds another CSRF secrets without removing the existing CSRF secrets + * + * @param string $secret A secret value + * @return FormContextInterface This object + * @see csrfSecrets() + */ + function addCsrfSecret($secret); + + /** + * Creates a new form with the settings stored in this context + * + * @param string $name The name for the form + * @param array|object $data The data displayed and modified by the form + * @return Form The new form + */ + function getForm($name, $data = null); +} \ No newline at end of file diff --git a/src/Symfony/Component/Form/FormFactory.php b/src/Symfony/Component/Form/FormFactory.php new file mode 100644 index 0000000000..586325518a --- /dev/null +++ b/src/Symfony/Component/Form/FormFactory.php @@ -0,0 +1,175 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Component\Form\FieldFactory\FieldFactoryInterface; +use Symfony\Component\Validator\ValidatorInterface; + +/** + * Creates and configures new form objects + * + * The default configuration of form objects can be passed to the constructor + * as a FormContextInterface object. Call getForm() to create new form objects. + * + * + * $defaultContext = new FormContext(); + * $defaultContext->locale('en_US'); + * $defaultContext->csrfProtection(true); + * $factory = new FormFactory($defaultContext); + * + * $form = $factory->getForm('author'); + * + * + * You can also override the default configuration by calling any of the + * methods in this class. These methods return a FormContextInterface object + * on which you can override further settings or call getForm() to create + * a form. + * + * + * $form = $factory + * ->locale('de_DE') + * ->csrfProtection(false) + * ->getForm('author'); + * + * + * FormFactory instances should be cached and reused in your application. + * + * @author Bernhard Schussek + */ +class FormFactory implements FormContextInterface +{ + /** + * Holds the context with the default configuration + * @var FormContextInterface + */ + protected $defaultContext; + + /** + * Sets the given context as default context + * + * @param FormContextInterface $defaultContext A preconfigured context + */ + public function __construct(FormContextInterface $defaultContext = null) + { + $this->defaultContext = null === $defaultContext ? new FormContext() : $defaultContext; + } + + /** + * Overrides the validator of the default context and returns the new context + * + * @param ValidatorInterface $validator The new validator instance + * @return FormContextInterface The preconfigured form context + */ + public function validator(ValidatorInterface $validator) + { + $context = clone $this->defaultContext; + + return $context->validator($validator); + } + + /** + * Overrides the validation groups of the default context and returns + * the new context + * + * @param string|array $validationGroups One or more validation groups + * @return FormContextInterface The preconfigured form context + */ + public function validationGroups($validationGroups) + { + $context = clone $this->defaultContext; + + return $context->validationGroups($validationGroups); + } + + /** + * Overrides the field factory of the default context and returns + * the new context + * + * @param FieldFactoryInterface $fieldFactory A field factory instance + * @return FormContextInterface The preconfigured form context + */ + public function fieldFactory(FieldFactoryInterface $fieldFactory) + { + $context = clone $this->defaultContext; + + return $context->fieldFactory($fieldFactory); + } + + /** + * Overrides the CSRF protection setting of the default context and returns + * the new context + * + * @param boolean $enabled Whether the form should be CSRF protected + * @return FormContextInterface The preconfigured form context + */ + public function csrfProtection($enabled) + { + $context = clone $this->defaultContext; + + return $context->csrfProtection($enabled); + } + + /** + * Overrides the CSRF field name setting of the default context and returns + * the new context + * + * @param string $name The field name to use for CSRF protection + * @return FormContextInterface The preconfigured form context + */ + public function csrfFieldName($name) + { + $context = clone $this->defaultContext; + + return $context->csrfFieldName($name); + } + + /** + * Overrides the CSRF secrets setting of the default context and returns + * the new context + * + * @param array $secrets The secrets to use for CSRF protection + * @return FormContextInterface The preconfigured form context + */ + public function csrfSecrets(array $secrets) + { + $context = clone $this->defaultContext; + + return $context->csrfSecrets($secrets); + } + + /** + * Adds a new CSRF secret to the ones in the default context and returns + * the new context + * + * @param string $secret The secret to add to the secrets used for + * CSRF protection + * @return FormContextInterface The preconfigured form context + */ + public function addCsrfSecret($secret) + { + $context = clone $this->defaultContext; + + return $context->addCsrfSecret($secret); + } + + /** + * Creates a new form with the settings stored in the default context + * + * @param string $name The name for the form + * @param array|object $data The data displayed and modified by the form + * @return Form The new form + */ + public function getForm($name, $data = null) + { + return $this->defaultContext->getForm($name, $data); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/Form/CountryFieldTest.php b/tests/Symfony/Tests/Component/Form/CountryFieldTest.php index 68fed0397e..600bedd964 100644 --- a/tests/Symfony/Tests/Component/Form/CountryFieldTest.php +++ b/tests/Symfony/Tests/Component/Form/CountryFieldTest.php @@ -12,13 +12,13 @@ namespace Symfony\Tests\Component\Form; use Symfony\Component\Form\CountryField; -use Symfony\Component\Form\FormConfiguration; +use Symfony\Component\Form\FormContext; class CountryFieldTest extends \PHPUnit_Framework_TestCase { public function testCountriesAreSelectable() { - FormConfiguration::setDefaultLocale('de_AT'); + FormContext::setLocale('de_AT'); $field = new CountryField('country'); $choices = $field->getOtherChoices(); diff --git a/tests/Symfony/Tests/Component/Form/DateFieldTest.php b/tests/Symfony/Tests/Component/Form/DateFieldTest.php index c536af9655..feb8c49d9b 100644 --- a/tests/Symfony/Tests/Component/Form/DateFieldTest.php +++ b/tests/Symfony/Tests/Component/Form/DateFieldTest.php @@ -14,13 +14,13 @@ namespace Symfony\Tests\Component\Form; require_once __DIR__ . '/DateTimeTestCase.php'; use Symfony\Component\Form\DateField; -use Symfony\Component\Form\FormConfiguration; +use Symfony\Component\Form\FormContext; class DateFieldTest extends DateTimeTestCase { protected function setUp() { - FormConfiguration::setDefaultLocale('de_AT'); + FormContext::setLocale('de_AT'); } public function testBind_fromInput_dateTime() diff --git a/tests/Symfony/Tests/Component/Form/FieldTest.php b/tests/Symfony/Tests/Component/Form/FieldTest.php index c233bb1683..d7a25f88b2 100644 --- a/tests/Symfony/Tests/Component/Form/FieldTest.php +++ b/tests/Symfony/Tests/Component/Form/FieldTest.php @@ -19,7 +19,7 @@ require_once __DIR__ . '/Fixtures/RequiredOptionsField.php'; use Symfony\Component\Form\ValueTransformer\ValueTransformerInterface; use Symfony\Component\Form\PropertyPath; use Symfony\Component\Form\FieldError; -use Symfony\Component\Form\FormConfiguration; +use Symfony\Component\Form\FormContext; use Symfony\Component\Form\ValueTransformer\TransformationFailedException; use Symfony\Tests\Component\Form\Fixtures\Author; use Symfony\Tests\Component\Form\Fixtures\TestField; @@ -147,7 +147,7 @@ class FieldTest extends \PHPUnit_Framework_TestCase public function testLocaleIsPassedToValueTransformer() { - FormConfiguration::setDefaultLocale('de_DE'); + FormContext::setLocale('de_DE'); $transformer = $this->getMock('Symfony\Component\Form\ValueTransformer\ValueTransformerInterface'); $transformer->expects($this->exactly(1)) diff --git a/tests/Symfony/Tests/Component/Form/FormTest.php b/tests/Symfony/Tests/Component/Form/FormTest.php index 3eef2671e3..8ba2e0714c 100644 --- a/tests/Symfony/Tests/Component/Form/FormTest.php +++ b/tests/Symfony/Tests/Component/Form/FormTest.php @@ -15,7 +15,7 @@ require_once __DIR__ . '/Fixtures/Author.php'; require_once __DIR__ . '/Fixtures/TestField.php'; use Symfony\Component\Form\Form; -use Symfony\Component\Form\FormConfiguration; +use Symfony\Component\Form\FormContext; use Symfony\Component\Form\Field; use Symfony\Component\Form\HiddenField; use Symfony\Component\Form\FieldGroup; @@ -68,10 +68,11 @@ class FormTest extends \PHPUnit_Framework_TestCase protected function setUp() { - FormConfiguration::disableDefaultCsrfProtection(); - FormConfiguration::setDefaultCsrfSecrets(array()); $this->validator = $this->createMockValidator(); - $this->form = new Form('author', new Author(), $this->validator); + $this->form = new Form('author', new Author(), $this->validator, array( + 'csrf_protection' => false, + 'csrf_secrets' => array(), + )); } public function testConstructInitializesObject() @@ -84,19 +85,6 @@ class FormTest extends \PHPUnit_Framework_TestCase new TestSetDataBeforeConfigureForm($this, 'author', new Author(), $this->validator); } - public function testIsCsrfProtected() - { - $this->assertFalse($this->form->isCsrfProtected()); - - $this->form->enableCsrfProtection(); - - $this->assertTrue($this->form->isCsrfProtected()); - - $this->form->disableCsrfProtection(); - - $this->assertFalse($this->form->isCsrfProtected()); - } - public function testNoCsrfProtectionByDefault() { $form = new Form('author', new Author(), $this->validator); @@ -104,60 +92,72 @@ class FormTest extends \PHPUnit_Framework_TestCase $this->assertFalse($form->isCsrfProtected()); } - public function testDefaultCsrfProtectionCanBeEnabled() + public function testCsrfProtectionCanBeEnabled() { - FormConfiguration::enableDefaultCsrfProtection(); - $form = new Form('author', new Author(), $this->validator); + $form = new Form('author', new Author(), $this->validator, array( + 'csrf_protection' => true, + )); $this->assertTrue($form->isCsrfProtected()); } public function testGeneratedCsrfSecretByDefault() { - $form = new Form('author', new Author(), $this->validator); - $form->enableCsrfProtection(); + $form = new Form('author', new Author(), $this->validator, array( + 'csrf_protection' => true, + )); - $this->assertTrue(strlen($form->getCsrfSecret()) >= 32); + $secrets = $form->getCsrfSecrets(); + + $this->assertEquals(1, count($secrets)); + $this->assertTrue(strlen($secrets[0]) >= 32); } - public function testDefaultCsrfSecretsCanBeAdded() + public function testCsrfSecretsCanBeSet() { - FormConfiguration::addDefaultCsrfSecret('foobar'); + $form = new Form('author', new Author(), $this->validator, array( + 'csrf_protection' => true, + 'csrf_field_name' => '_token', + 'csrf_secrets' => array('foobar', 'secret'), + )); - $form = new Form('author', new Author(), $this->validator); - $form->enableCsrfProtection('_token', 'secret'); - - $this->assertEquals(md5('secret'.get_class($form).'foobar'), $form['_token']->getData()); + $this->assertEquals(md5(get_class($form).'foobarsecret'), $form['_token']->getData()); } - public function testDefaultCsrfSecretsCanBeAddedAsClosures() + public function testCsrfSecretsCanBeSetAsClosures() { - FormConfiguration::addDefaultCsrfSecret(function () { + $closure = function () { return 'foobar'; - }); + }; - $form = new Form('author', new Author(), $this->validator); - $form->enableCsrfProtection('_token', 'secret'); + $form = new Form('author', new Author(), $this->validator, array( + 'csrf_protection' => true, + 'csrf_field_name' => '_token', + 'csrf_secrets' => array($closure, 'secret'), + )); - $this->assertEquals(md5('secret'.get_class($form).'foobar'), $form['_token']->getData()); + $this->assertEquals(md5(get_class($form).'foobarsecret'), $form['_token']->getData()); } - public function testDefaultCsrfFieldNameCanBeSet() + public function testCsrfFieldNameCanBeSet() { - FormConfiguration::setDefaultCsrfFieldName('foobar'); - $form = new Form('author', new Author(), $this->validator); - $form->enableCsrfProtection(); + $form = new Form('author', new Author(), $this->validator, array( + 'csrf_protection' => true, + 'csrf_field_name' => 'foobar', + )); $this->assertEquals('foobar', $form->getCsrfFieldName()); } public function testCsrfProtectedFormsHaveExtraField() { - $this->form->enableCsrfProtection(); + $form = new Form('author', new Author(), $this->validator, array( + 'csrf_protection' => true, + )); - $this->assertTrue($this->form->has($this->form->getCsrfFieldName())); + $this->assertTrue($form->has($this->form->getCsrfFieldName())); - $field = $this->form->get($this->form->getCsrfFieldName()); + $field = $form->get($form->getCsrfFieldName()); $this->assertTrue($field instanceof HiddenField); $this->assertGreaterThanOrEqual(32, strlen($field->getDisplayedData())); @@ -172,46 +172,61 @@ class FormTest extends \PHPUnit_Framework_TestCase public function testIsCsrfTokenValidPasses() { - $this->form->enableCsrfProtection(); + $form = new Form('author', new Author(), $this->validator, array( + 'csrf_protection' => true, + )); - $field = $this->form->getCsrfFieldName(); - $token = $this->form->get($field)->getDisplayedData(); + $field = $form->getCsrfFieldName(); + $token = $form->get($field)->getDisplayedData(); - $this->form->bind(array($field => $token)); + $form->bind(array($field => $token)); - $this->assertTrue($this->form->isCsrfTokenValid()); + $this->assertTrue($form->isCsrfTokenValid()); } public function testIsCsrfTokenValidFails() { - $this->form->enableCsrfProtection(); + $form = new Form('author', new Author(), $this->validator, array( + 'csrf_protection' => true, + )); - $field = $this->form->getCsrfFieldName(); + $field = $form->getCsrfFieldName(); - $this->form->bind(array($field => 'foobar')); + $form->bind(array($field => 'foobar')); - $this->assertFalse($this->form->isCsrfTokenValid()); + $this->assertFalse($form->isCsrfTokenValid()); } - public function testValidationGroupsCanBeSet() + public function testValidationGroupNullByDefault() { - $form = new Form('author', new Author(), $this->validator); + $this->assertNull($this->form->getValidationGroups()); + } + + public function testValidationGroupsCanBeSetToString() + { + $form = new Form('author', new Author(), $this->validator, array( + 'validation_groups' => 'group', + )); - $this->assertNull($form->getValidationGroups()); - $form->setValidationGroups('group'); $this->assertEquals(array('group'), $form->getValidationGroups()); - $form->setValidationGroups(array('group1', 'group2')); + } + + public function testValidationGroupsCanBeSetToArray() + { + $form = new Form('author', new Author(), $this->validator, array( + 'validation_groups' => array('group1', 'group2'), + )); + $this->assertEquals(array('group1', 'group2'), $form->getValidationGroups()); - $form->setValidationGroups(null); - $this->assertNull($form->getValidationGroups()); } public function testBindUsesValidationGroups() { $field = $this->createMockField('firstName'); - $form = new Form('author', new Author(), $this->validator); + $form = new Form('author', new Author(), $this->validator, array( + 'validation_groups' => 'group', + )); $form->add($field); - $form->setValidationGroups('group'); $this->validator->expects($this->once()) ->method('validate') @@ -225,7 +240,6 @@ class FormTest extends \PHPUnit_Framework_TestCase $field = $this->createMockField('firstName'); $form = new Form('author', new Author()); $form->add($field); - $form->setValidationGroups('group'); $this->setExpectedException('Symfony\Component\Form\Exception\FormException'); diff --git a/tests/Symfony/Tests/Component/Form/LanguageFieldTest.php b/tests/Symfony/Tests/Component/Form/LanguageFieldTest.php index e28bd2f5db..7c94820512 100644 --- a/tests/Symfony/Tests/Component/Form/LanguageFieldTest.php +++ b/tests/Symfony/Tests/Component/Form/LanguageFieldTest.php @@ -12,13 +12,13 @@ namespace Symfony\Tests\Component\Form; use Symfony\Component\Form\LanguageField; -use Symfony\Component\Form\FormConfiguration; +use Symfony\Component\Form\FormContext; class LanguageFieldTest extends \PHPUnit_Framework_TestCase { public function testCountriesAreSelectable() { - FormConfiguration::setDefaultLocale('de_AT'); + FormContext::setLocale('de_AT'); $field = new LanguageField('language'); $choices = $field->getOtherChoices(); diff --git a/tests/Symfony/Tests/Component/Form/LocaleFieldTest.php b/tests/Symfony/Tests/Component/Form/LocaleFieldTest.php index f778ba7476..7d6eb127d0 100644 --- a/tests/Symfony/Tests/Component/Form/LocaleFieldTest.php +++ b/tests/Symfony/Tests/Component/Form/LocaleFieldTest.php @@ -12,13 +12,13 @@ namespace Symfony\Tests\Component\Form; use Symfony\Component\Form\LocaleField; -use Symfony\Component\Form\FormConfiguration; +use Symfony\Component\Form\FormContext; class LocaleFieldTest extends \PHPUnit_Framework_TestCase { public function testLocalesAreSelectable() { - FormConfiguration::setDefaultLocale('de_AT'); + FormContext::setLocale('de_AT'); $field = new LocaleField('language'); $choices = $field->getOtherChoices();