MediaWiki REL1_37
MWExceptionRenderer.php
Go to the documentation of this file.
1<?php
22use Wikimedia\AtEase;
26use Wikimedia\RequestTimeout\RequestTimeoutException;
27
33 public const AS_RAW = 1; // show as text
34 public const AS_PRETTY = 2; // show as HTML
35
41 public static function output( Throwable $e, $mode, Throwable $eNew = null ) {
43
44 if ( $e instanceof RequestTimeoutException && headers_sent() ) {
45 // Excimer's flag check happens on function return, so, a timeout
46 // can be thrown after exiting, say, `doPostOutputShutdown`, where
47 // headers are sent. In which case, it's probably fine not to
48 // report this in any user visible way. The general question of
49 // what to do about reporting an exception when headers have been
50 // sent is still unclear, but you probably don't want to
51 // `useOutputPage`.
52 return;
53 }
54
55 if ( function_exists( 'apache_setenv' ) ) {
56 // The client should not be blocked on "post-send" updates. If apache decides that
57 // a response should be gzipped, it will wait for PHP to finish since it cannot gzip
58 // anything until it has the full response (even with "Transfer-Encoding: chunked").
59 AtEase\AtEase::suppressWarnings();
60 apache_setenv( 'no-gzip', '1' );
61 AtEase\AtEase::restoreWarnings();
62 }
63
64 if ( defined( 'MW_API' ) ) {
65 self::header( 'MediaWiki-API-Error: internal_api_error_' . get_class( $e ) );
66 }
67
68 if ( self::isCommandLine() ) {
69 self::printError( self::getText( $e ) );
70 } elseif ( $mode === self::AS_PRETTY ) {
71 self::statusHeader( 500 );
72 self::header( "Content-Type: $wgMimeType; charset=UTF-8" );
73 ob_start();
74 if ( $e instanceof DBConnectionError ) {
76 } else {
77 self::reportHTML( $e );
78 }
79 self::header( "Content-Length: " . ob_get_length() );
80 ob_end_flush();
81 } else {
82 ob_start();
83 self::statusHeader( 500 );
84 self::header( "Content-Type: $wgMimeType; charset=UTF-8" );
85 if ( $eNew ) {
86 $message = "MediaWiki internal error.\n\n";
88 $message .= 'Original exception: ' .
89 MWExceptionHandler::getLogMessage( $e ) .
90 "\nBacktrace:\n" . MWExceptionHandler::getRedactedTraceAsString( $e ) .
91 "\n\nException caught inside exception handler: " .
92 MWExceptionHandler::getLogMessage( $eNew ) .
93 "\nBacktrace:\n" . MWExceptionHandler::getRedactedTraceAsString( $eNew );
94 } else {
95 $message .= 'Original exception: ' .
96 MWExceptionHandler::getPublicLogMessage( $e );
97 $message .= "\n\nException caught inside exception handler.\n\n" .
99 }
100 $message .= "\n";
101 } elseif ( $wgShowExceptionDetails ) {
102 $message = MWExceptionHandler::getLogMessage( $e ) .
103 "\nBacktrace:\n" .
104 MWExceptionHandler::getRedactedTraceAsString( $e ) . "\n";
105 } else {
106 $message = MWExceptionHandler::getPublicLogMessage( $e );
107 }
108 print nl2br( htmlspecialchars( $message ) ) . "\n";
109 self::header( "Content-Length: " . ob_get_length() );
110 ob_end_flush();
111 }
112 }
113
118 private static function useOutputPage( Throwable $e ) {
119 // Can the extension use the Message class/wfMessage to get i18n-ed messages?
120 foreach ( $e->getTrace() as $frame ) {
121 if ( isset( $frame['class'] ) && $frame['class'] === LocalisationCache::class ) {
122 return false;
123 }
124 }
125
126 // Don't even bother with OutputPage if there's no Title context set,
127 // (e.g. we're in RL code on load.php) - the Skin system (and probably
128 // most of MediaWiki) won't work.
129
130 return (
131 !empty( $GLOBALS['wgFullyInitialised'] ) &&
132 !empty( $GLOBALS['wgOut'] ) &&
133 RequestContext::getMain()->getTitle() &&
134 !defined( 'MEDIAWIKI_INSTALL' ) &&
135 // Don't send a skinned HTTP 500 page to API clients.
136 !defined( 'MW_API' )
137 );
138 }
139
145 private static function reportHTML( Throwable $e ) {
146 global $wgOut, $wgSitename;
147
148 if ( self::useOutputPage( $e ) ) {
149 $wgOut->prepareErrorPage( self::getExceptionTitle( $e ) );
150
151 // Show any custom GUI message before the details
152 $customMessage = self::getCustomMessage( $e );
153 if ( $customMessage !== null ) {
154 $wgOut->addHTML( Html::element( 'p', [], $customMessage ) );
155 }
156 $wgOut->addHTML( self::getHTML( $e ) );
157
158 $wgOut->output();
159 } else {
160 self::header( 'Content-Type: text/html; charset=utf-8' );
161 $pageTitle = self::msg( 'internalerror', 'Internal error' );
162 echo "<!DOCTYPE html>\n" .
163 '<html><head>' .
164 // Mimick OutputPage::setPageTitle behaviour
165 '<title>' .
166 htmlspecialchars( self::msg( 'pagetitle', "$1 - $wgSitename", $pageTitle ) ) .
167 '</title>' .
168 '<style>body { font-family: sans-serif; margin: 0; padding: 0.5em 2em; }</style>' .
169 "</head><body>\n";
170
171 echo self::getHTML( $e );
172
173 echo "</body></html>\n";
174 }
175 }
176
185 public static function getHTML( Throwable $e ) {
187
189 $html = "<div class=\"errorbox mw-content-ltr\"><p>" .
190 nl2br( htmlspecialchars( MWExceptionHandler::getLogMessage( $e ) ) ) .
191 '</p><p>Backtrace:</p><p>' .
192 nl2br( htmlspecialchars( MWExceptionHandler::getRedactedTraceAsString( $e ) ) ) .
193 "</p></div>\n";
194 } else {
195 $logId = WebRequest::getRequestId();
196 $html = "<div class=\"errorbox mw-content-ltr\">" .
197 htmlspecialchars(
198 '[' . $logId . '] ' .
199 gmdate( 'Y-m-d H:i:s' ) . ": " .
200 self::msg( "internalerror-fatal-exception",
201 "Fatal exception of type $1",
202 get_class( $e ),
203 $logId,
204 MWExceptionHandler::getURL()
205 ) ) . "</div>\n" .
206 "<!-- " . wordwrap( self::getShowBacktraceError( $e ), 50 ) . " -->";
207 }
208
209 return $html;
210 }
211
221 private static function msg( $key, $fallback, ...$params ) {
222 global $wgSitename;
223
224 // FIXME: Keep logic in sync with MWException::msg.
225 try {
226 $res = wfMessage( $key, ...$params )->text();
227 } catch ( Exception $e ) {
228 $res = wfMsgReplaceArgs( $fallback, $params );
229 // If an exception happens inside message rendering,
230 // {{SITENAME}} sometimes won't be replaced.
231 $res = strtr( $res, [
232 '{{SITENAME}}' => $wgSitename,
233 ] );
234 }
235 return $res;
236 }
237
242 private static function getText( Throwable $e ) {
244
246 return MWExceptionHandler::getLogMessage( $e ) .
247 "\nBacktrace:\n" .
248 MWExceptionHandler::getRedactedTraceAsString( $e ) . "\n";
249 } else {
250 return self::getShowBacktraceError( $e ) . "\n";
251 }
252 }
253
258 private static function getShowBacktraceError( Throwable $e ) {
259 $var = '$wgShowExceptionDetails = true;';
260 return "Set $var at the bottom of LocalSettings.php to show detailed debugging information.";
261 }
262
269 private static function getExceptionTitle( Throwable $e ) {
270 if ( $e instanceof MWException ) {
271 return $e->getPageTitle();
272 } elseif ( $e instanceof DBReadOnlyError ) {
273 return self::msg( 'readonly', 'Database is locked' );
274 } elseif ( $e instanceof DBExpectedError ) {
275 return self::msg( 'databaseerror', 'Database error' );
276 } elseif ( $e instanceof RequestTimeoutException ) {
277 return self::msg( 'timeouterror', 'Request timeout' );
278 } else {
279 return self::msg( 'internalerror', 'Internal error' );
280 }
281 }
282
290 private static function getCustomMessage( Throwable $e ) {
291 try {
292 if ( $e instanceof MessageSpecifier ) {
293 $msg = Message::newFromSpecifier( $e );
294 } elseif ( $e instanceof RequestTimeoutException ) {
295 $msg = wfMessage( 'timeouterror-text', $e->getLimit() );
296 } else {
297 return null;
298 }
299 $text = $msg->text();
300 } catch ( Exception $e2 ) {
301 return null;
302 }
303 return $text;
304 }
305
309 private static function isCommandLine() {
310 return !empty( $GLOBALS['wgCommandLineMode'] );
311 }
312
316 private static function header( $header ) {
317 if ( !headers_sent() ) {
318 header( $header );
319 }
320 }
321
325 private static function statusHeader( $code ) {
326 if ( !headers_sent() ) {
327 HttpStatus::header( $code );
328 }
329 }
330
338 private static function printError( $message ) {
339 // NOTE: STDERR may not be available, especially if php-cgi is used from the
340 // command line (T17602). Try to produce meaningful output anyway. Using
341 // echo may corrupt output to STDOUT though.
342 if ( defined( 'STDERR' ) ) {
343 fwrite( STDERR, $message );
344 } else {
345 echo $message;
346 }
347 }
348
352 private static function reportOutageHTML( Throwable $e ) {
354
355 $sorry = htmlspecialchars( self::msg(
356 'dberr-problems',
357 'Sorry! This site is experiencing technical difficulties.'
358 ) );
359 $again = htmlspecialchars( self::msg(
360 'dberr-again',
361 'Try waiting a few minutes and reloading.'
362 ) );
363
364 if ( $wgShowHostnames ) {
365 $info = str_replace(
366 '$1',
367 Html::element( 'span', [ 'dir' => 'ltr' ], $e->getMessage() ),
368 htmlspecialchars( self::msg( 'dberr-info', '($1)' ) )
369 );
370 } else {
371 $info = htmlspecialchars( self::msg(
372 'dberr-info-hidden',
373 '(Cannot access the database)'
374 ) );
375 }
376
377 MediaWikiServices::getInstance()->getMessageCache()->disable(); // no DB access
378 $html = "<!DOCTYPE html>\n" .
379 '<html><head>' .
380 '<title>' .
381 htmlspecialchars( $wgSitename ) .
382 '</title>' .
383 '<style>body { font-family: sans-serif; margin: 0; padding: 0.5em 2em; }</style>' .
384 "</head><body><h1>$sorry</h1><p>$again</p><p><small>$info</small></p>";
385
387 $html .= '<p>Backtrace:</p><pre>' .
388 htmlspecialchars( $e->getTraceAsString() ) . '</pre>';
389 }
390
391 $html .= '</body></html>';
392 echo $html;
393 }
394}
$wgMimeType
The default Content-Type header.
$wgShowHostnames
Expose backend server host names through the API and various HTML comments.
$wgSitename
Name of the site.
$wgShowExceptionDetails
If set to true, uncaught exceptions will print the exception message and a complete stack trace to ou...
wfMsgReplaceArgs( $message, $args)
Replace message parameter keys on the given formatted output.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
$fallback
$wgOut
Definition Setup.php:836
Class to expose exceptions to the client (API bots, users, admins using CLI scripts)
static getText(Throwable $e)
static printError( $message)
Print a message, if possible to STDERR.
static reportOutageHTML(Throwable $e)
static useOutputPage(Throwable $e)
static getCustomMessage(Throwable $e)
Extract an additional user-visible message from an exception, or null if it has none.
static getExceptionTitle(Throwable $e)
Get the page title to be used for a given exception.
static msg( $key, $fallback,... $params)
Get a message from i18n.
static getHTML(Throwable $e)
If $wgShowExceptionDetails is true, return a HTML message with a backtrace to the error,...
static output(Throwable $e, $mode, Throwable $eNew=null)
static getShowBacktraceError(Throwable $e)
static reportHTML(Throwable $e)
Output the throwable report using HTML.
MediaWiki exception.
MediaWikiServices is the service locator for the application scope of MediaWiki.
static newFromSpecifier( $value)
Transform a MessageSpecifier or a primitive value used interchangeably with specifiers (a message key...
Definition Message.php:414
Base class for the more common types of database errors.
while(( $__line=Maintenance::readconsole()) !==false) print
Definition eval.php:69
$header