8000 turn failed file uploads into form errors · symfony/symfony@1a21ca7 · GitHub
[go: up one dir, main page]

Skip to content

Commit 1a21ca7

Browse files
committed
turn failed file uploads into form errors
1 parent 86210b3 commit 1a21ca7

File tree

7 files changed

+314
-0
lines changed

7 files changed

+314
-0
lines changed

src/Symfony/Component/Form/Extension/Core/Type/FileType.php

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\Form\AbstractType;
1515
use Symfony\Component\Form\FormBuilderInterface;
16+
use Symfony\Component\Form\FormError;
1617
use Symfony\Component\Form\FormEvent;
1718
use Symfony\Component\Form\FormEvents;
1819
use Symfony\Component\Form\FormInterface;
@@ -22,6 +23,15 @@
2223

2324
class FileType extends AbstractType
2425
{
26+
const KIB_BYTES = 1024;
27+
const MIB_BYTES = 1048576;
28+
29+
private static $suffixes = [
30+
1 => 'bytes',
31+
self::KIB_BYTES => 'KiB',
32+
self::MIB_BYTES => 'MiB',
33+
];
34+
2535
/**
2636
* {@inheritdoc}
2737
*/
@@ -43,6 +53,10 @@ public function buildForm(FormBuilderInterface $builder, array $options)
4353
foreach ($files as $file) {
4454
if ($requestHandler->isFileUpload($file)) {
4555
$data[] = $file;
56+
57+
if (method_exists($requestHandler, 'getUploadFileError') && null !== $errorCode = $requestHandler->getUploadFileError($file)) {
58+
$form->addError($this->getFileUploadError($errorCode));
59+
}
4660
}
4761
}
4862

@@ -54,6 +68,8 @@ public function buildForm(FormBuilderInterface $builder, array $options)
5468
}
5569

5670
$event->setData($data);
71+
} elseif ($requestHandler->isFileUpload($event->getData()) && method_exists($requestHandler, 'getUploadFileError') && null !== $errorCode = $requestHandler->getUploadFileError($event->getData())) {
72+
$form->addError($this->getFileUploadError($errorCode));
5773
} elseif (!$requestHandler->isFileUpload($event->getData())) {
5874
$event->setData(null);
5975
}
@@ -116,4 +132,103 @@ public function getBlockPrefix()
116132
{
117133
return 'file';
118134
}
135+
136+
private function getFileUploadError($errorCode)
137+
{
138+
$messageParameters = [];
139+
140+
if (UPLOAD_ERR_INI_SIZE === $errorCode) {
141+
list($limitAsString, $suffix) = $this->factorizeSizes(0, self::getMaxFilesize());
142+
$messageTemplate = 'The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.';
143+
$messageParameters = [
144+
'{{ limit }}' => $limitAsString,
145+
'{{ suffix }}' => $suffix,
146+
];
147+
} elseif (UPLOAD_ERR_FORM_SIZE === $errorCode) {
148+
$messageTemplate = 'The file is too large.';
149+
} else {
150+
$messageTemplate = 'The file could not be uploaded.';
151+
}
152+
153+
return new FormError($messageTemplate, $messageTemplate, $messageParameters);
154+
}
155+
156+
/**
157+
* Retu 10000 rns the maximum size of an uploaded file as configured in php.ini.
158+
*
159+
* This method should be kept in sync with Symfony\Component\HttpFoundation\File\UploadedFile::getMaxFilesize().
160+
*
161+
* @return int The maximum size of an uploaded file in bytes
162+
*/
163+
private static function getMaxFilesize()
164+
{
165+
$iniMax = strtolower(ini_get('upload_max_filesize'));
166+
167+
if ('' === $iniMax) {
168+
return PHP_INT_MAX;
169+
}
170+
171+
$max = ltrim($iniMax, '+');
172+
if (0 === strpos($max, '0x')) {
173+
$max = \intval($max, 16);
174+
} elseif (0 === strpos($max, '0')) {
175+
$max = \intval($max, 8);
176+
} else {
177+
$max = (int) $max;
178+
}
179+
180+
switch (substr($iniMax, -1)) {
181+
case 't': $max *= 1024;
182+
// no break
183+
case 'g': $max *= 1024;
184+
// no break
185+
case 'm': $max *= 1024;
186+
// no break
187+
case 'k': $max *= 1024;
188+
}
189+
190+
return $max;
191+
}
192+
193+
/**
194+
* Converts the limit to the smallest possible number
195+
* (i.e. try "MB", then "kB", then "bytes").
196+
*
197+
* This method should be kept in sync with Symfony\Component\Validator\Constraints\FileValidator::factorizeSizes().
198+
*/
199+
private function factorizeSizes($size, $limit)
200+
{
201+
$coef = self::MIB_BYTES;
202+
$coefFactor = self::KIB_BYTES;
203+
204+
$limitAsString = (string) ($limit / $coef);
205+
206+
// Restrict the limit to 2 decimals (without rounding! we
207+
// need the precise value)
208+
while (self::moreDecimalsThan($limitAsString, 2)) {
209+
$coef /= $coefFactor;
210+
$limitAsString = (string) ($limit / $coef);
211+
}
212+
213+
// Convert size to the same measure, but round to 2 decimals
214+
$sizeAsString = (string) round($size / $coef, 2);
215+
216+
// If the size and limit produce the same string output
217+
// (due to rounding), reduce the coefficient
218+
while ($sizeAsString === $limitAsString) {
219+
$coef /= $coefFactor;
220+
$limitAsString = (string) ($limit / $coef);
221+
$sizeAsString = (string) round($size / $coef, 2);
222+
}
223+
224+
return [$limitAsString, self::$suffixes[$coef]];
225+
}
226+
227+
/**
228+
* This method should be kept in sync with Symfony\Component\Validator\Constraints\FileValidator::moreDecimalsThan().
229+
*/
230+
private static function moreDecimalsThan($double, $numberOfDecimals)
231+
{
232+
return \strlen((string) $double) > \strlen(round($double, $numberOfDecimals));
233+
}
119234
}

