1244 lines
36 KiB
PHP
1244 lines
36 KiB
PHP
|
<?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 __construct($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
|
||
|
*/
|
||
|
static 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
|
||
|
*/
|
||
|
static 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>
|
||
|
*/
|
||
|
static 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>
|
||
|
*/
|
||
|
static 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']) || $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] : NULL;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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;
|
||
|
}
|
||
|
// }}}
|
||
|
}
|
||
|
?>
|