X-Git-Url: http://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2FStreamFile.php;h=0fc79802f3de82d4ec49ffcda0f4df252627f950;hb=35e7f1c71750489f1bf8725c155fcd3774cdaef3;hp=a52b25b02f9d217c4183418e6a3d3ba4d473552e;hpb=badc035712ded02e8ec7ee4c5e8a0fe09e2811d2;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/StreamFile.php b/includes/StreamFile.php index a52b25b02f..0fc79802f3 100644 --- a/includes/StreamFile.php +++ b/includes/StreamFile.php @@ -24,8 +24,10 @@ * Functions related to the output of file content */ class StreamFile { - const READY_STREAM = 1; - const NOT_MODIFIED = 2; + // Do not send any HTTP headers unless requested by caller (e.g. body only) + const STREAM_HEADLESS = 1; + // Do not try to tear down any PHP output buffers + const STREAM_ALLOW_OB = 2; /** * Stream a file to the browser, adding all the headings and fun stuff. @@ -33,107 +35,183 @@ class StreamFile { * and Content-Disposition. * * @param string $fname Full name and path of the file to stream - * @param array $headers Any additional headers to send + * @param array $headers Any additional headers to send if the file exists * @param bool $sendErrors Send error messages if errors occur (like 404) + * @param array $optHeaders HTTP request header map (e.g. "range") (use lowercase keys) + * @param integer $flags Bitfield of STREAM_* constants * @throws MWException * @return bool Success */ - public static function stream( $fname, $headers = array(), $sendErrors = true ) { + public static function stream( + $fname, $headers = [], $sendErrors = true, $optHeaders = [], $flags = 0 + ) { + $section = new ProfileSection( __METHOD__ ); if ( FileBackend::isStoragePath( $fname ) ) { // sanity throw new MWException( __FUNCTION__ . " given storage path '$fname'." ); } - wfSuppressWarnings(); - $stat = stat( $fname ); - wfRestoreWarnings(); - - $res = self::prepareForStream( $fname, $stat, $headers, $sendErrors ); - if ( $res == self::NOT_MODIFIED ) { - $ok = true; // use client cache - } elseif ( $res == self::READY_STREAM ) { - $ok = readfile( $fname ); - } else { - $ok = false; // failed + // Don't stream it out as text/html if there was a PHP error + if ( ( ( $flags & self::STREAM_HEADLESS ) == 0 || $headers ) && headers_sent() ) { + echo "Headers already sent, terminating.\n"; + return false; } - return $ok; - } + $headerFunc = ( $flags & self::STREAM_HEADLESS ) + ? function ( $header ) { + // no-op + } + : function ( $header ) { + is_int( $header ) ? HttpStatus::header( $header ) : header( $header ); + }; + + MediaWiki\suppressWarnings(); + $info = stat( $fname ); + MediaWiki\restoreWarnings(); - /** - * Call this function used in preparation before streaming a file. - * This function does the following: - * (a) sends Last-Modified, Content-type, and Content-Disposition headers - * (b) cancels any PHP output buffering and automatic gzipping of output - * (c) sends Content-Length header based on HTTP_IF_MODIFIED_SINCE check - * - * @param string $path Storage path or file system path - * @param array|bool $info File stat info with 'mtime' and 'size' fields - * @param array $headers Additional headers to send - * @param bool $sendErrors Send error messages if errors occur (like 404) - * @return int|bool READY_STREAM, NOT_MODIFIED, or false on failure - */ - public static function prepareForStream( - $path, $info, $headers = array(), $sendErrors = true - ) { if ( !is_array( $info ) ) { if ( $sendErrors ) { - header( 'HTTP/1.0 404 Not Found' ); - header( 'Cache-Control: no-cache' ); - header( 'Content-Type: text/html; charset=utf-8' ); - $encFile = htmlspecialchars( $path ); - $encScript = htmlspecialchars( $_SERVER['SCRIPT_NAME'] ); - echo " -

File not found

-

Although this PHP script ($encScript) exists, the file requested for output - ($encFile) does not.