src/Symfony/Component/Form/Extension/HttpFoundation/HttpFoundationRequestHandler.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Component\Form\RequestHandlerInterface;
1818
use Symfony\Component\Form\Util\ServerParams;
1919
use Symfony\Component\HttpFoundation\File\File;
20+
use Symfony\Component\HttpFoundation\File\UploadedFile;
2021
use Symfony\Component\HttpFoundation\Request;
2122

2223
/**
@@ -115,4 +116,16 @@ public function isFileUpload($data)
115116
{
116117
return $data instanceof File;
117118
}
119+
120+
/**
121+
* @return int|null
122+
*/
123+
public function getUploadFileError($data)
124+
{
125+
if (!$data instanceof UploadedFile || $data->isValid()) {
126+
return null;
127+
}
128+
129+
return $data->getError();
130+
}
118131
}

src/Symfony/Component/Form/NativeRequestHandler.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,30 @@ public function isFileUpload($data)
135135
return \is_array($data) && isset($data['error']) && \is_int($data['error']);
136136
}
137137

138+
/**
139+
* @return int|null
140+
*/
141+
public function getUploadFileError($data)
142+
{
143+
if (!\is_array($data)) {
144+
return null;
145+
}
146+
147+
if (!isset($data['error'])) {
148+
return null;
149+
}
150+
151+
if (!\is_int($data['error'])) {
152+
return null;
153+
}
154+
155+
if (UPLOAD_ERR_OK === $data['error']) {
156+
return null;
157+
}
158+
159+
return $data['error'];
160+
}
161+
138162
/**
139163
* Returns the method used to submit the request to the server.
140164
*

src/Symfony/Component/Form/Tests/AbstractRequestHandlerTest.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,28 @@ public function testInvalidFilesAreRejected()
360360
$this->assertFalse($this->requestHandler->isFileUpload($this->getInvalidFile()));
361361
}
362362

363+
/**
364+
* @dataProvider uploadFileErrorCodes
365+
*/
366+
public function testFailedFileUploadIsTurnedIntoFormError($errorCode, $expectedErrorCode)
367+
{
368+
$this->assertSame($expectedErrorCode, $this->requestHandler->getUploadFileError($this->getFailedUploadedFile($errorCode)));
369+
}
370+
371+
public function uploadFileErrorCodes()
372+
{
373+
return [
374+
'no error' => [UPLOAD_ERR_OK, null],
375+
'upload_max_filesize ini directive' => [UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_INI_SIZE],
376+
'MAX_FILE_SIZE from form' => [UPLOAD_ERR_FORM_SIZE, UPLOAD_ERR_FORM_SIZE],
377+
'partially uploaded' => [UPLOAD_ERR_PARTIAL, UPLOAD_ERR_PARTIAL],
378+
'no file upload' => [UPLOAD_ERR_NO_FILE, UPLOAD_ERR_NO_FILE],
379+
'missing temporary directory' => [UPLOAD_ERR_NO_TMP_DIR, UPLOAD_ERR_NO_TMP_DIR],
380+
'write failure' => [UPLOAD_ERR_CANT_WRITE, UPLOAD_ERR_CANT_WRITE],
381+
'stopped by extension' => [UPLOAD_ERR_EXTENSION, UPLOAD_ERR_EXTENSION],
382+
];
383+
}
384+
363385
abstract protected function setRequestData($method, $data, $files = []);
364386

