* @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. * * ATTENTION: * 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 * one of: * * and any of: * * * '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. * * * Array( * 'throttledelay' => 1, * '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. * * @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 * * Example: * * $HTTP_Download->setContentDisposition( * HTTP_DOWNLOAD_ATTACHMENT, * 'download.tgz' * ); * */ 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: * * require_once 'HTTP/Download.php'; * HTTP_Download::sendArchive( * 'myArchive.tgz', * '/var/ftp/pub/mike', * HTTP_DOWNLOAD_TGZ, * '', * '/var/ftp/pub' * ); * * * @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 */ 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 */ 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; } // }}} } ?>