Source for file Download.php
Documentation is available at Download.php
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
* @author Michael Wallner <mike@php.net>
* @copyright 2003-2005 Michael Wallner
* @version CVS: $Id: Download.php,v 1.79 2007/10/05 07:02:03 mike Exp $
* @link http://pear.php.net/package/HTTP_Download
require_once 'HTTP/Header.php';
/**#@+ Use with HTTP_Download::setContentDisposition() **/
* Send data as attachment
define('HTTP_DOWNLOAD_ATTACHMENT', 'attachment');
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');
define('HTTP_DOWNLOAD_ZIP', 'ZIP');
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.
* 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!
* @version $Revision: 1.79 $
// {{{ protected member variables
* Path to file for download
* @see HTTP_Download::setFile()
* @see HTTP_Download::setData()
* Resource handle for download
* @see HTTP_Download::setResource()
* Whether to gzip the download
* Whether to allow caching of the download on the clients side
'Content-Type' => 'application/x-octetstream',
'Cache-Control' => 'public, must-revalidate, max-age=0',
'Accept-Ranges' => 'bytes',
'X-Sent-By' => 'PEAR::HTTP::Download'
var $bufferSize = 2097152;
* Set supplied parameters.
* @param array $params associative array of parameters
* o 'file' => path to file for download
* o 'data' => raw data for download
* o 'resource' => resource handle for download
* o 'cache' => whether to allow cs caching
* o 'gzip' => whether to gzip the download
* o 'lastmodified' => unix timestamp
* o 'contenttype' => content type of download
* o 'contentdisposition' => content disposition
* o 'buffersize' => amount of bytes to buffer
* o 'throttledelay' => amount of secs to sleep
* o 'cachecontrol' => cache privacy and validity
* 'Content-Disposition' is not HTTP compliant, but most browsers
* follow this header, so it was borrowed from MIME standard.
* It looks like this: <br />
* "Content-Disposition: attachment; filename=example.tgz".
* @see HTTP_Download::setContentDisposition()
function HTTP_Download ($params = array ())
$this->HTTP = &new HTTP_Header;
$this->setParams ($params);
* Set supplied parameters through its accessor methods.
* @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)
foreach((array) $params as $param => $value){
" Method '$method' doesn't exist." ,
* 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 status if $send_404 is set to true.
* @return mixed Returns true on success or PEAR_Error on failure.
* @param string $file path to file for download
* @param bool $send_404 whether to send HTTP/404 if
function setFile ($file, $send_404 = true )
$this->HTTP->sendStatusCode (404 );
" File '$file' not found." ,
* Set $data to null if you want to unset this.
* @param $data raw data to send
function setData ($data = null )
* 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.
* @return mixed Returns true on success or PEAR_Error on failure.
* @param int $handle resource handle
function setResource ($handle = null )
$filestats = fstat($handle);
$this->size = $filestats['size'];
" Handle '$handle' is no valid resource." ,
* Whether to gzip the download
* Returns a PEAR_Error (HTTP_DOWNLOAD_E_NO_EXT_ZLIB)
* if ext/zlib is not available/loadable.
* @return mixed Returns true on success or PEAR_Error on failure.
* @param bool $gzip whether to gzip the download
function setGzip ($gzip = false )
if ($gzip && !PEAR ::loadExtension ('zlib')){
'GZIP compression (ext/zlib) not available.',
$this->gzip = (bool) $gzip;
* 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.
* @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.
* @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))
$this->headers['Cache-Control'] =
$cache . ', must-revalidate, max-age='. abs($maxage);
* Sets a user-defined ETag for cache-validation. The ETag is usually
* generated by HTTP_Download through its payload information.
* @param string $etag Entity tag used for strong cache validation.
function setETag ($etag = null )
$this->etag = (string) $etag;
* 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
* @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 )
'Buffer size must be greater than 0 bytes ('. $bytes . ' given)',
$this->bufferSize = abs($bytes);
* 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.
* 'buffersize' => 1024 * 25,
* 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.
* @param float $seconds Amount of seconds to sleep after each
* chunk that has been sent.
function setThrottleDelay ($seconds = 0 )
$this->throttleDelay = abs($seconds) * 1000;
* 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
* @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
* @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
* $HTTP_Download->setContentDisposition(
* HTTP_DOWNLOAD_ATTACHMENT,
function setContentDisposition ( $disposition = HTTP_DOWNLOAD_ATTACHMENT ,
$cd .= '; filename="' . $file_name . '"';
$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.
* @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')
if (!preg_match('/^[a-z]+\w*\/[a-z]+[\w.;= -]*$/', $content_type)) {
" Invalid content type '$content_type' supplied." ,
$this->headers['Content-Type'] = $content_type;
* 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.
* 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)
* @return mixed Returns true on success or PEAR_Error on failure.
function guessContentType ()
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 (),
return $this->setContentType ($mime_type);
'This feature requires ext/mime_magic!',
'ext/mime_magic is loaded but not properly configured!',
if (!$content_type = @mime_content_type ($this->file)) {
'Couldn\'t guess content type with mime_content_type().',
return $this->setContentType ($content_type);
* o HTTP headers were already sent (HTTP_DOWNLOAD_E_HEADERS_SENT)
* o HTTP Range was invalid (HTTP_DOWNLOAD_E_INVALID_REQUEST)
* @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 )
if ($autoSetContentDisposition &&
!isset ($this->headers['Content-Disposition'])) {
$this->setContentDisposition ();
$this->headers['ETag'] = $this->generateETag ();
$this->HTTP->sendStatusCode (304 );
unset ($this->headers['Last-Modified']);
if ($this->isRangeRequest ()) {
$this->HTTP->sendStatusCode (206 );
$chunks = $this->getChunks ();
$this->HTTP->sendStatusCode (200 );
$chunks = array (array (0 , $this->size));
$this->headers['Content-Length'] = $this->size;
if (PEAR ::isError ($e = $this->sendChunks ($chunks))) {
|