Skip to content

Commit

Permalink
Fix for HTML5 multiple attribute on file element
Browse files Browse the repository at this point in the history
  • Loading branch information
cgmartin committed Oct 31, 2012
1 parent 420b811 commit e9ac49d
Show file tree
Hide file tree
Showing 9 changed files with 337 additions and 14 deletions.
34 changes: 32 additions & 2 deletions library/Zend/Form/Element/File.php
Expand Up @@ -26,7 +26,8 @@
use Zend\Form\ElementPrepareAwareInterface; use Zend\Form\ElementPrepareAwareInterface;
use Zend\Form\Exception; use Zend\Form\Exception;
use Zend\InputFilter\InputProviderInterface; use Zend\InputFilter\InputProviderInterface;
use Zend\Stdlib\ErrorHandler; use Zend\Validator\File\Explode as FileExplodeValidator;
use Zend\Validator\File\Upload as FileUploadValidator;


/** /**
* @category Zend * @category Zend
Expand All @@ -46,6 +47,11 @@ class File extends Element implements InputProviderInterface, ElementPrepareAwar
'type' => 'file', 'type' => 'file',
); );


/**
* @var ValidatorInterface
*/
protected $validator;

/** /**
* Prepare the form element (mostly used for rendering purposes) * Prepare the form element (mostly used for rendering purposes)
* *
Expand All @@ -58,6 +64,30 @@ public function prepareElement(Form $form)
$form->setAttribute('enctype', 'multipart/form-data'); $form->setAttribute('enctype', 'multipart/form-data');
} }


/**
* Get validator
*
* @return ValidatorInterface
*/
protected function getValidator()
{
if (null === $this->validator) {
$validator = new FileUploadValidator();

$multiple = (isset($this->attributes['multiple']))
? $this->attributes['multiple'] : null;

if (true === $multiple || 'multiple' === $multiple) {
$validator = new FileExplodeValidator(array(
'validator' => $validator
));
}

$this->validator = $validator;
}
return $this->validator;
}

