From c80d60663e3bd1f4f47d88fc6574f0f8fa0d56ce Mon Sep 17 00:00:00 2001 From: =?utf8?q?Sebastian=20=C3=96ttl?= Date: Mon, 22 Jul 2013 16:18:52 +0200 Subject: [PATCH] Added FileReader class --- .../files/lib/page/AttachmentPage.class.php | 126 ++++------ .../files/lib/util/FileReader.class.php | 227 ++++++++++++++++++ 2 files changed, 268 insertions(+), 85 deletions(-) create mode 100644 wcfsetup/install/files/lib/util/FileReader.class.php diff --git a/wcfsetup/install/files/lib/page/AttachmentPage.class.php b/wcfsetup/install/files/lib/page/AttachmentPage.class.php index 985b42793b..a2dc173314 100644 --- a/wcfsetup/install/files/lib/page/AttachmentPage.class.php +++ b/wcfsetup/install/files/lib/page/AttachmentPage.class.php @@ -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 */ - 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 index 0000000000..7bc1f66b84 --- /dev/null +++ b/wcfsetup/install/files/lib/util/FileReader.class.php @@ -0,0 +1,227 @@ + + * @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; + } +} -- 2.20.1