MediaWiki REL1_31
HTTPFileStreamer.php
Go to the documentation of this file.
1<?php
22use Wikimedia\Timestamp\ConvertibleTimestamp;
23
31 protected $path;
33 protected $obResetFunc;
35 protected $streamMimeFunc;
36
37 // Do not send any HTTP headers unless requested by caller (e.g. body only)
38 const STREAM_HEADLESS = 1;
39 // Do not try to tear down any PHP output buffers
40 const STREAM_ALLOW_OB = 2;
41
49 public static function preprocessHeaders( $headers ) {
50 $rawHeaders = [];
51 $optHeaders = [];
52 foreach ( $headers as $name => $header ) {
53 $nameLower = strtolower( $name );
54 if ( in_array( $nameLower, [ 'range', 'if-modified-since' ], true ) ) {
55 $optHeaders[$nameLower] = $header;
56 } else {
57 $rawHeaders[] = "$name: $header";
58 }
59 }
60 return [ $rawHeaders, $optHeaders ];
61 }
62
69 public function __construct( $path, array $params = [] ) {
70 $this->path = $path;
71 $this->obResetFunc = isset( $params['obResetFunc'] )
72 ? $params['obResetFunc']
73 : [ __CLASS__, 'resetOutputBuffers' ];
74 $this->streamMimeFunc = isset( $params['streamMimeFunc'] )
75 ? $params['streamMimeFunc']
76 : [ __CLASS__, 'contentTypeFromPath' ];
77 }
78
91 public function stream(
92 $headers = [], $sendErrors = true, $optHeaders = [], $flags = 0
93 ) {
94 // Don't stream it out as text/html if there was a PHP error
95 if ( ( ( $flags & self::STREAM_HEADLESS ) == 0 || $headers ) && headers_sent() ) {
96 echo "Headers already sent, terminating.\n";
97 return false;
98 }
99
100 $headerFunc = ( $flags & self::STREAM_HEADLESS )
101 ? function ( $header ) {
102 // no-op
103 }
104 : function ( $header ) {
105 is_int( $header ) ? HttpStatus::header( $header ) : header( $header );
106 };
107
108 Wikimedia\suppressWarnings();
109 $info = stat( $this->path );
110 Wikimedia\restoreWarnings();
111
112 if ( !is_array( $info ) ) {
113 if ( $sendErrors ) {
114 self::send404Message( $this->path, $flags );
115 }
116 return false;
117 }
118
119 // Send Last-Modified HTTP header for client-side caching
120 $mtimeCT = new ConvertibleTimestamp( $info['mtime'] );
121 $headerFunc( 'Last-Modified: ' . $mtimeCT->getTimestamp( TS_RFC2822 ) );
122
123 if ( ( $flags & self::STREAM_ALLOW_OB ) == 0 ) {
124 call_user_func( $this->obResetFunc );
125 }
126
127 $type = call_user_func( $this->streamMimeFunc, $this->path );
128 if ( $type && $type != 'unknown/unknown' ) {
129 $headerFunc( "Content-type: $type" );
130 } else {
131 // Send a content type which is not known to Internet Explorer, to
132 // avoid triggering IE's content type detection. Sending a standard
133 // unknown content type here essentially gives IE license to apply
134 // whatever content type it likes.
135 $headerFunc( 'Content-type: application/x-wiki' );
136 }
137
138 // Don't send if client has up to date cache
139 if ( isset( $optHeaders['if-modified-since'] ) ) {
140 $modsince = preg_replace( '/;.*$/', '', $optHeaders['if-modified-since'] );
141 if ( $mtimeCT->getTimestamp( TS_UNIX ) <= strtotime( $modsince ) ) {
142 ini_set( 'zlib.output_compression', 0 );
143 $headerFunc( 304 );
144 return true; // ok
145 }
146 }
147
148 // Send additional headers
149 foreach ( $headers as $header ) {
150 header( $header ); // always use header(); specifically requested
151 }
152
153 if ( isset( $optHeaders['range'] ) ) {
154 $range = self::parseRange( $optHeaders['range'], $info['size'] );
155 if ( is_array( $range ) ) {
156 $headerFunc( 206 );
157 $headerFunc( 'Content-Length: ' . $range[2] );
158 $headerFunc( "Content-Range: bytes {$range[0]}-{$range[1]}/{$info['size']}" );
159 } elseif ( $range === 'invalid' ) {
160 if ( $sendErrors ) {
161 $headerFunc( 416 );
162 $headerFunc( 'Cache-Control: no-cache' );
163 $headerFunc( 'Content-Type: text/html; charset=utf-8' );
164 $headerFunc( 'Content-Range: bytes */' . $info['size'] );
165 }
166 return false;
167 } else { // unsupported Range request (e.g. multiple ranges)
168 $range = null;
169 $headerFunc( 'Content-Length: ' . $info['size'] );
170 }
171 } else {
172 $range = null;
173 $headerFunc( 'Content-Length: ' . $info['size'] );
174 }
175
176 if ( is_array( $range ) ) {
177 $handle = fopen( $this->path, 'rb' );
178 if ( $handle ) {
179 $ok = true;
180 fseek( $handle, $range[0] );
181 $remaining = $range[2];
182 while ( $remaining > 0 && $ok ) {
183 $bytes = min( $remaining, 8 * 1024 );
184 $data = fread( $handle, $bytes );
185 $remaining -= $bytes;
186 $ok = ( $data !== false );
187 print $data;
188 }
189 } else {
190 return false;
191 }
192 } else {
193 return readfile( $this->path ) !== false; // faster
194 }
195
196 return true;
197 }
198
206 public static function send404Message( $fname, $flags = 0 ) {
207 if ( ( $flags & self::STREAM_HEADLESS ) == 0 ) {
208 HttpStatus::header( 404 );
209 header( 'Cache-Control: no-cache' );
210 header( 'Content-Type: text/html; charset=utf-8' );
211 }
212 $encFile = htmlspecialchars( $fname );
213 $encScript = htmlspecialchars( $_SERVER['SCRIPT_NAME'] );
214 echo "<!DOCTYPE html><html><body>
215 <h1>File not found</h1>
216 <p>Although this PHP script ($encScript) exists, the file requested for output
217 ($encFile) does not.</p>
218 </body></html>
219 ";
220 }
221
230 public static function parseRange( $range, $size ) {
231 $m = [];
232 if ( preg_match( '#^bytes=(\d*)-(\d*)$#', $range, $m ) ) {
233 list( , $start, $end ) = $m;
234 if ( $start === '' && $end === '' ) {
235 $absRange = [ 0, $size - 1 ];
236 } elseif ( $start === '' ) {
237 $absRange = [ $size - $end, $size - 1 ];
238 } elseif ( $end === '' ) {
239 $absRange = [ $start, $size - 1 ];
240 } else {
241 $absRange = [ $start, $end ];
242 }
243 if ( $absRange[0] >= 0 && $absRange[1] >= $absRange[0] ) {
244 if ( $absRange[0] < $size ) {
245 $absRange[1] = min( $absRange[1], $size - 1 ); // stop at EOF
246 $absRange[2] = $absRange[1] - $absRange[0] + 1;
247 return $absRange;
248 } elseif ( $absRange[0] == 0 && $size == 0 ) {
249 return 'unrecognized'; // the whole file should just be sent
250 }
251 }
252 return 'invalid';
253 }
254 return 'unrecognized';
255 }
256
257 protected static function resetOutputBuffers() {
258 while ( ob_get_status() ) {
259 if ( !ob_end_clean() ) {
260 // Could not remove output buffer handler; abort now
261 // to avoid getting in some kind of infinite loop.
262 break;
263 }
264 }
265 }
266
273 protected static function contentTypeFromPath( $filename ) {
274 $ext = strrchr( $filename, '.' );
275 $ext = $ext === false ? '' : strtolower( substr( $ext, 1 ) );
276
277 switch ( $ext ) {
278 case 'gif':
279 return 'image/gif';
280 case 'png':
281 return 'image/png';
282 case 'jpg':
283 return 'image/jpeg';
284 case 'jpeg':
285 return 'image/jpeg';
286 }
287
288 return 'unknown/unknown';
289 }
290}
if(defined( 'MW_SETUP_CALLBACK')) $fname
Customization point after all loading (constants, functions, classes, DefaultSettings,...
Definition Setup.php:112
Functions related to the output of file content.
static preprocessHeaders( $headers)
Takes HTTP headers in a name => value format and converts them to the weird format expected by stream...
static send404Message( $fname, $flags=0)
Send out a standard 404 message for a file.
static contentTypeFromPath( $filename)
Determine the file type of a file based on the path.
static parseRange( $range, $size)
Convert a Range header value to an absolute (start, end) range tuple.
__construct( $path, array $params=[])
stream( $headers=[], $sendErrors=true, $optHeaders=[], $flags=0)
Stream a file to the browser, adding all the headings and fun stuff.
static header( $code)
Output an HTTP status code header.
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
while(( $__line=Maintenance::readconsole()) !==false) print
Definition eval.php:64
if(!is_readable( $file)) $ext
Definition router.php:55
$params
$header