Skip to content

Commit

Permalink
Add validation to prevent accidental submit with interrupted uploads
Browse files Browse the repository at this point in the history
  • Loading branch information
xificurk committed Jan 20, 2024
1 parent 1838d1d commit c93df24
Show file tree
Hide file tree
Showing 12 changed files with 71 additions and 10 deletions.
4 changes: 4 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,7 @@ parameters:
message: "#^Invalid var phpdoc of \\$files\\. Cannot assign array.* to array\\<int, Nette\\\\Http\\\\FileUpload\\>$#"
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
11 changes: 11 additions & 0 deletions src/FileUploadControl/FileUploadControl.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@
use Nepada\FileUploadControl\Storage\UploadNamespace;
use Nepada\FileUploadControl\Thumbnail\NullThumbnailProvider;
use Nepada\FileUploadControl\Thumbnail\ThumbnailProvider;
use Nepada\FileUploadControl\Validation\ClientSide;
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;
Expand Down Expand Up @@ -58,6 +61,14 @@ 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]);
$this->addRule(ClientSide::NO_UPLOAD_IN_PROGRESS, 'File upload is still in progress - wait until it is finished, or abort it.');
}

private function validateUploadSuccess(FakeUploadControl $control): bool
{
return Arrays::every($control->getValue(), fn (FileUpload $upload): bool => $upload->isOk());
}

public function setThumbnailProvider(ThumbnailProvider $thumbnailProvider): void
Expand Down
23 changes: 23 additions & 0 deletions src/FileUploadControl/Validation/ClientSide.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
declare(strict_types = 1);

namespace Nepada\FileUploadControl\Validation;

/**
* Dummy validator for client-side form validation rules
*/
final class ClientSide
{

public const NO_UPLOAD_IN_PROGRESS = self::class . '::noUploadInProgress';

private function __construct()
{
}

public static function noUploadInProgress(FakeUploadControl $control): bool
{
return true;
}

}
1 change: 1 addition & 0 deletions src/FileUploadControl/Validation/FakeUploadControl.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
}

Expand Down
5 changes: 5 additions & 0 deletions src/assets/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ function initializeFileUploadControl(Nette) {
e.preventDefault();
});

// Validation
Nette.validators.NepadaFileUploadControlValidationClientSide_noUploadInProgress = (element) => {
return $(element).closest('[data-file-upload-url]').find('[data-file-upload-status=processing] [data-file-upload-role=file-delete]').length === 0;
};

