turn failed file uploads into form errors
This commit is contained in:
parent
86210b3fa1
commit
1a21ca7362
@ -13,6 +13,7 @@ namespace Symfony\Component\Form\Extension\Core\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
@ -22,6 +23,15 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class FileType extends AbstractType
|
||||
{
|
||||
const KIB_BYTES = 1024;
|
||||
const MIB_BYTES = 1048576;
|
||||
|
||||
private static $suffixes = [
|
||||
1 => 'bytes',
|
||||
self::KIB_BYTES => 'KiB',
|
||||
self::MIB_BYTES => 'MiB',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
@ -43,6 +53,10 @@ class FileType extends AbstractType
|
||||
foreach ($files as $file) {
|
||||
if ($requestHandler->isFileUpload($file)) {
|
||||
$data[] = $file;
|
||||
|
||||
if (method_exists($requestHandler, 'getUploadFileError') && null !== $errorCode = $requestHandler->getUploadFileError($file)) {
|
||||
$form->addError($this->getFileUploadError($errorCode));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,6 +68,8 @@ class FileType extends AbstractType
|
||||
}
|
||||
|
||||
$event->setData($data);
|
||||
} elseif ($requestHandler->isFileUpload($event->getData()) && method_exists($requestHandler, 'getUploadFileError') && null !== $errorCode = $requestHandler->getUploadFileError($event->getData())) {
|
||||
$form->addError($this->getFileUploadError($errorCode));
|
||||
} elseif (!$requestHandler->isFileUpload($event->getData())) {
|
||||
$event->setData(null);
|
||||
}
|
||||
@ -116,4 +132,103 @@ class FileType extends AbstractType
|
||||
{
|
||||
return 'file';
|
||||
}
|
||||
|
||||
private function getFileUploadError($errorCode)
|
||||
{
|
||||
$messageParameters = [];
|
||||
|
||||
if (UPLOAD_ERR_INI_SIZE === $errorCode) {
|
||||
list($limitAsString, $suffix) = $this->factorizeSizes(0, self::getMaxFilesize());
|
||||
$messageTemplate = 'The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.';
|
||||
$messageParameters = [
|
||||
'{{ limit }}' => $limitAsString,
|
||||
'{{ suffix }}' => $suffix,
|
||||
];
|
||||
} elseif (UPLOAD_ERR_FORM_SIZE === $errorCode) {
|
||||
$messageTemplate = 'The file is too large.';
|
||||
} else {
|
||||
$messageTemplate = 'The file could not be uploaded.';
|
||||
}
|
||||
|
||||
return new FormError($messageTemplate, $messageTemplate, $messageParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum size of an uploaded file as configured in php.ini.
|
||||
*
|
||||
* This method should be kept in sync with Symfony\Component\HttpFoundation\File\UploadedFile::getMaxFilesize().
|
||||
*
|
||||
* @return int The maximum size of an uploaded file in bytes
|
||||
*/
|
||||
private static function getMaxFilesize()
|
||||
{
|
||||
$iniMax = strtolower(ini_get('upload_max_filesize'));
|
||||
|
||||
if ('' === $iniMax) {
|
||||
return PHP_INT_MAX;
|
||||
}
|
||||
|
||||
$max = ltrim($iniMax, '+');
|
||||
if (0 === strpos($max, '0x')) {
|
||||
$max = \intval($max, 16);
|
||||
} elseif (0 === strpos($max, '0')) {
|
||||
$max = \intval($max, 8);
|
||||
} else {
|
||||
$max = (int) $max;
|
||||
}
|
||||
|
||||
switch (substr($iniMax, -1)) {
|
||||
case 't': $max *= 1024;
|
||||
// no break
|
||||
case 'g': $max *= 1024;
|
||||
// no break
|
||||
case 'm': $max *= 1024;
|
||||
// no break
|
||||
case 'k': $max *= 1024;
|
||||
}
|
||||
|
||||
return $max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the limit to the smallest possible number
|
||||
* (i.e. try "MB", then "kB", then "bytes").
|
||||
*
|
||||
* This method should be kept in sync with Symfony\Component\Validator\Constraints\FileValidator::factorizeSizes().
|
||||
*/
|
||||
private function factorizeSizes($size, $limit)
|
||||
{
|
||||
$coef = self::MIB_BYTES;
|
||||
$coefFactor = self::KIB_BYTES;
|
||||
|
||||
$limitAsString = (string) ($limit / $coef);
|
||||
|
||||
// Restrict the limit to 2 decimals (without rounding! we
|
||||
// need the precise value)
|
||||
while (self::moreDecimalsThan($limitAsString, 2)) {
|
||||
$coef /= $coefFactor;
|
||||
$limitAsString = (string) ($limit / $coef);
|
||||
}
|
||||
|
||||
// Convert size to the same measure, but round to 2 decimals
|
||||
$sizeAsString = (string) round($size / $coef, 2);
|
||||
|
||||
// If the size and limit produce the same string output
|
||||
// (due to rounding), reduce the coefficient
|
||||
while ($sizeAsString === $limitAsString) {
|
||||
$coef /= $coefFactor;
|
||||
$limitAsString = (string) ($limit / $coef);
|
||||
$sizeAsString = (string) round($size / $coef, 2);
|
||||
}
|
||||
|
||||
return [$limitAsString, self::$suffixes[$coef]];
|
||||
}
|
||||
|
||||
/**
|
||||
* This method should be kept in sync with Symfony\Component\Validator\Constraints\FileValidator::moreDecimalsThan().
|
||||
*/
|
||||
private static function moreDecimalsThan($double, $numberOfDecimals)
|
||||
{
|
||||
return \strlen((string) $double) > \strlen(round($double, $numberOfDecimals));
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\RequestHandlerInterface;
|
||||
use Symfony\Component\Form\Util\ServerParams;
|
||||
use Symfony\Component\HttpFoundation\File\File;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
@ -115,4 +116,16 @@ class HttpFoundationRequestHandler implements RequestHandlerInterface
|
||||
{
|
||||
return $data instanceof File;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getUploadFileError($data)
|
||||
{
|
||||
if (!$data instanceof UploadedFile || $data->isValid()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $data->getError();
|
||||
}
|
||||
}
|
||||
|
@ -135,6 +135,30 @@ class NativeRequestHandler implements RequestHandlerInterface
|
||||
return \is_array($data) && isset($data['error']) && \is_int($data['error']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getUploadFileError($data)
|
||||
{
|
||||
if (!\is_array($data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isset($data['error'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!\is_int($data['error'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (UPLOAD_ERR_OK === $data['error']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $data['error'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the method used to submit the request to the server.
|
||||
*
|
||||
|
@ -360,6 +360,28 @@ abstract class AbstractRequestHandlerTest extends TestCase
|
||||
$this->assertFalse($this->requestHandler->isFileUpload($this->getInvalidFile()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider uploadFileErrorCodes
|
||||
*/
|
||||
public function testFailedFileUploadIsTurnedIntoFormError($errorCode, $expectedErrorCode)
|
||||
{
|
||||
$this->assertSame($expectedErrorCode, $this->requestHandler->getUploadFileError($this->getFailedUploadedFile($errorCode)));
|
||||
}
|
||||
|
||||
public function uploadFileErrorCodes()
|
||||
{
|
||||
return [
|
||||
'no error' => [UPLOAD_ERR_OK, null],
|
||||
'upload_max_filesize ini directive' => [UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_INI_SIZE],
|
||||
'MAX_FILE_SIZE from form' => [UPLOAD_ERR_FORM_SIZE, UPLOAD_ERR_FORM_SIZE],
|
||||
'partially uploaded' => [UPLOAD_ERR_PARTIAL, UPLOAD_ERR_PARTIAL],
|
||||
'no file upload' => [UPLOAD_ERR_NO_FILE, UPLOAD_ERR_NO_FILE],
|
||||
'missing temporary directory' => [UPLOAD_ERR_NO_TMP_DIR, UPLOAD_ERR_NO_TMP_DIR],
|
||||
'write failure' => [UPLOAD_ERR_CANT_WRITE, UPLOAD_ERR_CANT_WRITE],
|
||||
'stopped by extension' => [UPLOAD_ERR_EXTENSION, UPLOAD_ERR_EXTENSION],
|
||||
];
|
||||
}
|
||||
|
||||
abstract protected function setRequestData($method, $data, $files = []);
|
||||
|
||||
abstract protected function getRequestHandler();
|
||||
@ -368,6 +390,8 @@ abstract class AbstractRequestHandlerTest extends TestCase
|
||||
|
||||
abstract protected function getInvalidFile();
|
||||
|
||||
abstract protected function getFailedUploadedFile($errorCode);
|
||||
|
||||
protected function createForm($name, $method = null, $compound = false)
|
||||
{
|
||||
$config = $this->createBuilder($name, $compound);
|
||||
|
@ -184,6 +184,128 @@ class FileTypeTest extends BaseTypeTest
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider uploadFileErrorCodes
|
||||
*/
|
||||
public function testFailedFileUploadIsTurnedIntoFormErrorUsingHttpFoundationRequestHandler($errorCode, $expectedErrorMessage)
|
||||
{
|
||||
$form = $this->factory
|
||||
->createBuilder(static::TESTED_TYPE)
|
||||
->setRequestHandler(new HttpFoundationRequestHandler())
|
||||
->getForm();
|
||||
$form->submit(new UploadedFile(__DIR__.'/../../../Fixtures/foo', 'foo', null, null, $errorCode, true));
|
||||
|
||||
if (UPLOAD_ERR_OK === $errorCode) {
|
||||
$this->assertTrue($form->isValid());
|
||||
} else {
|
||||
$this->assertFalse($form->isValid());
|
||||
$this->assertSame($expectedErrorMessage, $form->getErrors()[0]->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider uploadFileErrorCodes
|
||||
*/
|
||||
public function testFailedFileUploadIsTurnedIntoFormErrorUsingNativeRequestHandler($errorCode, $expectedErrorMessage)
|
||||
{
|
||||
$form = $this->factory
|
||||
->createBuilder(static::TESTED_TYPE)
|
||||
->setRequestHandler(new NativeRequestHandler())
|
||||
->getForm();
|
||||
$form->submit([
|
||||
'name' => 'foo.txt',
|
||||
'type' => 'text/plain',
|
||||
'tmp_name' => 'owfdskjasdfsa',
|
||||
'error' => $errorCode,
|
||||
'size' => 100,
|
||||
]);
|
||||
|
||||
if (UPLOAD_ERR_OK === $errorCode) {
|
||||
$this->assertTrue($form->isValid());
|
||||
} else {
|
||||
$this->assertFalse($form->isValid());
|
||||
$this->assertSame($expectedErrorMessage, $form->getErrors()[0]->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider uploadFileErrorCodes
|
||||
*/
|
||||
public function testMultipleSubmittedFailedFileUploadsAreTurnedIntoFormErrorUsingHttpFoundationRequestHandler($errorCode, $expectedErrorMessage)
|
||||
{
|
||||
$form = $this->factory
|
||||
->createBuilder(static::TESTED_TYPE, null, [
|
||||
'multiple' => true,
|
||||
])
|
||||
->setRequestHandler(new HttpFoundationRequestHandler())
|
||||
->getForm();
|
||||
$form->submit([
|
||||
new UploadedFile(__DIR__.'/../../../Fixtures/foo', 'foo', null, null, $errorCode, true),
|
||||
new UploadedFile(__DIR__.'/../../../Fixtures/foo', 'bar', null, null, $errorCode, true),
|
||||
]);
|
||||
|
||||
if (UPLOAD_ERR_OK === $errorCode) {
|
||||
$this->assertTrue($form->isValid());
|
||||
} else {
|
||||
$this->assertFalse($form->isValid());
|
||||
$this->assertCount(2, $form->getErrors());
|
||||
$this->assertSame($expectedErrorMessage, $form->getErrors()[0]->getMessage());
|
||||
$this->assertSame($expectedErrorMessage, $form->getErrors()[1]->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider uploadFileErrorCodes
|
||||
*/
|
||||
public function testMultipleSubmittedFailedFileUploadsAreTurnedIntoFormErrorUsingNativeRequestHandler($errorCode, $expectedErrorMessage)
|
||||
{
|
||||
$form = $this->factory
|
||||
->createBuilder(static::TESTED_TYPE, null, [
|
||||
'multiple' => true,
|
||||
])
|
||||
->setRequestHandler(new NativeRequestHandler())
|
||||
->getForm();
|
||||
$form->submit([
|
||||
[
|
||||
'name' => 'foo.txt',
|
||||
'type' => 'text/plain',
|
||||
'tmp_name' => 'owfdskjasdfsa',
|
||||
'error' => $errorCode,
|
||||
'size' => 100,
|
||||
],
|
||||
[
|
||||
'name' => 'bar.txt',
|
||||
'type' => 'text/plain',
|
||||
'tmp_name' => 'owfdskjasdfsa',
|
||||
'error' => $errorCode,
|
||||
'size' => 100,
|
||||
],
|
||||
]);
|
||||
|
||||
if (UPLOAD_ERR_OK === $errorCode) {
|
||||
$this->assertTrue($form->isValid());
|
||||
} else {
|
||||
$this->assertFalse($form->isValid());
|
||||
$this->assertCount(2, $form->getErrors());
|
||||
$this->assertSame($expectedErrorMessage, $form->getErrors()[0]->getMessage());
|
||||
$this->assertSame($expectedErrorMessage, $form->getErrors()[1]->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function uploadFileErrorCodes()
|
||||
{
|
||||
return [
|
||||
'no error' => [UPLOAD_ERR_OK, null],
|
||||
'upload_max_filesize ini directive' => [UPLOAD_ERR_INI_SIZE, 'The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.'],
|
||||
'MAX_FILE_SIZE from form' => [UPLOAD_ERR_FORM_SIZE, 'The file is too large.'],
|
||||
'partially uploaded' => [UPLOAD_ERR_PARTIAL, 'The file could not be uploaded.'],
|
||||
'no file upload' => [UPLOAD_ERR_NO_FILE, 'The file could not be uploaded.'],
|
||||
'missing temporary directory' => [UPLOAD_ERR_NO_TMP_DIR, 'The file could not be uploaded.'],
|
||||
'write failure' => [UPLOAD_ERR_CANT_WRITE, 'The file could not be uploaded.'],
|
||||
'stopped by extension' => [UPLOAD_ERR_EXTENSION, 'The file could not be uploaded.'],
|
||||
];
|
||||
}
|
||||
|
||||
private function createUploadedFile(RequestHandlerInterface $requestHandler, $path, $originalName)
|
||||
{
|
||||
if ($requestHandler instanceof HttpFoundationRequestHandler) {
|
||||
|
@ -56,4 +56,9 @@ class HttpFoundationRequestHandlerTest extends AbstractRequestHandlerTest
|
||||
{
|
||||
return 'file:///etc/passwd';
|
||||
}
|
||||
|
||||
protected function getFailedUploadedFile($errorCode)
|
||||
{
|
||||
return new UploadedFile(__DIR__.'/../../Fixtures/foo', 'foo', null, null, $errorCode, true);
|
||||
}
|
||||
}
|
||||
|
@ -275,4 +275,15 @@ class NativeRequestHandlerTest extends AbstractRequestHandlerTest
|
||||
'size' => '100',
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFailedUploadedFile($errorCode)
|
||||
{
|
||||
return [
|
||||
'name' => 'upload.txt',
|
||||
'type' => 'text/plain',
|
||||
'tmp_name' => 'owfdskjasdfsa',
|
||||
'error' => $errorCode,
|
||||
'size' => 100,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user