MediaWiki  master
OutputHandler.php
Go to the documentation of this file.
1 <?php
23 namespace MediaWiki;
24 
26 
41  public static function handle( $s, $phase ) {
42  $config = MediaWikiServices::getInstance()->getMainConfig();
43  $disableOutputCompression = $config->get( MainConfigNames::DisableOutputCompression );
44  $mangleFlashPolicy = $config->get( MainConfigNames::MangleFlashPolicy );
45  // Don't send headers if output is being discarded (T278579)
46  if ( ( $phase & PHP_OUTPUT_HANDLER_CLEAN ) === PHP_OUTPUT_HANDLER_CLEAN ) {
47  $logger = LoggerFactory::getInstance( 'output' );
48  $logger->debug( __METHOD__ . " entrypoint={entry}; size={size}; phase=$phase", [
49  'entry' => MW_ENTRY_POINT,
50  'size' => strlen( $s ),
51  ] );
52 
53  return $s;
54  }
55 
56  if ( $mangleFlashPolicy ) {
57  $s = self::mangleFlashPolicy( $s );
58  }
59 
60  // Check if a compression output buffer is already enabled via php.ini. Such
61  // buffers exists at the start of the request and are reflected by ob_get_level().
62  $phpHandlesCompression = (
63  ini_get( 'output_handler' ) === 'ob_gzhandler' ||
64  ini_get( 'zlib.output_handler' ) === 'ob_gzhandler' ||
65  !in_array(
66  strtolower( ini_get( 'zlib.output_compression' ) ),
67  [ '', 'off', '0' ]
68  )
69  );
70 
71  if (
72  // Compression is not already handled by an internal PHP buffer
73  !$phpHandlesCompression &&
74  // Compression is not disabled by the application entry point
75  !defined( 'MW_NO_OUTPUT_COMPRESSION' ) &&
76  // Compression is not disabled by site configuration
77  !$disableOutputCompression
78  ) {
79  $s = self::handleGzip( $s );
80  }
81 
82  if (
83  // Response body length does not depend on internal PHP compression buffer
84  !$phpHandlesCompression &&
85  // Response body length does not depend on mangling by a custom buffer
86  !ini_get( 'output_handler' ) &&
87  !ini_get( 'zlib.output_handler' )
88  ) {
89  self::emitContentLength( strlen( $s ) );
90  }
91 
92  return $s;
93  }
94 
105  private static function findUriExtension() {
106  // @todo FIXME: this sort of dupes some code in WebRequest::getRequestUrl()
107  if ( isset( $_SERVER['REQUEST_URI'] ) ) {
108  // Strip the query string...
109  $path = explode( '?', $_SERVER['REQUEST_URI'], 2 )[0];
110  } elseif ( isset( $_SERVER['SCRIPT_NAME'] ) ) {
111  // Probably IIS. QUERY_STRING appears separately.
112  $path = $_SERVER['SCRIPT_NAME'];
113  } else {
114  // Can't get the path from the server? :(
115  return '';
116  }
117 
118  $period = strrpos( $path, '.' );
119  if ( $period !== false ) {
120  return strtolower( substr( $path, $period ) );
121  }
122  return '';
123  }
124 
135  private static function handleGzip( $s ) {
136  if ( !function_exists( 'gzencode' ) ) {
137  wfDebug( __METHOD__ . "() skipping compression (gzencode unavailable)" );
138  return $s;
139  }
140  if ( headers_sent() ) {
141  wfDebug( __METHOD__ . "() skipping compression (headers already sent)" );
142  return $s;
143  }
144 
145  $ext = self::findUriExtension();
146  if ( $ext == '.gz' || $ext == '.tgz' ) {
147  // Don't do gzip compression if the URL path ends in .gz or .tgz
148  // This confuses Safari and triggers a download of the page,
149  // even though it's pretty clearly labeled as viewable HTML.
150  // Bad Safari! Bad!
151  return $s;
152  }
153 
154  if ( $s === '' ) {
155  // Do not gzip empty HTTP responses since that would not only bloat the body
156  // length, but it would result in invalid HTTP responses when the HTTP status code
157  // is one that must not be accompanied by a body (e.g. "204 No Content").
158  return $s;
159  }
160 
161  if ( wfClientAcceptsGzip() ) {
162  wfDebug( __METHOD__ . "() is compressing output" );
163  header( 'Content-Encoding: gzip' );
164  $s = gzencode( $s, 6 );
165  }
166 
167  // Set vary header if it hasn't been set already
168  $headers = headers_list();
169  $foundVary = false;
170  foreach ( $headers as $header ) {
171  $headerName = strtolower( substr( $header, 0, 5 ) );
172  if ( $headerName == 'vary:' ) {
173  $foundVary = true;
174  break;
175  }
176  }
177  if ( !$foundVary ) {
178  header( 'Vary: Accept-Encoding' );
179  }
180  return $s;
181  }
182 
189  private static function mangleFlashPolicy( $s ) {
190  # Avoid weird excessive memory usage in PCRE on big articles
191  if ( preg_match( '/<\s*cross-domain-policy(?=\s|>)/i', $s ) ) {
192  return preg_replace( '/<(\s*)(cross-domain-policy(?=\s|>))/i', '<$1NOT-$2', $s );
193  } else {
194  return $s;
195  }
196  }
197 
213  private static function emitContentLength( $length ) {
214  if ( headers_sent() ) {
215  wfDebug( __METHOD__ . "() headers already sent" );
216  return;
217  }
218 
219  if (
220  in_array( http_response_code(), [ 200, 404 ], true ) ||
221  ( $_SERVER['SERVER_PROTOCOL'] ?? null ) === 'HTTP/1.0'
222  ) {
223  header( "Content-Length: $length" );
224  }
225  }
226 }
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfClientAcceptsGzip( $force=false)
Whether the client accept gzip encoding.
const MW_ENTRY_POINT
Definition: api.php:41
PSR-3 logger instance factory.
static handle( $s, $phase)
Standard output handler for use with ob_start.
static handleGzip( $s)
Handler that compresses data with gzip if allowed by the Accept header.
static emitContentLength( $length)
Set the Content-Length header if possible.
static mangleFlashPolicy( $s)
Mangle flash policy tags which open up the site to XSS attacks.
static findUriExtension()
Get the "file extension" that some client apps will estimate from the currently-requested URL.
The MediaWiki class is the helper class for the index.php entry point.
Definition: MediaWiki.php:38
foreach( $mmfl['setupFiles'] as $fileName) if( $queue) if(empty( $mmfl['quiet'])) $s
if(!is_readable( $file)) $ext
Definition: router.php:48
$header