From c2bda62d5034791129ee1c4d02ab0eacc2844ffa Mon Sep 17 00:00:00 2001 From: Sukhwinder Dhillon <54990055+sukhwinder33445@users.noreply.github.com> Date: Tue, 24 Jan 2023 18:51:00 +0530 Subject: [PATCH] FormElement: Add `FileElement` (#66) * Introduce class `FileElement` Populate uploaded file values in Form::handleRequest() * composer.json: Add `guzzle/psr7` for unit tests * FileElement: Use correct separator for the `accept` attribute * FileElement: Register the `FileValidator` by default Co-authored-by: Johannes Meyer --- composer.json | 3 + src/Form.php | 1 + src/FormElement/FileElement.php | 162 ++++++++++++++++++ tests/FormElement/FileElementTest.php | 148 ++++++++++++++++ tests/Lib/FileElementWithAdjustableConfig.php | 24 +++ 5 files changed, 338 insertions(+) create mode 100644 src/FormElement/FileElement.php create mode 100644 tests/FormElement/FileElementTest.php create mode 100644 tests/Lib/FileElementWithAdjustableConfig.php diff --git a/composer.json b/composer.json index 4fbfc659..2cf61f4d 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,9 @@ "ipl/validator": "dev-master", "psr/http-message": "~1.0" }, + "require-dev": { + "guzzlehttp/psr7": "^1" + }, "autoload": { "psr-4": { "ipl\\Html\\": "src" diff --git a/src/Form.php b/src/Form.php index d97aa161..93bb6d98 100644 --- a/src/Form.php +++ b/src/Form.php @@ -210,6 +210,7 @@ public function handleRequest(ServerRequestInterface $request) $params = []; } + $params = array_merge_recursive($params, $request->getUploadedFiles()); $this->populate($params); // Assemble after populate in order to conditionally provide form elements diff --git a/src/FormElement/FileElement.php b/src/FormElement/FileElement.php new file mode 100644 index 00000000..aa736a4d --- /dev/null +++ b/src/FormElement/FileElement.php @@ -0,0 +1,162 @@ +getAttributes()->get('accept')->setSeparator(', '); + + parent::__construct($name, $attributes); + } + + public function getValueAttribute() + { + // Value attributes of file inputs are set only client-side. + return null; + } + + public function getNameAttribute() + { + $name = parent::getNameAttribute(); + + return $this->isMultiple() ? ($name . '[]') : $name; + } + + public function hasValue() + { + if ($this->value === null) { + return false; + } + + $file = $this->value; + + if ($this->isMultiple()) { + return $file[0]->getError() !== UPLOAD_ERR_NO_FILE; + } + + return $file->getError() !== UPLOAD_ERR_NO_FILE; + } + + public function getValue() + { + return $this->hasValue() ? $this->value : null; + } + + protected function addDefaultValidators(ValidatorChain $chain): void + { + $chain->add(new FileValidator([ + 'maxSize' => $this->getDefaultMaxFileSize(), + 'mimeType' => array_filter( + (array) $this->getAttributes()->get('accept')->getValue(), + function ($type) { + // file inputs also allow file extensions in the accept attribute. These + // must not be passed as they don't resemble valid mime type definitions. + return is_string($type) && ltrim($type)[0] !== '.'; + } + ) + ])); + } + + protected function registerAttributeCallbacks(Attributes $attributes) + { + parent::registerAttributeCallbacks($attributes); + $this->registerMultipleAttributeCallback($attributes); + } + + /** + * Get the system's default maximum file upload size + * + * @return int + */ + public function getDefaultMaxFileSize(): int + { + if (static::$defaultMaxFileSize === null) { + $ini = $this->convertIniToInteger(trim(static::getPostMaxSize())); + $max = $this->convertIniToInteger(trim(static::getUploadMaxFilesize())); + $min = max($ini, $max); + if ($ini > 0) { + $min = min($min, $ini); + } + + if ($max > 0) { + $min = min($min, $max); + } + + static::$defaultMaxFileSize = $min; + } + + return static::$defaultMaxFileSize; + } + + /** + * Converts a ini setting to a integer value + * + * @param string $setting + * + * @return int + */ + private function convertIniToInteger(string $setting): int + { + if (! is_numeric($setting)) { + $type = strtoupper(substr($setting, -1)); + $setting = (int) substr($setting, 0, -1); + + switch ($type) { + case 'K': + $setting *= 1024; + break; + + case 'M': + $setting *= 1024 * 1024; + break; + + case 'G': + $setting *= 1024 * 1024 * 1024; + break; + + default: + break; + } + } + + return (int) $setting; + } + + /** + * Get the `post_max_size` INI setting + * + * @return string + */ + protected static function getPostMaxSize(): string + { + return ini_get('post_max_size') ?: '8M'; + } + + /** + * Get the `upload_max_filesize` INI setting + * + * @return string + */ + protected static function getUploadMaxFilesize(): string + { + return ini_get('upload_max_filesize') ?: '2M'; + } +} diff --git a/tests/FormElement/FileElementTest.php b/tests/FormElement/FileElementTest.php new file mode 100644 index 00000000..f90df45a --- /dev/null +++ b/tests/FormElement/FileElementTest.php @@ -0,0 +1,148 @@ +addElement('file', 'test_file'); + + $this->assertInstanceOf(FileElement::class, $form->getElement('test_file')); + } + + public function testRendering() + { + $file = new FileElement('test_file', ['accept' => ['image/png', 'image/jpeg']]); + + $this->assertHtml('', $file); + } + + public function testUploadedFiles() + { + $fileToUpload = new UploadedFile( + 'test/test.pdf', + 500, + 0, + 'test.pdf', + 'application/pdf' + ); + + $req = (new ServerRequest('POST', ServerRequest::getUriFromGlobals())) + ->withUploadedFiles(['test_file' => $fileToUpload]) + ->withParsedBody([]); + + $form = (new Form()) + ->addElement('file', 'test_file') + ->handleRequest($req); + + $this->assertSame($form->getValue('test_file'), $fileToUpload); + } + + public function testMutipleAttributeAlsoChangesNameAttribute() + { + $file = new FileElement('test_file', ['multiple' => true]); + + $this->assertHtml('', $file); + $this->assertSame($file->getName(), 'test_file'); + } + + public function testValueAttributeIsNotRendered() + { + $file = new FileElement('test_file'); + + $file->setValue('test'); + $this->assertHtml('', $file); + } + + public function testDefaultMaxFileSizeAsBytesIsParsedCorrectly() + { + $element = new FileElementWithAdjustableConfig('test'); + $element->setValue(new UploadedFile('test', 500, 0)); + + $element::$defaultMaxFileSize = null; + $element::$uploadMaxFilesize = '500'; + $element::$postMaxSize = '1000'; // Just needs to be bigger than 500 + + $this->assertTrue($element->isValid(), implode("\n", $element->getMessages())); + } + + public function testDefaultMaxFileSizeAsKiloBytesIsParsedCorrectly() + { + $element = new FileElementWithAdjustableConfig('test'); + $element->setValue(new UploadedFile('test', 1024, 0)); + + $element::$defaultMaxFileSize = null; + $element::$uploadMaxFilesize = '1K'; + $element::$postMaxSize = '2K'; // Just needs to be bigger than 1K + + $this->assertTrue($element->isValid(), implode("\n", $element->getMessages())); + } + + public function testDefaultMaxFileSizeAsMegaBytesIsParsedCorrectly() + { + $element = new FileElementWithAdjustableConfig('test'); + $element->setValue(new UploadedFile('test', 102400, 0)); + + $element::$defaultMaxFileSize = null; + $element::$uploadMaxFilesize = '1M'; + $element::$postMaxSize = '2M'; // Just needs to be bigger than 1M + + $this->assertTrue($element->isValid(), implode("\n", $element->getMessages())); + } + + public function testDefaultMaxFileSizeAsGigaBytesIsParsedCorrectly() + { + $element = new FileElementWithAdjustableConfig('test'); + $element->setValue(new UploadedFile('test', 1073741824, 0)); + + $element::$defaultMaxFileSize = null; + $element::$uploadMaxFilesize = '1G'; + $element::$postMaxSize = '2G'; // Just needs to be bigger than 1G + + $this->assertTrue($element->isValid(), implode("\n", $element->getMessages())); + } + + /** + * @depends testDefaultMaxFileSizeAsKiloBytesIsParsedCorrectly + */ + public function testUploadMaxFilesizeOverrulesPostMaxSize() + { + $element = new FileElementWithAdjustableConfig('test'); + $element->setValue(new UploadedFile('test', 1024, 0)); + + $element::$defaultMaxFileSize = null; + $element::$uploadMaxFilesize = '1K'; + $element::$postMaxSize = '2K'; // Just needs to be bigger than 1K + + $this->assertTrue($element->isValid(), implode("\n", $element->getMessages())); + + // ...if possible + + $element::$defaultMaxFileSize = null; + $element::$uploadMaxFilesize = '2K'; + $element::$postMaxSize = '1K'; + + $this->assertTrue($element->isValid(), implode("\n", $element->getMessages())); + + $element->setValue(new UploadedFile('test', 2048, 0)); + $element::$defaultMaxFileSize = null; + + $this->assertFalse($element->isValid()); + } +} diff --git a/tests/Lib/FileElementWithAdjustableConfig.php b/tests/Lib/FileElementWithAdjustableConfig.php new file mode 100644 index 00000000..b0bc5070 --- /dev/null +++ b/tests/Lib/FileElementWithAdjustableConfig.php @@ -0,0 +1,24 @@ +