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.
*/
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()
}
/**
- * @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) {
$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;
}
}
--- /dev/null
+<?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;
+ }
+}