-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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 <[email protected]>
- Loading branch information
1 parent
32e2332
commit c2bda62
Showing
5 changed files
with
338 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
<?php | ||
|
||
namespace ipl\Html\FormElement; | ||
|
||
use ipl\Html\Attributes; | ||
use ipl\Validator\FileValidator; | ||
use ipl\Validator\ValidatorChain; | ||
use Psr\Http\Message\UploadedFileInterface; | ||
use ipl\Html\Common\MultipleAttribute; | ||
|
||
class FileElement extends InputElement | ||
{ | ||
use MultipleAttribute; | ||
|
||
protected $type = 'file'; | ||
|
||
/** @var UploadedFileInterface|UploadedFileInterface[] */ | ||
protected $value; | ||
|
||
/** @var int The default maximum file size */ | ||
protected static $defaultMaxFileSize; | ||
|
||
public function __construct($name, $attributes = null) | ||
{ | ||
$this->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'; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
<?php | ||
|
||
namespace ipl\Tests\Html\FormElement; | ||
|
||
use GuzzleHttp\Psr7\ServerRequest; | ||
use GuzzleHttp\Psr7\UploadedFile; | ||
use ipl\Html\Form; | ||
use ipl\Html\FormElement\FileElement; | ||
use ipl\I18n\NoopTranslator; | ||
use ipl\I18n\StaticTranslator; | ||
use ipl\Tests\Html\Lib\FileElementWithAdjustableConfig; | ||
use ipl\Tests\Html\TestCase; | ||
|
||
class FileElementTest extends TestCase | ||
{ | ||
protected function setUp(): void | ||
{ | ||
StaticTranslator::$instance = new NoopTranslator(); | ||
} | ||
|
||
public function testElementLoading(): void | ||
{ | ||
$form = (new Form()) | ||
->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('<input name="test_file" type="file" accept="image/png, image/jpeg">', $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('<input multiple name="test_file[]" type="file">', $file); | ||
$this->assertSame($file->getName(), 'test_file'); | ||
} | ||
|
||
public function testValueAttributeIsNotRendered() | ||
{ | ||
$file = new FileElement('test_file'); | ||
|
||
$file->setValue('test'); | ||
$this->assertHtml('<input name="test_file" type="file">', $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()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<?php | ||
|
||
namespace ipl\Tests\Html\Lib; | ||
|
||
use ipl\Html\FormElement\FileElement; | ||
|
||
class FileElementWithAdjustableConfig extends FileElement | ||
{ | ||
public static $defaultMaxFileSize; | ||
|
||
public static $postMaxSize = '8M'; | ||
|
||
public static $uploadMaxFilesize = '2M'; | ||
|
||
protected static function getPostMaxSize(): string | ||
{ | ||
return self::$postMaxSize; | ||
} | ||
|
||
protected static function getUploadMaxFilesize(): string | ||
{ | ||
return self::$uploadMaxFilesize; | ||
} | ||
} |