Location: PHPKode > projects > ZZ/OSS Installer > zic-1.1.0dev1/installer/lib/HTTP/Download.php
<?php
// +----------------------------------------------------------------------+
// | PEAR :: HTTP :: Download                                             |
// +----------------------------------------------------------------------+
// | This source file is subject to version 3.0 of the PHP license,       |
// | that is available at http://www.php.net/license/3_0.txt              |
// | If you did not receive a copy of the PHP license and are unable      |
// | to obtain it through the world-wide-web, please send a note to       |
// | hide@address.com so we can mail you a copy immediately.               |
// +----------------------------------------------------------------------+
// | Copyright (c) 2003-2004 Michael Wallner <hide@address.com>             |
// +----------------------------------------------------------------------+
//
// $Id: Download.php,v 1.21 2004/03/25 19:15:45 mike Exp $

/**
* Manage HTTP Downloads.
* 
* @author       Michael Wallner <hide@address.com>
* @package      HTTP_Download
* @category     HTTP
*/

/**
* Requires PEAR
*/
require_once 'PEAR.php';

/**
* Requires HTTP_Header
*/
require_once 'HTTP/Header.php';

/**#@+ 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 (not available yet)
*/
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.
*
* 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!
* 
* Usage Example 1:
* <code>
* $params = array(
*   'file'                  => '../hidden/download.tgz',
*   'contenttype'           => 'application/x-gzip',
*   'contentdisposition'    => array(HTTP_DOWNLOAD_ATTACHMENT, 'latest.tgz'),
* );
* 
* $error = HTTP_Download::staticSend($params, false);
* </code>
* 
* 
* Usage Example 2:
* <code>
* $dl = &new HTTP_Download();
* $dl->setFile('../hidden/download.tgz');
* $dl->setContentDisposition(HTTP_DOWNLOAD_ATTACHMENT, 'latest.tgz');
* // with ext/magic.mime
* // $dl->guessContentType();
* // else:
* $dl->setContentType('application/x-gzip');
* $dl->send();
* </code>
* 
* 
* Usage Example 3:
* <code>
* $dl = &new HTTP_Download();
* $dl->setData($blob_from_db);
* $dl->setLastModified($unix_timestamp);
* $dl->setContentType('application/x-gzip');
* $dl->setContentDisposition(HTTP_DOWNLOAD_ATTACHMENT, 'latest.tgz');
* $dl->send();
* </code>
* 
* @author   Michael Wallner <hide@address.com>
* @version  $Revision: 1.21 $
* @access   public
*/
class HTTP_Download extends HTTP_Header
{
    /**
    * Path to file for download
    *
    * @see      HTTP_Download::setFile()
    * @access   private
    * @var      string
    */
    var $_file = '';
    
    /**
    * Data for download
    *
    * @see      HTTP_Download::setData()
    * @access   private
    * @var      string
    */
    var $_data = null;
    
    /**
    * Resource handle for download
    *
    * @see      HTTP_Download::setResource()
    * @access   private
    * @var      int
    */
    var $_handle = null;
    
    /**
    * Whether to gzip the download
    *
    * @access   private
    * @var      bool
    */
    var $_gzip = false;
    
    /**
    * Size of download
    *
    * @access   private
    * @var      int
    */
    var $_size = 0;
    
    /**
    * Last modified (GMT)
    *
    * @access   private
    * @var      string
    */
    var $_last_modified = '';
    
    /**
    * HTTP headers
    *
    * @access   private
    * @var      array
    */
    var $_headers   = array(
        'Content-Type'  => 'application/x-octetstream',
        'Cache-Control' => 'public',
        'Accept-Ranges' => 'bytes',
        'Connection'    => 'close'
    );
    
	/**
    * Constructor
    *
    * Set supplied parameters.
    * 
    * @access   public
    * @param    array   $params     associative array of parameters
    * 
    *           <b>one of:</b>
    *                   o 'file'                => path to file for download
    *                   o 'data'                => raw data for download
    *                   o 'resource'            => resource handle for download
    * <br/>
    *           <b>and any of:</b>
    *                   o 'gzip'                => whether to gzip the download
    *                   o 'lastmodified'        => unix timestamp
    *                   o 'contenttype'         => content type of download
    *                   o 'contentdisposition'  => content disposition
    * 
    * <br />
    * '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->setParams($params);
    }
    
    /**
    * Set parameters
    *
    * @throws   PEAR_Error
    * @access   public
    * @return   mixed   true on success or PEAR_Error
    * @param    array   $params     associative array of parameters
    * 
    * @see      HTTP_Download::HTTP_Download()
    */
    function setParams($params)
    {
        foreach($params as $param => $value){
            if (!method_exists($this, 'set' . $param)) {
                return PEAR::raiseError(
                    "Method 'set$param' doesn't exist.",
                    HTTP_DOWNLOAD_E_INVALID_PARAM
                );
            }
            if (strToLower($param) == 'contentdisposition') {
                if (is_array($value)) {
                    $disp   = $value[0];
                    $fname  = @$value[1];
                } else {
                    $disp   = $value;
                    $fname  = null;
                }
                $e = $this->setContentDisposition($disp, $fname);
            } else {
                $e = $this->{'set' . $param}($value);
            }
            if (PEAR::isError($e)) {
                return $e;
            }
        }
        return true;
    }
    
