HTTP_Download
[ class tree: HTTP_Download ] [ index: HTTP_Download ] [ all elements ]

Source for file Download.php

Documentation is available at Download.php

  1. <?php
  2. /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
  3.  
  4. /**
  5.  * HTTP::Download
  6.  *
  7.  * PHP versions 4 and 5
  8.  *
  9.  * @category   HTTP
  10.  * @package    HTTP_Download
  11.  * @author     Michael Wallner <mike@php.net>
  12.  * @copyright  2003-2005 Michael Wallner
  13.  * @license    BSD, revised
  14.  * @version    CVS: $Id: Download.php 304423 2010-10-15 13:36:46Z clockwerx $
  15.  * @link       http://pear.php.net/package/HTTP_Download
  16.  */
  17.  
  18. // {{{ includes
  19. /**
  20.  * Requires PEAR
  21.  */
  22. require_once 'PEAR.php';
  23.  
  24. /**
  25.  * Requires HTTP_Header
  26.  */
  27. require_once 'HTTP/Header.php';
  28. // }}}
  29.  
  30. // {{{ constants
  31. /**#@+ Use with HTTP_Download::setContentDisposition() **/
  32. /**
  33.  * Send data as attachment
  34.  */
  35. define('HTTP_DOWNLOAD_ATTACHMENT''attachment');
  36. /**
  37.  * Send data inline
  38.  */
  39. define('HTTP_DOWNLOAD_INLINE''inline');
  40. /**#@-**/
  41.  
  42. /**#@+ Use with HTTP_Download::sendArchive() **/
  43. /**
  44.  * Send as uncompressed tar archive
  45.  */
  46. define('HTTP_DOWNLOAD_TAR''TAR');
  47. /**
  48.  * Send as gzipped tar archive
  49.  */
  50. define('HTTP_DOWNLOAD_TGZ''TGZ');
  51. /**
  52.  * Send as bzip2 compressed tar archive
  53.  */
  54. define('HTTP_DOWNLOAD_BZ2''BZ2');
  55. /**
  56.  * Send as zip archive
  57.  */
  58. define('HTTP_DOWNLOAD_ZIP''ZIP');
  59. /**#@-**/
  60.  
  61. /**#@+
  62.  * Error constants
  63.  */
  64. define('HTTP_DOWNLOAD_E_HEADERS_SENT',          -1);
  65. define('HTTP_DOWNLOAD_E_NO_EXT_ZLIB',           -2);
  66. define('HTTP_DOWNLOAD_E_NO_EXT_MMAGIC',         -3);
  67. define('HTTP_DOWNLOAD_E_INVALID_FILE',          -4);
  68. define('HTTP_DOWNLOAD_E_INVALID_PARAM',         -5);
  69. define('HTTP_DOWNLOAD_E_INVALID_RESOURCE',      -6);
  70. define('HTTP_DOWNLOAD_E_INVALID_REQUEST',       -7);
  71. define('HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE',  -8);
  72. define('HTTP_DOWNLOAD_E_INVALID_ARCHIVE_TYPE',  -9);
  73. /**#@-**/
  74. // }}}
  75.  
  76. /**
  77.  * Send HTTP Downloads/Responses.
  78.  *
  79.  * With this package you can handle (hidden) downloads.
  80.  * It supports partial downloads, resuming and sending
  81.  * raw data ie. from database BLOBs.
  82.  *
  83.  * <i>ATTENTION:</i>
  84.  * You shouldn't use this package together with ob_gzhandler or
  85.  * zlib.output_compression enabled in your php.ini, especially
  86.  * if you want to send already gzipped data!
  87.  *
  88.  * @access   public
  89.  * @version  $Revision: 304423 $
  90.  */
  91. class HTTP_Download
  92. {
  93.     // {{{ protected member variables
  94.     /**
  95.      * Path to file for download
  96.      *
  97.      * @see     HTTP_Download::setFile()
  98.      * @access  protected
  99.      * @var     string 
  100.      */
  101.     var $file '';
  102.  
  103.     /**
  104.      * Data for download
  105.      *
  106.      * @see     HTTP_Download::setData()
  107.      * @access  protected
  108.      * @var     string 
  109.      */
  110.     var $data = null;
  111.  
  112.     /**
  113.      * Resource handle for download
  114.      *
  115.      * @see     HTTP_Download::setResource()
  116.      * @access  protected
  117.      * @var     int 
  118.      */
  119.     var $handle = null;
  120.  
  121.     /**
  122.      * Whether to gzip the download
  123.      *
  124.      * @access  protected
  125.      * @var     bool 
  126.      */
  127.     var $gzip = false;
  128.  
  129.     /**
  130.      * Whether to allow caching of the download on the clients side
  131.      *
  132.      * @access  protected
  133.      * @var     bool 
  134.      */
  135.     var $cache = true;
  136.  
  137.     /**
  138.      * Size of download
  139.      *
  140.      * @access  protected
  141.      * @var     int 
  142.      */
  143.     var $size = 0;
  144.  
  145.     /**
  146.      * Last modified
  147.      *
  148.      * @access  protected
  149.      * @var     int 
  150.      */
  151.     var $lastModified = 0;
  152.  
  153.     /**
  154.      * HTTP headers
  155.      *
  156.      * @access  protected
  157.      * @var     array 
  158.      */
  159.     var $headers   = array(
  160.         'Content-Type'  => 'application/x-octetstream',
  161.         'Pragma'        => 'cache',
  162.         'Cache-Control' => 'public, must-revalidate, max-age=0',
  163.         'Accept-Ranges' => 'bytes',
  164.         'X-Sent-By'     => 'PEAR::HTTP::Download'
  165.     );
  166.  
  167.     /**
  168.      * HTTP_Header
  169.      *
  170.      * @access  protected
  171.      * @var     object 
  172.      */
  173.     var $HTTP = null;
  174.  
  175.     /**
  176.      * ETag
  177.      *
  178.      * @access  protected
  179.      * @var     string 
  180.      */
  181.     var $etag '';
  182.  
  183.     /**
  184.      * Buffer Size
  185.      *
  186.      * @access  protected
  187.      * @var     int 
  188.      */
  189.     var $bufferSize = 2097152;
  190.  
  191.     /**
  192.      * Throttle Delay
  193.      *
  194.      * @access  protected
  195.      * @var     float 
  196.      */
  197.     var $throttleDelay = 0;
  198.  
  199.     /**
  200.      * Sent Bytes
  201.      *
  202.      * @access  public
  203.      * @var     int 
  204.      */
  205.     var $sentBytes = 0;
  206.  
  207.     /**
  208.      * Startup error
  209.      *
  210.      * @var    PEAR_Error 
  211.      * @access protected
  212.      */
  213.     var $_error = null;
  214.     // }}}
  215.  
  216.     // {{{ constructor
  217.     /**
  218.      * Constructor
  219.      *
  220.      * Set supplied parameters.
  221.      *
  222.      * @access  public
  223.      * @param   array   $params     associative array of parameters
  224.      *   <strong>one of:</strong>
  225.      *   <ul>
  226.      *     <li>'file'               => path to file for download</li>
  227.      *     <li>'data'               => raw data for download</li>
  228.      *     <li>'resource'           => resource handle for download</li>
  229.      *   </ul>
  230.      *   <strong>and any of:</strong>
  231.      *   <ul>
  232.      *     <li>'cache'              => whether to allow cs caching</li>
  233.      *     <li>'gzip'               => whether to gzip the download</li>
  234.      *     <li>'lastmodified'       => unix timestamp</li>
  235.      *     <li>'contenttype'        => content type of download</li>
  236.      *     <li>'contentdisposition' => content disposition</li>
  237.      *     <li>'buffersize'         => amount of bytes to buffer</li>
  238.      *     <li>'throttledelay'      => amount of secs to sleep</li>
  239.      *     <li>'cachecontrol'       => cache privacy and validity</li>
  240.      *   </ul>
  241.      *
  242.      *  'Content-Disposition' is not HTTP compliant, but most browsers
  243.      *  follow this header, so it was borrowed from MIME standard.
  244.      *
  245.      *  It looks like this:
  246.      *  "Content-Disposition: attachment; filename=example.tgz".
  247.      *
  248.      * @see HTTP_Download::setContentDisposition()
  249.      */
  250.     function HTTP_Download($params = array())
  251.     {
  252.         $this->HTTP &new HTTP_Header;
  253.         $this->_error $this->setParams($params);
  254.     }
  255.     // }}}
  256.  
  257.     // {{{ public methods
  258.     /**
  259.      * Set parameters
  260.      *
  261.      * Set supplied parameters through its accessor methods.
  262.      *
  263.      * @access  public
  264.      * @return  mixed   Returns true on success or PEAR_Error on failure.
  265.      * @param   array   $params     associative array of parameters
  266.      *
  267.      * @see     HTTP_Download::HTTP_Download()
  268.      */
  269.     function setParams($params)
  270.     {
  271.         $error $this->_getError();
  272.         if ($error !== null{
  273.             return $error;
  274.         }
  275.         foreach((array) $params as $param => $value){
  276.             $method 'set'$param;
  277.  
  278.             if (!method_exists($this$method)) {
  279.                 return PEAR::raiseError(
  280.                     "Method '$method' doesn't exist.",
  281.                     HTTP_DOWNLOAD_E_INVALID_PARAM
  282.                 );
  283.             }
  284.  
  285.             $e call_user_func_array(array(&$this$method)(array) $value);
  286.  
  287.             if (PEAR::isError($e)) {
  288.                 return $e;
  289.             }
  290.         }
  291.         return true;
  292.     }
  293.  
  294.     /**
  295.      * Set path to file for download
  296.      *
  297.      * The Last-Modified header will be set to files filemtime(), actually.
  298.      * Returns PEAR_Error (HTTP_DOWNLOAD_E_INVALID_FILE) if file doesn't exist.
  299.      * Sends HTTP 404 or 403 status if $send_error is set to true.
  300.      *
  301.      * @access  public
  302.      * @return  mixed   Returns true on success or PEAR_Error on failure.
  303.      * @param   string  $file       path to file for download
  304.      * @param   bool    $send_error whether to send HTTP/404 or 403 if
  305.      *                               the file wasn't found or is not readable
  306.      */
  307.     function setFile($file$send_error = true)
  308.     {
  309.         $error $this->_getError();
  310.         if ($error !== null{
  311.             return $error;
  312.         }
  313.         $file realpath($file);
  314.         if (!is_file($file)) {
  315.             if ($send_error{
  316.                 $this->HTTP->sendStatusCode(404);
  317.             }
  318.             return PEAR::raiseError(
  319.                 "File '$file' not found.",
  320.                 HTTP_DOWNLOAD_E_INVALID_FILE
  321.             );
  322.         }
  323.         if (!is_readable($file)) {
  324.             if ($send_error{
  325.                 $this->HTTP->sendStatusCode(403);
  326.             }
  327.             return PEAR::raiseError(
  328.                 "Cannot read file '$file'.",
  329.                 HTTP_DOWNLOAD_E_INVALID_FILE
  330.             );
  331.         }
  332.         $this->setLastModified(filemtime($file));
  333.         $this->file $file;
  334.         $this->size filesize($file);
  335.         return true;
  336.     }
  337.  
  338.     /**
  339.      * Set data for download
  340.      *
  341.      * Set $data to null if you want to unset this.
  342.      *
  343.      * @access  public
  344.      * @return  void 
  345.      * @param   $data   raw data to send
  346.      */
  347.     function setData($data = null)
  348.     {
  349.         $this->data $data;
  350.         $this->size strlen($data);
  351.     }
  352.  
  353.     /**
  354.      * Set resource for download
  355.      *
  356.      * The resource handle supplied will be closed after sending the download.
  357.      * Returns a PEAR_Error (HTTP_DOWNLOAD_E_INVALID_RESOURCE) if $handle
  358.      * is no valid resource. Set $handle to null if you want to unset this.
  359.      *
  360.      * @access  public
  361.      * @return  mixed   Returns true on success or PEAR_Error on failure.
  362.      * @param   int     $handle     resource handle
  363.      */
  364.     function setResource($handle = null)
  365.     {
  366.         $error $this->_getError();
  367.         if ($error !== null{
  368.             return $error;
  369.         }
  370.         if (!isset($handle)) {
  371.             $this->handle = null;
  372.             $this->size = 0;
  373.             return true;
  374.         }
  375.  
  376.         if (is_resource($handle)) {
  377.             $this->handle $handle;
  378.             $filestats    fstat($handle);
  379.             $this->size   = isset($filestats['size']$filestats['size']
  380.                                                       : -1;
  381.             return true;
  382.         }
  383.  
  384.         return PEAR::raiseError(
  385.             "Handle '$handle' is no valid resource.",
  386.             HTTP_DOWNLOAD_E_INVALID_RESOURCE
  387.         );
  388.     }
  389.  
  390.     /**
  391.      * Whether to gzip the download
  392.      *
  393.      * Returns a PEAR_Error (HTTP_DOWNLOAD_E_NO_EXT_ZLIB)
  394.      * if ext/zlib is not available/loadable.
  395.      *
  396.      * @access  public
  397.      * @return  mixed   Returns true on success or PEAR_Error on failure.
  398.      * @param   bool    $gzip   whether to gzip the download
  399.      */
  400.     function setGzip($gzip = false)
  401.     {
  402.         $error $this->_getError();
  403.         if ($error !== null{
  404.             return $error;
  405.         }
  406.         if ($gzip && !PEAR::loadExtension('zlib')){
  407.             return PEAR::raiseError(
  408.                 'GZIP compression (ext/zlib) not available.',
  409.                 HTTP_DOWNLOAD_E_NO_EXT_ZLIB
  410.             );
  411.         }
  412.         $this->gzip = (bool) $gzip;
  413.         return true;
  414.     }
  415.  
  416.     /**
  417.      * Whether to allow caching
  418.      *
  419.      * If set to true (default) we'll send some headers that are commonly
  420.      * used for caching purposes like ETag, Cache-Control and Last-Modified.
  421.      *
  422.      * If caching is disabled, we'll send the download no matter if it
  423.      * would actually be cached at the client side.
  424.      *
  425.      * @access  public
  426.      * @return  void 
  427.      * @param   bool    $cache  whether to allow caching
  428.      */
  429.     function setCache($cache = true)
  430.     {
  431.         $this->cache = (bool) $cache;
  432.     }
  433.  
  434.     /**
  435.      * Whether to allow proxies to cache
  436.      *
  437.      * If set to 'private' proxies shouldn't cache the response.
  438.      * This setting defaults to 'public' and affects only cached responses.
  439.      *
  440.      * @access  public
  441.      * @return  bool 
  442.      * @param   string  $cache  private or public
  443.      * @param   int     $maxage maximum age of the client cache entry
  444.      */
  445.     function setCacheControl($cache 'public'$maxage = 0)
  446.     {
  447.         switch ($cache = strToLower($cache))
  448.         {
  449.             case 'private':
  450.             case 'public':
  451.                 $this->headers['Cache-Control'=
  452.                     $cache .', must-revalidate, max-age='abs($maxage);
  453.                 return true;
  454.             break;
  455.         }
  456.         return false;
  457.     }
  458.  
  459.     /**
  460.      * Set ETag
  461.      *
  462.      * Sets a user-defined ETag for cache-validation.  The ETag is usually
  463.      * generated by HTTP_Download through its payload information.
  464.      *
  465.      * @access  public
  466.      * @return  void 
  467.      * @param   string  $etag Entity tag used for strong cache validation.
  468.      */
  469.     function setETag($etag = null)
  470.     {
  471.         $this->etag = (string) $etag;
  472.     }
  473.  
  474.     /**
  475.      * Set Size of Buffer
  476.      *
  477.      * The amount of bytes specified as buffer size is the maximum amount
  478.      * of data read at once from resources or files.  The default size is 2M
  479.      * (2097152 bytes).  Be aware that if you enable gzip compression and
  480.      * you set a very low buffer size that the actual file size may grow
  481.      * due to added gzip headers for each sent chunk of the specified size.
  482.      *
  483.      * Returns PEAR_Error (HTTP_DOWNLOAD_E_INVALID_PARAM) if $size is not
  484.      * greater than 0 bytes.
  485.      *
  486.      * @access  public
  487.      * @return  mixed   Returns true on success or PEAR_Error on failure.
  488.      * @param   int     $bytes Amount of bytes to use as buffer.
  489.      */
  490.     function setBufferSize($bytes = 2097152)
  491.     {
  492.         $error $this->_getError();
  493.         if ($error !== null{
  494.             return $error;
  495.         }
  496.         if (0 >= $bytes{
  497.             return PEAR::raiseError(
  498.                 'Buffer size must be greater than 0 bytes ('$bytes .' given)',
  499.                 HTTP_DOWNLOAD_E_INVALID_PARAM);
  500.         }
  501.         $this->bufferSize abs($bytes);
  502.         return true;
  503.     }
  504.  
  505.     /**
  506.      * Set Throttle Delay
  507.      *
  508.      * Set the amount of seconds to sleep after each chunck that has been
  509.      * sent.  One can implement some sort of throttle through adjusting the
  510.      * buffer size and the throttle delay.  With the following settings
  511.      * HTTP_Download will sleep a second after each 25 K of data sent.
  512.      *
  513.      * <code>
  514.      *  Array(
  515.      *      'throttledelay' => 1,
  516.      *      'buffersize'    => 1024 * 25,
  517.      *  )
  518.      * </code>
  519.      *
  520.      * Just be aware that if gzipp'ing is enabled, decreasing the chunk size
  521.      * too much leads to proportionally increased network traffic due to added
  522.      * gzip header and bottom bytes around each chunk.
  523.      *
  524.      * @access  public
  525.      * @return  void 
  526.      * @param   float   $seconds    Amount of seconds to sleep after each
  527.      *                               chunk that has been sent.
  528.      */
  529.     function setThrottleDelay($seconds = 0)
  530.     {
  531.         $this->throttleDelay abs($seconds* 1000;
  532.     }
  533.  
  534.     /**
  535.      * Set "Last-Modified"
  536.      *
  537.      * This is usually determined by filemtime() in HTTP_Download::setFile()
  538.      * If you set raw data for download with HTTP_Download::setData() and you
  539.      * want do send an appropiate "Last-Modified" header, you should call this
  540.      * method.
  541.      *
  542.      * @access  public
  543.      * @return  void 
  544.      * @param   int     unix timestamp
  545.      */
  546.     function setLastModified($last_modified)
  547.     {
  548.         $this->lastModified $this->headers['Last-Modified'= (int) $last_modified;
  549.     }
  550.  
  551.     /**
  552.      * Set Content-Disposition header
  553.      *
  554.      * @see HTTP_Download::HTTP_Download
  555.      *
  556.      * @access  public
  557.      * @return  void 
  558.      * @param   string  $disposition    whether to send the download
  559.      *                                   inline or as attachment
  560.      * @param   string  $file_name      the filename to display in
  561.      *                                   the browser's download window
  562.      *
  563.      *  <b>Example:</b>
  564.      *  <code>
  565.      *  $HTTP_Download->setContentDisposition(
  566.      *    HTTP_DOWNLOAD_ATTACHMENT,
  567.      *    'download.tgz'
  568.      *  );
  569.      *  </code>
  570.      */
  571.     function setContentDisposition$disposition    = HTTP_DOWNLOAD_ATTACHMENT,
  572.                                     $file_name      = null)
  573.     {
  574.         $cd $disposition;
  575.         if (isset($file_name)) {
  576.             $cd .= '; filename="' $file_name '"';
  577.         elseif ($this->file{
  578.             $cd .= '; filename="' basename($this->file'"';
  579.         }
  580.         $this->headers['Content-Disposition'$cd;
  581.     }
  582.  
  583.     /**
  584.      * Set content type of the download
  585.      *
  586.      * Default content type of the download will be 'application/x-octetstream'.
  587.      * Returns PEAR_Error (HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE) if
  588.      * $content_type doesn't seem to be valid.
  589.      *
  590.      * @access  public
  591.      * @return  mixed   Returns true on success or PEAR_Error on failure.
  592.      * @param   string  $content_type   content type of file for download
  593.      */
  594.     function setContentType($content_type 'application/x-octetstream')
  595.     {
  596.         $error $this->_getError();
  597.         if ($error !== null{
  598.             return $error;
  599.         }
  600.         if (!preg_match('/^[a-z]+\w*\/[a-z]+[\w.;= -]*$/'$content_type)) {
  601.             return PEAR::raiseError(
  602.                 "Invalid content type '$content_type' supplied.",
  603.                 HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE
  604.             );
  605.         }
  606.         $this->headers['Content-Type'$content_type;
  607.         return true;
  608.     }
  609.  
  610.     /**
  611.      * Guess content type of file
  612.      *
  613.      * First we try to use PEAR::MIME_Type, if installed, to detect the content
  614.      * type, else we check if ext/mime_magic is loaded and properly configured.
  615.      *
  616.      * Returns PEAR_Error if:
  617.      *      o if PEAR::MIME_Type failed to detect a proper content type
  618.      *        (HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE)
  619.      *      o ext/magic.mime is not installed, or not properly configured
  620.      *        (HTTP_DOWNLOAD_E_NO_EXT_MMAGIC)
  621.      *      o mime_content_type() couldn't guess content type or returned
  622.      *        a content type considered to be bogus by setContentType()
  623.      *        (HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE)
  624.      *
  625.      * @access  public
  626.      * @return  mixed   Returns true on success or PEAR_Error on failure.
  627.      */
  628.     function guessContentType()
  629.     {
  630.         $error $this->_getError();
  631.         if ($error !== null{
  632.             return $error;
  633.         }
  634.         if (class_exists('MIME_Type'|| @include_once 'MIME/Type.php'{
  635.             if (PEAR::isError($mime_type = MIME_Type::autoDetect($this->file))) {
  636.                 return PEAR::raiseError($mime_type->getMessage(),
  637.                     HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE);
  638.             }
  639.             return $this->setContentType($mime_type);
  640.         }
  641.         if (!function_exists('mime_content_type')) {
  642.             return PEAR::raiseError(
  643.                 'This feature requires ext/mime_magic!',
  644.                 HTTP_DOWNLOAD_E_NO_EXT_MMAGIC
  645.             );
  646.         }
  647.         if (!is_file(ini_get('mime_magic.magicfile'))) {
  648.             return PEAR::raiseError(
  649.                 'ext/mime_magic is loaded but not properly configured!',
  650.                 HTTP_DOWNLOAD_E_NO_EXT_MMAGIC
  651.             );
  652.         }
  653.         if (!$content_type @mime_content_type($this->file)) {
  654.             return PEAR::raiseError(
  655.                 'Couldn\'t guess content type with mime_content_type().',
  656.                 HTTP_DOWNLOAD_E_INVALID_CONTENT_TYPE
  657.             );
  658.         }
  659.         return $this->setContentType($content_type);
  660.     }
  661.  
  662.     /**
  663.      * Send
  664.      *
  665.      * Returns PEAR_Error if:
  666.      *   o HTTP headers were already sent (HTTP_DOWNLOAD_E_HEADERS_SENT)
  667.      *   o HTTP Range was invalid (HTTP_DOWNLOAD_E_INVALID_REQUEST)
  668.      *
  669.      * @access  public
  670.      * @return  mixed   Returns true on success or PEAR_Error on failure.
  671.      * @param   bool    $autoSetContentDisposition Whether to set the
  672.      *                   Content-Disposition header if it isn't already.
  673.      */
  674.     function send($autoSetContentDisposition = true)
  675.     {
  676.         $error $this->_getError();
  677.         if ($error !== null{
  678.             return $error;
  679.         }
  680.         if (headers_sent()) {
  681.             return PEAR::raiseError(
  682.                 'Headers already sent.',
  683.                 HTTP_DOWNLOAD_E_HEADERS_SENT
  684.             );
  685.         }
  686.  
  687.         if (!ini_get('safe_mode')) {
  688.             @set_time_limit(0);
  689.         }
  690.  
  691.         if ($autoSetContentDisposition &&
  692.             !isset($this->headers['Content-Disposition'])) {
  693.             $this->setContentDisposition();
  694.         }
  695.  
  696.         if ($this->cache{
  697.             $this->headers['ETag'$this->generateETag();
  698.             if ($this->isCached()) {
  699.                 $this->HTTP->sendStatusCode(304);
  700.                 $this->sendHeaders();
  701.                 return true;
  702.             }
  703.         else {
  704.             unset($this->headers['Last-Modified']);
  705.         }
  706.  
  707.         if (ob_get_level()) {
  708.             while (@ob_end_clean());
  709.         }
  710.  
  711.         if ($this->gzip{
  712.             @ob_start('ob_gzhandler');
  713.         else {
  714.             ob_start();
  715.         }
  716.  
  717.         $this->sentBytes = 0;
  718.  
  719.         // Known content length?
  720.         $end ($this->size >= 0max($this->size - 10'*';
  721.  
  722.         if ($end != '*' && $this->isRangeRequest()) {
  723.              $chunks $this->getChunks();
  724.             if (empty($chunks)) {
  725.                 $this->HTTP->sendStatusCode(200);
  726.                 $chunks = array(array(0$end));
  727.  
  728.             elseif (PEAR::isError($chunks)) {
  729.                 ob_end_clean();
  730.                 $this->HTTP->sendStatusCode(416);
  731.                 return $chunks;
  732.  
  733.             else {
  734.                 $this->HTTP->sendStatusCode(206);
  735.             }
  736.         else {
  737.             $this->HTTP->sendStatusCode(200);
  738.             $chunks = array(array(0$end));
  739.             if (!$this->gzip && count(ob_list_handlers()) < 2 && $end != '*'{
  740.                 $this->headers['Content-Length'$this->size;
  741.             }
  742.         }
  743.  
  744.         $this->sendChunks($chunks);
  745.  
  746.         ob_end_flush();
  747.         flush();
  748.         return true;
  749.     }
  750.  
  751.     /**
  752.      * Static send
  753.      *
  754.      * @see     HTTP_Download::HTTP_Download()
  755.      * @see     HTTP_Download::send()
  756.      *
  757.      * @static
  758.      * @access  public
  759.      * @return  mixed   Returns true on success or PEAR_Error on failure.
  760.      * @param   array   $params     associative array of parameters
  761.      * @param   bool    $guess      whether HTTP_Download::guessContentType()
  762.      *                                should be called
  763.      */
  764.     function staticSend($params$guess = false)
  765.     {
  766.         $d &new HTTP_Download();
  767.         $e $d->setParams($params);
  768.         if (PEAR::isError($e)) {
  769.             return $e;
  770.         }
  771.         if ($guess{
  772.             $e $d->guessContentType();
  773.             if (PEAR::isError($e)) {
  774.                 return $e;
  775.             }
  776.         }
  777.         return $d->send();
  778.     }
  779.  
  780.     /**
  781.      * Send a bunch of files or directories as an archive
  782.      *
  783.      * Example:
  784.      * <code>
  785.      *  require_once 'HTTP/Download.php';
  786.      *  HTTP_Download::sendArchive(
  787.      *      'myArchive.tgz',
  788.      *      '/var/ftp/pub/mike',
  789.      *      HTTP_DOWNLOAD_TGZ,
  790.      *      '',
  791.      *      '/var/ftp/pub'
  792.      *  );
  793.      * </code>
  794.      *
  795.      * @see         Archive_Tar::createModify()
  796.      * @deprecated  use HTTP_Download_Archive::send()
  797.      * @static
  798.      * @access  public
  799.      * @return  mixed   Returns true on success or PEAR_Error on failure.
  800.      * @param   string  $name       name the sent archive should have
  801.      * @param   mixed   $files      files/directories
  802.      * @param   string  $type       archive type
  803.      * @param   string  $add_path   path that should be prepended to the files
  804.      * @param   string  $strip_path path that should be stripped from the files
  805.      */
  806.     function sendArchive(   $name,
  807.                             $files,
  808.                             $type       = HTTP_DOWNLOAD_TGZ,
  809.                             $add_path   '',
  810.                             $strip_path '')
  811.     {
  812.         require_once 'HTTP/Download/Archive.php';
  813.         return HTTP_Download_Archive::send($name$files$type,
  814.             $add_path$strip_path);
  815.     }
  816.     // }}}
  817.  
  818.     // {{{ protected methods
  819.     /**
  820.      * Generate ETag
  821.      *
  822.      * @access  protected
  823.      * @return  string 
  824.      */
  825.     function generateETag()
  826.     {
  827.         if (!$this->etag{
  828.             if ($this->data{
  829.                 $md5 md5($this->data);
  830.             else {
  831.                 $mtime time();
  832.                 $ino   = 0;
  833.                 $size  mt_rand();
  834.                 extract(is_resource($this->handlefstat($this->handle)
  835.                                                    : stat($this->file));
  836.                 $md5 md5($mtime .'='$ino .'='$size);
  837.             }
  838.             $this->etag '"' $md5 '-' crc32($md5'"';
  839.         }
  840.         return $this->etag;
  841.     }
  842.  
  843.     /**
  844.      * Send multiple chunks
  845.      *
  846.      * @access  protected
  847.      * @return  mixed   Returns true on success or PEAR_Error on failure.
  848.      * @param   array   $chunks 
  849.      */
  850.     function sendChunks($chunks)
  851.     {
  852.         if (count($chunks== 1{
  853.             return $this->sendChunk(current($chunks));
  854.         }
  855.  
  856.         $bound uniqid('HTTP_DOWNLOAD-'true);
  857.         $cType $this->headers['Content-Type'];
  858.         $this->headers['Content-Type'=
  859.             'multipart/byteranges; boundary=' $bound;
  860.         $this->sendHeaders();
  861.         foreach ($chunks as $chunk){
  862.             $this->sendChunk($chunk$cType$bound);
  863.         }
  864.         #echo "\r\n--$bound--\r\n";
  865.         return true;
  866.     }
  867.  
  868.     /**
  869.      * Send chunk of data
  870.      *
  871.      * @access  protected
  872.      * @return  mixed   Returns true on success or PEAR_Error on failure.
  873.      * @param   array   $chunk  start and end offset of the chunk to send
  874.      * @param   string  $cType  actual content type
  875.      * @param   string  $bound  boundary for multipart/byteranges
  876.      */
  877.     function sendChunk($chunk$cType = null$bound = null)
  878.     {
  879.         list($offset$lastbyte$chunk;
  880.         $length ($lastbyte $offset+ 1;
  881.  
  882.         $range $offset '-' $lastbyte '/'
  883.                  . (($this->size >= 0$this->size '*');
  884.  
  885.         if (isset($cType$bound)) {
  886.             echo    "\r\n--$bound\r\n",
  887.                     "Content-Type: $cType\r\n",
  888.                     "Content-Range: bytes $range\r\n\r\n";
  889.         else {
  890.             if ($lastbyte != '*' && $this->isRangeRequest()) {
  891.                 $this->headers['Content-Length'$length;
  892.                 $this->headers['Content-Range''bytes '$range;
  893.             }
  894.             $this->sendHeaders();
  895.         }
  896.  
  897.         if ($this->data{
  898.             while (($length -= $this->bufferSize> 0{
  899.                 $this->flush(substr($this->data$offset$this->bufferSize));
  900.                 $this->throttleDelay and $this->sleep();
  901.                 $offset += $this->bufferSize;
  902.             }
  903.             if ($length{
  904.                 $this->flush(substr($this->data$offset$this->bufferSize $length));
  905.             }
  906.         else {
  907.             if (!is_resource($this->handle)) {
  908.                 $this->handle fopen($this->file'rb');
  909.             }
  910.             fseek($this->handle$offset);
  911.             if ($lastbyte == '*'{
  912.                 while (!feof($this->handle)) {
  913.                     $this->flush(fread($this->handle$this->bufferSize));
  914.                     $this->throttleDelay and $this->sleep();
  915.                 }
  916.             else {
  917.                 while (($length -= $this->bufferSize> 0{
  918.                     $this->flush(fread($this->handle$this->bufferSize));
  919.                     $this->throttleDelay and $this->sleep();
  920.                 }
  921.                 if ($length{
  922.                     $this->flush(fread($this->handle$this->bufferSize $length));
  923.                 }
  924.              }
  925.          }
  926.          return true;
  927.     }
  928.  
  929.     /**
  930.      * Get chunks to send
  931.      *
  932.      * @access  protected
  933.      * @return  array Chunk list or PEAR_Error on invalid range request
  934.      * @link    http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
  935.      */
  936.     function getChunks()
  937.     {
  938.         $end ($this->size >= 0max($this->size - 10'*';
  939.  
  940.         // Trying to handle ranges on content with unknown length is too
  941.         // big of a mess (impossible to determine if a range is valid)
  942.         if ($end == '*'{
  943.             return array();
  944.         }
  945.  
  946.         $ranges $this->getRanges();
  947.         if (empty($ranges)) {
  948.             return array();
  949.         }
  950.  
  951.         $parts = array();
  952.         $satisfiable = false;
  953.         foreach (explode(','$rangesas $chunk){
  954.             list($o$eexplode('-'trim($chunk));
  955.  
  956.             // If the last-byte-pos value is present, it MUST be greater than
  957.             // or equal to the first-byte-pos in that byte-range-spec, or the
  958.             // byte- range-spec is syntactically invalid. The recipient of a
  959.             // byte-range- set that includes one or more syntactically invalid
  960.             // byte-range-spec values MUST ignore the header field that
  961.             // includes that byte-range- set.
  962.             if ($e !== '' && $o !== '' && $e $o{
  963.                 return array();
  964.             }
  965.  
  966.             // If the last-byte-pos value is absent, or if the value is
  967.             // greater than or equal to the current length of the entity-body,
  968.             // last-byte-pos is taken to be equal to one less than the current
  969.             // length of the entity- body in bytes.
  970.             if ($e === '' || $e $end{
  971.                 $e $end;
  972.             }
  973.  
  974.             // A suffix-byte-range-spec is used to specify the suffix of the
  975.             // entity-body, of a length given by the suffix-length value. (That
  976.             // is, this form specifies the last N bytes of an entity-body.) If
  977.             // the entity is shorter than the specified suffix-length, the
  978.             // entire entity-body is used.
  979.             if ($o === ''{
  980.                 // If a syntactically valid byte-range-set includes at least
  981.                 // one suffix-byte-range-spec with a non-zero suffix-length,
  982.                 // then the byte-range-set is satisfiable.
  983.                 $satisfiable |= ($e != 0);
  984.  
  985.                 $o max($this->size $e0);
  986.                 $e $end;
  987.  
  988.             elseif ($o <= $end{
  989.                 // If a syntactically valid byte-range-set includes at least
  990.                 // one byte- range-spec whose first-byte-pos is less than the
  991.                 // current length of the entity-body, then the byte-range-set
  992.                 // is satisfiable.
  993.                 $satisfiable = true;
  994.             else {
  995.                 continue;
  996.             }
  997.  
  998.             $parts[= array($o$e);
  999.         }
  1000.  
  1001.         // If the byte-range-set is unsatisfiable, the server SHOULD return a
  1002.         // response with a status of 416 (Requested range not satisfiable).
  1003.         if (!$satisfiable{
  1004.             $error = PEAR::raiseError('Error processing range request',
  1005.                                       HTTP_DOWNLOAD_E_INVALID_REQUEST);
  1006.             return $error;
  1007.         }
  1008.         //$this->sortChunks($parts);
  1009.         return $this->mergeChunks($parts);
  1010.     }
  1011.  
  1012.     /**
  1013.      * Sorts the ranges to be in ascending order
  1014.      *
  1015.      * @param array &$chunks ranges to sort
  1016.      *
  1017.      * @return void 
  1018.      * @access protected
  1019.      * @static
  1020.      * @author Philippe Jausions <jausions@php.net>
  1021.      */
  1022.     function sortChunks(&$chunks)
  1023.     {
  1024.         $sortFunc create_function('$a,$b',
  1025.             'if ($a[0] == $b[0]) {
  1026.                 if ($a[1] == $b[1]) {
  1027.                     return 0;
  1028.                 }
  1029.                 return (($a[1] != "*" && $a[1] < $b[1])
  1030.                         || $b[1] == "*") ? -1 : 1;
  1031.              }
  1032.  
  1033.              return ($a[0] < $b[0]) ? -1 : 1;');
  1034.  
  1035.         usort($chunks$sortFunc);
  1036.     }
  1037.  
  1038.     /**
  1039.      * Merges consecutive chunks to avoid overlaps
  1040.      *
  1041.      * @param array $chunks Ranges to merge
  1042.      *
  1043.      * @return array merged ranges
  1044.      * @access protected
  1045.      * @static
  1046.      * @author Philippe Jausions <jausions@php.net>
  1047.      */
  1048.     function mergeChunks($chunks)
  1049.     {
  1050.         do {
  1051.             $count count($chunks);
  1052.             $merged = array(current($chunks));
  1053.             $j = 0;
  1054.             for ($i = 1; $i count($chunks); ++$i{
  1055.                 list($o$e$chunks[$i];
  1056.                 if ($merged[$j][1== '*'{
  1057.                     if ($merged[$j][0<= $o{
  1058.                         continue;
  1059.                     elseif ($e == '*' || $merged[$j][0<= $e{
  1060.                         $merged[$j][0min($merged[$j][0]$o);
  1061.                     else {
  1062.                         $merged[++$j$chunks[$i];
  1063.                     }
  1064.                 elseif ($merged[$j][0<= $o && $o <= $merged[$j][1]{
  1065.                     $merged[$j][1($e == '*''*' max($e$merged[$j][1]);
  1066.                 elseif ($merged[$j][0<= $e && $e <= $merged[$j][1]{
  1067.                     $merged[$j][0min($o$merged[$j][0]);
  1068.                 else {
  1069.                     $merged[++$j$chunks[$i];
  1070.                 }
  1071.             }
  1072.             if ($count == count($merged)) {
  1073.                 break;
  1074.             }
  1075.             $chunks $merged;
  1076.         while (true);
  1077.         return $merged;
  1078.     }
  1079.  
  1080.     /**
  1081.      * Check if range is requested
  1082.      *
  1083.      * @access  protected
  1084.      * @return  bool 
  1085.      */
  1086.     function isRangeRequest()
  1087.     {
  1088.         if (!isset($_SERVER['HTTP_RANGE']|| !count($this->getRanges())) {
  1089.             return false;
  1090.         }
  1091.         return $this->isValidRange();
  1092.     }
  1093.  
  1094.     /**
  1095.      * Get range request
  1096.      *
  1097.      * @access  protected
  1098.      * @return  array 
  1099.      */
  1100.     function getRanges()
  1101.     {
  1102.         return preg_match('/^bytes=((\d+-|\d+-\d+|-\d+)(, ?(\d+-|\d+-\d+|-\d+))*)$/',
  1103.             @$_SERVER['HTTP_RANGE']$matches$matches[1: array();
  1104.     }
  1105.  
  1106.     /**
  1107.      * Check if entity is cached
  1108.      *
  1109.      * @access  protected
  1110.      * @return  bool 
  1111.      */
  1112.     function isCached()
  1113.     {
  1114.         return (
  1115.             (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']&&
  1116.             $this->lastModified == strtotime(current($a explode(
  1117.                 ';'$_SERVER['HTTP_IF_MODIFIED_SINCE'])))) ||
  1118.             (isset($_SERVER['HTTP_IF_NONE_MATCH']&&
  1119.             $this->compareAsterisk('HTTP_IF_NONE_MATCH'$this->etag))
  1120.         );
  1121.     }
  1122.  
  1123.     /**
  1124.      * Check if entity hasn't changed
  1125.      *
  1126.      * @access  protected
  1127.      * @return  bool 
  1128.      */
  1129.     function isValidRange()
  1130.     {
  1131.         if (isset($_SERVER['HTTP_IF_MATCH']&&
  1132.             !$this->compareAsterisk('HTTP_IF_MATCH'$this->etag)) {
  1133.             return false;
  1134.         }
  1135.         if (isset($_SERVER['HTTP_IF_RANGE']&&
  1136.                   $_SERVER['HTTP_IF_RANGE'!== $this->etag &&
  1137.                   strtotime($_SERVER['HTTP_IF_RANGE']!== $this->lastModified{
  1138.             return false;
  1139.         }
  1140.         if (isset($_SERVER['HTTP_IF_UNMODIFIED_SINCE'])) {
  1141.             $lm current($a explode(';'$_SERVER['HTTP_IF_UNMODIFIED_SINCE']));
  1142.             if (strtotime($lm!== $this->lastModified{
  1143.                 return false;
  1144.             }
  1145.         }
  1146.         if (isset($_SERVER['HTTP_UNLESS_MODIFIED_SINCE'])) {
  1147.             $lm current($a explode(';'$_SERVER['HTTP_UNLESS_MODIFIED_SINCE']));
  1148.             if (strtotime($lm!== $this->lastModified{
  1149.                 return false;
  1150.             }
  1151.         }
  1152.         return true;
  1153.     }
  1154.  
  1155.     /**
  1156.      * Compare against an asterisk or check for equality
  1157.      *
  1158.      * @access  protected
  1159.      * @return  bool 
  1160.      * @param   string  key for the $_SERVER array
  1161.      * @param   string  string to compare
  1162.      */
  1163.     function compareAsterisk($svar$compare)
  1164.     {
  1165.         foreach (array_map('trim'explode(','$_SERVER[$svar])) as $request{
  1166.             if ($request === '*' || $request === $compare{
  1167.                 return true;
  1168.             }
  1169.         }
  1170.         return false;
  1171.     }
  1172.  
  1173.     /**
  1174.      * Send HTTP headers
  1175.      *
  1176.      * @access  protected
  1177.      * @return  void 
  1178.      */
  1179.     function sendHeaders()
  1180.     {
  1181.         foreach ($this->headers as $header => $value{
  1182.             $this->HTTP->setHeader($header$value);
  1183.         }
  1184.         $this->HTTP->sendHeaders();
  1185.         /* NSAPI won't output anything if we did this */
  1186.         if (strncasecmp(PHP_SAPI'nsapi'5)) {
  1187.             if (ob_get_level()) {
  1188.                 ob_flush();
  1189.             }
  1190.             flush();
  1191.         }
  1192.     }
  1193.  
  1194.     /**
  1195.      * Flush
  1196.      *
  1197.      * @access  protected
  1198.      * @return  void 
  1199.      * @param   string  $data 
  1200.      */
  1201.     function flush($data '')
  1202.     {
  1203.         if ($dlen strlen($data)) {
  1204.             $this->sentBytes += $dlen;
  1205.             echo $data;
  1206.         }
  1207.         ob_flush();
  1208.         flush();
  1209.     }
  1210.  
  1211.     /**
  1212.      * Sleep
  1213.      *
  1214.      * @access  protected
  1215.      * @return  void 
  1216.      */
  1217.     function sleep()
  1218.     {
  1219.         if (OS_WINDOWS{
  1220.             com_message_pump($this->throttleDelay);
  1221.         else {
  1222.             usleep($this->throttleDelay * 1000);
  1223.         }
  1224.     }
  1225.  
  1226.     /**
  1227.      * Returns and clears startup error
  1228.      *
  1229.      * @return NULL|PEAR_Errorstartup error if one exists
  1230.      * @access protected
  1231.      */
  1232.     function _getError()
  1233.     {
  1234.         $error = null;
  1235.         if (PEAR::isError($this->_error)) {
  1236.             $error $this->_error;
  1237.             $this->_error = null;
  1238.         }
  1239.         return $error;
  1240.     }
  1241.     // }}}
  1242. }
  1243. ?>

Documentation generated on Fri, 15 Oct 2010 15:00:18 +0000 by phpDocumentor 1.4.3. PEAR Logo Copyright © PHP Group 2004.