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>
 
 
     <sect2 id="zend.file.transfer.introduction.informations">
     <sect2 id="zend.file.transfer.introduction.informations">
-
         <title>Additional File Informations</title>
         <title>Additional File Informations</title>
 
 
         <para>
         <para>
@@ -309,6 +308,16 @@ $size = $upload->getFileSize();
 
 
         </example>
         </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>
         <para>
             <methodname>getHash()</methodname> accepts the name of a hash algorithm as first
             <methodname>getHash()</methodname> accepts the name of a hash algorithm as first
             parameter. For a list of known algorithms refer to
             parameter. For a list of known algorithms refer to
@@ -333,6 +342,8 @@ $names = $upload->getHash('crc32', 'foo');
         </example>
         </example>
 
 
         <note>
         <note>
+            <title>Return value</title>
+
             <para>
             <para>
                 Note that if the given file or form name contains more than one file, the returned
                 Note that if the given file or form name contains more than one file, the returned
                 value will be an array.
                 value will be an array.
@@ -360,12 +371,40 @@ $names = $upload->getMimeType('foo');
         </example>
         </example>
 
 
         <note>
         <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>
             <para>
                 Note that this method uses the fileinfo extension if it is available. If this
                 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
                 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>
             </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>
 
 
     <sect2 id="zend.file.transfer.introduction.uploadprogress">
     <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
  * Base class for all protocols supporting file transfers
  *
  *
  * @category  Zend
  * @category  Zend