    /**
    * Set path to file for download
    *
    * @throws   PEAR_Error
    * @access   public
    * @return   mixed   true on success or PEAR_Error
    * @param    string  $file       path to file for download
    * @param    bool    $send_404   whether to send HTTP/404 if
    *                               the file wasn't found
    */
    function setFile($file, $send_404 = true)
    {
        $file = realpath($file);
        if (!is_file($file)) {
            if ($send_404) {
                $this->sendStatusCode(404);
            }
            return PEAR::raiseError(
                "File '$file' not found.",
                HTTP_DOWNLOAD_E_INVALID_FILE
            );
        }
        $this->setLastModified(filemtime($file));
        $this->_file = $file;
        $this->_size = filesize($file);
        return true;
    }
    
    /**
    * Set data for download
    *
    * Set <var>$data</var> 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 if <var>$handle</var> is no valid resource.
    * 
    * @throws   PEAR_Error
    * @access   public
    * @return   mixed   true on success or PEAR_Error
    * @param    int     $handle     resource handle
    */
    function setResource($handle = null)
    {
        // Check if $handle is a valid resource
        if (!is_resource($handle)) {
            if (!is_null($handle)) {
                return PEAR::raiseError(
                    "Handle '$handle' is no valid resource.",
                    HTTP_DOWNLOAD_E_INVALID_RESOURCE
                );
            } else {
                $this->_handle  = null;
                $this->_size    = 0;
            }
        } else {
            $this->_handle  = $handle;
            $stats          = fstat($handle);
            $this->_size    = $stats['size'];
        }
        return true;
    }
    
    /**
    * Whether to gzip the download
    *
    * Returns a PEAR_Error if ext/zlib is not available
    * 
    * @throws   PEAR_Error
    * @access   public
    * @return   mixed   true on success or PEAR_Error
    * @param    bool    $gzip   whether to gzip the download
    */
    function setGzip($gzip = false)
    {
        if ($gzip && !extension_loaded('zlib') && !PEAR::loadExtension('zlib')){
            return PEAR::raiseError(
                'Compression (ext/zlib) not available.',
                HTTP_DOWNLOAD_E_NO_EXT_ZLIB
            );
        }
        $this->_gzip = (bool) $gzip;
        return true;
    }

    /**
    * Set "Last-Modified"
    *
    * This is usually determined by filemtime($file) 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->_last_modified            = HTTP::Date((int) $last_modified);
        $this->_headers['Last-Modified'] = $this->_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 (!is_null($file_name)) {
            $cd .= '; filename="' . $file_name . '"';
        } elseif ($this->_file) {
            $cd .= '; filename="' . basename($this->_file) . '"';
        }
        $this->_headers['Content-Disposition'] = $cd;
    }
    
    /**
    * Set content type of file for download
    *
    * Returns PEAR_Error if <var>$content_type</var> doesn't seem to be valid.
    * 
    * @throws   PEAR_Error
    * @access   public
    * @return   mixed   true on success or PEAR_Error
    * @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)) {
            return PEAR::raiseError(
                "Invalid content type '$content_type' supplied.",
                HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE
            );
        }
        $this->_headers['Content-Type'] = $content_type;
        return true;
    }
    
    /**
    * Send file
    *
    * Returns PEAR_Error if:
    *   o HTTP headers were already sent
    *   o HTTP Range was invalid
    *   o Download was 'not modified since'
    * 
    * @throws   PEAR_Error
    * @access   public
    * @return   mixed   Returns true on success, false if cached or 
    *                   <classname>PEAR_Error</clasname> on failure.
    */
    function send()
    {
        if (headers_sent()) {
            return PEAR::raiseError(
                'Headers already sent.',
                HTTP_DOWNLOAD_E_HEADERS_SENT
            );
        }
        /**
        * Check for partial downloads
        */
        $range = $this->_processRequest();
        if (!$range || PEAR::isError($range)) {
            return $range;
        }
        list($begin, $length) = $range;
        $all = ($begin == 0 && $length == $this->_size);
        /**
        * Check if Content-Disposition header is already set
        */
        if (!isset($this->_headers['Content-Disposition'])) {
            $this->setContentDisposition();
        }
        
        /**
        * HTTP Compression
        */
        if (!$this->_gzip || !@ob_start('ob_gzhandler')) {
            $this->_headers['Content-Length'] = $length;
        }
        
        /**
        * Send HTTP headers
        */
        $this->sendHeaders();

        /**
        * Send requested data (part)
        */
        set_time_limit(0);
        if ($this->_data) {
            echo $all ? $this->_data : substr($this->_data, $begin, $length);
        } else {
            if ($all) {
                if ($this->_handle) {
                    rewind($this->_handle);
                    fpassthru($this->_handle);
                } else {
                    readfile($this->_file);
                }
            } else {
                $rb = 65536;
                if (!$this->_handle) {
                    $this->_handle = fopen($this->_file, 'rb');
                }
                fseek($this->_handle, $begin);
                while(0 < ($length -= $rb)) {
                    echo(fread($this->_handle, $rb));
                }
                echo(fread($this->_handle, $length+$rb));
                fclose($this->_handle);
            }
        }
        return true;
    }
    