- - "; + self::send404Message( $fname, $flags ); } return false; } - // Sent Last-Modified HTTP header for client-side caching - header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $info['mtime'] ) ); + // Send Last-Modified HTTP header for client-side caching + $headerFunc( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $info['mtime'] ) ); - // Cancel output buffering and gzipping if set - wfResetOutputBuffers(); + if ( ( $flags & self::STREAM_ALLOW_OB ) == 0 ) { + // Cancel output buffering and gzipping if set + wfResetOutputBuffers(); + } - $type = self::contentTypeFromPath( $path ); + $type = self::contentTypeFromPath( $fname ); if ( $type && $type != 'unknown/unknown' ) { - header( "Content-type: $type" ); + $headerFunc( "Content-type: $type" ); } else { // Send a content type which is not known to Internet Explorer, to // avoid triggering IE's content type detection. Sending a standard // unknown content type here essentially gives IE license to apply // whatever content type it likes. - header( 'Content-type: application/x-wiki' ); + $headerFunc( 'Content-type: application/x-wiki' ); } - // Don't stream it out as text/html if there was a PHP error - if ( headers_sent() ) { - echo "Headers already sent, terminating.\n"; - return false; + // Don't send if client has up to date cache + if ( isset( $optHeaders['if-modified-since'] ) ) { + $modsince = preg_replace( '/;.*$/', '', $optHeaders['if-modified-since'] ); + if ( wfTimestamp( TS_UNIX, $info['mtime'] ) <= strtotime( $modsince ) ) { + ini_set( 'zlib.output_compression', 0 ); + $headerFunc( 304 ); + return true; // ok + } } // Send additional headers foreach ( $headers as $header ) { - header( $header ); + header( $header ); // always use header(); specifically requested } - // Don't send if client has up to date cache - if ( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) { - $modsince = preg_replace( '/;.*$/', '', $_SERVER['HTTP_IF_MODIFIED_SINCE'] ); - if ( wfTimestamp( TS_UNIX, $info['mtime'] ) <= strtotime( $modsince ) ) { - ini_set( 'zlib.output_compression', 0 ); - header( "HTTP/1.0 304 Not Modified" ); - return self::NOT_MODIFIED; // ok + if ( isset( $optHeaders['range'] ) ) { + $range = self::parseRange( $optHeaders['range'], $info['size'] ); + if ( is_array( $range ) ) { + $headerFunc( 206 ); + $headerFunc( 'Content-Length: ' . $range[2] ); + $headerFunc( "Content-Range: bytes {$range[0]}-{$range[1]}/{$info['size']}" ); + } elseif ( $range === 'invalid' ) { + if ( $sendErrors ) { + $headerFunc( 416 ); + $headerFunc( 'Cache-Control: no-cache' ); + $headerFunc( 'Content-Type: text/html; charset=utf-8' ); + $headerFunc( 'Content-Range: bytes */' . $info['size'] ); + } + return false; + } else { // unsupported Range request (e.g. multiple ranges) + $range = null; + $headerFunc( 'Content-Length: ' . $info['size'] ); } + } else { + $range = null; + $headerFunc( 'Content-Length: ' . $info['size'] ); } - header( 'Content-Length: ' . $info['size'] ); + if ( is_array( $range ) ) { + $handle = fopen( $fname, 'rb' ); + if ( $handle ) { + $ok = true; + fseek( $handle, $range[0] ); + $remaining = $range[2]; + while ( $remaining > 0 && $ok ) { + $bytes = min( $remaining, 8 * 1024 ); + $data = fread( $handle, $bytes ); + $remaining -= $bytes; + $ok = ( $data !== false ); + print $data; + } + } else { + return false; + } + } else { + return readfile( $fname ) !== false; // faster + } + + return true; + } + + /** + * Send out a standard 404 message for a file + * + * @param string $fname Full name and path of the file to stream + * @param integer $flags Bitfield of STREAM_* constants + * @since 1.24 + */ + public static function send404Message( $fname, $flags = 0 ) { + if ( ( $flags & self::STREAM_HEADLESS ) == 0 ) { + HttpStatus::header( 404 ); + header( 'Cache-Control: no-cache' ); + header( 'Content-Type: text/html; charset=utf-8' ); + } + $encFile = htmlspecialchars( $fname ); + $encScript = htmlspecialchars( $_SERVER['SCRIPT_NAME'] ); + echo " +

File not found

+

Although this PHP script ($encScript) exists, the file requested for output + ($encFile) does not.

+ + "; + } - return self::READY_STREAM; // ok + /** + * Convert a Range header value to an absolute (start, end) range tuple + * + * @param string $range Range header value + * @param integer $size File size + * @return array|string Returns error string on failure (start, end, length) + * @since 1.24 + */ + public static function parseRange( $range, $size ) { + $m = []; + if ( preg_match( '#^bytes=(\d*)-(\d*)$#', $range, $m ) ) { + list( , $start, $end ) = $m; + if ( $start === '' && $end === '' ) { + $absRange = [ 0, $size - 1 ]; + } elseif ( $start === '' ) { + $absRange = [ $size - $end, $size - 1 ]; + } elseif ( $end === '' ) { + $absRange = [ $start, $size - 1 ]; + } else { + $absRange = [ $start, $end ]; + } + if ( $absRange[0] >= 0 && $absRange[1] >= $absRange[0] ) { + if ( $absRange[0] < $size ) { + $absRange[1] = min( $absRange[1], $size - 1 ); // stop at EOF + $absRange[2] = $absRange[1] - $absRange[0] + 1; + return $absRange; + } elseif ( $absRange[0] == 0 && $size == 0 ) { + return 'unrecognized'; // the whole file should just be sent + } + } + return 'invalid'; + } + return 'unrecognized'; } /**