365387
abstract protected function getRequestHandler();
@@ -368,6 +390,8 @@ abstract protected function getUploadedFile($suffix = '');
368390

369391
abstract protected function getInvalidFile();
370392

393+
abstract protected function getFailedUploadedFile($errorCode);
394+
371395
protected function createForm($name, $method = null, $compound = false)
372396
{
373397
$config = $this->createBuilder($name, $compound);

src/Symfony/Component/Form/Tests/Extension/Core/Type/FileTypeTest.php

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,128 @@ public function requestHandlerProvider()
184184
];
185185
}
186186

187+
/**
188+
* @dataProvider uploadFileErrorCodes
189+
*/
190+
public function testFailedFileUploadIsTurnedIntoFormErrorUsingHttpFoundationRequestHandler($errorCode, $expectedErrorMessage)
191+
{
192+
$form = $this->factory
193+
->createBuilder(static::TESTED_TYPE)
194+
->setRequestHandler(new HttpFoundationRequestHandler())
195+
->getForm();
196+
$form->submit(new UploadedFile(__DIR__.'/../../../Fixtures/foo', 'foo', null, null, $errorCode, true));
197+
198+
if (UPLOAD_ERR_OK === $errorCode) {
199+
$this->assertTrue($form->isValid());
200+
} else {
201+
$this->assertFalse($form->isValid());
202+
$this->assertSame($expectedErrorMessage, $form->getErrors()[0]->getMessage());
203+
}
204+
}
205+
206+
/**
207+
* @dataProvider uploadFileErrorCodes
208+
*/
209+
public function testFailedFileUploadIsTurnedIntoFormErrorUsingNativeRequestHandler($errorCode, $expectedErrorMessage)
210+
{
211+
$form = $this->factory
212+
->createBuilder(static::TESTED_TYPE)
213+
->setRequestHandler(new NativeRequestHandler())
214+
->getForm();
215+
$form->submit([
216+
'name' => 'foo.txt',
217+
'type' => 'text/plain',
218+
'tmp_name' => 'owfdskjasdfsa',
219+
'error' => $errorCode,
220+
'size' => 100,
221+
]);
222+
223+
if (UPLOAD_ERR_OK === $errorCode) {
224+
$this->assertTrue($form->isValid());
225+
} else {
226+
$this->assertFalse($form->isValid());
227+
$this->assertSame($expectedErrorMessage, $form->getErrors()[0]->getMessage());
228+
}
229+
}
230+
231+
/**
232+
* @dataProvider uploadFileErrorCodes
233+
*/
234+
public function testMultipleSubmittedFailedFileUploadsAreTurnedIntoFormErrorUsingHttpFoundationRequestHandler($errorCode, $expectedErrorMessage)
235+
{
236+
$form = $this->factory
237+
->createBuilder(static::TESTED_TYPE, null, [
238+
'multiple' => true,
239+
])
240+
->setRequestHandler(new HttpFoundationRequestHandler())
241+
->getForm();
242+
$form->submit([
243+
new UploadedFile(__DIR__.'/../../../Fixtures/foo', 'foo', null, null, $errorCode, true),
244+
new UploadedFile(__DIR__.'/../../../Fixtures/foo', 'bar', null, null, $errorCode, true),
245+
]);
246+
247+
if (UPLOAD_ERR_OK === $errorCode) {
248+
$this->assertTrue($form->isValid());
249+
} else {
250+
$this->assertFalse($form->isValid());
251+
$this->assertCount(2, $form->getErrors());
252+
$this->assertSame($expectedErrorMessage, $form->getErrors()[0]->getMessage());
253+
$this->assertSame($expectedErrorMessage, $form->getErrors()[1]->getMessage());
254+
}
255+
}
256+
257+
/**
258+
* @dataProvider uploadFileErrorCodes
259+
*/
260+
public function testMultipleSubmittedFailedFileUploadsAreTurnedIntoFormErrorUsingNativeRequestHandler($errorCode, $expectedErrorMessage)
261+
{
262+
$form = $this->factory
263+
->createBuilder(static::TESTED_TYPE, null, [
264+
'multiple' => true,
265+
])
266+
->setRequestHandler(new NativeRequestHandler())
267+
->getForm();
268+
$form->submit([
269+
[
270+
'name' => 'foo.txt',
271+
'type' => 'text/plain',
272+
'tmp_name' => 'owfdskjasdfsa',
273+
'error' => $errorCode,
274+
'size' => 100,
275+
],
276+
[
277+
'name' => 'bar.txt',
278+
'type' => 'text/plain',
279+
'tmp_name' => 'owfdskjasdfsa',
280+
'error' => $errorCode,
281+
'size' => 100,
282+
],
283+
]);
284+
285+
if (UPLOAD_ERR_OK === $errorCode) {
286+
$this->assertTrue($form->isValid());
287+
} else {
288+
$this->assertFalse($form->isValid());
289+
$this->assertCount(2, $form->getErrors());
290+
$this->assertSame($expectedErrorMessage, $form->getErrors()[0]->getMessage());
291+
$this->assertSame($expectedErrorMessage, $form->getErrors()[1]->getMessage());
292+
}
293+
}
294+
295+
public function uploadFileErrorCodes()
296+
{
297+
return [
298+
'no error' => [UPLOAD_ERR_OK, null],
299+
'upload_max_filesize ini directive' => [UPLOAD_ERR_INI_SIZE, 'The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.'],
300+
'MAX_FILE_SIZE from form' => [UPLOAD_ERR_FORM_SIZE, 'The file is too large.'],
301+
'partially uploaded' => [UPLOAD_ERR_PARTIAL, 'The file could not be uploaded.'],
302+
'no file upload' => [UPLOAD_ERR_NO_FILE, 'The file could not be uploaded.'],
303+
'missing temporary directory' => [UPLOAD_ERR_NO_TMP_DIR, 'The file could not be uploaded.'],
304+
'write failure' => [UPLOAD_ERR_CANT_WRITE, 'The file could not be uploaded.'],
305+
'stopped by extension' => [UPLOAD_ERR_EXTENSION, 'The file could not be uploaded.'],
306+
];
307+
}
308+
187309
private function createUploadedFile(RequestHandlerInterface $requestHandler, $path, $originalName)
188310
{
189311
if ($requestHandler instanceof HttpFoundationRequestHandler) {

src/Symfony/Component/Form/Tests/Extension/HttpFoundation/HttpFoundationRequestHandlerTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,9 @@ protected function getInvalidFile()
5656
{
5757
return 'file:///etc/passwd';
5858
}
59+
60+
protected function getFailedUploadedFile($errorCode)
61+
{
62+
return new UploadedFile(__DIR__.'/../../Fixtures/foo', 'foo', null, null, $errorCode, true);
63+
}
5964
}

src/Symfony/Component/Form/Tests/NativeRequestHandlerTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,4 +275,15 @@ protected function getInvalidFile()
275275
'size' => '100',
276276
];
277277
}
278+
279+
protected function getFailedUploadedFile($errorCode)
280+
{
281+
return [
282+
'name' => 'upload.txt',
283+
'type' => 'text/plain',
284+
'tmp_name' => 'owfdskjasdfsa',
285+
'error' => $errorCode,
286+
'size' => 100,
287+
];
288+
}
278289
}

0 commit comments

Comments
 (0)
0