[Form] Moved POST_MAX_SIZE validation from FormValidator to request handler

This commit is contained in:
Bernhard Schussek 2014-09-15 16:48:43 +02:00
parent 47802105d3
commit 759ae1a7a1
14 changed files with 201 additions and 193 deletions

View File

@ -151,8 +151,11 @@
</service> </service>
<!-- FormTypeHttpFoundationExtension --> <!-- FormTypeHttpFoundationExtension -->
<service id="form.server_params" class="Symfony\Component\Form\Util\ServerParams" public="false"/>
<service id="form.type_extension.form.http_foundation" class="Symfony\Component\Form\Extension\HttpFoundation\Type\FormTypeHttpFoundationExtension"> <service id="form.type_extension.form.http_foundation" class="Symfony\Component\Form\Extension\HttpFoundation\Type\FormTypeHttpFoundationExtension">
<tag name="form.type_extension" alias="form" /> <tag name="form.type_extension" alias="form" />
<argument type="service" id="form.server_params"/>
</service> </service>
<!-- FormTypeValidatorExtension --> <!-- FormTypeValidatorExtension -->

View File

@ -56,8 +56,7 @@ class FormType extends BaseType
->setDataLocked($isDataOptionSet) ->setDataLocked($isDataOptionSet)
->setDataMapper($options['compound'] ? new PropertyPathMapper($this->propertyAccessor) : null) ->setDataMapper($options['compound'] ? new PropertyPathMapper($this->propertyAccessor) : null)
->setMethod($options['method']) ->setMethod($options['method'])
->setAction($options['action']) ->setAction($options['action']);
;
if ($options['trim']) { if ($options['trim']) {
$builder->addEventSubscriber(new TrimListener()); $builder->addEventSubscriber(new TrimListener());
@ -170,25 +169,26 @@ class FormType extends BaseType
)); ));
$resolver->setDefaults(array( $resolver->setDefaults(array(
'data_class' => $dataClass, 'data_class' => $dataClass,
'empty_data' => $emptyData, 'empty_data' => $emptyData,
'trim' => true, 'trim' => true,
'required' => true, 'required' => true,
'read_only' => false, 'read_only' => false,
'max_length' => null, 'max_length' => null,
'pattern' => null, 'pattern' => null,
'property_path' => null, 'property_path' => null,
'mapped' => true, 'mapped' => true,
'by_reference' => true, 'by_reference' => true,
'error_bubbling' => $errorBubbling, 'error_bubbling' => $errorBubbling,
'label_attr' => array(), 'label_attr' => array(),
'virtual' => null, 'virtual' => null,
'inherit_data' => $inheritData, 'inherit_data' => $inheritData,
'compound' => true, 'compound' => true,
'method' => 'POST', 'method' => 'POST',
// According to RFC 2396 (http://www.ietf.org/rfc/rfc2396.txt) // According to RFC 2396 (http://www.ietf.org/rfc/rfc2396.txt)
// section 4.2., empty URIs are considered same-document references // section 4.2., empty URIs are considered same-document references
'action' => '', 'action' => '',
'post_max_size_message' => 'The uploaded file was too large. Please try to upload a smaller file.',
)); ));
$resolver->setAllowedTypes(array( $resolver->setAllowedTypes(array(

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Form\Extension\HttpFoundation; namespace Symfony\Component\Form\Extension\HttpFoundation;
use Symfony\Component\Form\AbstractExtension; use Symfony\Component\Form\AbstractExtension;
use Symfony\Component\Form\Util\ServerParams;
/** /**
* Integrates the HttpFoundation component with the Form library. * Integrates the HttpFoundation component with the Form library.
@ -20,10 +21,20 @@ use Symfony\Component\Form\AbstractExtension;
*/ */
class HttpFoundationExtension extends AbstractExtension class HttpFoundationExtension extends AbstractExtension
{ {
/**
* @var ServerParams
*/
private $serverParams;
public function __construct(ServerParams $serverParams = null)
{
$this->serverParams = $serverParams;
}
protected function loadTypeExtensions() protected function loadTypeExtensions()
{ {
return array( return array(
new Type\FormTypeHttpFoundationExtension(), new Type\FormTypeHttpFoundationExtension($this->serverParams),
); );
} }
} }

View File

@ -12,10 +12,10 @@
namespace Symfony\Component\Form\Extension\HttpFoundation; namespace Symfony\Component\Form\Extension\HttpFoundation;
use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Extension\Validator\Util\ServerParams;
use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\RequestHandlerInterface; use Symfony\Component\Form\RequestHandlerInterface;
use Symfony\Component\Form\Util\ServerParams;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
/** /**
@ -34,9 +34,9 @@ class HttpFoundationRequestHandler implements RequestHandlerInterface
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function __construct(ServerParams $params = null) public function __construct(ServerParams $serverParams = null)
{ {
$this->serverParams = $params ?: new ServerParams(); $this->serverParams = $serverParams ?: new ServerParams();
} }
/** /**
@ -68,6 +68,25 @@ class HttpFoundationRequestHandler implements RequestHandlerInterface
$data = $request->query->get($name); $data = $request->query->get($name);
} }
} else { } else {
// Mark the form with an error if the uploaded size was too large
// This is done here and not in FormValidator because $_POST is
// empty when that error occurs. Hence the form is never submitted.
$contentLength = $this->serverParams->getContentLength();
$maxContentLength = $this->serverParams->getPostMaxSize();
if (!empty($maxContentLength) && $contentLength > $maxContentLength) {
// Submit the form, but don't clear the default values
$form->submit(null, false);
$form->addError(new FormError(
$form->getConfig()->getOption('post_max_size_message'),
null,
array('{{ max }}' => $this->serverParams->getNormalizedIniPostMaxSize())
));
return;
}
if ('' === $name) { if ('' === $name) {
$params = $request->request->all(); $params = $request->request->all();
$files = $request->files->all(); $files = $request->files->all();
@ -76,10 +95,6 @@ class HttpFoundationRequestHandler implements RequestHandlerInterface
$params = $request->request->get($name, $default); $params = $request->request->get($name, $default);
$files = $request->files->get($name, $default); $files = $request->files->get($name, $default);
} else { } else {
if ($this->serverParams->getContentLength() > $this->serverParams->getPostMaxSize()) {
$form->addError(new FormError('Max post size exceeded.'));
}
// Don't submit the form if it is not present in the request // Don't submit the form if it is not present in the request
return; return;
} }

View File

@ -15,6 +15,7 @@ use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\HttpFoundation\EventListener\BindRequestListener; use Symfony\Component\Form\Extension\HttpFoundation\EventListener\BindRequestListener;
use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler; use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Util\ServerParams;
/** /**
* @author Bernhard Schussek <bschussek@gmail.com> * @author Bernhard Schussek <bschussek@gmail.com>
@ -31,10 +32,10 @@ class FormTypeHttpFoundationExtension extends AbstractTypeExtension
*/ */
private $requestHandler; private $requestHandler;
public function __construct() public function __construct(ServerParams $serverParams = null)
{ {
$this->listener = new BindRequestListener(); $this->listener = new BindRequestListener();
$this->requestHandler = new HttpFoundationRequestHandler(); $this->requestHandler = new HttpFoundationRequestHandler($serverParams);
} }
/** /**

View File

@ -12,7 +12,6 @@
namespace Symfony\Component\Form\Extension\Validator\Constraints; namespace Symfony\Component\Form\Extension\Validator\Constraints;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\Extension\Validator\Util\ServerParams;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\ConstraintValidator;
@ -21,22 +20,6 @@ use Symfony\Component\Validator\ConstraintValidator;
*/ */
class FormValidator extends ConstraintValidator class FormValidator extends ConstraintValidator
{ {
/**
* @var ServerParams
*/
private $serverParams;
/**
* Creates a validator with the given server parameters.
*
* @param ServerParams $params The server parameters. Default
* parameters are created if null.
*/
public function __construct(ServerParams $params = null)
{
$this->serverParams = $params ?: new ServerParams();
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
@ -113,21 +96,6 @@ class FormValidator extends ConstraintValidator
$form->getExtraData() $form->getExtraData()
); );
} }
// Mark the form with an error if the uploaded size was too large
$length = $this->serverParams->getContentLength();
if ($form->isRoot() && null !== $length) {
$max = $this->serverParams->getPostMaxSize();
if (!empty($max) && $length > $max) {
$this->context->addViolation(
$config->getOption('post_max_size_message'),
array('{{ max }}' => $this->serverParams->getNormalizedIniPostMaxSize()),
$length
);
}
}
} }
/** /**

View File

@ -66,7 +66,6 @@ class FormTypeValidatorExtension extends BaseValidatorExtension
'invalid_message' => 'This value is not valid.', 'invalid_message' => 'This value is not valid.',
'invalid_message_parameters' => array(), 'invalid_message_parameters' => array(),
'extra_fields_message' => 'This form should not contain extra fields.', 'extra_fields_message' => 'This form should not contain extra fields.',
'post_max_size_message' => 'The uploaded file was too large. Please try to upload a smaller file.',
)); ));
$resolver->setNormalizers(array( $resolver->setNormalizers(array(

View File

@ -14,59 +14,6 @@ namespace Symfony\Component\Form\Extension\Validator\Util;
/** /**
* @author Bernhard Schussek <bschussek@gmail.com> * @author Bernhard Schussek <bschussek@gmail.com>
*/ */
class ServerParams class ServerParams extends \Symfony\Component\Form\Util\ServerParams
{ {
/**
* Returns maximum post size in bytes.
*
* @return null|int The maximum post size in bytes
*/
public function getPostMaxSize()
{
$iniMax = strtolower($this->getNormalizedIniPostMaxSize());
if ('' === $iniMax) {
return;
}
$max = ltrim($iniMax, '+');
if (0 === strpos($max, '0x')) {
$max = intval($max, 16);
} elseif (0 === strpos($max, '0')) {
$max = intval($max, 8);
} else {
$max = intval($max);
}
switch (substr($iniMax, -1)) {
case 't': $max *= 1024;
case 'g': $max *= 1024;
case 'm': $max *= 1024;
case 'k': $max *= 1024;
}
return $max;
}
/**
* Returns the normalized "post_max_size" ini setting.
*
* @return string
*/
public function getNormalizedIniPostMaxSize()
{
return strtoupper(trim(ini_get('post_max_size')));
}
/**
* Returns the content length of the request.
*
* @return mixed The request content length.
*/
public function getContentLength()
{
return isset($_SERVER['CONTENT_LENGTH'])
? (int) $_SERVER['CONTENT_LENGTH']
: null;
}
} }

View File

@ -12,7 +12,7 @@
namespace Symfony\Component\Form; namespace Symfony\Component\Form;
use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Extension\Validator\Util\ServerParams; use Symfony\Component\Form\Util\ServerParams;
/** /**
* A request handler using PHP's super globals $_GET, $_POST and $_SERVER. * A request handler using PHP's super globals $_GET, $_POST and $_SERVER.
@ -76,6 +76,25 @@ class NativeRequestHandler implements RequestHandlerInterface
$data = $_GET[$name]; $data = $_GET[$name];
} }
} else { } else {
// Mark the form with an error if the uploaded size was too large
// This is done here and not in FormValidator because $_POST is
// empty when that error occurs. Hence the form is never submitted.
$contentLength = $this->serverParams->getContentLength();
$maxContentLength = $this->serverParams->getPostMaxSize();
if (!empty($maxContentLength) && $contentLength > $maxContentLength) {
// Submit the form, but don't clear the default values
$form->submit(null, false);
$form->addError(new FormError(
$form->getConfig()->getOption('post_max_size_message'),
null,
array('{{ max }}' => $this->serverParams->getNormalizedIniPostMaxSize())
));
return;
}
$fixedFiles = array(); $fixedFiles = array();
foreach ($_FILES as $name => $file) { foreach ($_FILES as $name => $file) {
$fixedFiles[$name] = self::stripEmptyFiles(self::fixPhpFilesArray($file)); $fixedFiles[$name] = self::stripEmptyFiles(self::fixPhpFilesArray($file));
@ -89,10 +108,6 @@ class NativeRequestHandler implements RequestHandlerInterface
$params = array_key_exists($name, $_POST) ? $_POST[$name] : $default; $params = array_key_exists($name, $_POST) ? $_POST[$name] : $default;
$files = array_key_exists($name, $fixedFiles) ? $fixedFiles[$name] : $default; $files = array_key_exists($name, $fixedFiles) ? $fixedFiles[$name] : $default;
} else { } else {
if ($this->serverParams->getContentLength() > $this->serverParams->getPostMaxSize()) {
$form->addError(new FormError('Max post size exceeded.'));
}
// Don't submit the form if it is not present in the request // Don't submit the form if it is not present in the request
return; return;
} }

View File

@ -11,7 +11,10 @@
namespace Symfony\Component\Form\Tests; namespace Symfony\Component\Form\Tests;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormFactory;
use Symfony\Component\Form\Forms; use Symfony\Component\Form\Forms;
use Symfony\Component\Form\RequestHandlerInterface;
/** /**
* @author Bernhard Schussek <bschussek@gmail.com> * @author Bernhard Schussek <bschussek@gmail.com>
@ -19,19 +22,25 @@ use Symfony\Component\Form\Forms;
abstract class AbstractRequestHandlerTest extends \PHPUnit_Framework_TestCase abstract class AbstractRequestHandlerTest extends \PHPUnit_Framework_TestCase
{ {
/** /**
* @var \Symfony\Component\Form\RequestHandlerInterface * @var RequestHandlerInterface
*/ */
protected $requestHandler; protected $requestHandler;
/** /**
* @var \Symfony\Component\Form\FormFactory * @var FormFactory
*/ */
protected $factory; protected $factory;
protected $request; protected $request;
protected $serverParams;
protected function setUp() protected function setUp()
{ {
$this->serverParams = $this->getMock(
'Symfony\Component\Form\Util\ServerParams',
array('getNormalizedIniPostMaxSize', 'getContentLength')
);
$this->requestHandler = $this->getRequestHandler(); $this->requestHandler = $this->getRequestHandler();
$this->factory = Forms::createFormFactoryBuilder()->getFormFactory(); $this->factory = Forms::createFormFactoryBuilder()->getFormFactory();
$this->request = null; $this->request = null;
@ -257,17 +266,48 @@ abstract class AbstractRequestHandlerTest extends \PHPUnit_Framework_TestCase
$this->requestHandler->handleRequest($form, $this->request); $this->requestHandler->handleRequest($form, $this->request);
} }
public function testAddFormErrorIfPostMaxSizeExceeded() /**
* @dataProvider getPostMaxSizeFixtures
*/
public function testAddFormErrorIfPostMaxSizeExceeded($contentLength, $iniMax, $shouldFail, array $errorParams = array())
{ {
$form = $this->factory->createNamed('name', 'text'); $this->serverParams->expects($this->once())
->method('getContentLength')
->will($this->returnValue($contentLength));
$this->serverParams->expects($this->any())
->method('getNormalizedIniPostMaxSize')
->will($this->returnValue($iniMax));
$options = array('post_max_size_message' => 'Max {{ max }}!');
$form = $this->factory->createNamed('name', 'text', null, $options);
$this->setRequestData('POST', array(), array()); $this->setRequestData('POST', array(), array());
$_SERVER['CONTENT_LENGTH'] = 1000000000;
$this->requestHandler->handleRequest($form, $this->request); $this->requestHandler->handleRequest($form, $this->request);
$this->assertEquals("ERROR: Max post size exceeded.\n", $form->getErrorsAsString()); if ($shouldFail) {
$errors = array(new FormError($options['post_max_size_message'], null, $errorParams));
unset($_SERVER['CONTENT_LENGTH']); $this->assertEquals($errors, $form->getErrors());
$this->assertTrue($form->isSubmitted());
} else {
$this->assertCount(0, $form->getErrors());
$this->assertFalse($form->isSubmitted());
}
}
public function getPostMaxSizeFixtures()
{
return array(
array(pow(1024, 3) + 1, '1G', true, array('{{ max }}' => '1G')),
array(pow(1024, 3), '1G', false),
array(pow(1024, 2) + 1, '1M', true, array('{{ max }}' => '1M')),
array(pow(1024, 2), '1M', false),
array(1024 + 1, '1K', true, array('{{ max }}' => '1K')),
array(1024, '1K', false),
array(null, '1K', false),
array(1024, '', false),
array(1024, 0, false),
);
} }
abstract protected function setRequestData($method, $data, $files = array()); abstract protected function setRequestData($method, $data, $files = array());

View File

@ -43,7 +43,7 @@ class HttpFoundationRequestHandlerTest extends AbstractRequestHandlerTest
protected function getRequestHandler() protected function getRequestHandler()
{ {
return new HttpFoundationRequestHandler(); return new HttpFoundationRequestHandler($this->serverParams);
} }
protected function getMockFile() protected function getMockFile()

View File

@ -542,69 +542,6 @@ class FormValidatorTest extends AbstractConstraintValidatorTest
), 'property.path', array('foo' => 'bar')); ), 'property.path', array('foo' => 'bar'));
} }
/**
* @dataProvider getPostMaxSizeFixtures
*/
public function testPostMaxSizeViolation($contentLength, $iniMax, $nbViolation, array $params = array())
{
$this->serverParams->expects($this->once())
->method('getContentLength')
->will($this->returnValue($contentLength));
$this->serverParams->expects($this->any())
->method('getNormalizedIniPostMaxSize')
->will($this->returnValue($iniMax));
$options = array('post_max_size_message' => 'Max {{ max }}!');
$form = $this->getBuilder('name', null, $options)->getForm();
$this->validator->validate($form, new Form());
$violations = array();
for ($i = 0; $i < $nbViolation; ++$i) {
$violations[] = $this->createViolation($options['post_max_size_message'], $params, 'property.path', $contentLength);
}
$this->assertViolations($violations);
}
public function getPostMaxSizeFixtures()
{
return array(
array(pow(1024, 3) + 1, '1G', 1, array('{{ max }}' => '1G')),
array(pow(1024, 3), '1G', 0),
array(pow(1024, 2) + 1, '1M', 1, array('{{ max }}' => '1M')),
array(pow(1024, 2), '1M', 0),
array(1024 + 1, '1K', 1, array('{{ max }}' => '1K')),
array(1024, '1K', 0),
array(null, '1K', 0),
array(1024, '', 0),
array(1024, 0, 0),
);
}
public function testNoViolationIfNotRoot()
{
$this->serverParams->expects($this->once())
->method('getContentLength')
->will($this->returnValue(1025));
$this->serverParams->expects($this->never())
->method('getNormalizedIniPostMaxSize');
$parent = $this->getBuilder()
->setCompound(true)
->setDataMapper($this->getDataMapper())
->getForm();
$form = $this->getForm();
$parent->add($form);
$this->expectNoValidate();
$this->validator->validate($form, new Form());
$this->assertNoViolation();
}
/** /**
* Access has to be public, as this method is called via callback array * Access has to be public, as this method is called via callback array
* in {@link testValidateFormDataCanHandleCallbackValidationGroups()} * in {@link testValidateFormDataCanHandleCallbackValidationGroups()}

View File

@ -203,7 +203,7 @@ class NativeRequestHandlerTest extends AbstractRequestHandlerTest
protected function getRequestHandler() protected function getRequestHandler()
{ {
return new NativeRequestHandler(); return new NativeRequestHandler($this->serverParams);
} }
protected function getMockFile() protected function getMockFile()

View File

@ -0,0 +1,72 @@
<?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\Util;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ServerParams
{
/**
* Returns maximum post size in bytes.
*
* @return null|int The maximum post size in bytes
*/
public function getPostMaxSize()
{
$iniMax = strtolower($this->getNormalizedIniPostMaxSize());
if ('' === $iniMax) {
return;
}
$max = ltrim($iniMax, '+');
if (0 === strpos($max, '0x')) {
$max = intval($max, 16);
} elseif (0 === strpos($max, '0')) {
$max = intval($max, 8);
} else {
$max = intval($max);
}
switch (substr($iniMax, -1)) {
case 't': $max *= 1024;
case 'g': $max *= 1024;
case 'm': $max *= 1024;
case 'k': $max *= 1024;
}
return $max;
}
/**
* Returns the normalized "post_max_size" ini setting.
*
* @return string
*/
public function getNormalizedIniPostMaxSize()
{
return strtoupper(trim(ini_get('post_max_size')));
}
/**
* Returns the content length of the request.
*
* @return mixed The request content length.
*/
public function getContentLength()
{
return isset($_SERVER['CONTENT_LENGTH'])
? (int) $_SERVER['CONTENT_LENGTH']
: null;
}
}