    /**
    * Process HTTP request
    * 
    * Check for partial downloads, sane Range headers and HTTP caching.
    *
    * Returns PEAR_Error if:
    *   o download is cached
    *   o HTTP Request was invalid
    * 
    * @access   private
    * @return   mixed   array((int) begin, (int) length) in bytes or PEAR_Error
    */
    function _processRequest()
    {
        $begin      = 0;
        $length     = $this->_size;
        $send_range = false;

        /**
        * Handle Ranges (partial downloads)
        */
        if (isset($_SERVER['HTTP_RANGE'])) {
            $send_range = true;

            // Check for conditional GET
            if (isset($_SERVER['HTTP_IF_UNMODIFIED_SINCE'])) {
                if ($_SERVER['HTTP_IF_UNMODIFIED_SINCE'] != 
                    $this->_last_modified ) 
                {
                    $send_range = false;
                }
            } elseif (isset($_SERVER['HTTP_IF_RANGE'])) {

                // If it doesn't match send the whole thingy
                if ( $_SERVER['HTTP_IF_RANGE'] != $this->_last_modified ) {
                    $send_range = false;
                } 
            }

            /**
            * We don't provide complete http compliance and
            * handle just basic range request (not combined)
            */
            if ($send_range) {
                if (preg_match( '/^bytes=(\d*).*?(\d*)$/i', 
                                $_SERVER['HTTP_RANGE'], 
                                $bytes )) 
                {

                    // First check if there is anything useable in Range header
                    if (    !$bytes[1] && !$bytes[2] && 
                            $bytes[1] !== '0' && $bytes[2] !== '0') {
                        // Range is not valid
                        $this->sendStatusCode(416);
                        return PEAR::raiseError(
                            'HTTP Error: ' . HTTP_HEADER_STATUS_416,
                            HTTP_DOWNLOAD_E_INVALID_REQUEST
                        );
                    }
                    
                    // Calculate the desired Range
                    if (!$bytes[1] && $bytes[1] !== '0') {
                        // OK - Range: bytes=-5
                        $length = $bytes[2];
                        $end    = $this->_size - 1;
                        $begin  = $this->_size - $length;
                    } elseif (!$bytes[2] && $bytes[2] !== '0') {
                        // OK - Range: bytes=5-
                        $begin  = $bytes[1];
                        $end    = $this->_size - 1;
                        $length = $this->_size - $begin;
                    } else {
                        // OK - Range: bytes=5-9
                        $begin  = $bytes[1];
                        $end    = $bytes[2];
                        $length = ($end - $begin) + 1;
                    }

                    // Check if we're out of range
                    if ($end > $this->_size || ($begin + $length) > $this->_size) {
                        $end    = $this->_size - 1;
                        $length = $this->_size - $begin;
                    }
                    
                    // Check if range and file size equal
                    // or second byte offset was less than
                    // the first, so we just ignore the range 
                    // header and send the hole thingy
                    if ($length >= $this->_size || $length <= 0) {
                        $begin      = 0;
                        $end        = $length;
                        $send_range = false;

                    // Check if we're totally out of bounds
                    } elseif ($begin > $this->_size) {
                        // Range is not valid
                        $this->sendStatusCode(416);
                        return PEAR::raiseError(
                            'HTTP Error: ' . HTTP_HEADER_STATUS_416,
                            HTTP_DOWNLOAD_E_INVALID_REQUEST
                        );

                    // Else all's gone fine
                    } else {

                        // Set content range header
                        $this->_headers['Content-Range'] =
                            'bytes: ' . $begin . '-' . $end . '/' . 
                            $this->_size;
                    }

                // If Range header didn't even contain "bytes="
                } else {

                    // Range is not valid
                    $this->sendStatusCode(416);
                    return PEAR::raiseError(
                        'HTTP Error: ' . HTTP_HEADER_STATUS_416,
                        HTTP_DOWNLOAD_E_INVALID_REQUEST
                    );
                }
            }
        }

        /**
        * Don't send data if cached - "HTTP/1.x 304 Not Modified"
        */
        if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
            if ($_SERVER['HTTP_IF_MODIFIED_SINCE'] == $this->_last_modified ) {

                // Not Modified
                $this->sendStatusCode(304);
                $this->sendHeaders(
                    array('Cache-Control', 'Accept-Ranges', 'Connection')
                );
                return false;
            }
        }
        
