Procházet zdrojové kódy

[ZF-8733] Zend_File_Transfer:

- increased security as proposed

git-svn-id: http://framework.zend.com/svn/framework/standard/trunk@20141 44c647ce-9c0f-0410-b52a-842ac1e357ba
thomas před 16 roky
rodič
revize
448d8caa82

+ 42 - 3
documentation/manual/en/module_specs/Zend_File_Transfer-Introduction.xml

@@ -203,7 +203,6 @@ $upload->receive();
     </sect2>
 
     <sect2 id="zend.file.transfer.introduction.informations">
-
         <title>Additional File Informations</title>
 
         <para>
@@ -309,6 +308,16 @@ $size = $upload->getFileSize();
 
         </example>
 
+        <note>
+            <title>Client given filesize</title>
+
+            <para>
+                Note that the filesize which is given by the client is not seen as save input.
+                Therefor the real size of the file will be detected and returned instead of the
+                filesize sent by the client.
+            </para>
+        </note>
+
         <para>
             <methodname>getHash()</methodname> accepts the name of a hash algorithm as first
             parameter. For a list of known algorithms refer to
@@ -333,6 +342,8 @@ $names = $upload->getHash('crc32', 'foo');
         </example>
 
         <note>
+            <title>Return value</title>
+
             <para>
                 Note that if the given file or form name contains more than one file, the returned
                 value will be an array.
@@ -360,12 +371,40 @@ $names = $upload->getMimeType('foo');
         </example>
 
         <note>
+            <title>Client given mimetype</title>
+
+            <para>
+                Note that the mimetype which is given by the client is not seen as save input.
+                Therefor the real mimetype of the file will be detected and returned instead of the
+                filesize sent by the client.
+            </para>
+        </note>
+
+        <warning>
+            <title>Possible exception</title>
+
             <para>
                 Note that this method uses the fileinfo extension if it is available. If this
                 extension can not be found, it uses the mimemagic extension. When no extension was
-                found it uses the mimetype given by the fileserver when the file was uploaded.
+                found it raises an exception.
             </para>
-        </note>
+        </warning>
+
+        <warning>
+            <title>Original data within $_FILES</title>
+
+            <para>
+                Due to security reasons also the original data within $_FILES will be overridden
+                as soon as <classname>Zend_File_Transfer</classname> is initiated. When you want
+                to omit this behaviour and have the original data simply set the
+                <property>detectInfos</property> option to false at initiation.
+            </para>
+
+            <para>
+                This option will have no effect after you initiated
+                <classname>Zend_File_Transfer</classname>.
+            </para>
+        </warning>
     </sect2>
 
     <sect2 id="zend.file.transfer.introduction.uploadprogress">

+ 77 - 13
library/Zend/File/Transfer.php

@@ -20,6 +20,11 @@
  */
 
 /**
+ * @see Zend_Loader
+ */
+require_once 'Zend/Loader.php';
+
+/**
  * Base class for all protocols supporting file transfers
  *
  * @category  Zend
@@ -30,32 +35,91 @@
 class Zend_File_Transfer
 {
     /**
+     * Array holding all directions
+     *
+     * @var array
+     */
+    protected $_adapter = array();
+
+    /**
      * Creates a file processing handler
      *
-     * @param string $protocol Protocol to use
+     * @param  string  $adapter   Adapter to use
+     * @param  boolean $direction OPTIONAL False means Download, true means upload
+     * @param  array   $options   OPTIONAL Options to set for this adapter
+     * @throws Zend_File_Transfer_Exception
      */
-    public function __construct($protocol = null)
+    public function __construct($adapter = 'Http', $direction = false, $options = array())
     {
-        require_once 'Zend/File/Transfer/Exception.php';
-        throw new Zend_File_Transfer_Exception('Implementation in progress');
+        $this->setAdapter($adapter, $direction, $options);
+    }
 