/** /**
* Should return an array specification compatible with * Should return an array specification compatible with
* {@link Zend\InputFilter\Factory::createInput()}. * {@link Zend\InputFilter\Factory::createInput()}.
Expand All @@ -71,7 +101,7 @@ public function getInputSpecification()
'name' => $this->getName(), 'name' => $this->getName(),
'required' => false, 'required' => false,
'validators' => array( 'validators' => array(
array('name' => 'fileupload'), $this->getValidator(),
), ),
); );
} }
Expand Down
28 changes: 25 additions & 3 deletions library/Zend/Form/View/Helper/FormFile.php
Expand Up @@ -57,10 +57,32 @@ protected function getType(ElementInterface $element)
*/ */
public function render(ElementInterface $element) public function render(ElementInterface $element)
{ {
$name = $element->getName();
if ($name === null || $name === '') {
throw new Exception\DomainException(sprintf(
'%s requires that the element has an assigned name; none discovered',
__METHOD__
));
}

$attributes = $element->getAttributes();
$attributes['type'] = $this->getType($element);
$attributes['name'] = $name;
if (array_key_exists('multiple', $attributes) && $attributes['multiple']) {
$attributes['name'] .= '[]';
}

$value = $element->getValue(); $value = $element->getValue();
if (is_array($value) && isset($value['name'])) { if (is_array($value) && isset($value['name']) && !is_array($value['name'])) {
$element->setValue($value['name']); $attributes['value'] = $value['name'];
} elseif (is_string($value)) {
$attributes['value'] = $value;
} }
return parent::render($element);
return sprintf(
'<input %s%s',
$this->createAttributesString($attributes),
$this->getInlineClosingBracket()
);
} }
} }
6 changes: 5 additions & 1 deletion library/Zend/InputFilter/BaseInputFilter.php
Expand Up @@ -165,7 +165,11 @@ public function isValid()
if (!array_key_exists($name, $this->data) if (!array_key_exists($name, $this->data)
|| (null === $this->data[$name]) || (null === $this->data[$name])
|| (is_string($this->data[$name]) && strlen($this->data[$name]) === 0) || (is_string($this->data[$name]) && strlen($this->data[$name]) === 0)
|| (isset($this->data[$name]['error']) && $this->data[$name]['error'] === UPLOAD_ERR_NO_FILE) // Single and Multi File Uploads
|| (is_array($this->data[$name])
&& isset($this->data[$name]['error']) && $this->data[$name]['error'] === UPLOAD_ERR_NO_FILE)
|| (is_array($this->data[$name]) && count($this->data[$name]) === 1
&& isset($this->data[$name][0]['error']) && $this->data[$name][0]['error'] === UPLOAD_ERR_NO_FILE)
) { ) {
if ($input instanceof InputInterface) { if ($input instanceof InputInterface) {
// - test if input is required // - test if input is required
Expand Down
76 changes: 76 additions & 0 deletions library/Zend/Validator/File/Explode.php
@@ -0,0 +1,76 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
* @package Zend_Validator
*/

namespace Zend\Validator\File;

use Zend\Validator\Exception;
use Zend\Validator\Explode as BaseExplode;

/**
* @category Zend
* @package Zend_Validate
*/
class Explode extends BaseExplode
{
const INVALID = 'fileExplodeInvalid';

/**
* @var array
*/
protected $messageTemplates = array(
self::INVALID => "Invalid type given. File array expected",
);

/**
* Defined by Zend_Validate_Interface
*
* Returns true if all values validate true
*
* @param array $value
* @return boolean
* @throws Exception\RuntimeException
*/
public function isValid($value)
{
if (!is_array($value)) {
$this->error(self::INVALID);
return false;
}

$values = $value;
$this->setValue($value);

$retval = true;
$messages = array();
$validator = $this->getValidator();

if (!$validator) {
throw new Exception\RuntimeException(sprintf(
'%s expects a validator to be set; none given',
__METHOD__
));
}

foreach ($values as $value) {
if (!$validator->isValid($value)) {
$messages[] = $validator->getMessages();
$retval = false;

if ($this->isBreakOnFirstFailure()) {
break;
}
}
}

$this->abstractOptions['messages'] = $messages;

return $retval;
}
}
1 change: 1 addition & 0 deletions library/Zend/Validator/ValidatorPluginManager.php
Expand Up @@ -71,6 +71,7 @@ class ValidatorPluginManager extends AbstractPluginManager
'fileexcludeextension' => 'Zend\Validator\File\ExcludeExtension', 'fileexcludeextension' => 'Zend\Validator\File\ExcludeExtension',
'fileexcludemimetype' => 'Zend\Validator\File\ExcludeMimeType', 'fileexcludemimetype' => 'Zend\Validator\File\ExcludeMimeType',
'fileexists' => 'Zend\Validator\File\Exists', 'fileexists' => 'Zend\Validator\File\Exists',
'fileexplode' => 'Zend\Validator\File\Explode',
'fileextension' => 'Zend\Validator\File\Extension', 'fileextension' => 'Zend\Validator\File\Extension',
'filehash' => 'Zend\Validator\File\Hash', 'filehash' => 'Zend\Validator\File\Hash',
'fileimagesize' => 'Zend\Validator\File\ImageSize', 'fileimagesize' => 'Zend\Validator\File\ImageSize',
Expand Down
18 changes: 18 additions & 0 deletions tests/ZendTest/Form/Element/FileTest.php
Expand Up @@ -31,6 +31,24 @@ public function testProvidesDefaultInputSpecification()
$this->assertInstanceOf('Zend\Validator\File\Upload', $validators[0]['instance']); $this->assertInstanceOf('Zend\Validator\File\Upload', $validators[0]['instance']);
} }


public function testProvidesDefaultInputSpecificationForMultiple()
{
$element = new FileElement('foo');
$element->setAttribute('multiple', true);
$this->assertEquals('file', $element->getAttribute('type'));

$inputSpec = $element->getInputSpecification();
$factory = new InputFilterFactory();
$input = $factory->createInput($inputSpec);
$this->assertInstanceOf('Zend\InputFilter\FileInput', $input);

$validators = $input->getValidatorChain()->getValidators();
$this->assertNotEmpty($validators);
$validator = $validators[0]['instance'];
$this->assertInstanceOf('Zend\Validator\File\Explode', $validator);
$this->assertInstanceOf('Zend\Validator\File\Upload', $validator->getValidator());
}

public function testWillAddFileEnctypeAttributeToForm() public function testWillAddFileEnctypeAttributeToForm()
{ {
$file = new FileElement('foo'); $file = new FileElement('foo');
Expand Down
54 changes: 46 additions & 8 deletions tests/ZendTest/Form/View/Helper/FormFileTest.php
Expand Up @@ -20,39 +20,54 @@
*/ */
class FormFileTest extends CommonTestCase class FormFileTest extends CommonTestCase
{ {
/**
* @return void
*/
public function setUp() public function setUp()
{ {
$this->helper = new FormFileHelper(); $this->helper = new FormFileHelper();
parent::setUp(); parent::setUp();
} }


/**
* @return void
*/
public function testRaisesExceptionWhenNameIsNotPresentInElement() public function testRaisesExceptionWhenNameIsNotPresentInElement()
{ {
$element = new Element(); $element = new Element\File();
$this->setExpectedException('Zend\Form\Exception\DomainException', 'name'); $this->setExpectedException('Zend\Form\Exception\DomainException', 'name');
$this->helper->render($element); $this->helper->render($element);
} }


/**
* @return void
*/
public function testGeneratesFileInputTagWithElement() public function testGeneratesFileInputTagWithElement()
{ {
$element = new Element('foo'); $element = new Element\File('foo');
$markup = $this->helper->render($element); $markup = $this->helper->render($element);
$this->assertContains('<input ', $markup); $this->assertContains('<input ', $markup);
$this->assertContains('type="file"', $markup); $this->assertContains('type="file"', $markup);
} }


/**
* @return void
*/
public function testGeneratesFileInputTagRegardlessOfElementType() public function testGeneratesFileInputTagRegardlessOfElementType()
{ {
$element = new Element('foo'); $element = new Element\File('foo');
$element->setAttribute('type', 'email'); $element->setAttribute('type', 'email');
$markup = $this->helper->render($element); $markup = $this->helper->render($element);
$this->assertContains('<input ', $markup); $this->assertContains('<input ', $markup);
$this->assertContains('type="file"', $markup); $this->assertContains('type="file"', $markup);
} }


/**
* @return void
*/
public function testRendersElementWithFileArrayValue() public function testRendersElementWithFileArrayValue()
{ {
$element = new Element('foo'); $element = new Element\File('foo');
$element->setValue(array( $element->setValue(array(
'tmp_name' => '/tmp/foofile', 'tmp_name' => '/tmp/foofile',
'name' => 'foofile', 'name' => 'foofile',
Expand All @@ -66,6 +81,9 @@ public function testRendersElementWithFileArrayValue()
$this->assertContains('value="foofile"', $markup); $this->assertContains('value="foofile"', $markup);
} }


/**
* @return array
*/
public function validAttributes() public function validAttributes()
{ {
return array( return array(
Expand All @@ -88,7 +106,7 @@ public function validAttributes()
array('max', 'assertNotContains'), array('max', 'assertNotContains'),
array('maxlength', 'assertNotContains'), array('maxlength', 'assertNotContains'),
array('min', 'assertNotContains'), array('min', 'assertNotContains'),
array('multiple', 'assertContains'), array('multiple', 'assertNotContains'),
array('pattern', 'assertNotContains'), array('pattern', 'assertNotContains'),
array('placeholder', 'assertNotContains'), array('placeholder', 'assertNotContains'),
array('readonly', 'assertNotContains'), array('readonly', 'assertNotContains'),
Expand All @@ -101,9 +119,12 @@ public function validAttributes()
); );
} }


/**
* @return Element\File
*/
public function getCompleteElement() public function getCompleteElement()
{ {
$element = new Element('foo'); $element = new Element\File('foo');
$element->setAttributes(array( $element->setAttributes(array(
'accept' => 'value', 'accept' => 'value',
'alt' => 'value', 'alt' => 'value',
Expand All @@ -124,7 +145,7 @@ public function getCompleteElement()
'max' => 'value', 'max' => 'value',
'maxlength' => 'value', 'maxlength' => 'value',
'min' => 'value', 'min' => 'value',
'multiple' => 'multiple', 'multiple' => false,
'name' => 'value', 'name' => 'value',
'pattern' => 'value', 'pattern' => 'value',
'placeholder' => 'value', 'placeholder' => 'value',
Expand Down Expand Up @@ -157,15 +178,32 @@ public function testAllValidFormMarkupAttributesPresentInElementAreRendered($att
$this->$assertion($expect, $markup); $this->$assertion($expect, $markup);
} }


/**
* @return void
*/
public function testNameShouldHaveArrayNotationWhenMultipleIsSpecified()
{
$element = new Element\File('foo');
$element->setAttribute('multiple', true);
$markup = $this->helper->render($element);
$this->assertRegexp('#<input[^>]*?(name="foo\[\]")#', $markup);
}

/**
* @return void
*/
public function testInvokeProxiesToRender() public function testInvokeProxiesToRender()
{ {
$element = new Element('foo'); $element = new Element\File('foo');
$markup = $this->helper->__invoke($element); $markup = $this->helper->__invoke($element);
$this->assertContains('<input', $markup); $this->assertContains('<input', $markup);
$this->assertContains('name="foo"', $markup); $this->assertContains('name="foo"', $markup);
$this->assertContains('type="file"', $markup); $this->assertContains('type="file"', $markup);
} }


/**
* @return void
*/
public function testInvokeWithNoElementChainsHelper() public function testInvokeWithNoElementChainsHelper()
{ {
$this->assertSame($this->helper, $this->helper->__invoke()); $this->assertSame($this->helper, $this->helper->__invoke());
Expand Down
31 changes: 31 additions & 0 deletions tests/ZendTest/InputFilter/BaseInputFilterTest.php
Expand Up @@ -443,6 +443,37 @@ public function testValidationSkipsFileInputsMarkedNotRequiredWhenNoFileDataIsPr
$this->assertFalse($filter->isValid()); $this->assertFalse($filter->isValid());
} }


public function testValidationSkipsFileInputsMarkedNotRequiredWhenNoMultiFileDataIsPresent()
{
$filter = new InputFilter();

$explode = new Validator\File\Explode();
$explode->setValidator(new Validator\File\Upload());

$foo = new FileInput();
$foo->getValidatorChain()->addValidator($explode);
$foo->setRequired(false);

$filter->add($foo, 'foo');

$data = array(
'foo' => array(array(
'tmp_name' => '/tmp/barfile',
'name' => 'barfile',
'type' => 'text',
'size' => 0,
'error' => 4, // UPLOAD_ERR_NO_FILE
)),
);
$filter->setData($data);
$this->assertTrue($filter->isValid());

// Negative test
$foo->setRequired(true);
$filter->setData($data);
$this->assertFalse($filter->isValid());
}

public function testValidationAllowsEmptyValuesToRequiredInputWhenAllowEmptyFlagIsTrue() public function testValidationAllowsEmptyValuesToRequiredInputWhenAllowEmptyFlagIsTrue()
{ {
$filter = new InputFilter(); $filter = new InputFilter();
Expand Down

0 comments on commit e9ac49d

Please sign in to comment.