Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 67 |
|
0.00% |
0 / 4 |
CRAP | |
0.00% |
0 / 1 |
OutputHandler | |
0.00% |
0 / 66 |
|
0.00% |
0 / 4 |
812 | |
0.00% |
0 / 1 |
handle | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
110 | |||
findUriExtension | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
handleGzip | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
110 | |||
emitContentLength | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 |
1 | <?php |
2 | /** |
3 | * Functions to be used with PHP's output buffer. |
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 | namespace MediaWiki\Output; |
24 | |
25 | use MediaWiki\Logger\LoggerFactory; |
26 | use MediaWiki\MainConfigNames; |
27 | use MediaWiki\MediaWikiServices; |
28 | |
29 | /** |
30 | * @since 1.31 |
31 | */ |
32 | class OutputHandler { |
33 | /** |
34 | * Standard output handler for use with ob_start. |
35 | * |
36 | * Output buffers using this method should only be started from MW_SETUP_CALLBACK, |
37 | * and only if there are no parent output buffers. |
38 | * |
39 | * @param string $s Web response output |
40 | * @param int $phase Flags indicating the reason for the call |
41 | * @return string |
42 | */ |
43 | public static function handle( $s, $phase ) { |
44 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
45 | $disableOutputCompression = $config->get( MainConfigNames::DisableOutputCompression ); |
46 | // Don't send headers if output is being discarded (T278579) |
47 | if ( ( $phase & PHP_OUTPUT_HANDLER_CLEAN ) === PHP_OUTPUT_HANDLER_CLEAN ) { |
48 | $logger = LoggerFactory::getInstance( 'output' ); |
49 | $logger->debug( __METHOD__ . " entrypoint={entry}; size={size}; phase=$phase", [ |
50 | 'entry' => MW_ENTRY_POINT, |
51 | 'size' => strlen( $s ), |
52 | ] ); |
53 | |
54 | return $s; |
55 | } |
56 | |
57 | // Check if a compression output buffer is already enabled via php.ini. Such |
58 | // buffers exists at the start of the request and are reflected by ob_get_level(). |
59 | $phpHandlesCompression = ( |
60 | ini_get( 'output_handler' ) === 'ob_gzhandler' || |
61 | ini_get( 'zlib.output_handler' ) === 'ob_gzhandler' || |
62 | !in_array( |
63 | strtolower( ini_get( 'zlib.output_compression' ) ), |
64 | [ '', 'off', '0' ] |
65 | ) |
66 | ); |
67 | |
68 | if ( |
69 | // Compression is not already handled by an internal PHP buffer |
70 | !$phpHandlesCompression && |
71 | // Compression is not disabled by the application entry point |
72 | !defined( 'MW_NO_OUTPUT_COMPRESSION' ) && |
73 | // Compression is not disabled by site configuration |
74 | !$disableOutputCompression |
75 | ) { |
76 | $s = self::handleGzip( $s ); |
77 | } |
78 | |
79 | if ( |
80 | // Response body length does not depend on internal PHP compression buffer |
81 | !$phpHandlesCompression && |
82 | // Response body length does not depend on mangling by a custom buffer |
83 | !ini_get( 'output_handler' ) && |
84 | !ini_get( 'zlib.output_handler' ) |
85 | ) { |
86 | self::emitContentLength( strlen( $s ) ); |
87 | } |
88 | |
89 | return $s; |
90 | } |
91 | |
92 | /** |
93 | * Get the "file extension" that some client apps will estimate from |
94 | * the currently-requested URL. |
95 | * |
96 | * This isn't a WebRequest method, because we need it before the class loads. |
97 | * @todo As of 2018, this actually runs after autoloader in Setup.php, so |
98 | * WebRequest seems like a good place for this. |
99 | * |
100 | * @return string |
101 | */ |
102 | private static function findUriExtension() { |
103 | // @todo FIXME: this sort of dupes some code in WebRequest::getRequestUrl() |
104 | if ( isset( $_SERVER['REQUEST_URI'] ) ) { |
105 | // Strip the query string... |
106 | $path = explode( '?', $_SERVER['REQUEST_URI'], 2 )[0]; |
107 | } elseif ( isset( $_SERVER['SCRIPT_NAME'] ) ) { |
108 | // Probably IIS. QUERY_STRING appears separately. |
109 | $path = $_SERVER['SCRIPT_NAME']; |
110 | } else { |
111 | // Can't get the path from the server? :( |
112 | return ''; |
113 | } |
114 | |
115 | $period = strrpos( $path, '.' ); |
116 | if ( $period !== false ) { |
117 | return strtolower( substr( $path, $period ) ); |
118 | } |
119 | return ''; |
120 | } |
121 | |
122 | /** |
123 | * Handler that compresses data with gzip if allowed by the Accept header. |
124 | * |
125 | * Unlike ob_gzhandler, it works for HEAD requests too. This assumes that the application |
126 | * processes them as normal GET request and that the webserver is tasked with stripping out |
127 | * the response body before sending the response the client. |
128 | * |
129 | * @param string $s Web response output |
130 | * @return string |
131 | */ |
132 | private static function handleGzip( $s ) { |
133 | if ( !function_exists( 'gzencode' ) ) { |
134 | wfDebug( __METHOD__ . "() skipping compression (gzencode unavailable)" ); |
135 | return $s; |
136 | } |
137 | if ( headers_sent() ) { |
138 | wfDebug( __METHOD__ . "() skipping compression (headers already sent)" ); |
139 | return $s; |
140 | } |
141 | |
142 | $ext = self::findUriExtension(); |
143 | if ( $ext == '.gz' || $ext == '.tgz' ) { |
144 | // Don't do gzip compression if the URL path ends in .gz or .tgz |
145 | // This confuses Safari and triggers a download of the page, |
146 | // even though it's pretty clearly labeled as viewable HTML. |
147 | // Bad Safari! Bad! |
148 | return $s; |
149 | } |
150 | |
151 | if ( $s === '' ) { |
152 | // Do not gzip empty HTTP responses since that would not only bloat the body |
153 | // length, but it would result in invalid HTTP responses when the HTTP status code |
154 | // is one that must not be accompanied by a body (e.g. "204 No Content"). |
155 | return $s; |
156 | } |
157 | |
158 | if ( wfClientAcceptsGzip() ) { |
159 | wfDebug( __METHOD__ . "() is compressing output" ); |
160 | header( 'Content-Encoding: gzip' ); |
161 | $s = gzencode( $s, 6 ); |
162 | } |
163 | |
164 | // Set vary header if it hasn't been set already |
165 | $headers = headers_list(); |
166 | $foundVary = false; |
167 | foreach ( $headers as $header ) { |
168 | $headerName = strtolower( substr( $header, 0, 5 ) ); |
169 | if ( $headerName == 'vary:' ) { |
170 | $foundVary = true; |
171 | break; |
172 | } |
173 | } |
174 | if ( !$foundVary ) { |
175 | header( 'Vary: Accept-Encoding' ); |
176 | } |
177 | return $s; |
178 | } |
179 | |
180 | /** |
181 | * Set the Content-Length header if possible |
182 | * |
183 | * This sets Content-Length for the following cases: |
184 | * - When the response body is meaningful (HTTP 200/404). |
185 | * - On any HTTP 1.0 request response. This improves cooperation with certain CDNs. |
186 | * |
187 | * This assumes that HEAD requests are processed as GET requests by MediaWiki and that |
188 | * the webserver is tasked with stripping out the body. |
189 | * |
190 | * Setting Content-Length can prevent clients from getting stuck waiting on PHP to finish |
191 | * while deferred updates are running. |
192 | * |
193 | * @param int $length |
194 | */ |
195 | private static function emitContentLength( $length ) { |
196 | if ( headers_sent() ) { |
197 | wfDebug( __METHOD__ . "() headers already sent" ); |
198 | return; |
199 | } |
200 | |
201 | if ( |
202 | in_array( http_response_code(), [ 200, 404 ], true ) || |
203 | ( $_SERVER['SERVER_PROTOCOL'] ?? null ) === 'HTTP/1.0' |
204 | ) { |
205 | header( "Content-Length: $length" ); |
206 | } |
207 | } |
208 | } |
209 | |
210 | /** @deprecated class alias since 1.41 */ |
211 | class_alias( OutputHandler::class, 'MediaWiki\\OutputHandler' ); |