Completed
Branch master (5998bb)
by
unknown
29:17
created

StreamFile::prepareForStream()   C

Complexity

Conditions 9
Paths 16

Size

Total Lines 62
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 33
nc 16
nop 4
dl 0
loc 62
rs 6.6867
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Functions related to the output of file content.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 */
22
23
/**
24
 * Functions related to the output of file content
25
 */
26
class StreamFile {
27
	// Do not send any HTTP headers unless requested by caller (e.g. body only)
28
	const STREAM_HEADLESS = 1;
29
	// Do not try to tear down any PHP output buffers
30
	const STREAM_ALLOW_OB = 2;
31
32
	/**
33
	 * Stream a file to the browser, adding all the headings and fun stuff.
34
	 * Headers sent include: Content-type, Content-Length, Last-Modified,
35
	 * and Content-Disposition.
36
	 *
37
	 * @param string $fname Full name and path of the file to stream
38
	 * @param array $headers Any additional headers to send if the file exists
39
	 * @param bool $sendErrors Send error messages if errors occur (like 404)
40
	 * @param array $optHeaders HTTP request header map (e.g. "range") (use lowercase keys)
41
	 * @param integer $flags Bitfield of STREAM_* constants
42
	 * @throws MWException
43
	 * @return bool Success
44
	 */
45
	public static function stream(
46
		$fname, $headers = [], $sendErrors = true, $optHeaders = [], $flags = 0
47
	) {
48
		$section = new ProfileSection( __METHOD__ );
0 ignored issues
show
Unused Code introduced by
$section is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
Deprecated Code introduced by
The class ProfileSection has been deprecated with message: 1.25 No-op now

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
49
50
		if ( FileBackend::isStoragePath( $fname ) ) { // sanity
51
			throw new MWException( __FUNCTION__ . " given storage path '$fname'." );
52
		}
53
54
		// Don't stream it out as text/html if there was a PHP error
55
		if ( ( ( $flags & self::STREAM_HEADLESS ) == 0 || $headers ) && headers_sent() ) {
56
			echo "Headers already sent, terminating.\n";
57
			return false;
58
		}
59
60
		$headerFunc = ( $flags & self::STREAM_HEADLESS )
61
			? function ( $header ) {
62
				 // no-op
63
			}
64
			: function ( $header ) {
65
				is_int( $header ) ? HttpStatus::header( $header ) : header( $header );
66
			};
67
68
		MediaWiki\suppressWarnings();
69
		$info = stat( $fname );
70
		MediaWiki\restoreWarnings();
71
72
		if ( !is_array( $info ) ) {
73
			if ( $sendErrors ) {
74
				self::send404Message( $fname, $flags );
75
			}
76
			return false;
77
		}
78
79
		// Send Last-Modified HTTP header for client-side caching
80
		$headerFunc( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $info['mtime'] ) );
81
82
		if ( ( $flags & self::STREAM_ALLOW_OB ) == 0 ) {
83
			// Cancel output buffering and gzipping if set
84
			wfResetOutputBuffers();
85
		}
86
87
		$type = self::contentTypeFromPath( $fname );
88
		if ( $type && $type != 'unknown/unknown' ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $type of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
89
			$headerFunc( "Content-type: $type" );
90
		} else {
91
			// Send a content type which is not known to Internet Explorer, to
92
			// avoid triggering IE's content type detection. Sending a standard
93
			// unknown content type here essentially gives IE license to apply
94
			// whatever content type it likes.
95
			$headerFunc( 'Content-type: application/x-wiki' );
96
		}
97
98
		// Don't send if client has up to date cache
99
		if ( isset( $optHeaders['if-modified-since'] ) ) {
100
			$modsince = preg_replace( '/;.*$/', '', $optHeaders['if-modified-since'] );
101
			if ( wfTimestamp( TS_UNIX, $info['mtime'] ) <= strtotime( $modsince ) ) {
102
				ini_set( 'zlib.output_compression', 0 );
103
				$headerFunc( 304 );
104
				return true; // ok
105
			}
106
		}
107
108
		// Send additional headers
109
		foreach ( $headers as $header ) {
110
			header( $header ); // always use header(); specifically requested
111
		}
112
113
		if ( isset( $optHeaders['range'] ) ) {
114
			$range = self::parseRange( $optHeaders['range'], $info['size'] );
115
			if ( is_array( $range ) ) {
116
				$headerFunc( 206 );
117
				$headerFunc( 'Content-Length: ' . $range[2] );
118
				$headerFunc( "Content-Range: bytes {$range[0]}-{$range[1]}/{$info['size']}" );
119
			} elseif ( $range === 'invalid' ) {
120
				if ( $sendErrors ) {
121
					$headerFunc( 416 );
122
					$headerFunc( 'Cache-Control: no-cache' );
123
					$headerFunc( 'Content-Type: text/html; charset=utf-8' );
124
					$headerFunc( 'Content-Range: bytes */' . $info['size'] );
125
				}
126
				return false;
127
			} else { // unsupported Range request (e.g. multiple ranges)
128
				$range = null;
129
				$headerFunc( 'Content-Length: ' . $info['size'] );
130
			}
131
		} else {
132
			$range = null;
133
			$headerFunc( 'Content-Length: ' . $info['size'] );
134
		}
135
136
		if ( is_array( $range ) ) {
137
			$handle = fopen( $fname, 'rb' );
138
			if ( $handle ) {
139
				$ok = true;
140
				fseek( $handle, $range[0] );
141
				$remaining = $range[2];
142
				while ( $remaining > 0 && $ok ) {
143
					$bytes = min( $remaining, 8 * 1024 );
144
					$data = fread( $handle, $bytes );
145
					$remaining -= $bytes;
146
					$ok = ( $data !== false );
147
					print $data;
148
				}
149
			} else {
150
				return false;
151
			}
152
		} else {
153
			return readfile( $fname ) !== false; // faster
154
		}
155
156
		return true;
157
	}
