From 95bdeb7409519dedcd510360f8d8b2d783b11d8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mor=C3=A1vek?= Date: Fri, 19 Jan 2024 08:40:28 +0100 Subject: [PATCH] Add validation to prevent accidental submit with interrupted uploads --- phpstan.neon.dist | 4 ++++ src/FileUploadControl/FileUploadControl.php | 9 +++++++++ .../Validation/FakeUploadControl.php | 1 + .../FileUploadControlValidationTest.phpt | 17 +++++++++++++++++ 4 files changed, 31 insertions(+) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index a3a08ec..f3da779 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -95,3 +95,7 @@ parameters: message: "#^Invalid var phpdoc of \\$files\\. Cannot assign array.* to array\\$#" count: 1 path: src/FileUploadControl/FileUploadControl.php + - # false positive + message: "#^Parameter \\#1 \\$validator of method Nepada\\\\FileUploadControl\\\\FileUploadControl\\:\\:addCondition\\(\\) expects \\(callable\\(\\)\\: mixed\\)\\|string, true given\\.$#" + count: 1 + path: src/FileUploadControl/FileUploadControl.php diff --git a/src/FileUploadControl/FileUploadControl.php b/src/FileUploadControl/FileUploadControl.php index a37f3e8..81b48fb 100644 --- a/src/FileUploadControl/FileUploadControl.php +++ b/src/FileUploadControl/FileUploadControl.php @@ -20,11 +20,13 @@ use Nepada\FileUploadControl\Storage\UploadNamespace; use Nepada\FileUploadControl\Thumbnail\NullThumbnailProvider; use Nepada\FileUploadControl\Thumbnail\ThumbnailProvider; +use Nepada\FileUploadControl\Validation\FakeUploadControl; use Nepada\FileUploadControl\Validation\UploadValidation; use Nette; use Nette\Bridges\ApplicationLatte\Template; use Nette\Forms\Form; use Nette\Http\FileUpload; +use Nette\Utils\Arrays; use Nette\Utils\Html; use Nette\Utils\Strings; use Nextras\FormComponents\Fragments\UIControl\BaseControl; @@ -58,6 +60,13 @@ public function __construct(StorageManager $storageManager, string|\Stringable|n $this->addComponent(new Nette\Forms\Controls\UploadControl($caption, true), 'upload'); $this->addComponent(new Nette\Forms\Controls\HiddenField(), 'namespace'); $this->initializeValidation($this); + $this->addCondition(true) // avoid export to JS + ->addRule($this->validateUploadSuccess(...), Nette\Forms\Validator::$messages[Nette\Forms\Controls\UploadControl::Valid]); + } + + private function validateUploadSuccess(FakeUploadControl $control): bool + { + return Arrays::every($control->getValue(), fn (FileUpload $upload): bool => $upload->isOk()); } public function setThumbnailProvider(ThumbnailProvider $thumbnailProvider): void diff --git a/src/FileUploadControl/Validation/FakeUploadControl.php b/src/FileUploadControl/Validation/FakeUploadControl.php index 3987b8e..8c647a7 100644 --- a/src/FileUploadControl/Validation/FakeUploadControl.php +++ b/src/FileUploadControl/Validation/FakeUploadControl.php @@ -24,6 +24,7 @@ public function __construct(FileUploadControl $fileUploadControl) $this->fileUploadControl = $fileUploadControl; $fileUploadControl->monitor(Form::class, function (Form $form): void { $this->setParent(null, $this->fileUploadControl->getName()); + $this->control->name = $this->fileUploadControl->getComponent('upload')->getHtmlName(); }); } diff --git a/tests/FileUploadControl/FileUploadControlValidationTest.phpt b/tests/FileUploadControl/FileUploadControlValidationTest.phpt index edd0c01..b091246 100644 --- a/tests/FileUploadControl/FileUploadControlValidationTest.phpt +++ b/tests/FileUploadControl/FileUploadControlValidationTest.phpt @@ -4,6 +4,8 @@ declare(strict_types = 1); namespace NepadaTests\FileUploadControl; use Nepada\FileUploadControl\FileUploadControl; +use Nepada\FileUploadControl\Storage\ContentRange; +use Nepada\FileUploadControl\Storage\FileUploadChunk; use Nepada\FileUploadControl\Storage\Storage; use NepadaTests\Environment; use NepadaTests\FileUploadControl\Fixtures\TestPresenter; @@ -86,6 +88,21 @@ class FileUploadControlValidationTest extends TestCase Assert::same(['translated:max 1 upload allowed'], $control->getErrors()); } + public function testSubmittedWithInterruptedUpload(): void + { + $storage = InMemoryStorage::createWithFiles(); + $storage->save(FileUploadChunk::partialUpload( + FileUploadFactory::createFromFile(__DIR__ . '/Fixtures/test.txt', 'partial.txt'), + ContentRange::fromHttpHeaderValue('bytes 0-8/100'), + )); // interrupted partial upload + + $control = $this->createFileUploadControl($storage); + + $this->submitForm($control); + + Assert::same(['translated:Upload error'], $control->getErrors()); + } + public function testUploadWithFailedUpload(): void { $control = $this->createFileUploadControl();