-        switch (strtoupper($protocol)) {
-            default:
-                $adapter = 'Zend_File_Transfer_Adapter_Http';
-                break;
+    /**
+     * Sets a new adapter
+     *
+     * @param  string  $adapter   Adapter to use
+     * @param  boolean $direction OPTIONAL False means Download, true means upload
+     * @param  array   $options   OPTIONAL Options to set for this adapter
+     * @throws Zend_File_Transfer_Exception
+     */
+    public function setAdapter($adapter, $direction = false, $options = array())
+    {
+        if (Zend_Loader::isReadable('Zend/File/Transfer/Adapter/' . ucfirst($adapter). '.php')) {
+            $adapter = 'Zend_File_Transfer_Adapter_' . ucfirst($adapter);
         }
 
         if (!class_exists($adapter)) {
-            require_once 'Zend/Loader.php';
             Zend_Loader::loadClass($adapter);
         }
 
-        $this->_adapter = new $adapter();
-        if (!$this->_adapter instanceof Zend_File_Transfer_Adapter) {
+        $direction = (integer) $direction;
+        $this->_adapter[$direction] = new $adapter($options);
+        if (!$this->_adapter[$direction] instanceof Zend_File_Transfer_Adapter) {
             require_once 'Zend/File/Transfer/Exception.php';
-            throw new Zend_File_Transfer_Exception("Adapter " . $adapter . " does not extend Zend_File_Transfer_Adapter'");
+            throw new Zend_File_Transfer_Exception("Adapter " . $adapter . " does not extend Zend_File_Transfer_Adapter");
+        }
+
+        return $this;
+    }
+
+    /**
+     * Returns all set adapters
+     *
+     * @param boolean $direction On null, all directions are returned
+     *                           On false, download direction is returned
+     *                           On true, upload direction is returned
+     * @return array|Zend_File_Transfer_Adapter
+     */
+    public function getAdapter($direction = null)
+    {
+        if ($direction === null) {
+            return $this->_adapter;
+        }
+
+        $direction = (integer) $direction;
+        return $this->_adapter[$direction];
+    }
+
+    /**
+     * Calls all methods from the adapter
+     *
+     * @param  string $method  Method to call
+     * @param  array  $options Options for this method
+     * @return mixed
+     */
+    public function __call($method, array $options)
+    {
+        if (array_key_exists('direction', $options)) {
+            $direction = (integer) $options['direction'];
+        } else {
+            $direction = 0;
+        }
+
+        if (method_exists($this->_adapter[$direction], $method)) {
+            return call_user_func_array(array($this->_adapter[$direction], $method), $options);
         }
 
-        return $this->_adapter;
+        require_once 'Zend/File/Transfer/Exception.php';
+        throw new Zend_File_Transfer_Exception("Unknown method '" . $method . "' called!");
     }
 }

+ 67 - 35
library/Zend/File/Transfer/Adapter/Abstract.php

@@ -112,7 +112,8 @@ abstract class Zend_File_Transfer_Adapter_Abstract
     protected $_options = array(
         'ignoreNoFile'  => false,
         'useByteString' => true,
-        'magicFile'     => null
+        'magicFile'     => null,
+        'detectInfos'   => true,
     );
 
     /**
@@ -1183,22 +1184,18 @@ abstract class Zend_File_Transfer_Adapter_Abstract
         $files  = $this->_getFiles($files);
         $result = array();
         foreach($files as $key => $value) {
-            if (file_exists($value['name'])) {
-                $size = sprintf("%u", @filesize($value['name']));
-            } else if (file_exists($value['tmp_name'])) {
-                $size = sprintf("%u", @filesize($value['tmp_name']));
+            if (file_exists($value['name']) || file_exists($value['tmp_name'])) {
+                if ($value['options']['useByteString']) {
+                    $result[$key] = self::_toByteString($value['size']);
+                } else {
+                    $result[$key] = $value['size'];
+                }
             } else if (empty($value['options']['ignoreNoFile'])) {
                 require_once 'Zend/File/Transfer/Exception.php';
                 throw new Zend_File_Transfer_Exception("File '{$value['name']}' does not exist");
             } else {
                 continue;
             }
-
-            if ($value['options']['useByteString']) {
-                $result[$key] = self::_toByteString($size);
-            } else {
-                $result[$key] = $size;
-            }
         }
 
         if (count($result) == 1) {
@@ -1209,6 +1206,25 @@ abstract class Zend_File_Transfer_Adapter_Abstract
     }
 
     /**
+     * Internal method to detect the size of a file
+     *
+     * @param  array $value File infos
+     * @return string Filesize of given file
+     */
+    protected function _detectFileSize($value)
+    {
+        if (file_exists($value['name'])) {
+            $result = sprintf("%u", @filesize($value['name']));
+        } else if (file_exists($value['tmp_name'])) {
+            $result = sprintf("%u", @filesize($value['tmp_name']));
+        } else {
+            return null;
+        }
+
+        return $result;
+    }
+
+    /**
      * Returns the real mimetype of the file
      * Uses fileinfo, when not available mime_magic and as last fallback a manual given mimetype
      *
@@ -1221,45 +1237,61 @@ abstract class Zend_File_Transfer_Adapter_Abstract
         $files  = $this->_getFiles($files);
         $result = array();
         foreach($files as $key => $value) {
-            if (file_exists($value['name'])) {
-                $file = $value['name'];
-            } else if (file_exists($value['tmp_name'])) {
-                $file = $value['tmp_name'];
+            if (file_exists($value['name']) || file_exists($value['tmp_name'])) {
+                $result[$key] = $value['type'];
             } else if (empty($value['options']['ignoreNoFile'])) {
                 require_once 'Zend/File/Transfer/Exception.php';
                 throw new Zend_File_Transfer_Exception("File '{$value['name']}' does not exist");
             } else {
                 continue;
             }
+        }
 
-            if (class_exists('finfo', false)) {
-                $const = defined('FILEINFO_MIME_TYPE') ? FILEINFO_MIME_TYPE : FILEINFO_MIME;
-                if (!empty($value['options']['magicFile'])) {
-                    $mime = new finfo($const, $value['options']['magicFile']);
-                } else {
-                    $mime = new finfo($const);
-                }
+        if (count($result) == 1) {
+            return current($result);
+        }
 
-                if ($mime !== false) {
-                    $result[$key] = $mime->file($file);
-                }
+        return $result;
+    }
 
-                unset($mime);
-            }
+    /**
+     * Internal method to detect the mime type of a file
+     *
+     * @param  array $value File infos
+     * @return string Mimetype of given file
+     */
+    protected function _detectMimeType($value)
+    {
+        if (file_exists($value['name'])) {
+            $file = $value['name'];
+        } else if (file_exists($value['tmp_name'])) {
+            $file = $value['tmp_name'];
+        } else {
+            return null;
+        }
 
-            if (empty($result[$key])) {
-                if (function_exists('mime_content_type') && ini_get('mime_magic.magicfile')) {
-                    $result[$key] = mime_content_type($file);
-                }
+        if (class_exists('finfo', false)) {
+            $const = defined('FILEINFO_MIME_TYPE') ? FILEINFO_MIME_TYPE : FILEINFO_MIME;
+            if (!empty($magicFile)) {
+                $mime = new finfo($const, $magicFile);
+            } else {
+                $mime = new finfo($const);
             }
 
-            if (empty($result[$key])) {
-                $result[$key] = 'application/octet-stream';
+            if ($mime !== false) {
+                $result = $mime->file($file);
             }
+
+            unset($mime);
         }
 
-        if (count($result) == 1) {
-            return current($result);
+        if (empty($result) && (function_exists('mime_content_type')
+            && ini_get('mime_magic.magicfile'))) {
+            $result = mime_content_type($file);
+        }
+
+        if (empty($result)) {
+            $result = 'application/octet-stream';
         }
 
         return $result;

+ 39 - 17
library/Zend/File/Transfer/Adapter/Http.php

@@ -49,7 +49,7 @@ class Zend_File_Transfer_Adapter_Http extends Zend_File_Transfer_Adapter_Abstrac
             throw new Zend_File_Transfer_Exception('File uploads are not allowed in your php config!');
         }
 
-        $this->_files = $this->_prepareFiles($_FILES);
+        $this->_prepareFiles();
         $this->addValidator('Upload', false, $this->_files);
 
         if (is_array($options)) {
@@ -431,31 +431,53 @@ class Zend_File_Transfer_Adapter_Http extends Zend_File_Transfer_Adapter_Abstrac
      * @param  array $files
      * @return array
      */
-    protected function _prepareFiles(array $files = array())
+    protected function _prepareFiles()
     {
-        $result = array();
-        foreach ($files as $form => $content) {
+        $this->_files = array();
+        foreach ($_FILES as $form => $content) {
             if (is_array($content['name'])) {
                 foreach ($content as $param => $file) {
                     foreach ($file as $number => $target) {
-                        $result[$form . '_' . $number . '_'][$param]      = $target;
-                        $result[$form . '_' . $number . '_']['options']   = $this->_options;
-                        $result[$form . '_' . $number . '_']['validated'] = false;
-                        $result[$form . '_' . $number . '_']['received']  = false;
-                        $result[$form . '_' . $number . '_']['filtered']  = false;
-                        $result[$form]['multifiles'][$number] = $form . '_' . $number . '_';
-                        $result[$form]['name'] = $form;
+                        $this->_files[$form . '_' . $number . '_'][$param]      = $target;
+                        $this->_files[$form . '_' . $number . '_']['options']   = $this->_options;
+                        $this->_files[$form . '_' . $number . '_']['validated'] = false;
+                        $this->_files[$form . '_' . $number . '_']['received']  = false;
+                        $this->_files[$form . '_' . $number . '_']['filtered']  = false;
+                        $this->_files[$form]['multifiles'][$number] = $form . '_' . $number . '_';
+                        $this->_files[$form]['name'] = $form;
+
+                        $mimetype = $this->_detectMimeType($this->_files[$form . '_' . $number . '_']);
+                        $this->_files[$form . '_' . $number . '_']['type'] = $mimetype;
+
+                        $filesize = $this->_detectFileSize($this->_files[$form . '_' . $number . '_']);
+                        $this->_files[$form . '_' . $number . '_']['size'] = $filesize;
+
+                        if ($this->_options['detectInfos']) {
+                            $_FILES[$form]['type'][$number] = $mimetype;
+                            $_FILES[$form]['size'][$number] = $filesize;
+                        }
                     }
                 }
             } else {
-                $result[$form]              = $content;
-                $result[$form]['options']   = $this->_options;
-                $result[$form]['validated'] = false;
-                $result[$form]['received']  = false;
-                $result[$form]['filtered']  = false;
+                $this->_files[$form]              = $content;
+                $this->_files[$form]['options']   = $this->_options;
+                $this->_files[$form]['validated'] = false;
+                $this->_files[$form]['received']  = false;
+                $this->_files[$form]['filtered']  = false;
+
+                $mimetype = $this->_detectMimeType($this->_files[$form]);
+                $this->_files[$form]['type'] = $mimetype;
+
+                $filesize = $this->_detectFileSize($this->_files[$form]);
+                $this->_files[$form]['size'] = $filesize;
+
+                if ($this->_options['detectInfos']) {
+                    $_FILES[$form]['type'] = $mimetype;
+                    $_FILES[$form]['size'] = $filesize;
+                }
             }
         }
 
-        return $result;
+        return $this;
     }
 }

+ 2 - 2
tests/Zend/File/Transfer/Adapter/AbstractTest.php

@@ -734,11 +734,11 @@ class Zend_File_Transfer_Adapter_AbstractTest extends PHPUnit_Framework_TestCase
     {
         $options = $this->adapter->getOptions();
         $this->assertTrue($options['baz']['useByteString']);
-        $this->assertEquals('8B', $this->adapter->getFileSize('baz.text'));
+        $this->assertEquals('1.14kB', $this->adapter->getFileSize('baz.text'));
         $this->adapter->setOptions(array('useByteString' => false));
         $options = $this->adapter->getOptions();
         $this->assertFalse($options['baz']['useByteString']);
-        $this->assertEquals(8, $this->adapter->getFileSize('baz.text'));
+        $this->assertEquals(1172, $this->adapter->getFileSize('baz.text'));
     }
 
     public function testMimeTypeButNoFileFound()

+ 20 - 12
tests/Zend/File/Transfer/Adapter/HttpTest.php

@@ -68,10 +68,10 @@ class Zend_File_Transfer_Adapter_HttpTest extends PHPUnit_Framework_TestCase
     {
         $_FILES = array(
             'txt' => array(
-                'name' => 'file.txt',
+                'name' => dirname(__FILE__) . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'test.txt',
                 'type' => 'plain/text',
                 'size' => 8,
-                'tmp_name' => 'file.txt',
+                'tmp_name' => dirname(__FILE__) . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'test.txt',
                 'error' => 0));
         $this->adapter = new Zend_File_Transfer_Adapter_HttpTest_MockAdapter();
     }
@@ -89,7 +89,7 @@ class Zend_File_Transfer_Adapter_HttpTest extends PHPUnit_Framework_TestCase
     public function testEmptyAdapter()
     {
         $files = $this->adapter->getFileName();
-        $this->assertContains('file.txt', $files);
+        $this->assertContains('test.txt', $files);
     }
 
     public function testAutoSetUploadValidator()
@@ -152,7 +152,15 @@ class Zend_File_Transfer_Adapter_HttpTest extends PHPUnit_Framework_TestCase
 
     public function testReceiveValidatedFile()
     {
-        $this->assertFalse($this->adapter->receive());
+        $_FILES = array(
+            'txt' => array(
+                'name' => 'unknown.txt',
+                'type' => 'plain/text',
+                'size' => 8,
+                'tmp_name' => 'unknown.txt',
+                'error' => 0));
+        $adapter = new Zend_File_Transfer_Adapter_HttpTest_MockAdapter();
+        $this->assertFalse($adapter->receive());
     }
 
     public function testReceiveIgnoredFile()
@@ -180,15 +188,15 @@ class Zend_File_Transfer_Adapter_HttpTest extends PHPUnit_Framework_TestCase
     {
         $_FILES = array(
             'txt' => array(
-                'name' => 'file.txt',
+                'name' => dirname(__FILE__) . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'test.txt',
                 'type' => 'plain/text',
                 'size' => 8,
-                'tmp_name' => 'file.txt',
+                'tmp_name' => dirname(__FILE__) . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'test.txt',
                 'error' => 0),
             'exe' => array(
                 'name' => array(
-                    0 => 'file1.txt',
-                    1 => 'file2.txt'),
+                    0 => dirname(__FILE__) . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'file1.txt',
+                    1 => dirname(__FILE__) . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'file2.txt'),
                 'type' => array(
                     0 => 'plain/text',
                     1 => 'plain/text'),
@@ -196,8 +204,8 @@ class Zend_File_Transfer_Adapter_HttpTest extends PHPUnit_Framework_TestCase
                     0 => 8,
                     1 => 8),
                 'tmp_name' => array(
-                    0 => 'file1.txt',
-                    1 => 'file2.txt'),
+                    0 => dirname(__FILE__) . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'file1.txt',
+                    1 => dirname(__FILE__) . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'file2.txt'),
                 'error' => array(
                     0 => 0,
                     1 => 0)));
@@ -205,8 +213,8 @@ class Zend_File_Transfer_Adapter_HttpTest extends PHPUnit_Framework_TestCase
         $adapter->setOptions(array('ignoreNoFile' => true));
         $this->assertTrue($adapter->receive('exe'));
         $this->assertEquals(
-            array('exe_0_' => 'file1.txt',
-                  'exe_1_' => 'file2.txt'),
+            array('exe_0_' => dirname(__FILE__) . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'file1.txt',
+                  'exe_1_' => dirname(__FILE__) . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'file2.txt'),
             $adapter->getFileName('exe', false));
     }
 

+ 1 - 0
tests/Zend/File/Transfer/Adapter/_files/file1.txt

@@ -0,0 +1 @@
+testfile

+ 1 - 0
tests/Zend/File/Transfer/Adapter/_files/file2.txt

@@ -0,0 +1 @@
+testfile