Added FileReader class
authorSebastian Öttl <sebastian.oettl@wcfsolutions.com>
Mon, 22 Jul 2013 14:18:52 +0000 (16:18 +0200)
committerSebastian Öttl <sebastian.oettl@wcfsolutions.com>
Mon, 22 Jul 2013 14:18:52 +0000 (16:18 +0200)
wcfsetup/install/files/lib/page/AttachmentPage.class.php
wcfsetup/install/files/lib/util/FileReader.class.php [new file with mode: 0644]

index 985b42793b54d738c1961154820f24cfeb788eb1..a2dc17331427986fccdee197da193a4928abb63e 100644 (file)
@@ -4,9 +4,8 @@ use wcf\data\attachment\Attachment;
 use wcf\data\attachment\AttachmentEditor;
 use wcf\system\exception\IllegalLinkException;
 use wcf\system\exception\PermissionDeniedException;
-use wcf\system\io\File;
-use wcf\system\Regex;
 use wcf\system\WCF;
+use wcf\util\FileReader;
 
 /**
  * Shows an attachment.
@@ -48,11 +47,17 @@ class AttachmentPage extends AbstractPage {
         */
        public $thumbnail = 0;
        
+       /**
+        * file reader object
+        * @var wcf\util\FileReader
+        */
+       public $fileReader = null;
+       
        /**
         * list of mime types which belong to files that are displayed inline
         * @var array<string>
         */
-       public static $inlineMimeTypes = array('image/gif', 'image/jpeg', 'image/png', 'application/pdf', 'image/pjpeg');
+       public static $inlineMimeTypes = array('image/gif', 'image/jpeg', 'image/png', 'image/x-png', 'application/pdf', 'image/pjpeg');
        
        /**
         * @see wcf\page\IPage::readParameters()
@@ -94,19 +99,10 @@ class AttachmentPage extends AbstractPage {
        }
        
        /**
-        * @see wcf\page\IPage::show()
+        * @see wcf\page\IPage::readData()
         */