158
159
	/**
160
	 * Send out a standard 404 message for a file
161
	 *
162
	 * @param string $fname Full name and path of the file to stream
163
	 * @param integer $flags Bitfield of STREAM_* constants
164
	 * @since 1.24
165
	 */
166
	public static function send404Message( $fname, $flags = 0 ) {
0 ignored issues
show
Coding Style introduced by
send404Message uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
167
		if ( ( $flags & self::STREAM_HEADLESS ) == 0 ) {
168
			HttpStatus::header( 404 );
169
			header( 'Cache-Control: no-cache' );
170
			header( 'Content-Type: text/html; charset=utf-8' );
171
		}
172
		$encFile = htmlspecialchars( $fname );
173
		$encScript = htmlspecialchars( $_SERVER['SCRIPT_NAME'] );
174
		echo "<!DOCTYPE html><html><body>
175
			<h1>File not found</h1>
176
			<p>Although this PHP script ($encScript) exists, the file requested for output
177
			($encFile) does not.</p>
178
			</body></html>
179
			";
180
	}
181
182
	/**
183
	 * Convert a Range header value to an absolute (start, end) range tuple
184
	 *
185
	 * @param string $range Range header value
186
	 * @param integer $size File size
187
	 * @return array|string Returns error string on failure (start, end, length)
188
	 * @since 1.24
189
	 */
190
	public static function parseRange( $range, $size ) {
191
		$m = [];
192
		if ( preg_match( '#^bytes=(\d*)-(\d*)$#', $range, $m ) ) {
193
			list( , $start, $end ) = $m;
194
			if ( $start === '' && $end === '' ) {
195
				$absRange = [ 0, $size - 1 ];
196
			} elseif ( $start === '' ) {
197
				$absRange = [ $size - $end, $size - 1 ];
198
			} elseif ( $end === '' ) {
199
				$absRange = [ $start, $size - 1 ];
200
			} else {
201
				$absRange = [ $start, $end ];
202
			}
203
			if ( $absRange[0] >= 0 && $absRange[1] >= $absRange[0] ) {
204
				if ( $absRange[0] < $size ) {
205
					$absRange[1] = min( $absRange[1], $size - 1 ); // stop at EOF
206
					$absRange[2] = $absRange[1] - $absRange[0] + 1;
207
					return $absRange;
208
				} elseif ( $absRange[0] == 0 && $size == 0 ) {
209
					return 'unrecognized'; // the whole file should just be sent
210
				}
211
			}
212
			return 'invalid';
213
		}
214
		return 'unrecognized';
215
	}
216
217
	/**
218
	 * Determine the file type of a file based on the path
219
	 *
220
	 * @param string $filename Storage path or file system path
221
	 * @param bool $safe Whether to do retroactive upload blacklist checks
222
	 * @return null|string
223
	 */
224
	public static function contentTypeFromPath( $filename, $safe = true ) {
225
		global $wgTrivialMimeDetection;
226
227
		$ext = strrchr( $filename, '.' );
228
		$ext = $ext === false ? '' : strtolower( substr( $ext, 1 ) );
229
230
		# trivial detection by file extension,
231
		# used for thumbnails (thumb.php)
232
		if ( $wgTrivialMimeDetection ) {
233
			switch ( $ext ) {
234
				case 'gif':
235
					return 'image/gif';
236
				case 'png':
237
					return 'image/png';
238
				case 'jpg':
239
					return 'image/jpeg';
240
				case 'jpeg':
241
					return 'image/jpeg';
242
			}
243
244
			return 'unknown/unknown';
245
		}
246
247
		$magic = MimeMagic::singleton();
248
		// Use the extension only, rather than magic numbers, to avoid opening
249
		// up vulnerabilities due to uploads of files with allowed extensions
250
		// but disallowed types.
251
		$type = $magic->guessTypesForExtension( $ext );
252
253
		/**
254
		 * Double-check some security settings that were done on upload but might
255
		 * have changed since.
256
		 */
257
		if ( $safe ) {
258
			global $wgFileBlacklist, $wgCheckFileExtensions, $wgStrictFileExtensions,
259
				$wgFileExtensions, $wgVerifyMimeType, $wgMimeTypeBlacklist;
260
			list( , $extList ) = UploadBase::splitExtensions( $filename );
261
			if ( UploadBase::checkFileExtensionList( $extList, $wgFileBlacklist ) ) {
262
				return 'unknown/unknown';
263
			}
264
			if ( $wgCheckFileExtensions && $wgStrictFileExtensions
265
				&& !UploadBase::checkFileExtensionList( $extList, $wgFileExtensions )
266
			) {
267
				return 'unknown/unknown';
268
			}
269
			if ( $wgVerifyMimeType && in_array( strtolower( $type ), $wgMimeTypeBlacklist ) ) {
270
				return 'unknown/unknown';
271
			}
272
		}
273
		return $type;
274
	}
275
}
276