From b33dc179a73343bc584ec2d1a082c97a85514c26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mor=C3=A1vek?= Date: Mon, 15 Jan 2024 19:44:40 +0100 Subject: [PATCH] Chunked upload validation bugfix - validate only the first chunk (fixes #143) --- src/FileUploadControl/FileUploadControl.php | 26 +++---- .../FileUploadControlValidationTest.phpt | 68 +++++++++++++++++-- 2 files changed, 76 insertions(+), 18 deletions(-) diff --git a/src/FileUploadControl/FileUploadControl.php b/src/FileUploadControl/FileUploadControl.php index d5fcc20..eed46f2 100644 --- a/src/FileUploadControl/FileUploadControl.php +++ b/src/FileUploadControl/FileUploadControl.php @@ -258,18 +258,20 @@ public function handleUpload(string $namespace): void continue; } - $fakeFileUpload = new FileUpload([ - 'name' => $fileUploadChunk->fileUpload->getUntrustedName(), - 'size' => $fileUploadChunk->contentRange->getSize(), - 'tmp_name' => $fileUploadChunk->fileUpload->getTemporaryFile(), - 'error' => UPLOAD_ERR_OK, - ]); - $this->fakeUploadControl->setNewFileUpload($fakeFileUpload); - $this->validate(); - $error = $this->getError(); - if ($error !== null) { - $responses[] = $this->createUploadErrorResponse($fileUploadChunk, $error); - continue; + if ($fileUploadChunk->contentRange->containsFirstByte()) { + $fakeFileUpload = new FileUpload([ + 'name' => $fileUploadChunk->fileUpload->getUntrustedName(), + 'size' => $fileUploadChunk->contentRange->getSize(), + 'tmp_name' => $fileUploadChunk->fileUpload->getTemporaryFile(), + 'error' => UPLOAD_ERR_OK, + ]); + $this->fakeUploadControl->setNewFileUpload($fakeFileUpload); + $this->validate(); + $error = $this->getError(); + if ($error !== null) { + $responses[] = $this->createUploadErrorResponse($fileUploadChunk, $error); + continue; + } } try { diff --git a/tests/FileUploadControl/FileUploadControlValidationTest.phpt b/tests/FileUploadControl/FileUploadControlValidationTest.phpt index 21a8c05..f36a309 100644 --- a/tests/FileUploadControl/FileUploadControlValidationTest.phpt +++ b/tests/FileUploadControl/FileUploadControlValidationTest.phpt @@ -21,6 +21,7 @@ use Nette\Utils\Helpers; use Nette\Utils\Json; use Nette\Utils\Random; use Tester\Assert; +use function fseek; require_once __DIR__ . '/../bootstrap.php'; @@ -132,16 +133,46 @@ class FileUploadControlValidationTest extends TestCase ); } - public function testPartialUploadWithImageValidation(): void + public function testPartialUploadFailsOnMimeTypeValidation(): void { $control = $this->createFileUploadControl(); - $control->addRule(Form::IMAGE, 'only PNG is allowed'); + $control->addRule(Form::MIME_TYPE, 'only plain-text', 'text/plain'); + + $chunkFile = Environment::getTempDir() . '/' . Random::generate(); + FileSystem::write($chunkFile, $this->readChunk(__DIR__ . '/Fixtures/image.png', 32)); + + $files = ['fileUpload' => ['upload' => [ + FileUploadFactory::createFromFile($chunkFile, 'image.png'), + ]]]; + $this->doUpload($control, $files, 'bytes 0-31/666'); + + Assert::same( + Json::encode(['files' => [ + [ + 'name' => 'image.png', + 'size' => 666, + 'error' => 'translated:only plain-text', + ], + ]]), + $this->extractJsonResponsePayload($control), + ); + } + + public function testPartialUploadWithSingleImageValidation(): void + { + $controlFactory = function (): FileUploadControl { + $control = $this->createFileUploadControl(); + $control->addRule(Form::IMAGE, 'only images are allowed'); + $control->addRule(Form::MAX_LENGTH, 'max 1 image', 1); + return $control; + }; - $file = Environment::getTempDir() . '/' . Random::generate(); - FileSystem::write($file, $this->readChunk(__DIR__ . '/Fixtures/image.png', 64)); + $control = $controlFactory(); + $chunk1File = Environment::getTempDir() . '/' . Random::generate(); + FileSystem::write($chunk1File, $this->readChunk(__DIR__ . '/Fixtures/image.png', 64)); $files = ['fileUpload' => ['upload' => [ - FileUploadFactory::createFromFile($file, 'image.png'), + FileUploadFactory::createFromFile($chunk1File, 'image.png'), ]]]; $this->doUpload($control, $files, 'bytes 0-63/666'); @@ -158,6 +189,29 @@ class FileUploadControlValidationTest extends TestCase ]]), $this->extractJsonResponsePayload($control), ); + + $control = $controlFactory(); + $chunk2File = Environment::getTempDir() . '/' . Random::generate(); + FileSystem::write($chunk2File, $this->readChunk(__DIR__ . '/Fixtures/image.png', 64, 64)); + + $files = ['fileUpload' => ['upload' => [ + FileUploadFactory::createFromFile($chunk2File, 'image.png'), + ]]]; + $this->doUpload($control, $files, 'bytes 64-127/666'); + + Assert::same( + Json::encode(['files' => [ + [ + 'name' => 'image.png', + 'size' => 666, + 'url' => '/?form-fileUpload-namespace=testStorage&form-fileUpload-id=image-png&action=default&do=form-fileUpload-download&presenter=Test', + 'type' => null, + 'deleteType' => 'GET', + 'deleteUrl' => '/?form-fileUpload-namespace=testStorage&form-fileUpload-id=image-png&action=default&do=form-fileUpload-delete&presenter=Test', + ], + ]]), + $this->extractJsonResponsePayload($control), + ); } private function extractJsonResponsePayload(FileUploadControl $control): string @@ -233,11 +287,13 @@ class FileUploadControlValidationTest extends TestCase /** * @param int<0, max> $size + * @param int<0, max> $offset */ - private function readChunk(string $file, int $size): string + private function readChunk(string $file, int $size, int $offset = 0): string { $fp = fopen($file, 'r'); assert($fp !== false); + fseek($fp, $offset); $contents = fread($fp, $size); assert(is_string($contents)); fclose($fp);