// Effective value
const originalGetEffectiveValue = Nette.getEffectiveValue;
Nette.getEffectiveValue = (elem, filter) => {
Expand Down
17 changes: 17 additions & 0 deletions tests/FileUploadControl/FileUploadControlValidationTest.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
<div class="fuc-buttons">
<span class="fuc-add btn btn-success disabled">
translated:Add files
<input data-files="[]" type="file" name="fileUpload[upload][]" multiple id="frm-form-fileUpload-upload" disabled data-nette-rules="[]">
<input data-files="[]" type="file" name="fileUpload[upload][]" multiple id="frm-form-fileUpload-upload" disabled data-nette-rules='[{"op":"Nepada\\FileUploadControl\\Validation\\ClientSide::noUploadInProgress","msg":"translated:File upload is still in progress - wait until it is finished, or abort it."}]'>
</span>
<span data-file-upload-role="abort" class="btn btn-warning disabled">translated:Abort upload</span>
<span data-file-upload-role="delete" class="btn btn-danger disabled">translated:Delete all</span>
</div>
<div class="fuc-container">




<div
data-file-upload-role="files"
class="fuc-files row"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<div class="fuc-buttons">
<span class="fuc-add btn btn-success">
translated:Add files
<input data-files="[]" type="file" name="fileUpload[upload][]" multiple accept="image/gif,image/png,image/jpeg,image/webp" id="frm-form-fileUpload-upload" data-nette-rules='[{"op":":image","msg":"translated:The uploaded file must be image in format JPEG, GIF, PNG or WebP."%a?%}]'>
<input data-files="[]" type="file" name="fileUpload[upload][]" multiple accept="image/gif,image/png,image/jpeg,image/webp" id="frm-form-fileUpload-upload" data-nette-rules='[{"op":"Nepada\\FileUploadControl\\Validation\\ClientSide::noUploadInProgress","msg":"translated:File upload is still in progress - wait until it is finished, or abort it."},{"op":":image","msg":"translated:The uploaded file must be image in format JPEG, GIF, PNG or WebP."%a?%}]'>
</span>
<span data-file-upload-role="abort" class="btn btn-warning disabled">translated:Abort upload</span>
<span data-file-upload-role="delete" class="btn btn-danger disabled">translated:Delete all</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<div class="fuc-buttons">
<span class="fuc-add btn btn-success">
translated:Add files
<input data-files="[&#123;&quot;name&quot;:&quot;test.txt&quot;,&quot;size&quot;:9,&quot;url&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=test-txt&amp;action=default&amp;do=form-fileUpload-download&amp;presenter=Test&quot;,&quot;type&quot;:&quot;text/plain&quot;,&quot;deleteType&quot;:&quot;GET&quot;,&quot;deleteUrl&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=test-txt&amp;action=default&amp;do=form-fileUpload-delete&amp;presenter=Test&quot;},&#123;&quot;name&quot;:&quot;image.png&quot;,&quot;size&quot;:770,&quot;url&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=image-png&amp;action=default&amp;do=form-fileUpload-download&amp;presenter=Test&quot;,&quot;type&quot;:&quot;image/png&quot;,&quot;deleteType&quot;:&quot;GET&quot;,&quot;deleteUrl&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=image-png&amp;action=default&amp;do=form-fileUpload-delete&amp;presenter=Test&quot;,&quot;thumbnailUrl&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=image-png&amp;action=default&amp;do=form-fileUpload-thumbnail&amp;presenter=Test&quot;},&#123;&quot;name&quot;:&quot;partial.txt&quot;,&quot;size&quot;:100,&quot;url&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=partial-txt&amp;action=default&amp;do=form-fileUpload-download&amp;presenter=Test&quot;,&quot;type&quot;:null,&quot;deleteType&quot;:&quot;GET&quot;,&quot;deleteUrl&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=partial-txt&amp;action=default&amp;do=form-fileUpload-delete&amp;presenter=Test&quot;}]" type="file" name="fileUpload[upload][]" multiple id="frm-form-fileUpload-upload" data-nette-rules='[{"op":":filled","msg":"translated:This field is required."}]'>
<input data-files="[&#123;&quot;name&quot;:&quot;test.txt&quot;,&quot;size&quot;:9,&quot;url&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=test-txt&amp;action=default&amp;do=form-fileUpload-download&amp;presenter=Test&quot;,&quot;type&quot;:&quot;text/plain&quot;,&quot;deleteType&quot;:&quot;GET&quot;,&quot;deleteUrl&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=test-txt&amp;action=default&amp;do=form-fileUpload-delete&amp;presenter=Test&quot;},&#123;&quot;name&quot;:&quot;image.png&quot;,&quot;size&quot;:770,&quot;url&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=image-png&amp;action=default&amp;do=form-fileUpload-download&amp;presenter=Test&quot;,&quot;type&quot;:&quot;image/png&quot;,&quot;deleteType&quot;:&quot;GET&quot;,&quot;deleteUrl&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=image-png&amp;action=default&amp;do=form-fileUpload-delete&amp;presenter=Test&quot;,&quot;thumbnailUrl&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=image-png&amp;action=default&amp;do=form-fileUpload-thumbnail&amp;presenter=Test&quot;},&#123;&quot;name&quot;:&quot;partial.txt&quot;,&quot;size&quot;:100,&quot;url&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=partial-txt&amp;action=default&amp;do=form-fileUpload-download&amp;presenter=Test&quot;,&quot;type&quot;:null,&quot;deleteType&quot;:&quot;GET&quot;,&quot;deleteUrl&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=partial-txt&amp;action=default&amp;do=form-fileUpload-delete&amp;presenter=Test&quot;}]" type="file" name="fileUpload[upload][]" multiple id="frm-form-fileUpload-upload" data-nette-rules='[{"op":":filled","msg":"translated:This field is required."},{"op":"Nepada\\FileUploadControl\\Validation\\ClientSide::noUploadInProgress","msg":"translated:File upload is still in progress - wait until it is finished, or abort it."}]'>
</span>
<span data-file-upload-role="abort" class="btn btn-warning disabled">translated:Abort upload</span>
<span data-file-upload-role="delete" class="btn btn-danger">translated:Delete all</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<div class="fuc-buttons">
<span class="fuc-add btn btn-success disabled">
translated:Add files
<input data-files="[]" type="file" name="fileUpload[upload][]" multiple id="frm-form-fileUpload-upload" disabled data-nette-rules="[]">
<input data-files="[]" type="file" name="fileUpload[upload][]" multiple id="frm-form-fileUpload-upload" disabled data-nette-rules='[{"op":"Nepada\\FileUploadControl\\Validation\\ClientSide::noUploadInProgress","msg":"translated:File upload is still in progress - wait until it is finished, or abort it."}]'>
</span>
<span data-file-upload-role="abort" class="btn btn-warning disabled">translated:Abort upload</span>
<span data-file-upload-role="delete" class="btn btn-danger disabled">translated:Delete all</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<div class="fuc-buttons">
<span class="fuc-add btn btn-success">
translated:Add files
<input data-files="[]" type="file" name="fileUpload[upload][]" multiple accept="image/gif,image/png,image/jpeg,image/webp" id="frm-form-fileUpload-upload" data-nette-rules='[{"op":":image","msg":"translated:The uploaded file must be image in format JPEG, GIF, PNG or WebP."%a?%}]'>
<input data-files="[]" type="file" name="fileUpload[upload][]" multiple accept="image/gif,image/png,image/jpeg,image/webp" id="frm-form-fileUpload-upload" data-nette-rules='[{"op":"Nepada\\FileUploadControl\\Validation\\ClientSide::noUploadInProgress","msg":"translated:File upload is still in progress - wait until it is finished, or abort it."},{"op":":image","msg":"translated:The uploaded file must be image in format JPEG, GIF, PNG or WebP."%a?%}]'>
</span>
<span data-file-upload-role="abort" class="btn btn-warning disabled">translated:Abort upload</span>
<span data-file-upload-role="delete" class="btn btn-danger disabled">translated:Delete all</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<div class="fuc-buttons">
<span class="fuc-add btn btn-success">
translated:Add files
<input data-files="[&#123;&quot;name&quot;:&quot;test.txt&quot;,&quot;size&quot;:9,&quot;url&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=test-txt&amp;action=default&amp;do=form-fileUpload-download&amp;presenter=Test&quot;,&quot;type&quot;:&quot;text/plain&quot;,&quot;deleteType&quot;:&quot;GET&quot;,&quot;deleteUrl&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=test-txt&amp;action=default&amp;do=form-fileUpload-delete&amp;presenter=Test&quot;},&#123;&quot;name&quot;:&quot;image.png&quot;,&quot;size&quot;:770,&quot;url&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=image-png&amp;action=default&amp;do=form-fileUpload-download&amp;presenter=Test&quot;,&quot;type&quot;:&quot;image/png&quot;,&quot;deleteType&quot;:&quot;GET&quot;,&quot;deleteUrl&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=image-png&amp;action=default&amp;do=form-fileUpload-delete&amp;presenter=Test&quot;,&quot;thumbnailUrl&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=image-png&amp;action=default&amp;do=form-fileUpload-thumbnail&amp;presenter=Test&quot;},&#123;&quot;name&quot;:&quot;partial.txt&quot;,&quot;size&quot;:100,&quot;url&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=partial-txt&amp;action=default&amp;do=form-fileUpload-download&amp;presenter=Test&quot;,&quot;type&quot;:null,&quot;deleteType&quot;:&quot;GET&quot;,&quot;deleteUrl&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=partial-txt&amp;action=default&amp;do=form-fileUpload-delete&amp;presenter=Test&quot;}]" type="file" name="fileUpload[upload][]" multiple id="frm-form-fileUpload-upload" data-nette-rules='[{"op":":filled","msg":"translated:This field is required."}]'>
<input data-files="[&#123;&quot;name&quot;:&quot;test.txt&quot;,&quot;size&quot;:9,&quot;url&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=test-txt&amp;action=default&amp;do=form-fileUpload-download&amp;presenter=Test&quot;,&quot;type&quot;:&quot;text/plain&quot;,&quot;deleteType&quot;:&quot;GET&quot;,&quot;deleteUrl&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=test-txt&amp;action=default&amp;do=form-fileUpload-delete&amp;presenter=Test&quot;},&#123;&quot;name&quot;:&quot;image.png&quot;,&quot;size&quot;:770,&quot;url&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=image-png&amp;action=default&amp;do=form-fileUpload-download&amp;presenter=Test&quot;,&quot;type&quot;:&quot;image/png&quot;,&quot;deleteType&quot;:&quot;GET&quot;,&quot;deleteUrl&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=image-png&amp;action=default&amp;do=form-fileUpload-delete&amp;presenter=Test&quot;,&quot;thumbnailUrl&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=image-png&amp;action=default&amp;do=form-fileUpload-thumbnail&amp;presenter=Test&quot;},&#123;&quot;name&quot;:&quot;partial.txt&quot;,&quot;size&quot;:100,&quot;url&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=partial-txt&amp;action=default&amp;do=form-fileUpload-download&amp;presenter=Test&quot;,&quot;type&quot;:null,&quot;deleteType&quot;:&quot;GET&quot;,&quot;deleteUrl&quot;:&quot;/?form-fileUpload-namespace=testStorage&amp;form-fileUpload-id=partial-txt&amp;action=default&amp;do=form-fileUpload-delete&amp;presenter=Test&quot;}]" type="file" name="fileUpload[upload][]" multiple id="frm-form-fileUpload-upload" data-nette-rules='[{"op":":filled","msg":"translated:This field is required."},{"op":"Nepada\\FileUploadControl\\Validation\\ClientSide::noUploadInProgress","msg":"translated:File upload is still in progress - wait until it is finished, or abort it."}]'>
</span>
<span data-file-upload-role="abort" class="btn btn-warning disabled">translated:Abort upload</span>
<span data-file-upload-role="delete" class="btn btn-danger">translated:Delete all</span>
Expand Down

0 comments on commit c93df24

Please sign in to comment.