-       public function show() {
-               parent::show();
-               
-               // update download count
-               if (!$this->tiny && !$this->thumbnail) {
-                       $editor = new AttachmentEditor($this->attachment);
-                       $editor->update(array(
-                               'downloads' => $this->attachment->downloads + 1,
-                               'lastDownloadTime' => TIME_NOW
-                       ));
-               }
+       public function readData() {
+               parent::readData();
                
                // get file data
                if ($this->tiny) {
@@ -125,81 +121,41 @@ class AttachmentPage extends AbstractPage {
                        $location = $this->attachment->getLocation();
                }
                
-               // range support
-               $startByte = 0;
-               $endByte = $filesize - 1;
-               if (!$this->tiny && !$this->thumbnail) {
-                       if (!empty($_SERVER['HTTP_RANGE'])) {
-                               $regex = new Regex('^bytes=(-?\d+)(?:-(\d+))?$');
-                               if ($regex->match($_SERVER['HTTP_RANGE'])) {
-                                       $matches = $regex->getMatches();
-                                       $first = intval($matches[1]);
-                                       $last = (isset($matches[2]) ? intval($matches[2]) : 0);
-                                       
-                                       if ($first < 0) {
-                                               // negative value; subtract from filesize
-                                               $startByte = $filesize + $first;
-                                       }
-                                       else {
-                                               $startByte = $first;
-                                               if ($last > 0) {
-                                                       $endByte = $last;
-                                               }
-                                       }
-                                       
-                                       // validate given range
-                                       if ($startByte < 0 || $startByte >= $filesize || $endByte >= $filesize) {
-                                               // invalid range given
-                                               @header('HTTP/1.1 416 Requested Range Not Satisfiable');
-                                               @header('Accept-Ranges: bytes');
-                                               @header('Content-Range: bytes */'.$filesize);
-                                               exit;
-                                       }
-                               }
-                       }
-               }
-               
-               // send headers
-               // file type
-               if ($mimeType == 'image/x-png') $mimeType = 'image/png';
-               @header('Content-Type: '.$mimeType);
-               
-               // file name
-               @header('Content-disposition: '.(!in_array($mimeType, self::$inlineMimeTypes) ? 'attachment; ' : 'inline; ').'filename="'.$this->attachment->filename.'"');
+               // init file reader
+               $this->fileReader = new FileReader($location, array(
+                       'filename' => $this->attachment->filename,
+                       'mimeType' => $mimeType,
+                       'filesize' => $filesize,
+                       'showInline' => (in_array($mimeType, self::$inlineMimeTypes)),
+                       'enableRangeSupport' => (!$this->tiny && !$this->thumbnail),
+                       'lastModificationTime' => $this->attachment->uploadTime,
+                       'expirationDate' => TIME_NOW + 31536000,
+                       'maxAge' => 31536000
+               ));
                
-               // range
-               if ($startByte > 0 || $endByte < $filesize - 1) {
-                       @header('HTTP/1.1 206 Partial Content');
-                       @header('Content-Range: bytes '.$startByte.'-'.$endByte.'/'.$filesize);
+               // add etag for non-thumbnail
+               if (!$this->thumbnail && !$this->tiny) {
+                       $this->fileReader->addHeader('ETag', '"'.$this->attachmentID."'");
                }
+       }
+       
+       /**
+        * @see wcf\page\IPage::show()
+        */
+       public function show() {
+               parent::show();
+               
                if (!$this->tiny && !$this->thumbnail) {
-                       @header('ETag: "'.$this->attachmentID.'"');
-                       @header('Accept-Ranges: bytes');
+                       // update download count
+                       $editor = new AttachmentEditor($this->attachment);
+                       $editor->update(array(
+                               'downloads' => $this->attachment->downloads + 1,
+                               'lastDownloadTime' => TIME_NOW
+                       ));
                }
                
-               // send file size
-               @header('Content-Length: '.($endByte + 1 - $startByte));
-               
-               // cache headers
-               @header('Cache-control: max-age=31536000, private');
-               @header('Expires: '.gmdate('D, d M Y H:i:s', TIME_NOW + 31536000).' GMT');
-               @header('Last-Modified: '.gmdate('D, d M Y H:i:s', $this->attachment->uploadTime).' GMT');
-               
-               // show attachment
-               if ($startByte > 0 || $endByte < $filesize - 1) {
-                       $file = new File($location, 'rb');
-                       if ($startByte > 0) $file->seek($startByte);
-                       while ($startByte <= $endByte) {
-                               $remainingBytes = $endByte - $startByte;
-                               $readBytes = ($remainingBytes > 1048576) ? 1048576 : $remainingBytes + 1;
-                               echo $file->read($readBytes);
-                               $startByte += $readBytes;
-                       }
-                       $file->close();
-               }
-               else {
-                       readfile($location);
-               }
+               // send file to client
+               $this->fileReader->send();
                exit;
        }
 }
diff --git a/wcfsetup/install/files/lib/util/FileReader.class.php b/wcfsetup/install/files/lib/util/FileReader.class.php
new file mode 100644 (file)
index 0000000..7bc1f66
--- /dev/null
@@ -0,0 +1,227 @@
+<?php
+namespace wcf\util;
+use wcf\system\io\File;
+use wcf\system\Regex;
+
+/**
+ * Provides functions to send files to the client via PHP.
+ * 
+ * @author     Sebastian Oettl, Marcel Werk
+ * @copyright  2001-2013 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage util
+ * @category   Community Framework
+ */
+final class FileReader {
+       /**
+        * http options
+        * @var array
+        */
+       private $options = array(
+               'filename' => '',
+               'mimeType' => 'application/octet-stream',
+               'filesize' => 0,
+               'showInline' => false,
+               'enableRangeSupport' => true,
+               'lastModificationTime' => 0,
+               'expirationDate' => 0,
+               'maxAge' => 0
+       );
+       
+       /**
+        * list of header items
+        * @var array
+        */
+       private $headers = array();
+       
+       /**
+        * start byte
+        * @var integer
+        */
+       private $startByte = 0;
+       
+       /**
+        * end byte
+        * @var integer
+        */
+       private $endByte = 0;
+       
+       /**
+        * True if http range is invalid.
+        * @var boolean
+        */
+       private $invalidRange = false;
+       
+       /**
+        * Creates a new instance of the HTTPFileReader class.
+        * 
+        * @param       string          $location
+        * @param       array           $options
+        */
+       public function __construct($location, array $options) {
+               $this->location = $location;
+               
+               // check location
+               if (empty($this->location) || !file_exists($this->location)) {
+                       throw new SystemException('Location of file is not set or invalid');
+               }
+               
+               // set options
+               $this->setOptions($options);
+       }
+       
+       /**
+        * Sends the file to the client.
+        */
+       public function send() {
+               // set filename if necessary
+               if (empty($this->options['filename'])) {
+                       $this->options['filename'] = basename($this->location);
+               }
+               
+               // detect filesize if necessary
+               if (empty($this->options['filesize'])) {
+                       $this->options['filesize'] = @filesize($this->location);
+               }
+                       
+               // handle range
+               $this->handleRange();
+               
+               // handle headers
+               $this->handleHeaders();
+               
+               // show headers
+               foreach ($this->headers as $name => $value) {
+                       if (empty($name)) {
+                               @header($value);
+                       }
+                       else {
+                               @header($name.': '.$value);
+                       }
+               }
+               
+               // show file
+               if (!$this->invalidRange) {
+                       if ($this->startByte > 0 || $this->endByte < $this->options['filesize'] - 1) {
+                               $file = new File($this->location, 'rb');
+                               if ($this->startByte > 0) $file->seek($this->startByte);
+                               while ($this->startByte <= $this->endByte) {
+                                       $remainingBytes = $this->endByte - $this->startByte;
+                                       $readBytes = ($remainingBytes > 1048576 ? 1048576 : $remainingBytes + 1);
+                                       echo $file->read($readBytes);
+                                       $this->startByte += $readBytes;
+                               }
+                               $file->close();
+                       }
+                       else {
+                               readfile($this->location);
+                       }
+               }
+       }
+       
+       /**
+        * Handles the given range options.
+        */
+       private function handleRange() {
+               $this->startByte = 0;
+               $this->endByte = $this->options['filesize'] - 1;
+               if ($this->options['enableRangeSupport']) {
+                       if (!empty($_SERVER['HTTP_RANGE'])) {
+                               $regex = new Regex('^bytes=(-?\d+)(?:-(\d+))?$');
+                               if ($regex->match($_SERVER['HTTP_RANGE'])) {
+                                       $matches = $regex->getMatches();
+                                       $first = intval($matches[1]);
+                                       $last = (isset($matches[2]) ? intval($matches[2]) : 0);
+                                       
+                                       if ($first < 0) {
+                                               // negative value; subtract from filesize
+                                               $this->startByte = $this->options['filesize'] + $first;
+                                       }
+                                       else {
+                                               $this->startByte = $first;
+                                               if ($last > 0) {
+                                                       $this->endByte = $last;
+                                               }
+                                       }
+                               }
+                       }
+               }
+       }
+       
+       /**
+        * Handles the given header items.
+        */
+       private function handleHeaders() {
+               if ($this->startByte < 0 || $this->startByte >= $this->options['filesize'] || $this->endByte >= $this->options['filesize']) {
+                       // invalid range given
+                       $this->addHeader('', 'HTTP/1.1 416 Requested Range Not Satisfiable');
+                       $this->addHeader('Accept-Ranges', 'bytes');
+                       $this->addHeader('Content-Range', 'bytes */'.$this->options['filesize']);
+                       $this->invalidRange = true;
+               }
+               else {
+                       // file type
+                       $this->addHeader('Content-Type', $this->options['mimeType']);
+                       
+                       // file name
+                       $this->addHeader('Content-disposition', ($this->options['showInline'] ? 'inline' : 'attachment').'; filename="'.$this->options['filename'].'"');
+                       
+                       // range
+                       if ($this->startByte > 0 || $this->endByte < $this->options['filesize'] - 1) {
+                               $this->addHeader('', 'HTTP/1.1 206 Partial Content');
+                               $this->addHeader('Content-Range', 'bytes '.$this->startByte.'-'.$this->endByte.'/'.$this->options['filesize']);
+                       }
+                       if ($this->options['enableRangeSupport']) {
+                               $this->addHeader('Accept-Ranges', 'bytes');
+                       }
+                       
+                       // send file size
+                       $this->addHeader('Content-Length', ($this->endByte + 1 - $this->startByte));
+                       
+                       // cache headers
+                       if ($this->options['maxAge']) {
+                               $this->addHeader('Cache-control', 'max-age='.$this->options['maxAge'].', private');
+                       }
+                       if ($this->options['expirationDate']) {
+                               $this->addHeader('Expires', gmdate('D, d M Y H:i:s', $this->options['expirationDate']).' GMT');
+                       }
+                       if ($this->options['lastModificationTime']) {
+                               $this->addHeader('Last-Modified', gmdate('D, d M Y H:i:s', $this->options['lastModificationTime']).' GMT');
+                       }
+               }
+       }
+       
+       /**
+        * Sets the options for the http response.
+        * 
+        * @param       array           $options
+        */
+       public function setOptions(array $options) {
+               if (isset($options['mimeType']) && $options['mimeType'] == 'image/x-png') {
+                       $options['mimeType'] = 'image/png';
+               }
+               
+               $this->options = array_merge($this->options, $options);
+       }
+       
+       /**
+        * Adds the header with the given name and value.
+        * 
+        * @param       string          $name
+        * @param       string          $value
+        */     
+       public function addHeader($name, $value) {
+               $this->headers[$name] = $value;
+       }
+       
+       /**
+        * Removes the header with the given name.
+        * 
+        * @param       string          $name
+        */
+       public function removeHeader($name) {
+               unset($this->headers[$name]);
+               return;
+       }
+}