summaryrefslogtreecommitdiff
path: root/includes/pear/HTTP/Download.php
diff options
context:
space:
mode:
Diffstat (limited to 'includes/pear/HTTP/Download.php')
-rw-r--r--includes/pear/HTTP/Download.php1243
1 files changed, 1243 insertions, 0 deletions
diff --git a/includes/pear/HTTP/Download.php b/includes/pear/HTTP/Download.php
new file mode 100644
index 0000000..8cfc348
--- /dev/null
+++ b/includes/pear/HTTP/Download.php
@@ -0,0 +1,1243 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * HTTP::Download
+ *
+ * PHP versions 4 and 5
+ *
+ * @category HTTP
+ * @package HTTP_Download
+ * @author Michael Wallner <mike@php.net>
+ * @copyright 2003-2005 Michael Wallner
+ * @license BSD, revised
+ * @version CVS: $Id: Download.php 304423 2010-10-15 13:36:46Z clockwerx $
+ * @link http://pear.php.net/package/HTTP_Download
+ */
+
+// {{{ includes
+/**
+ * Requires PEAR
+ */
+require_once 'PEAR.php';
+
+/**
+ * Requires HTTP_Header
+ */
+require_once 'HTTP/Header.php';
+// }}}
+
+// {{{ constants
+/**#@+ Use with HTTP_Download::setContentDisposition() **/
+/**
+ * Send data as attachment
+ */
+define('HTTP_DOWNLOAD_ATTACHMENT', 'attachment');
+/**
+ * Send data inline
+ */
+define('HTTP_DOWNLOAD_INLINE', 'inline');
+/**#@-**/
+
+/**#@+ Use with HTTP_Download::sendArchive() **/
+/**
+ * Send as uncompressed tar archive
+ */
+define('HTTP_DOWNLOAD_TAR', 'TAR');
+/**
+ * Send as gzipped tar archive
+ */
+define('HTTP_DOWNLOAD_TGZ', 'TGZ');
+/**
+ * Send as bzip2 compressed tar archive
+ */
+define('HTTP_DOWNLOAD_BZ2', 'BZ2');
+/**
+ * Send as zip archive
+ */
+define('HTTP_DOWNLOAD_ZIP', 'ZIP');
+/**#@-**/
+
+/**#@+
+ * Error constants
+ */
+define('HTTP_DOWNLOAD_E_HEADERS_SENT', -1);
+define('HTTP_DOWNLOAD_E_NO_EXT_ZLIB', -2);
+define('HTTP_DOWNLOAD_E_NO_EXT_MMAGIC', -3);
+define('HTTP_DOWNLOAD_E_INVALID_FILE', -4);
+define('HTTP_DOWNLOAD_E_INVALID_PARAM', -5);
+define('HTTP_DOWNLOAD_E_INVALID_RESOURCE', -6);
+define('HTTP_DOWNLOAD_E_INVALID_REQUEST', -7);
+define('HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE', -8);
+define('HTTP_DOWNLOAD_E_INVALID_ARCHIVE_TYPE', -9);
+/**#@-**/
+// }}}
+
+/**
+ * Send HTTP Downloads/Responses.
+ *
+ * With this package you can handle (hidden) downloads.
+ * It supports partial downloads, resuming and sending
+ * raw data ie. from database BLOBs.
+ *
+ * <i>ATTENTION:</i>
+ * You shouldn't use this package together with ob_gzhandler or
+ * zlib.output_compression enabled in your php.ini, especially
+ * if you want to send already gzipped data!
+ *
+ * @access public
+ * @version $Revision: 304423 $
+ */
+class HTTP_Download
+{
+ // {{{ protected member variables
+ /**
+ * Path to file for download
+ *
+ * @see HTTP_Download::setFile()
+ * @access protected
+ * @var string
+ */
+ var $file = '';
+
+ /**
+ * Data for download
+ *
+ * @see HTTP_Download::setData()
+ * @access protected
+ * @var string
+ */
+ var $data = null;
+
+ /**
+ * Resource handle for download
+ *
+ * @see HTTP_Download::setResource()
+ * @access protected
+ * @var int
+ */
+ var $handle = null;
+
+ /**
+ * Whether to gzip the download
+ *
+ * @access protected
+ * @var bool
+ */
+ var $gzip = false;
+
+ /**
+ * Whether to allow caching of the download on the clients side
+ *
+ * @access protected
+ * @var bool
+ */
+ var $cache = true;
+
+ /**
+ * Size of download
+ *
+ * @access protected
+ * @var int
+ */
+ var $size = 0;
+
+ /**
+ * Last modified
+ *
+ * @access protected
+ * @var int
+ */
+ var $lastModified = 0;
+
+ /**
+ * HTTP headers
+ *
+ * @access protected
+ * @var array
+ */
+ var $headers = array(
+ 'Content-Type' => 'application/x-octetstream',
+ 'Pragma' => 'cache',
+ 'Cache-Control' => 'public, must-revalidate, max-age=0',
+ 'Accept-Ranges' => 'bytes',
+ 'X-Sent-By' => 'PEAR::HTTP::Download'
+ );
+
+ /**
+ * HTTP_Header
+ *
+ * @access protected
+ * @var object
+ */
+ var $HTTP = null;
+
+ /**
+ * ETag
+ *
+ * @access protected
+ * @var string
+ */
+ var $etag = '';
+
+ /**
+ * Buffer Size
+ *
+ * @access protected
+ * @var int
+ */
+ var $bufferSize = 2097152;
+
+ /**
+ * Throttle Delay
+ *
+ * @access protected
+ * @var float
+ */
+ var $throttleDelay = 0;
+
+ /**
+ * Sent Bytes
+ *
+ * @access public
+ * @var int
+ */
+ var $sentBytes = 0;
+
+ /**
+ * Startup error
+ *
+ * @var PEAR_Error
+ * @access protected
+ */
+ var $_error = null;
+ // }}}
+
+ // {{{ constructor
+ /**
+ * Constructor
+ *
+ * Set supplied parameters.
+ *
+ * @access public
+ * @param array $params associative array of parameters
+ * <strong>one of:</strong>
+ * <ul>
+ * <li>'file' => path to file for download</li>
+ * <li>'data' => raw data for download</li>
+ * <li>'resource' => resource handle for download</li>
+ * </ul>
+ * <strong>and any of:</strong>
+ * <ul>
+ * <li>'cache' => whether to allow cs caching</li>
+ * <li>'gzip' => whether to gzip the download</li>
+ * <li>'lastmodified' => unix timestamp</li>
+ * <li>'contenttype' => content type of download</li>
+ * <li>'contentdisposition' => content disposition</li>
+ * <li>'buffersize' => amount of bytes to buffer</li>
+ * <li>'throttledelay' => amount of secs to sleep</li>
+ * <li>'cachecontrol' => cache privacy and validity</li>
+ * </ul>
+ *
+ * 'Content-Disposition' is not HTTP compliant, but most browsers
+ * follow this header, so it was borrowed from MIME standard.
+ *
+ * It looks like this:
+ * "Content-Disposition: attachment; filename=example.tgz".
+ *
+ * @see HTTP_Download::setContentDisposition()
+ */
+ function HTTP_Download($params = array())
+ {
+ $this->HTTP = &new HTTP_Header;
+ $this->_error = $this->setParams($params);
+ }
+ // }}}
+
+ // {{{ public methods
+ /**
+ * Set parameters
+ *
+ * Set supplied parameters through its accessor methods.
+ *
+ * @access public
+ * @return mixed Returns true on success or PEAR_Error on failure.
+ * @param array $params associative array of parameters
+ *
+ * @see HTTP_Download::HTTP_Download()
+ */
+ function setParams($params)
+ {
+ $error = $this->_getError();
+ if ($error !== null) {
+ return $error;
+ }
+ foreach((array) $params as $param => $value){
+ $method = 'set'. $param;
+
+ if (!method_exists($this, $method)) {
+ return PEAR::raiseError(
+ "Method '$method' doesn't exist.",
+ HTTP_DOWNLOAD_E_INVALID_PARAM
+ );
+ }
+
+ $e = call_user_func_array(array(&$this, $method), (array) $value);
+
+ if (PEAR::isError($e)) {
+ return $e;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Set path to file for download
+ *
+ * The Last-Modified header will be set to files filemtime(), actually.
+ * Returns PEAR_Error (HTTP_DOWNLOAD_E_INVALID_FILE) if file doesn't exist.
+ * Sends HTTP 404 or 403 status if $send_error is set to true.
+ *
+ * @access public
+ * @return mixed Returns true on success or PEAR_Error on failure.
+ * @param string $file path to file for download
+ * @param bool $send_error whether to send HTTP/404 or 403 if
+ * the file wasn't found or is not readable
+ */
+ function setFile($file, $send_error = true)
+ {
+ $error = $this->_getError();
+ if ($error !== null) {
+ return $error;
+ }
+ $file = realpath($file);
+ if (!is_file($file)) {
+ if ($send_error) {
+ $this->HTTP->sendStatusCode(404);
+ }
+ return PEAR::raiseError(
+ "File '$file' not found.",
+ HTTP_DOWNLOAD_E_INVALID_FILE
+ );
+ }
+ if (!is_readable($file)) {
+ if ($send_error) {
+ $this->HTTP->sendStatusCode(403);
+ }
+ return PEAR::raiseError(
+ "Cannot read file '$file'.",
+ HTTP_DOWNLOAD_E_INVALID_FILE
+ );
+ }
+ $this->setLastModified(filemtime($file));
+ $this->file = $file;
+ $this->size = filesize($file);
+ return true;
+ }
+
+ /**
+ * Set data for download
+ *
+ * Set $data to null if you want to unset this.
+ *
+ * @access public
+ * @return void
+ * @param $data raw data to send
+ */
+ function setData($data = null)
+ {
+ $this->data = $data;
+ $this->size = strlen($data);
+ }
+
+ /**
+ * Set resource for download
+ *
+ * The resource handle supplied will be closed after sending the download.
+ * Returns a PEAR_Error (HTTP_DOWNLOAD_E_INVALID_RESOURCE) if $handle
+ * is no valid resource. Set $handle to null if you want to unset this.
+ *
+ * @access public
+ * @return mixed Returns true on success or PEAR_Error on failure.
+ * @param int $handle resource handle
+ */
+ function setResource($handle = null)
+ {
+ $error = $this->_getError();
+ if ($error !== null) {
+ return $error;
+ }
+ if (!isset($handle)) {
+ $this->handle = null;
+ $this->size = 0;
+ return true;
+ }
+
+ if (is_resource($handle)) {
+ $this->handle = $handle;
+ $filestats = fstat($handle);
+ $this->size = isset($filestats['size']) ? $filestats['size']
+ : -1;
+ return true;
+ }
+
+ return PEAR::raiseError(
+ "Handle '$handle' is no valid resource.",
+ HTTP_DOWNLOAD_E_INVALID_RESOURCE
+ );
+ }
+
+ /**
+ * Whether to gzip the download
+ *
+ * Returns a PEAR_Error (HTTP_DOWNLOAD_E_NO_EXT_ZLIB)
+ * if ext/zlib is not available/loadable.
+ *
+ * @access public
+ * @return mixed Returns true on success or PEAR_Error on failure.
+ * @param bool $gzip whether to gzip the download
+ */
+ function setGzip($gzip = false)
+ {
+ $error = $this->_getError();
+ if ($error !== null) {
+ return $error;
+ }
+ if ($gzip && !PEAR::loadExtension('zlib')){
+ return PEAR::raiseError(
+ 'GZIP compression (ext/zlib) not available.',
+ HTTP_DOWNLOAD_E_NO_EXT_ZLIB
+ );
+ }
+ $this->gzip = (bool) $gzip;
+ return true;
+ }
+
+ /**
+ * Whether to allow caching
+ *
+ * If set to true (default) we'll send some headers that are commonly
+ * used for caching purposes like ETag, Cache-Control and Last-Modified.
+ *
+ * If caching is disabled, we'll send the download no matter if it
+ * would actually be cached at the client side.
+ *
+ * @access public
+ * @return void
+ * @param bool $cache whether to allow caching
+ */
+ function setCache($cache = true)
+ {
+ $this->cache = (bool) $cache;
+ }
+
+ /**
+ * Whether to allow proxies to cache
+ *
+ * If set to 'private' proxies shouldn't cache the response.
+ * This setting defaults to 'public' and affects only cached responses.
+ *
+ * @access public
+ * @return bool
+ * @param string $cache private or public
+ * @param int $maxage maximum age of the client cache entry
+ */
+ function setCacheControl($cache = 'public', $maxage = 0)
+ {
+ switch ($cache = strToLower($cache))
+ {
+ case 'private':
+ case 'public':
+ $this->headers['Cache-Control'] =
+ $cache .', must-revalidate, max-age='. abs($maxage);
+ return true;
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * Set ETag
+ *
+ * Sets a user-defined ETag for cache-validation. The ETag is usually
+ * generated by HTTP_Download through its payload information.
+ *
+ * @access public
+ * @return void
+ * @param string $etag Entity tag used for strong cache validation.
+ */
+ function setETag($etag = null)
+ {
+ $this->etag = (string) $etag;
+ }
+
+ /**
+ * Set Size of Buffer
+ *
+ * The amount of bytes specified as buffer size is the maximum amount
+ * of data read at once from resources or files. The default size is 2M
+ * (2097152 bytes). Be aware that if you enable gzip compression and
+ * you set a very low buffer size that the actual file size may grow
+ * due to added gzip headers for each sent chunk of the specified size.
+ *
+ * Returns PEAR_Error (HTTP_DOWNLOAD_E_INVALID_PARAM) if $size is not
+ * greater than 0 bytes.
+ *
+ * @access public
+ * @return mixed Returns true on success or PEAR_Error on failure.
+ * @param int $bytes Amount of bytes to use as buffer.
+ */
+ function setBufferSize($bytes = 2097152)
+ {
+ $error = $this->_getError();
+ if ($error !== null) {
+ return $error;
+ }
+ if (0 >= $bytes) {
+ return PEAR::raiseError(
+ 'Buffer size must be greater than 0 bytes ('. $bytes .' given)',
+ HTTP_DOWNLOAD_E_INVALID_PARAM);
+ }
+ $this->bufferSize = abs($bytes);
+ return true;
+ }
+
+ /**
+ * Set Throttle Delay
+ *
+ * Set the amount of seconds to sleep after each chunck that has been
+ * sent. One can implement some sort of throttle through adjusting the
+ * buffer size and the throttle delay. With the following settings
+ * HTTP_Download will sleep a second after each 25 K of data sent.
+ *
+ * <code>
+ * Array(
+ * 'throttledelay' => 1,
+ * 'buffersize' => 1024 * 25,
+ * )
+ * </code>
+ *
+ * Just be aware that if gzipp'ing is enabled, decreasing the chunk size
+ * too much leads to proportionally increased network traffic due to added
+ * gzip header and bottom bytes around each chunk.
+ *
+ * @access public
+ * @return void
+ * @param float $seconds Amount of seconds to sleep after each
+ * chunk that has been sent.
+ */
+ function setThrottleDelay($seconds = 0)
+ {
+ $this->throttleDelay = abs($seconds) * 1000;
+ }
+
+ /**
+ * Set "Last-Modified"
+ *
+ * This is usually determined by filemtime() in HTTP_Download::setFile()
+ * If you set raw data for download with HTTP_Download::setData() and you
+ * want do send an appropiate "Last-Modified" header, you should call this
+ * method.
+ *
+ * @access public
+ * @return void
+ * @param int unix timestamp
+ */
+ function setLastModified($last_modified)
+ {
+ $this->lastModified = $this->headers['Last-Modified'] = (int) $last_modified;
+ }
+
+ /**
+ * Set Content-Disposition header
+ *
+ * @see HTTP_Download::HTTP_Download
+ *
+ * @access public
+ * @return void
+ * @param string $disposition whether to send the download
+ * inline or as attachment
+ * @param string $file_name the filename to display in
+ * the browser's download window
+ *
+ * <b>Example:</b>
+ * <code>
+ * $HTTP_Download->setContentDisposition(
+ * HTTP_DOWNLOAD_ATTACHMENT,
+ * 'download.tgz'
+ * );
+ * </code>
+ */
+ function setContentDisposition( $disposition = HTTP_DOWNLOAD_ATTACHMENT,
+ $file_name = null)
+ {
+ $cd = $disposition;
+ if (isset($file_name)) {
+ $cd .= '; filename="' . $file_name . '"';
+ } elseif ($this->file) {
+ $cd .= '; filename="' . basename($this->file) . '"';
+ }
+ $this->headers['Content-Disposition'] = $cd;
+ }
+
+ /**
+ * Set content type of the download
+ *
+ * Default content type of the download will be 'application/x-octetstream'.
+ * Returns PEAR_Error (HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE) if
+ * $content_type doesn't seem to be valid.
+ *
+ * @access public
+ * @return mixed Returns true on success or PEAR_Error on failure.
+ * @param string $content_type content type of file for download
+ */
+ function setContentType($content_type = 'application/x-octetstream')
+ {
+ $error = $this->_getError();
+ if ($error !== null) {
+ return $error;
+ }
+ if (!preg_match('/^[a-z]+\w*\/[a-z]+[\w.;= -]*$/', $content_type)) {
+ return PEAR::raiseError(
+ "Invalid content type '$content_type' supplied.",
+ HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE
+ );
+ }
+ $this->headers['Content-Type'] = $content_type;
+ return true;
+ }
+
+ /**
+ * Guess content type of file
+ *
+ * First we try to use PEAR::MIME_Type, if installed, to detect the content
+ * type, else we check if ext/mime_magic is loaded and properly configured.
+ *
+ * Returns PEAR_Error if:
+ * o if PEAR::MIME_Type failed to detect a proper content type
+ * (HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE)
+ * o ext/magic.mime is not installed, or not properly configured
+ * (HTTP_DOWNLOAD_E_NO_EXT_MMAGIC)
+ * o mime_content_type() couldn't guess content type or returned
+ * a content type considered to be bogus by setContentType()
+ * (HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE)
+ *
+ * @access public
+ * @return mixed Returns true on success or PEAR_Error on failure.
+ */
+ function guessContentType()
+ {
+ $error = $this->_getError();
+ if ($error !== null) {
+ return $error;
+ }
+ if (class_exists('MIME_Type') || @include_once 'MIME/Type.php') {
+ if (PEAR::isError($mime_type = MIME_Type::autoDetect($this->file))) {
+ return PEAR::raiseError($mime_type->getMessage(),
+ HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE);
+ }
+ return $this->setContentType($mime_type);
+ }
+ if (!function_exists('mime_content_type')) {
+ return PEAR::raiseError(
+ 'This feature requires ext/mime_magic!',
+ HTTP_DOWNLOAD_E_NO_EXT_MMAGIC
+ );
+ }
+ if (!is_file(ini_get('mime_magic.magicfile'))) {
+ return PEAR::raiseError(
+ 'ext/mime_magic is loaded but not properly configured!',
+ HTTP_DOWNLOAD_E_NO_EXT_MMAGIC
+ );
+ }
+ if (!$content_type = @mime_content_type($this->file)) {
+ return PEAR::raiseError(
+ 'Couldn\'t guess content type with mime_content_type().',
+ HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE
+ );
+ }
+ return $this->setContentType($content_type);
+ }
+
+ /**
+ * Send
+ *
+ * Returns PEAR_Error if:
+ * o HTTP headers were already sent (HTTP_DOWNLOAD_E_HEADERS_SENT)
+ * o HTTP Range was invalid (HTTP_DOWNLOAD_E_INVALID_REQUEST)
+ *
+ * @access public
+ * @return mixed Returns true on success or PEAR_Error on failure.
+ * @param bool $autoSetContentDisposition Whether to set the
+ * Content-Disposition header if it isn't already.
+ */
+ function send($autoSetContentDisposition = true)
+ {
+ $error = $this->_getError();
+ if ($error !== null) {
+ return $error;
+ }
+ if (headers_sent()) {
+ return PEAR::raiseError(
+ 'Headers already sent.',
+ HTTP_DOWNLOAD_E_HEADERS_SENT
+ );
+ }
+
+ if (!ini_get('safe_mode')) {
+ @set_time_limit(0);
+ }
+
+ if ($autoSetContentDisposition &&
+ !isset($this->headers['Content-Disposition'])) {
+ $this->setContentDisposition();
+ }
+
+ if ($this->cache) {
+ $this->headers['ETag'] = $this->generateETag();
+ if ($this->isCached()) {
+ $this->HTTP->sendStatusCode(304);
+ $this->sendHeaders();
+ return true;
+ }
+ } else {
+ unset($this->headers['Last-Modified']);
+ }
+
+ if (ob_get_level()) {
+ while (@ob_end_clean());
+ }
+
+ if ($this->gzip) {
+ @ob_start('ob_gzhandler');
+ } else {
+ ob_start();
+ }
+
+ $this->sentBytes = 0;
+
+ // Known content length?
+ $end = ($this->size >= 0) ? max($this->size - 1, 0) : '*';
+
+ if ($end != '*' && $this->isRangeRequest()) {
+ $chunks = $this->getChunks();
+ if (empty($chunks)) {
+ $this->HTTP->sendStatusCode(200);
+ $chunks = array(array(0, $end));
+
+ } elseif (PEAR::isError($chunks)) {
+ ob_end_clean();
+ $this->HTTP->sendStatusCode(416);
+ return $chunks;
+
+ } else {
+ $this->HTTP->sendStatusCode(206);
+ }
+ } else {
+ $this->HTTP->sendStatusCode(200);
+ $chunks = array(array(0, $end));
+ if (!$this->gzip && count(ob_list_handlers()) < 2 && $end != '*') {
+ $this->headers['Content-Length'] = $this->size;
+ }
+ }
+
+ $this->sendChunks($chunks);
+
+ ob_end_flush();
+ flush();
+ return true;
+ }
+
+ /**
+ * Static send
+ *
+ * @see HTTP_Download::HTTP_Download()
+ * @see HTTP_Download::send()
+ *
+ * @static
+ * @access public
+ * @return mixed Returns true on success or PEAR_Error on failure.
+ * @param array $params associative array of parameters
+ * @param bool $guess whether HTTP_Download::guessContentType()
+ * should be called
+ */
+ function staticSend($params, $guess = false)
+ {
+ $d = &new HTTP_Download();
+ $e = $d->setParams($params);
+ if (PEAR::isError($e)) {
+ return $e;
+ }
+ if ($guess) {
+ $e = $d->guessContentType();
+ if (PEAR::isError($e)) {
+ return $e;
+ }
+ }
+ return $d->send();
+ }
+
+ /**
+ * Send a bunch of files or directories as an archive
+ *
+ * Example:
+ * <code>
+ * require_once 'HTTP/Download.php';
+ * HTTP_Download::sendArchive(
+ * 'myArchive.tgz',
+ * '/var/ftp/pub/mike',
+ * HTTP_DOWNLOAD_TGZ,
+ * '',
+ * '/var/ftp/pub'
+ * );
+ * </code>
+ *
+ * @see Archive_Tar::createModify()
+ * @deprecated use HTTP_Download_Archive::send()
+ * @static
+ * @access public
+ * @return mixed Returns true on success or PEAR_Error on failure.
+ * @param string $name name the sent archive should have
+ * @param mixed $files files/directories
+ * @param string $type archive type
+ * @param string $add_path path that should be prepended to the files
+ * @param string $strip_path path that should be stripped from the files
+ */
+ function sendArchive( $name,
+ $files,
+ $type = HTTP_DOWNLOAD_TGZ,
+ $add_path = '',
+ $strip_path = '')
+ {
+ require_once 'HTTP/Download/Archive.php';
+ return HTTP_Download_Archive::send($name, $files, $type,
+ $add_path, $strip_path);
+ }
+ // }}}
+
+ // {{{ protected methods
+ /**
+ * Generate ETag
+ *
+ * @access protected
+ * @return string
+ */
+ function generateETag()
+ {
+ if (!$this->etag) {
+ if ($this->data) {
+ $md5 = md5($this->data);
+ } else {
+ $mtime = time();
+ $ino = 0;
+ $size = mt_rand();
+ extract(is_resource($this->handle) ? fstat($this->handle)
+ : stat($this->file));
+ $md5 = md5($mtime .'='. $ino .'='. $size);
+ }
+ $this->etag = '"' . $md5 . '-' . crc32($md5) . '"';
+ }
+ return $this->etag;
+ }
+
+ /**
+ * Send multiple chunks
+ *
+ * @access protected
+ * @return mixed Returns true on success or PEAR_Error on failure.
+ * @param array $chunks
+ */
+ function sendChunks($chunks)
+ {
+ if (count($chunks) == 1) {
+ return $this->sendChunk(current($chunks));
+ }
+
+ $bound = uniqid('HTTP_DOWNLOAD-', true);
+ $cType = $this->headers['Content-Type'];
+ $this->headers['Content-Type'] =
+ 'multipart/byteranges; boundary=' . $bound;
+ $this->sendHeaders();
+ foreach ($chunks as $chunk){
+ $this->sendChunk($chunk, $cType, $bound);
+ }
+ #echo "\r\n--$bound--\r\n";
+ return true;
+ }
+
+ /**
+ * Send chunk of data
+ *
+ * @access protected
+ * @return mixed Returns true on success or PEAR_Error on failure.
+ * @param array $chunk start and end offset of the chunk to send
+ * @param string $cType actual content type
+ * @param string $bound boundary for multipart/byteranges
+ */
+ function sendChunk($chunk, $cType = null, $bound = null)
+ {
+ list($offset, $lastbyte) = $chunk;
+ $length = ($lastbyte - $offset) + 1;
+
+ $range = $offset . '-' . $lastbyte . '/'
+ . (($this->size >= 0) ? $this->size : '*');
+
+ if (isset($cType, $bound)) {
+ echo "\r\n--$bound\r\n",
+ "Content-Type: $cType\r\n",
+ "Content-Range: bytes $range\r\n\r\n";
+ } else {
+ if ($lastbyte != '*' && $this->isRangeRequest()) {
+ $this->headers['Content-Length'] = $length;
+ $this->headers['Content-Range'] = 'bytes '. $range;
+ }
+ $this->sendHeaders();
+ }
+
+ if ($this->data) {
+ while (($length -= $this->bufferSize) > 0) {
+ $this->flush(substr($this->data, $offset, $this->bufferSize));
+ $this->throttleDelay and $this->sleep();
+ $offset += $this->bufferSize;
+ }
+ if ($length) {
+ $this->flush(substr($this->data, $offset, $this->bufferSize + $length));
+ }
+ } else {
+ if (!is_resource($this->handle)) {
+ $this->handle = fopen($this->file, 'rb');
+ }
+ fseek($this->handle, $offset);
+ if ($lastbyte == '*') {
+ while (!feof($this->handle)) {
+ $this->flush(fread($this->handle, $this->bufferSize));
+ $this->throttleDelay and $this->sleep();
+ }
+ } else {
+ while (($length -= $this->bufferSize) > 0) {
+ $this->flush(fread($this->handle, $this->bufferSize));
+ $this->throttleDelay and $this->sleep();
+ }
+ if ($length) {
+ $this->flush(fread($this->handle, $this->bufferSize + $length));
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Get chunks to send
+ *
+ * @access protected
+ * @return array Chunk list or PEAR_Error on invalid range request
+ * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
+ */
+ function getChunks()
+ {
+ $end = ($this->size >= 0) ? max($this->size - 1, 0) : '*';
+
+ // Trying to handle ranges on content with unknown length is too
+ // big of a mess (impossible to determine if a range is valid)
+ if ($end == '*') {
+ return array();
+ }
+
+ $ranges = $this->getRanges();
+ if (empty($ranges)) {
+ return array();
+ }
+
+ $parts = array();
+ $satisfiable = false;
+ foreach (explode(',', $ranges) as $chunk){
+ list($o, $e) = explode('-', trim($chunk));
+
+ // If the last-byte-pos value is present, it MUST be greater than
+ // or equal to the first-byte-pos in that byte-range-spec, or the
+ // byte- range-spec is syntactically invalid. The recipient of a
+ // byte-range- set that includes one or more syntactically invalid
+ // byte-range-spec values MUST ignore the header field that
+ // includes that byte-range- set.
+ if ($e !== '' && $o !== '' && $e < $o) {
+ return array();
+ }
+
+ // If the last-byte-pos value is absent, or if the value is
+ // greater than or equal to the current length of the entity-body,
+ // last-byte-pos is taken to be equal to one less than the current
+ // length of the entity- body in bytes.
+ if ($e === '' || $e > $end) {
+ $e = $end;
+ }
+
+ // A suffix-byte-range-spec is used to specify the suffix of the
+ // entity-body, of a length given by the suffix-length value. (That
+ // is, this form specifies the last N bytes of an entity-body.) If
+ // the entity is shorter than the specified suffix-length, the
+ // entire entity-body is used.
+ if ($o === '') {
+ // If a syntactically valid byte-range-set includes at least
+ // one suffix-byte-range-spec with a non-zero suffix-length,
+ // then the byte-range-set is satisfiable.
+ $satisfiable |= ($e != 0);
+
+ $o = max($this->size - $e, 0);
+ $e = $end;
+
+ } elseif ($o <= $end) {
+ // If a syntactically valid byte-range-set includes at least
+ // one byte- range-spec whose first-byte-pos is less than the
+ // current length of the entity-body, then the byte-range-set
+ // is satisfiable.
+ $satisfiable = true;
+ } else {
+ continue;
+ }
+
+ $parts[] = array($o, $e);
+ }
+
+ // If the byte-range-set is unsatisfiable, the server SHOULD return a
+ // response with a status of 416 (Requested range not satisfiable).
+ if (!$satisfiable) {
+ $error = PEAR::raiseError('Error processing range request',
+ HTTP_DOWNLOAD_E_INVALID_REQUEST);
+ return $error;
+ }
+ //$this->sortChunks($parts);
+ return $this->mergeChunks($parts);
+ }
+
+ /**
+ * Sorts the ranges to be in ascending order
+ *
+ * @param array &$chunks ranges to sort
+ *
+ * @return void
+ * @access protected
+ * @static
+ * @author Philippe Jausions <jausions@php.net>
+ */
+ function sortChunks(&$chunks)
+ {
+ $sortFunc = create_function('$a,$b',
+ 'if ($a[0] == $b[0]) {
+ if ($a[1] == $b[1]) {
+ return 0;
+ }
+ return (($a[1] != "*" && $a[1] < $b[1])
+ || $b[1] == "*") ? -1 : 1;
+ }
+
+ return ($a[0] < $b[0]) ? -1 : 1;');
+
+ usort($chunks, $sortFunc);
+ }
+
+ /**
+ * Merges consecutive chunks to avoid overlaps
+ *
+ * @param array $chunks Ranges to merge
+ *
+ * @return array merged ranges
+ * @access protected
+ * @static
+ * @author Philippe Jausions <jausions@php.net>
+ */
+ function mergeChunks($chunks)
+ {
+ do {
+ $count = count($chunks);
+ $merged = array(current($chunks));
+ $j = 0;
+ for ($i = 1; $i < count($chunks); ++$i) {
+ list($o, $e) = $chunks[$i];
+ if ($merged[$j][1] == '*') {
+ if ($merged[$j][0] <= $o) {
+ continue;
+ } elseif ($e == '*' || $merged[$j][0] <= $e) {
+ $merged[$j][0] = min($merged[$j][0], $o);
+ } else {
+ $merged[++$j] = $chunks[$i];
+ }
+ } elseif ($merged[$j][0] <= $o && $o <= $merged[$j][1]) {
+ $merged[$j][1] = ($e == '*') ? '*' : max($e, $merged[$j][1]);
+ } elseif ($merged[$j][0] <= $e && $e <= $merged[$j][1]) {
+ $merged[$j][0] = min($o, $merged[$j][0]);
+ } else {
+ $merged[++$j] = $chunks[$i];
+ }
+ }
+ if ($count == count($merged)) {
+ break;
+ }
+ $chunks = $merged;
+ } while (true);
+ return $merged;
+ }
+
+ /**
+ * Check if range is requested
+ *
+ * @access protected
+ * @return bool
+ */
+ function isRangeRequest()
+ {
+ if (!isset($_SERVER['HTTP_RANGE']) || !count($this->getRanges())) {
+ return false;
+ }
+ return $this->isValidRange();
+ }
+
+ /**
+ * Get range request
+ *
+ * @access protected
+ * @return array
+ */
+ function getRanges()
+ {
+ return preg_match('/^bytes=((\d+-|\d+-\d+|-\d+)(, ?(\d+-|\d+-\d+|-\d+))*)$/',
+ @$_SERVER['HTTP_RANGE'], $matches) ? $matches[1] : array();
+ }
+
+ /**
+ * Check if entity is cached
+ *
+ * @access protected
+ * @return bool
+ */
+ function isCached()
+ {
+ return (
+ (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) &&
+ $this->lastModified == strtotime(current($a = explode(
+ ';', $_SERVER['HTTP_IF_MODIFIED_SINCE'])))) ||
+ (isset($_SERVER['HTTP_IF_NONE_MATCH']) &&
+ $this->compareAsterisk('HTTP_IF_NONE_MATCH', $this->etag))
+ );
+ }
+
+ /**
+ * Check if entity hasn't changed
+ *
+ * @access protected
+ * @return bool
+ */
+ function isValidRange()
+ {
+ if (isset($_SERVER['HTTP_IF_MATCH']) &&
+ !$this->compareAsterisk('HTTP_IF_MATCH', $this->etag)) {
+ return false;
+ }
+ if (isset($_SERVER['HTTP_IF_RANGE']) &&
+ $_SERVER['HTTP_IF_RANGE'] !== $this->etag &&
+ strtotime($_SERVER['HTTP_IF_RANGE']) !== $this->lastModified) {
+ return false;
+ }
+ if (isset($_SERVER['HTTP_IF_UNMODIFIED_SINCE'])) {
+ $lm = current($a = explode(';', $_SERVER['HTTP_IF_UNMODIFIED_SINCE']));
+ if (strtotime($lm) !== $this->lastModified) {
+ return false;
+ }
+ }
+ if (isset($_SERVER['HTTP_UNLESS_MODIFIED_SINCE'])) {
+ $lm = current($a = explode(';', $_SERVER['HTTP_UNLESS_MODIFIED_SINCE']));
+ if (strtotime($lm) !== $this->lastModified) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Compare against an asterisk or check for equality
+ *
+ * @access protected
+ * @return bool
+ * @param string key for the $_SERVER array
+ * @param string string to compare
+ */
+ function compareAsterisk($svar, $compare)
+ {
+ foreach (array_map('trim', explode(',', $_SERVER[$svar])) as $request) {
+ if ($request === '*' || $request === $compare) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Send HTTP headers
+ *
+ * @access protected
+ * @return void
+ */
+ function sendHeaders()
+ {
+ foreach ($this->headers as $header => $value) {
+ $this->HTTP->setHeader($header, $value);
+ }
+ $this->HTTP->sendHeaders();
+ /* NSAPI won't output anything if we did this */
+ if (strncasecmp(PHP_SAPI, 'nsapi', 5)) {
+ if (ob_get_level()) {
+ ob_flush();
+ }
+ flush();
+ }
+ }
+
+ /**
+ * Flush
+ *
+ * @access protected
+ * @return void
+ * @param string $data
+ */
+ function flush($data = '')
+ {
+ if ($dlen = strlen($data)) {
+ $this->sentBytes += $dlen;
+ echo $data;
+ }
+ ob_flush();
+ flush();
+ }
+
+ /**
+ * Sleep
+ *
+ * @access protected
+ * @return void
+ */
+ function sleep()
+ {
+ if (OS_WINDOWS) {
+ com_message_pump($this->throttleDelay);
+ } else {
+ usleep($this->throttleDelay * 1000);
+ }
+ }
+
+ /**
+ * Returns and clears startup error
+ *
+ * @return NULL|PEAR_Error startup error if one exists
+ * @access protected
+ */
+ function _getError()
+ {
+ $error = null;
+ if (PEAR::isError($this->_error)) {
+ $error = $this->_error;
+ $this->_error = null;
+ }
+ return $error;
+ }
+ // }}}
+}
+?>