        /**
        * Send "HTTP/1.x 206 Partial Content" if we send a part
        * Send "HTTP/1.x 200 Ok" if we send the whole thingy
        */
        if ($send_range) {
            $this->sendStatusCode(206);
        } else {
            $this->sendStatusCode(200);
        }
        
        return array($begin, $length);
    }
    
    /**
    * Guess content type of file
    * 
    * This only works if PHP is installed with ext/magic.mime AND php.ini
    * is setup correct! Otherwise it will result in a FATAL ERROR.
    * <b>So be WARNED!</b>
    *
    * Returns PEAR_Error if ext/magic.mime is not installed,
    * or content type couldn't be guessed.
    * 
    * @throws   PEAR_Error
    * @access   public
    * @return   mixed   true on success or PEAR_Error
    */
    function guessContentType()
    {
        if (!function_exists('mime_content_type')) {
            return PEAR::raiseError(
                'This feature requires ext/magic.mime!',
                HTTP_DOWNLOAD_E_NO_EXT_MMAGIC
            );
        }
        if (!is_file(ini_get('mime_magic.magicfile'))) {
            return PEAR::raiseError(
                'mime_magic is loaded but not configured!',
                HTTP_DOWNLOAD_E_NO_EXT_MMAGIC
            );
        }
        return $this->setContentType(mime_content_type($this->_file));
    }
    
    /**
    * Static send
    *
    * @see      HTTP_Download::HTTP_Download()
    * @see      HTTP_Download::send()
    * 
    * @throws   PEAR_Error
    * @static   call this method statically
    * @access   public
    * @return   mixed   true on success or PEAR_Error
    * @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
    *
    * @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
    * @param    string  $strip_path
    */
    function sendArchive($name, $files, $type = HTTP_DOWNLOAD_TGZ, $add_path = '', $strip_path = '')
    {
        require_once 'System.php';
        
        $tmp = System::mktemp();
        
        switch (strToUpper($type)) {
            case HTTP_DOWNLOAD_TAR:
                include_once 'Archive/Tar.php';
                $arc = &new Archive_Tar($tmp);
                $content_type = 'x-tar';
                break;

            case HTTP_DOWNLOAD_TGZ:
                include_once 'Archive/Tar.php';
                $arc = &new Archive_Tar($tmp, 'gz');
                $content_type = 'x-gzip';
                break;

            case HTTP_DOWNLOAD_BZ2:
                include_once 'Archive/Tar.php';
                $arc = &new Archive_Tar($tmp, 'bz2');
                $content_type = 'x-bzip2';
                break;

            default:
                return PEAR::raiseError(
                    'Archive type not supported: ' . $type,
                    HTTP_DOWNLOAD_E_INVALID_ARCHIVE_TYPE
                );
        }
        
        if (!$e = $arc->createModify($files, $add_path, $strip_path)) {
            return PEAR::raiseError('Archive creation failed.');
        }
        if (PEAR::isError($e)) {
            return $e;
        }
        unset($arc);
        
        $dl = &new HTTP_Download(array('file' => $tmp));
        $dl->setContentType('application/' . $content_type);
        $dl->setContentDisposition(HTTP_DOWNLOAD_ATTACHMENT, $name);
        return $dl->send();
    }
}
?>
Return current item: ZZ/OSS Installer