MediaWiki  master
MWExceptionRenderer.php
Go to the documentation of this file.
1 <?php
22 use Wikimedia\AtEase;
26 use 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: ' .
90  "\nBacktrace:\n" . MWExceptionHandler::getRedactedTraceAsString( $e ) .
91  "\n\nException caught inside exception handler: " .
93  "\nBacktrace:\n" . MWExceptionHandler::getRedactedTraceAsString( $eNew );
94  } else {
95  $message .= 'Original exception: ' .
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" .
105  } else {
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'] ) &&
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 
188  if ( $wgShowExceptionDetails ) {
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,
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 
245  if ( $wgShowExceptionDetails ) {
246  return MWExceptionHandler::getLogMessage( $e ) .
247  "\nBacktrace:\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 
386  if ( $wgShowExceptionDetails ) {
387  $html .= '<p>Backtrace:</p><pre>' .
388  htmlspecialchars( $e->getTraceAsString() ) . '</pre>';
389  }
390 
391  $html .= '</body></html>';
392  echo $html;
393  }
394 }
Message\newFromSpecifier
static newFromSpecifier( $value)
Transform a MessageSpecifier or a primitive value used interchangeably with specifiers (a message key...
Definition: Message.php:398
$wgMimeType
$wgMimeType
The default Content-Type header.
Definition: DefaultSettings.php:3733
MWExceptionRenderer\getHTML
static getHTML(Throwable $e)
If $wgShowExceptionDetails is true, return a HTML message with a backtrace to the error,...
Definition: MWExceptionRenderer.php:185
MWExceptionRenderer\getText
static getText(Throwable $e)
Definition: MWExceptionRenderer.php:242
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:193
MWExceptionRenderer\msg
static msg( $key, $fallback,... $params)
Get a message from i18n.
Definition: MWExceptionRenderer.php:221
MWExceptionHandler\getPublicLogMessage
static getPublicLogMessage(Throwable $e)
Definition: MWExceptionHandler.php:535
MWExceptionRenderer\reportOutageHTML
static reportOutageHTML(Throwable $e)
Definition: MWExceptionRenderer.php:352
$fallback
$fallback
Definition: MessagesAb.php:11
MessageSpecifier
Definition: MessageSpecifier.php:24
$wgShowHostnames
$wgShowHostnames
Expose backend server host names through the API and various HTML comments.
Definition: DefaultSettings.php:7431
MWExceptionRenderer\reportHTML
static reportHTML(Throwable $e)
Output the throwable report using HTML.
Definition: MWExceptionRenderer.php:145
wfMsgReplaceArgs
wfMsgReplaceArgs( $message, $args)
Replace message parameter keys on the given formatted output.
Definition: GlobalFunctions.php:1221
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1186
$res
$res
Definition: testCompression.php:57
MWExceptionHandler\getRedactedTraceAsString
static getRedactedTraceAsString(Throwable $e)
Generate a string representation of a throwable's stack trace.
Definition: MWExceptionHandler.php:375
MWException
MediaWiki exception.
Definition: MWException.php:29
MWExceptionHandler\getURL
static getURL()
If the exception occurred in the course of responding to a request, returns the requested URL.
Definition: MWExceptionHandler.php:467
MWExceptionRenderer\output
static output(Throwable $e, $mode, Throwable $eNew=null)
Definition: MWExceptionRenderer.php:41
MWExceptionRenderer\isCommandLine
static isCommandLine()
Definition: MWExceptionRenderer.php:309
MWExceptionRenderer\AS_PRETTY
const AS_PRETTY
Definition: MWExceptionRenderer.php:34
MWExceptionRenderer\getCustomMessage
static getCustomMessage(Throwable $e)
Extract an additional user-visible message from an exception, or null if it has none.
Definition: MWExceptionRenderer.php:290
Wikimedia\Rdbms\DBReadOnlyError
@newable
Definition: DBReadOnlyError.php:28
MWExceptionRenderer\useOutputPage
static useOutputPage(Throwable $e)
Definition: MWExceptionRenderer.php:118
$header
$header
Definition: updateCredits.php:37
MWExceptionRenderer\header
static header( $header)
Definition: MWExceptionRenderer.php:316
$wgSitename
$wgSitename
Name of the site.
Definition: DefaultSettings.php:82
MWExceptionRenderer\getShowBacktraceError
static getShowBacktraceError(Throwable $e)
Definition: MWExceptionRenderer.php:258
MWExceptionRenderer\printError
static printError( $message)
Print a message, if possible to STDERR.
Definition: MWExceptionRenderer.php:338
RequestContext\getMain
static getMain()
Get the RequestContext object associated with the main request.
Definition: RequestContext.php:484
HttpStatus\header
static header( $code)
Output an HTTP status code header.
Definition: HttpStatus.php:96
Wikimedia\Rdbms\DBExpectedError
Base class for the more common types of database errors.
Definition: DBExpectedError.php:34
WebRequest\getRequestId
static getRequestId()
Get the current request ID.
Definition: WebRequest.php:333
$wgShowExceptionDetails
$wgShowExceptionDetails
If set to true, uncaught exceptions will print the exception message and a complete stack trace to ou...
Definition: DefaultSettings.php:7415
getTitle
getTitle()
Definition: RevisionSearchResultTrait.php:81
MWExceptionRenderer\statusHeader
static statusHeader( $code)
Definition: MWExceptionRenderer.php:325
Wikimedia\Rdbms\DBConnectionError
@newable
Definition: DBConnectionError.php:27
Html\element
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:232
$wgOut
$wgOut
Definition: Setup.php:836
MWExceptionRenderer
Class to expose exceptions to the client (API bots, users, admins using CLI scripts)
Definition: MWExceptionRenderer.php:32
MWExceptionHandler\getLogMessage
static getLogMessage(Throwable $e)
Get a message formatting the throwable message and its origin.
Definition: MWExceptionHandler.php:486
MWExceptionRenderer\getExceptionTitle
static getExceptionTitle(Throwable $e)
Get the page title to be used for a given exception.
Definition: MWExceptionRenderer.php:269
MWExceptionRenderer\AS_RAW
const AS_RAW
Definition: MWExceptionRenderer.php:33