Forráskód Böngészése

[Form] Improved FileField to store files in a temporary location in case validation fails

Bernhard Schussek 14 éve
szülő
commit
e0aa3f30a8

+ 7 - 4
src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/file_field.php

@@ -1,5 +1,8 @@
 <input type="file"
-	id="<?php echo $field->getId() ?>"
-	name="<?php echo $field->getName() ?>"
-	<?php if ($field->isDisabled()): ?>disabled="disabled"<?php endif ?>
-/>
+	id="<?php echo $field['file']->getId() ?>"
+	name="<?php echo $field['file']->getName() ?>"
+	<?php if ($field['file']->isDisabled()): ?>disabled="disabled"<?php endif ?>
+/>
+
+<?php echo $view['form']->render($field['token']) ?>
+<?php echo $view['form']->render($field['original_name']) ?>

+ 4 - 0
src/Symfony/Bundle/TwigBundle/Resources/views/form.twig

@@ -135,5 +135,9 @@
 {% endblock percent_field %}
 
 {% block file_field %}
+    {% set group = field %}
+    {% set field = group.file %}
     <input type="file" {% display field_attributes %} />
+    {{ group.token|render }}
+    {{ group.original_name|render }}
 {% endblock file_field %}

+ 98 - 1
src/Symfony/Component/Form/FileField.php

@@ -2,6 +2,8 @@
 
 namespace Symfony\Component\Form;
 
+use Symfony\Component\HttpFoundation\File\File;
+
 /*
  * This file is part of the Symfony framework.
  *
@@ -14,8 +16,103 @@ namespace Symfony\Component\Form;
 /**
  * A file field to upload files.
  */