@@ -30,32 +35,91 @@
 class Zend_File_Transfer
 class Zend_File_Transfer
 {
 {
     /**
     /**
+     * Array holding all directions
+     *
+     * @var array
+     */
+    protected $_adapter = array();
+
+    /**
      * Creates a file processing handler
      * 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)) {
         if (!class_exists($adapter)) {
-            require_once 'Zend/Loader.php';
             Zend_Loader::loadClass($adapter);
             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';
             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(
     protected $_options = array(
         'ignoreNoFile'  => false,
         'ignoreNoFile'  => false,
         'useByteString' => true,
         'useByteString' => true,
-        'magicFile'     => null
+        'magicFile'     => null,
+        'detectInfos'   => true,
     );
     );
 
 
     /**
     /**
@@ -1183,22 +1184,18 @@ abstract class Zend_File_Transfer_Adapter_Abstract
         $files  = $this->_getFiles($files);
         $files  = $this->_getFiles($files);
         $result = array();
         $result = array();
         foreach($files as $key => $value) {
         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'])) {
             } else if (empty($value['options']['ignoreNoFile'])) {
                 require_once 'Zend/File/Transfer/Exception.php';
                 require_once 'Zend/File/Transfer/Exception.php';
                 throw new Zend_File_Transfer_Exception("File '{$value['name']}' does not exist");
                 throw new Zend_File_Transfer_Exception("File '{$value['name']}' does not exist");
             } else {
             } else {
                 continue;
                 continue;
             }
             }
-
-            if ($value['options']['useByteString']) {
-                $result[$key] = self::_toByteString($size);
-            } else {
-                $result[$key] = $size;
-            }
         }
         }
 
 
         if (count($result) == 1) {
         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
      * Returns the real mimetype of the file
      * Uses fileinfo, when not available mime_magic and as last fallback a manual given mimetype
      * 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);
         $files  = $this->_getFiles($files);
         $result = array();
         $result = array();
         foreach($files as $key => $value) {
         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'])) {
             } else if (empty($value['options']['ignoreNoFile'])) {
                 require_once 'Zend/File/Transfer/Exception.php';
                 require_once 'Zend/File/Transfer/Exception.php';
                 throw new Zend_File_Transfer_Exception("File '{$value['name']}' does not exist");
                 throw new Zend_File_Transfer_Exception("File '{$value['name']}' does not exist");
             } else {
             } else {
                 continue;
                 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;
         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!');
             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);
         $this->addValidator('Upload', false, $this->_files);
 
 
         if (is_array($options)) {
         if (is_array($options)) {
@@ -431,31 +431,53 @@ class Zend_File_Transfer_Adapter_Http extends Zend_File_Transfer_Adapter_Abstrac
      * @param  array $files
      * @param  array $files
      * @return array
      * @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'])) {
             if (is_array($content['name'])) {
                 foreach ($content as $param => $file) {
                 foreach ($content as $param => $file) {
                     foreach ($file as $number => $target) {
                     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 {
             } 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();
         $options = $this->adapter->getOptions();
         $this->assertTrue($options['baz']['useByteString']);
         $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));
         $this->adapter->setOptions(array('useByteString' => false));
         $options = $this->adapter->getOptions();
         $options = $this->adapter->getOptions();
         $this->assertFalse($options['baz']['useByteString']);
         $this->assertFalse($options['baz']['useByteString']);
-        $this->assertEquals(8, $this->adapter->getFileSize('baz.text'));
+        $this->assertEquals(1172, $this->adapter->getFileSize('baz.text'));
     }
     }
 
 
     public function testMimeTypeButNoFileFound()
     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(
         $_FILES = array(
             'txt' => array(
             'txt' => array(
-                'name' => 'file.txt',
+                'name' => dirname(__FILE__) . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'test.txt',
                 'type' => 'plain/text',
                 'type' => 'plain/text',
                 'size' => 8,
                 'size' => 8,
-                'tmp_name' => 'file.txt',
+                'tmp_name' => dirname(__FILE__) . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'test.txt',
                 'error' => 0));
                 'error' => 0));
         $this->adapter = new Zend_File_Transfer_Adapter_HttpTest_MockAdapter();
         $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()
     public function testEmptyAdapter()
     {
     {
         $files = $this->adapter->getFileName();
         $files = $this->adapter->getFileName();
-        $this->assertContains('file.txt', $files);
+        $this->assertContains('test.txt', $files);
     }
     }
 
 
     public function testAutoSetUploadValidator()
     public function testAutoSetUploadValidator()
@@ -152,7 +152,15 @@ class Zend_File_Transfer_Adapter_HttpTest extends PHPUnit_Framework_TestCase
 
 
     public function testReceiveValidatedFile()
     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()
     public function testReceiveIgnoredFile()
@@ -180,15 +188,15 @@ class Zend_File_Transfer_Adapter_HttpTest extends PHPUnit_Framework_TestCase
     {
     {
         $_FILES = array(
         $_FILES = array(
             'txt' => array(
             'txt' => array(
-                'name' => 'file.txt',
+                'name' => dirname(__FILE__) . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'test.txt',
                 'type' => 'plain/text',
                 'type' => 'plain/text',
                 'size' => 8,
                 'size' => 8,
-                'tmp_name' => 'file.txt',
+                'tmp_name' => dirname(__FILE__) . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'test.txt',
                 'error' => 0),
                 'error' => 0),
             'exe' => array(
             'exe' => array(
                 'name' => 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(
                 'type' => array(
                     0 => 'plain/text',
                     0 => 'plain/text',
                     1 => 'plain/text'),
                     1 => 'plain/text'),
@@ -196,8 +204,8 @@ class Zend_File_Transfer_Adapter_HttpTest extends PHPUnit_Framework_TestCase
                     0 => 8,
                     0 => 8,
                     1 => 8),
                     1 => 8),
                 'tmp_name' => array(
                 '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(
                 'error' => array(
                     0 => 0,
                     0 => 0,
                     1 => 0)));
                     1 => 0)));
@@ -205,8 +213,8 @@ class Zend_File_Transfer_Adapter_HttpTest extends PHPUnit_Framework_TestCase
         $adapter->setOptions(array('ignoreNoFile' => true));
         $adapter->setOptions(array('ignoreNoFile' => true));
         $this->assertTrue($adapter->receive('exe'));
         $this->assertTrue($adapter->receive('exe'));
         $this->assertEquals(
         $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));
             $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