2010-06-24 09:40:05 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace Symfony\Components\Form;
|
|
|
|
|
|
|
|
use Symfony\Components\Validator\ValidatorInterface;
|
|
|
|
use Symfony\Components\Validator\ConstraintViolation;
|
|
|
|
use Symfony\Components\I18N\TranslatorInterface;
|
|
|
|
use Symfony\Components\File\UploadedFile;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Form represents a form.
|
|
|
|
*
|
|
|
|
* A form is composed of a validator schema and a widget form schema.
|
|
|
|
*
|
|
|
|
* Form also takes care of Csrf protection by default.
|
|
|
|
*
|
|
|
|
* A Csrf secret can be any random string. If set to false, it disables the
|
|
|
|
* Csrf protection, and if set to null, it forces the form to use the global
|
|
|
|
* Csrf secret. If the global Csrf secret is also null, then a random one
|
|
|
|
* is generated on the fly.
|
|
|
|
*
|
|
|
|
* @author Fabien Potencier <fabien.potencier@symfony-project.com>
|
|
|
|
* @version SVN: $Id: Form.php 245 2010-01-31 22:22:39Z flo $
|
|
|
|
*/
|
|
|
|
class Form extends FieldGroup
|
|
|
|
{
|
2010-06-24 10:24:08 +01:00
|
|
|
protected static $defaultCsrfSecret = null;
|
|
|
|
protected static $defaultCsrfProtection = false;
|
|
|
|
protected static $defaultCsrfFieldName = '_csrf_token';
|
|
|
|
protected static $defaultLocale = null;
|
|
|
|
protected static $defaultTranslator = null;
|
|
|
|
|
|
|
|
protected $validator = null;
|
|
|
|
protected $validationGroups = null;
|
|
|
|
|
|
|
|
private $csrfSecret = null;
|
|
|
|
private $csrfFieldName = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Constructor.
|
|
|
|
*
|
|
|
|
* @param array $defaults An array of field default values
|
|
|
|
* @param array $options An array of options
|
|
|
|
* @param string $defaultCsrfSecret A Csrf secret
|
|
|
|
*/
|
|
|
|
public function __construct($name, $object, ValidatorInterface $validator, array $options = array())
|
|
|
|
{
|
|
|
|
$this->generator = new HtmlGenerator();
|
|
|
|
$this->validator = $validator;
|
|
|
|
|
|
|
|
$this->setData($object);
|
|
|
|
$this->setCsrfFieldName(self::$defaultCsrfFieldName);
|
|
|
|
|
|
|
|
if (self::$defaultCsrfSecret !== null) {
|
|
|
|
$this->setCsrfSecret(self::$defaultCsrfSecret);
|
|
|
|
} else {
|
|
|
|
$this->setCsrfSecret(md5(__FILE__.php_uname()));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (self::$defaultCsrfProtection !== false) {
|
|
|
|
$this->enableCsrfProtection();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (self::$defaultLocale !== null) {
|
|
|
|
$this->setLocale(self::$defaultLocale);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (self::$defaultTranslator !== null) {
|
|
|
|
$this->setTranslator(self::$defaultTranslator);
|
|
|
|
}
|
2010-07-04 15:20:10 +01:00
|
|
|
|
|
|
|
parent::__construct($name, $options);
|
2010-06-24 10:24:08 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the charset used for rendering HTML
|
|
|
|
*
|
|
|
|
* This method overrides the internal HTML generator! If you want to use
|
|
|
|
* your own generator, use setGenerator() instead.
|
|
|
|
*
|
|
|
|
* @param string $charset
|
|
|
|
*/
|
|
|
|
public function setCharset($charset)
|
|
|
|
{
|
|
|
|
$this->setGenerator(new HtmlGenerator($charset));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the validation groups for this form.
|
|
|
|
*
|
|
|
|
* @param array|string $validationGroups
|
|
|
|
*/
|
|
|
|
public function setValidationGroups($validationGroups)
|
|
|
|
{
|
|
|
|
$this->validationGroups = $validationGroups === null ? $validationGroups : (array) $validationGroups;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the validation groups for this form.
|
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function getValidationGroups()
|
|
|
|
{
|
|
|
|
return $this->validationGroups;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 self::$defaultLocale;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the default translator for newly created forms.
|
|
|
|
*
|
|
|
|
* @param TranslatorInterface $defaultTranslator
|
|
|
|
*/
|
|
|
|
static public function setDefaultTranslator(TranslatorInterface $defaultTranslator)
|
|
|
|
{
|
|
|
|
self::$defaultTranslator = $defaultTranslator;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the default translator for newly created forms.
|
|
|
|
*
|
|
|
|
* @return TranslatorInterface
|
|
|
|
*/
|
|
|
|
static public function getDefaultTranslator()
|
|
|
|
{
|
|
|
|
return self::$defaultTranslator;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Binds the form with values and files.
|
|
|
|
*
|
|
|
|
* This method is final because it is very easy to break a form when
|
|
|
|
* overriding this method and adding logic that depends on $taintedFiles.
|
|
|
|
* You should override doBind() instead where the uploaded files are
|
|
|
|
* already merged into the data array.
|
|
|
|
*
|
|
|
|
* @param array $taintedValues The form data of the $_POST array
|
|
|
|
* @param array $taintedFiles The form data of the $_FILES array
|
|
|
|
* @return boolean Whether the form is valid
|
|
|
|
*/
|
|
|
|
final public function bind($taintedValues, array $taintedFiles = null)
|
|
|
|
{
|
|
|
|
if ($taintedFiles === null) {
|
|
|
|
if ($this->isMultipart() && $this->getParent() === null) {
|
|
|
|
throw new \InvalidArgumentException('You must provide a files array for multipart forms');
|
|
|
|
}
|
|
|
|
|
|
|
|
$taintedFiles = array();
|
|
|
|
} else {
|
|
|
|
$taintedFiles = self::convertFileInformation(self::fixPhpFilesArray($taintedFiles));
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->doBind(self::deepArrayUnion($taintedValues, $taintedFiles));
|
|
|
|
|
|
|
|
if ($this->getParent() === null) {
|
|
|
|
if ($violations = $this->validator->validate($this, $this->getValidationGroups())) {
|
|
|
|
foreach ($violations as $violation) {
|
|
|
|
$propertyPath = new PropertyPath($violation->getPropertyPath());
|
|
|
|
|
|
|
|
if ($propertyPath->getCurrent() == 'data') {
|
|
|
|
$type = self::DATA_ERROR;
|
|
|
|
$propertyPath->next(); // point at the first data element
|
|
|
|
} else {
|
|
|
|
$type = self::FIELD_ERROR;
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->addError($violation->getMessage(), $propertyPath, $type);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Binds the form with the given data.
|
|
|
|
*
|
|
|
|
* @param array $taintedData The data to bind to the form
|
|
|
|
* @return boolean Whether the form is valid
|
|
|
|
*/
|
|
|
|
protected function doBind(array $taintedData)
|
|
|
|
{
|
|
|
|
parent::bind($taintedData);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the stylesheet paths associated with the form.
|
|
|
|
*
|
|
|
|
* @return array An array of stylesheet paths
|
|
|
|
*/
|
|
|
|
public function getStylesheets()
|
|
|
|
{
|
|
|
|
return $this->getWidget()->getStylesheets();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the JavaScript paths associated with the form.
|
|
|
|
*
|
|
|
|
* @return array An array of JavaScript paths
|
|
|
|
*/
|
|
|
|
public function getJavaScripts()
|
|
|
|
{
|
|
|
|
return $this->getWidget()->getJavaScripts();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a CSRF token for the set CSRF secret
|
|
|
|
*
|
|
|
|
* If you want to change the algorithm used to compute the token, you
|
|
|
|
* can override this method.
|
|
|
|
*
|
|
|
|
* @param string $secret The secret string to use (null to use the current secret)
|
|
|
|
*
|
|
|
|
* @return string A token string
|
|
|
|
*/
|
|
|
|
protected function getCsrfToken()
|
|
|
|
{
|
|
|
|
return md5($this->csrfSecret.session_id().get_class($this));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return true if this form is CSRF protected
|
|
|
|
*/
|
|
|
|
public function isCsrfProtected()
|
|
|
|
{
|
|
|
|
return $this->has($this->getCsrfFieldName());
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Enables CSRF protection for this form.
|
|
|
|
*/
|
|
|
|
public function enableCsrfProtection()
|
|
|
|
{
|
|
|
|
if (!$this->isCsrfProtected()) {
|
|
|
|
$field = new HiddenField($this->getCsrfFieldName(), array(
|
|
|
|
'property_path' => null,
|
|
|
|
));
|
|
|
|
$field->setData($this->getCsrfToken());
|
|
|
|
$this->add($field);
|
2010-06-24 09:40:05 +01:00
|
|
|
}
|
2010-06-24 10:24:08 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Disables CSRF protection for this form.
|
|
|
|
*/
|
|
|
|
public function disableCsrfProtection()
|
|
|
|
{
|
|
|
|
if ($this->isCsrfProtected()) {
|
|
|
|
$this->remove($this->getCsrfFieldName());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the CSRF field name used in this form
|
|
|
|
*
|
|
|
|
* @param string $name The CSRF field name
|
|
|
|
*/
|
|
|
|
public function setCsrfFieldName($name)
|
|
|
|
{
|
|
|
|
$this->csrfFieldName = $name;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the CSRF field name used in this form
|
|
|
|
*
|
|
|
|
* @return string The CSRF field name
|
|
|
|
*/
|
|
|
|
public function getCsrfFieldName()
|
|
|
|
{
|
|
|
|
return $this->csrfFieldName;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the CSRF secret used in this form
|
|
|
|
*
|
|
|
|
* @param string $secret
|
|
|
|
*/
|
|
|
|
public function setCsrfSecret($secret)
|
|
|
|
{
|
|
|
|
$this->csrfSecret = $secret;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the CSRF secret used in this form
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function getCsrfSecret()
|
|
|
|
{
|
|
|
|
return $this->csrfSecret;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns whether the CSRF token is valid
|
|
|
|
*
|
|
|
|
* @return boolean
|
|
|
|
*/
|
|
|
|
public function isCsrfTokenValid()
|
|
|
|
{
|
|
|
|
if (!$this->isCsrfProtected()) {
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
return $this->get($this->getCsrfFieldName())->getDisplayedData() === $this->getCsrfToken();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Enables CSRF protection for all new forms
|
|
|
|
*/
|
|
|
|
static public function enableDefaultCsrfProtection()
|
|
|
|
{
|
|
|
|
self::$defaultCsrfProtection = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 CSRF secret used in all new CSRF protected forms
|
|
|
|
*
|
|
|
|
* @param string $secret
|
|
|
|
*/
|
|
|
|
static public function setDefaultCsrfSecret($secret)
|
|
|
|
{
|
|
|
|
self::$defaultCsrfSecret = $secret;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the default CSRF secret
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
static public function getDefaultCsrfSecret()
|
|
|
|
{
|
|
|
|
return self::$defaultCsrfSecret;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Renders the form tag.
|
|
|
|
*
|
|
|
|
* This method only renders the opening form tag.
|
|
|
|
* You need to close it after the form rendering.
|
|
|
|
*
|
|
|
|
* This method takes into account the multipart widgets.
|
|
|
|
*
|
|
|
|
* @param string $url The URL for the action
|
|
|
|
* @param array $attributes An array of HTML attributes
|
|
|
|
*
|
|
|
|
* @return string An HTML representation of the opening form tag
|
|
|
|
*/
|
|
|
|
public function renderFormTag($url, array $attributes = array())
|
|
|
|
{
|
|
|
|
return sprintf('<form%s>', $this->generator->attributes(array_merge(array(
|
|
|
|
'action' => $url,
|
|
|
|
'method' => isset($attributes['method']) ? strtolower($attributes['method']) : 'post',
|
|
|
|
'enctype' => $this->isMultipart() ? 'multipart/form-data' : null,
|
|
|
|
), $attributes)));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns whether the maximum POST size was reached in this request.
|
|
|
|
*
|
|
|
|
* @return boolean
|
|
|
|
*/
|
|
|
|
public function isPostMaxSizeReached()
|
|
|
|
{
|
|
|
|
if (isset($_SERVER['CONTENT_LENGTH'])) {
|
|
|
|
$length = (int) $_SERVER['CONTENT_LENGTH'];
|
|
|
|
$max = trim(ini_get('post_max_size'));
|
|
|
|
|
|
|
|
switch (strtolower(substr($max, -1))) {
|
|
|
|
// The 'G' modifier is available since PHP 5.1.0
|
|
|
|
case 'g':
|
|
|
|
$max *= 1024;
|
|
|
|
case 'm':
|
|
|
|
$max *= 1024;
|
|
|
|
case 'k':
|
|
|
|
$max *= 1024;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $length > $max;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Merges two arrays without reindexing numeric keys.
|
|
|
|
*
|
|
|
|
* @param array $array1 An array to merge
|
|
|
|
* @param array $array2 An array to merge
|
|
|
|
*
|
|
|
|
* @return array The merged array
|
|
|
|
*/
|
|
|
|
static protected function deepArrayUnion($array1, $array2)
|
|
|
|
{
|
|
|
|
foreach ($array2 as $key => $value) {
|
|
|
|
if (is_array($value) && isset($array1[$key]) && is_array($array1[$key])) {
|
|
|
|
$array1[$key] = self::deepArrayUnion($array1[$key], $value);
|
|
|
|
} else {
|
|
|
|
$array1[$key] = $value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $array1;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fixes a malformed PHP $_FILES array.
|
|
|
|
*
|
|
|
|
* PHP has a bug that the format of the $_FILES array differs, depending on
|
|
|
|
* whether the uploaded file fields had normal field names or array-like
|
|
|
|
* field names ("normal" vs. "parent[child]").
|
|
|
|
*
|
|
|
|
* This method fixes the array to look like the "normal" $_FILES array.
|
|
|
|
*
|
|
|
|
* @param array $data
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
static protected function fixPhpFilesArray(array $data)
|
|
|
|
{
|
|
|
|
$fileKeys = array('error', 'name', 'size', 'tmp_name', 'type');
|
2010-06-24 09:40:05 +01:00
|
|
|
$keys = array_keys($data);
|
|
|
|
sort($keys);
|
|
|
|
|
2010-06-24 10:24:08 +01:00
|
|
|
$files = $data;
|
|
|
|
|
|
|
|
if ($fileKeys == $keys && isset($data['name']) && is_array($data['name'])) {
|
|
|
|
foreach ($fileKeys as $k) {
|
|
|
|
unset($files[$k]);
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach (array_keys($data['name']) as $key) {
|
|
|
|
$files[$key] = self::fixPhpFilesArray(array(
|
|
|
|
'error' => $data['error'][$key],
|
|
|
|
'name' => $data['name'][$key],
|
|
|
|
'type' => $data['type'][$key],
|
|
|
|
'tmp_name' => $data['tmp_name'][$key],
|
|
|
|
'size' => $data['size'][$key],
|
|
|
|
));
|
|
|
|
}
|
2010-06-24 09:40:05 +01:00
|
|
|
}
|
2010-06-24 10:24:08 +01:00
|
|
|
|
|
|
|
return $files;
|
2010-06-24 09:40:05 +01:00
|
|
|
}
|
|
|
|
|
2010-06-24 10:24:08 +01:00
|
|
|
/**
|
|
|
|
* Converts uploaded files to instances of clsas UploadedFile.
|
|
|
|
*
|
|
|
|
* @param array $files A (multi-dimensional) array of uploaded file information
|
|
|
|
* @return array A (multi-dimensional) array of UploadedFile instances
|
|
|
|
*/
|
|
|
|
static protected function convertFileInformation(array $files)
|
|
|
|
{
|
|
|
|
$fileKeys = array('error', 'name', 'size', 'tmp_name', 'type');
|
|
|
|
|
|
|
|
foreach ($files as $key => $data) {
|
|
|
|
if (is_array($data)) {
|
|
|
|
$keys = array_keys($data);
|
|
|
|
sort($keys);
|
|
|
|
|
|
|
|
if ($keys == $fileKeys) {
|
|
|
|
$files[$key] = new UploadedFile($data['tmp_name'], $data['name'], $data['type'], $data['size'], $data['error']);
|
|
|
|
} else {
|
|
|
|
$files[$key] = self::convertFileInformation($data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $files;
|
|
|
|
}
|
2010-06-24 09:40:05 +01:00
|
|
|
}
|