Skip to content

Commit

Permalink
FormElement: Add FileElement (#66)
Browse files Browse the repository at this point in the history
* 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
sukhwinder33445 and nilmerg authored Jan 24, 2023
1 parent 32e2332 commit c2bda62
Show file tree
Hide file tree
Showing 5 changed files with 338 additions and 0 deletions.
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"ipl/validator": "dev-master",
"psr/http-message": "~1.0"
},
"require-dev": {
"guzzlehttp/psr7": "^1"
},
"autoload": {
"psr-4": {
"ipl\\Html\\": "src"
Expand Down
1 change: 1 addition & 0 deletions src/Form.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
162 changes: 162 additions & 0 deletions src/FormElement/FileElement.php
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';
}
}
148 changes: 148 additions & 0 deletions tests/FormElement/FileElementTest.php
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());
}
}
24 changes: 24 additions & 0 deletions tests/Lib/FileElementWithAdjustableConfig.php
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;
}
}

0 comments on commit c2bda62

Please sign in to comment.