From 758447cfa419f1a11bc022ef8168447992364e52 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Fri, 13 Mar 2009 20:02:47 +0100 Subject: Support for multirange requests for media FS#1630 Ignore-this: 50de569608231b910a62327d2f3af1de This patch moves all HTTP sending related functions to inc/httputils.php Handling of range requests was rewritten completely to support mutirange requests. This should fix problems with Adobe Reader but needs testing. darcs-hash:20090313190247-7ad00-e6ec1f81acb9f7ac651357dd034c2689aea6868d.gz --- feed.php | 1 + inc/httputils.php | 198 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ inc/pageutils.php | 92 ------------------------- lib/exe/css.php | 1 + lib/exe/fetch.php | 56 +-------------- lib/exe/js.php | 1 + 6 files changed, 203 insertions(+), 146 deletions(-) create mode 100644 inc/httputils.php diff --git a/feed.php b/feed.php index 7150a8e1e..dd790fec5 100644 --- a/feed.php +++ b/feed.php @@ -14,6 +14,7 @@ require_once(DOKU_INC.'inc/feedcreator.class.php'); require_once(DOKU_INC.'inc/auth.php'); require_once(DOKU_INC.'inc/pageutils.php'); + require_once(DOKU_INC.'inc/httputils.php'); //close session session_write_close(); diff --git a/inc/httputils.php b/inc/httputils.php new file mode 100644 index 000000000..271b8272f --- /dev/null +++ b/inc/httputils.php @@ -0,0 +1,198 @@ + + */ + +define('HTTP_MULTIPART_BOUNDARY','D0KuW1K1B0uNDARY'); +define('HTTP_HEADER_LF',"\r\n"); +define('HTTP_CHUNK_SIZE',16*1024); + +/** + * Checks and sets HTTP headers for conditional HTTP requests + * + * @author Simon Willison + * @link http://simon.incutio.com/archive/2003/04/23/conditionalGet + * @param timestamp $timestamp lastmodified time of the cache file + * @returns void or exits with previously header() commands executed + */ +function http_conditionalRequest($timestamp){ + // A PHP implementation of conditional get, see + // http://fishbowl.pastiche.org/archives/001132.html + $last_modified = substr(gmdate('r', $timestamp), 0, -5).'GMT'; + $etag = '"'.md5($last_modified).'"'; + // Send the headers + header("Last-Modified: $last_modified"); + header("ETag: $etag"); + // See if the client has provided the required headers + if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])){ + $if_modified_since = stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']); + }else{ + $if_modified_since = false; + } + + if (isset($_SERVER['HTTP_IF_NONE_MATCH'])){ + $if_none_match = stripslashes($_SERVER['HTTP_IF_NONE_MATCH']); + }else{ + $if_none_match = false; + } + + if (!$if_modified_since && !$if_none_match){ + return; + } + + // At least one of the headers is there - check them + if ($if_none_match && $if_none_match != $etag) { + return; // etag is there but doesn't match + } + + if ($if_modified_since && $if_modified_since != $last_modified) { + return; // if-modified-since is there but doesn't match + } + + // Nothing has changed since their last request - serve a 304 and exit + header('HTTP/1.0 304 Not Modified'); + + // don't produce output, even if compression is on + ob_end_clean(); + exit; +} + +/** + * Let the webserver send the given file vi x-sendfile method + * + * @author Chris Smith + * @returns void or exits with previously header() commands executed + */ +function http_sendfile($file) { + global $conf; + + //use x-sendfile header to pass the delivery to compatible webservers + if($conf['xsendfile'] == 1){ + header("X-LIGHTTPD-send-file: $file"); + ob_end_clean(); + exit; + }elseif($conf['xsendfile'] == 2){ + header("X-Sendfile: $file"); + ob_end_clean(); + exit; + }elseif($conf['xsendfile'] == 3){ + header("X-Accel-Redirect: $file"); + ob_end_clean(); + exit; + } + + return false; +} + +/** + * Send file contents supporting rangeRequests + * + * This function exits the running script + * + * @param ressource $fh - file handle for an already open file + * @param int $size - size of the whole file + * @param int $mime - MIME type of the file + * + * @author Andreas Gohr + */ +function http_rangeRequest($fh,$size,$mime){ + $ranges = array(); + $isrange = false; + + header('Accept-Ranges: bytes'); + + if(!isset($_SERVER['HTTP_RANGE'])){ + // no range requested - send the whole file + $ranges[] = array(0,$size,$size); + }else{ + $t = explode('=', $_SERVER['HTTP_RANGE']); + if (!$t[0]=='bytes') { + // we only understand byte ranges - send the whole file + $ranges[] = array(0,$size,$size); + }else{ + $isrange = true; + // handle multiple ranges + $r = explode(',',$t[1]); + foreach($r as $x){ + $p = explode('-', $x); + $start = (int)$p[0]; + $end = (int)$p[1]; + if (!$end) $end = $size - 1; + if ($start > $end || $start > $size || $end > $size){ + header('HTTP/1.1 416 Requested Range Not Satisfiable'); + print 'Bad Range Request!'; + exit; + } + $len = $end - $start + 1; + $ranges[] = array($start,$end,$len); + } + } + } + $parts = count($ranges); + + // now send the type and length headers + if(!$isrange){ + header("Content-Type: $mime",true); + }else{ + header('HTTP/1.1 206 Partial Content'); + if($parts == 1){ + header("Content-Type: $mime",true); + }else{ + header('Content-Type: multipart/byteranges; boundary='.HTTP_MULTIPART_BOUNDARY,true); + } + } + + // send all ranges + for($i=0; $i<$parts; $i++){ + list($start,$end,$len) = $ranges[$i]; + + // multipart or normal headers + if($parts > 1){ + echo HTTP_HEADER_LF.'--'.HTTP_MULTIPART_BOUNDARY.HTTP_HEADER_LF; + echo "Content-Type: $mime".HTTP_HEADER_LF; + echo "Content-Range: bytes $start-$end/$size".HTTP_HEADER_LF; + }else{ + header("Content-Length: $len"); + if($isrange){ + header("Content-Range: bytes $start-$end/$size"); + } + } + + // send file content + fseek($fh,$start); //seek to start of range + $chunk = ($len > HTTP_CHUNK_SIZE) ? HTTP_CHUNK_SIZE : $len; + while (!feof($fh) && $chunk > 0) { + @set_time_limit(30); // large files can take a lot of time + print fread($fh, $chunk); + flush(); + $len -= $chunk; + $chunk = ($len > HTTP_CHUNK_SIZE) ? HTTP_CHUNK_SIZE : $len; + } + } + if($parts > 1){ + echo HTTP_HEADER_LF.'--'.HTTP_MULTIPART_BOUNDARY.'--'.HTTP_HEADER_LF; + } + + // everything should be done here, exit + exit; +} + +/** + * Check for a gzipped version and create if necessary + * + * return true if there exists a gzip version of the uncompressed file + * (samepath/samefilename.sameext.gz) created after the uncompressed file + * + * @author Chris Smith + */ +function http_gzip_valid($uncompressed_file) { + $gzip = $uncompressed_file.'.gz'; + if (filemtime($gzip) < filemtime($uncompressed_file)) { // filemtime returns false (0) if file doesn't exist + return copy($uncompressed_file, 'compress.zlib://'.$gzip); + } + + return true; +} diff --git a/inc/pageutils.php b/inc/pageutils.php index 105cfa18e..872191d12 100644 --- a/inc/pageutils.php +++ b/inc/pageutils.php @@ -528,97 +528,5 @@ function isVisiblePage($id){ return !isHiddenPage($id); } -/** - * Checks and sets HTTP headers for conditional HTTP requests - * - * @author Simon Willison - * @link http://simon.incutio.com/archive/2003/04/23/conditionalGet - * @param timestamp $timestamp lastmodified time of the cache file - * @returns void or exits with previously header() commands executed - */ -function http_conditionalRequest($timestamp){ - // A PHP implementation of conditional get, see - // http://fishbowl.pastiche.org/archives/001132.html - $last_modified = substr(gmdate('r', $timestamp), 0, -5).'GMT'; - $etag = '"'.md5($last_modified).'"'; - // Send the headers - header("Last-Modified: $last_modified"); - header("ETag: $etag"); - // See if the client has provided the required headers - if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])){ - $if_modified_since = stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']); - }else{ - $if_modified_since = false; - } - - if (isset($_SERVER['HTTP_IF_NONE_MATCH'])){ - $if_none_match = stripslashes($_SERVER['HTTP_IF_NONE_MATCH']); - }else{ - $if_none_match = false; - } - - if (!$if_modified_since && !$if_none_match){ - return; - } - - // At least one of the headers is there - check them - if ($if_none_match && $if_none_match != $etag) { - return; // etag is there but doesn't match - } - - if ($if_modified_since && $if_modified_since != $last_modified) { - return; // if-modified-since is there but doesn't match - } - - // Nothing has changed since their last request - serve a 304 and exit - header('HTTP/1.0 304 Not Modified'); - - // don't produce output, even if compression is on - ob_end_clean(); - exit; -} -/** - * Let the webserver send the given file vi x-sendfile method - * - * @author Chris Smith - * @returns void or exits with previously header() commands executed - */ -function http_sendfile($file) { - global $conf; - - //use x-sendfile header to pass the delivery to compatible webservers - if($conf['xsendfile'] == 1){ - header("X-LIGHTTPD-send-file: $file"); - ob_end_clean(); - exit; - }elseif($conf['xsendfile'] == 2){ - header("X-Sendfile: $file"); - ob_end_clean(); - exit; - }elseif($conf['xsendfile'] == 3){ - header("X-Accel-Redirect: $file"); - ob_end_clean(); - exit; - } - - return false; -} - -/** - * Check for a gzipped version and create if necessary - * - * return true if there exists a gzip version of the uncompressed file - * (samepath/samefilename.sameext.gz) created after the uncompressed file - * - * @author Chris Smith - */ -function http_gzip_valid($uncompressed_file) { - $gzip = $uncompressed_file.'.gz'; - if (filemtime($gzip) < filemtime($uncompressed_file)) { // filemtime returns false (0) if file doesn't exist - return copy($uncompressed_file, 'compress.zlib://'.$gzip); - } - - return true; -} //Setup VIM: ex: et ts=2 enc=utf-8 : diff --git a/lib/exe/css.php b/lib/exe/css.php index 7fa56f4cc..2517db38d 100644 --- a/lib/exe/css.php +++ b/lib/exe/css.php @@ -11,6 +11,7 @@ if(!defined('NOSESSION')) define('NOSESSION',true); // we do not use a session o if(!defined('DOKU_DISABLE_GZIP_OUTPUT')) define('DOKU_DISABLE_GZIP_OUTPUT',1); // we gzip ourself here require_once(DOKU_INC.'inc/init.php'); require_once(DOKU_INC.'inc/pageutils.php'); +require_once(DOKU_INC.'inc/httputils.php'); require_once(DOKU_INC.'inc/io.php'); require_once(DOKU_INC.'inc/confutils.php'); diff --git a/lib/exe/fetch.php b/lib/exe/fetch.php index ddfff4b4e..728b0b448 100644 --- a/lib/exe/fetch.php +++ b/lib/exe/fetch.php @@ -12,12 +12,12 @@ require_once(DOKU_INC.'inc/common.php'); require_once(DOKU_INC.'inc/media.php'); require_once(DOKU_INC.'inc/pageutils.php'); + require_once(DOKU_INC.'inc/httputils.php'); require_once(DOKU_INC.'inc/confutils.php'); require_once(DOKU_INC.'inc/auth.php'); //close sesseion session_write_close(); - if(!defined('CHUNK_SIZE')) define('CHUNK_SIZE',16*1024); $mimetypes = getMimeTypes(); @@ -139,68 +139,16 @@ function sendFile($file,$mime,$dl,$cache){ //use x-sendfile header to pass the delivery to compatible webservers if (http_sendfile($file)) exit; - //support download continueing - header('Accept-Ranges: bytes'); - list($start,$len) = http_rangeRequest(filesize($file)); - // send file contents $fp = @fopen($file,"rb"); if($fp){ - fseek($fp,$start); //seek to start of range - - $chunk = ($len > CHUNK_SIZE) ? CHUNK_SIZE : $len; - while (!feof($fp) && $chunk > 0) { - @set_time_limit(30); // large files can take a lot of time - print fread($fp, $chunk); - flush(); - $len -= $chunk; - $chunk = ($len > CHUNK_SIZE) ? CHUNK_SIZE : $len; - } - fclose($fp); + http_rangeRequest($fp,filesize($file),$mime); }else{ header("HTTP/1.0 500 Internal Server Error"); print "Could not read $file - bad permissions?"; } } -/** - * Checks and sets headers to handle range requets - * - * @author Andreas Gohr - * @returns array The start byte and the amount of bytes to send - */ -function http_rangeRequest($size){ - if(!isset($_SERVER['HTTP_RANGE'])){ - // no range requested - send the whole file - header("Content-Length: $size"); - return array(0,$size); - } - - $t = explode('=', $_SERVER['HTTP_RANGE']); - if (!$t[0]=='bytes') { - // we only understand byte ranges - send the whole file - header("Content-Length: $size"); - return array(0,$size); - } - - $r = explode('-', $t[1]); - $start = (int)$r[0]; - $end = (int)$r[1]; - if (!$end) $end = $size - 1; - if ($start > $end || $start > $size || $end > $size){ - header('HTTP/1.1 416 Requested Range Not Satisfiable'); - print 'Bad Range Request!'; - exit; - } - - $tot = $end - $start + 1; - header('HTTP/1.1 206 Partial Content'); - header("Content-Range: bytes {$start}-{$end}/{$size}"); - header("Content-Length: $tot"); - - return array($start,$tot); -} - /** * Returns the wanted cachetime in seconds * diff --git a/lib/exe/js.php b/lib/exe/js.php index 7746edcd9..7ba777928 100644 --- a/lib/exe/js.php +++ b/lib/exe/js.php @@ -12,6 +12,7 @@ if(!defined('NL')) define('NL',"\n"); if(!defined('DOKU_DISABLE_GZIP_OUTPUT')) define('DOKU_DISABLE_GZIP_OUTPUT',1); // we gzip ourself here require_once(DOKU_INC.'inc/init.php'); require_once(DOKU_INC.'inc/pageutils.php'); +require_once(DOKU_INC.'inc/httputils.php'); require_once(DOKU_INC.'inc/io.php'); require_once(DOKU_INC.'inc/JSON.php'); -- cgit v1.2.3