-class FileField extends Field
+class FileField extends FieldGroup
 {
+    /**
+     * {@inheritDoc}
+     */
+    protected function configure()
+    {
+        $this->addRequiredOption('secret');
+        $this->addOption('tmp_dir', sys_get_temp_dir());
+
+        parent::configure();
+
+        $this->add(new Field('file'));
+        $this->add(new HiddenField('token'));
+        $this->add(new HiddenField('original_name'));
+    }
+
+    /**
+     * Moves the file to a temporary location to prevent its deletion when
+     * the PHP process dies
+     *
+     * This way the file can survive if the form does not validate and is
+     * resubmitted.
+     *
+     * @see Symfony\Component\Form\FieldGroup::preprocessData()
+     */
+    protected function preprocessData(array $data)
+    {
+        if ($data['file']) {
+            $data['file']->move($this->getTmpPath($data['token']));
+            $data['original_name'] = $data['file']->getOriginalName();
+            $data['file'] = '';
+        }
+
+        return $data;
+    }
+
+    /**
+     * Turns a file path into an array of field values
+     *
+     * @see Symfony\Component\Form\Field::normalize()
+     */
+    protected function normalize($path)
+    {
+        srand(microtime(true));
+
+        return array(
+            'file' => '',
+            'token' => rand(100000, 999999),
+            'original_name' => '',
+        );
+    }
+
+    /**
+     * Turns an array of field values into a file path
+     *
+     * @see Symfony\Component\Form\Field::denormalize()
+     */
+    protected function denormalize($data)
+    {
+        $path = $this->getTmpPath($data['token']);
+
+        return file_exists($path) ? $path : $this->getData();
+    }
+
+    /**
+     * Returns the absolute temporary file path for the given token
+     *
+     * @param string $token
+     */
+    protected function getTmpPath($token)
+    {
+        return realpath($this->getOption('tmp_dir')) . '/' . $this->getTmpName($token);
+    }
+
+    /**
+     * Returns the temporary file name for the given token
+     *
+     * @param string $token
+     */
+    protected function getTmpName($token)
+    {
+        return md5(session_id() . $this->getOption('secret') . $token);
+    }
+
+    /**
+     * Returns the original name of the uploaded file
+     *
+     * @return string
+     */
+    public function getOriginalName()
+    {
+        $data = $this->getNormalizedData();
+
+        return $data['original_name'];
+    }
+
     /**
      * {@inheritDoc}
      */

+ 125 - 0
tests/Symfony/Tests/Component/Form/FileFieldTest.php

@@ -0,0 +1,125 @@
+<?php
+
+namespace Symfony\Tests\Component\Form;
+
+use Symfony\Component\HttpFoundation\File\File;
+use Symfony\Component\Form\FileField;
+
+class FileFieldTest extends \PHPUnit_Framework_TestCase
+{
+    public static $tmpFiles = array();
+
+    protected static $tmpDir;
+
+    public static function setUpBeforeClass()
+    {
+        self::$tmpDir = sys_get_temp_dir();
+
+        // we need a session ID
+        @session_start();
+    }
+
+    public function tearDown()
+    {
+        foreach (self::$tmpFiles as $key => $file) {
+            @unlink($file);
+            unset(self::$tmpFiles[$key]);
+        }
+    }
+
+    public function createTmpFile($path)
+    {
+        self::$tmpFiles[] = $path;
+        file_put_contents($path, 'foobar');
+    }
+
+    public function testBindUploadsNewFiles()
+    {
+        $field = new FileField('file', array(
+            'secret' => '$secret$',
+            'tmp_dir' => self::$tmpDir,
+        ));
+
+        $tmpPath = self::$tmpDir . '/' . md5(session_id() . '$secret$' . '12345');
+        $that = $this;
+
+        $file = $this->getMock('Symfony\Component\HttpFoundation\File\UploadedFile', array(), array(), '', false);
+        $file->expects($this->once())
+             ->method('move')
+             ->with($this->equalTo($tmpPath))
+             ->will($this->returnCallback(function ($path) use ($that) {
+                $that->createTmpFile($path);
+             }));
+        $file->expects($this->any())
+             ->method('getOriginalName')
+             ->will($this->returnValue('original_name.jpg'));
+
+        $field->bind(array(
+            'file' => $file,
+            'token' => '12345',
+            'original_name' => '',
+        ));
+
+        $this->assertTrue(file_exists($tmpPath));
+        $this->assertEquals(array(
+            'file' => '',
+            'token' => '12345',
+            'original_name' => 'original_name.jpg',
+        ), $field->getDisplayedData());
+        $this->assertEquals($tmpPath, $field->getData());
+    }
+
+    public function testBindKeepsUploadedFilesOnErrors()
+    {
+        $field = new FileField('file', array(
+            'secret' => '$secret$',
+            'tmp_dir' => self::$tmpDir,
+        ));
+
+        $tmpPath = self::$tmpDir . '/' . md5(session_id() . '$secret$' . '12345');
+        $this->createTmpFile($tmpPath);
+
+        $field->bind(array(
+            'file' => '',
+            'token' => '12345',
+            'original_name' => 'original_name.jpg',
+        ));
+
+        $this->assertTrue(file_exists($tmpPath));
+        $this->assertEquals(array(
+            'file' => '',
+            'token' => '12345',
+            'original_name' => 'original_name.jpg',
+        ), $field->getDisplayedData());
+        $this->assertEquals($tmpPath, $field->getData());
+    }
+
+    public function testBindKeepsOldFileIfNotOverwritten()
+    {
+        $field = new FileField('file', array(
+            'secret' => '$secret$',
+            'tmp_dir' => self::$tmpDir,
+        ));
+
+        $oldPath = tempnam(sys_get_temp_dir(), 'FileFieldTest');
+        $this->createTmpFile($oldPath);
+
+        $field->setData($oldPath);
+
+        $this->assertEquals($oldPath, $field->getData());
+
+        $field->bind(array(
+            'file' => '',
+            'token' => '12345',
+            'original_name' => '',
+        ));
+
+        $this->assertTrue(file_exists($oldPath));
+        $this->assertEquals(array(
+            'file' => '',
+            'token' => '12345',
+            'original_name' => '',
+        ), $field->getDisplayedData());
+        $this->assertEquals($oldPath, $field->getData());
+    }
+}