MediaWiki  master
MWExceptionRenderer.php
Go to the documentation of this file.
1 <?php
22 use Wikimedia\AtEase;
26 
32  public const AS_RAW = 1; // show as text
33  public const AS_PRETTY = 2; // show as HTML
34 
40  public static function output( Throwable $e, $mode, Throwable $eNew = null ) {
42 
43  if ( function_exists( 'apache_setenv' ) ) {
44  // The client should not be blocked on "post-send" updates. If apache decides that
45  // a response should be gzipped, it will wait for PHP to finish since it cannot gzip
46  // anything until it has the full response (even with "Transfer-Encoding: chunked").
47  AtEase\AtEase::suppressWarnings();
48  apache_setenv( 'no-gzip', '1' );
49  AtEase\AtEase::restoreWarnings();
50  }
51 
52  if ( defined( 'MW_API' ) ) {
53  self::header( 'MediaWiki-API-Error: internal_api_error_' . get_class( $e ) );
54  }
55 
56  if ( self::isCommandLine() ) {
57  self::printError( self::getText( $e ) );
58  } elseif ( $mode === self::AS_PRETTY ) {
59  self::statusHeader( 500 );
60  self::header( "Content-Type: $wgMimeType; charset=UTF-8" );
61  ob_start();
62  if ( $e instanceof DBConnectionError ) {
64  } else {
65  self::reportHTML( $e );
66  }
67  self::header( "Content-Length: " . ob_get_length() );
68  ob_end_flush();
69  } else {
70  ob_start();
71  self::statusHeader( 500 );
72  self::header( "Content-Type: $wgMimeType; charset=UTF-8" );
73  if ( $eNew ) {
74  $message = "MediaWiki internal error.\n\n";
76  $message .= 'Original exception: ' .
78  "\nBacktrace:\n" . MWExceptionHandler::getRedactedTraceAsString( $e ) .
79  "\n\nException caught inside exception handler: " .
81  "\nBacktrace:\n" . MWExceptionHandler::getRedactedTraceAsString( $eNew );
82  } else {
83  $message .= 'Original exception: ' .
85  $message .= "\n\nException caught inside exception handler.\n\n" .
87  }
88  $message .= "\n";
89  } elseif ( $wgShowExceptionDetails ) {
90  $message = MWExceptionHandler::getLogMessage( $e ) .
91  "\nBacktrace:\n" .
93  } else {
95  }
96  print nl2br( htmlspecialchars( $message ) ) . "\n";
97  self::header( "Content-Length: " . ob_get_length() );
98  ob_end_flush();
99  }
100  }
101 
106  private static function useOutputPage( Throwable $e ) {
107  // Can the extension use the Message class/wfMessage to get i18n-ed messages?
108  foreach ( $e->getTrace() as $frame ) {
109  if ( isset( $frame['class'] ) && $frame['class'] === LocalisationCache::class ) {
110  return false;
111  }
112  }
113 
114  // Don't even bother with OutputPage if there's no Title context set,
115  // (e.g. we're in RL code on load.php) - the Skin system (and probably
116  // most of MediaWiki) won't work.
117 
118  return (
119  !empty( $GLOBALS['wgFullyInitialised'] ) &&
120  !empty( $GLOBALS['wgOut'] ) &&
122  !defined( 'MEDIAWIKI_INSTALL' ) &&
123  // Don't send a skinned HTTP 500 page to API clients.
124  !defined( 'MW_API' )
125  );
126  }
127 
133  private static function reportHTML( Throwable $e ) {
134  global $wgOut, $wgSitename;
135 
136  if ( self::useOutputPage( $e ) ) {
137  if ( $e instanceof MWException ) {
138  $wgOut->prepareErrorPage( $e->getPageTitle() );
139  } elseif ( $e instanceof DBReadOnlyError ) {
140  $wgOut->prepareErrorPage( self::msg( 'readonly', 'Database is locked' ) );
141  } elseif ( $e instanceof DBExpectedError ) {
142  $wgOut->prepareErrorPage( self::msg( 'databaseerror', 'Database error' ) );
143  } else {
144  $wgOut->prepareErrorPage( self::msg( 'internalerror', 'Internal error' ) );
145  }
146 
147  // Show any custom GUI message before the details
148  if ( $e instanceof MessageSpecifier ) {
149  $wgOut->addHTML( Html::element( 'p', [], Message::newFromSpecifier( $e )->text() ) );
150  }
151  $wgOut->addHTML( self::getHTML( $e ) );
152 
153  $wgOut->output();
154  } else {
155  self::header( 'Content-Type: text/html; charset=utf-8' );
156  $pageTitle = self::msg( 'internalerror', 'Internal error' );
157  echo "<!DOCTYPE html>\n" .
158  '<html><head>' .
159  // Mimick OutputPage::setPageTitle behaviour
160  '<title>' .
161  htmlspecialchars( self::msg( 'pagetitle', "$1 - $wgSitename", $pageTitle ) ) .
162  '</title>' .
163  '<style>body { font-family: sans-serif; margin: 0; padding: 0.5em 2em; }</style>' .
164  "</head><body>\n";
165 
166  echo self::getHTML( $e );
167 
168  echo "</body></html>\n";
169  }
170  }
171 
180  public static function getHTML( Throwable $e ) {
182 
183  if ( $wgShowExceptionDetails ) {
184  $html = "<div class=\"errorbox mw-content-ltr\"><p>" .
185  nl2br( htmlspecialchars( MWExceptionHandler::getLogMessage( $e ) ) ) .
186  '</p><p>Backtrace:</p><p>' .
187  nl2br( htmlspecialchars( MWExceptionHandler::getRedactedTraceAsString( $e ) ) ) .
188  "</p></div>\n";
189  } else {
190  $logId = WebRequest::getRequestId();
191  $html = "<div class=\"errorbox mw-content-ltr\">" .
192  htmlspecialchars(
193  '[' . $logId . '] ' .
194  gmdate( 'Y-m-d H:i:s' ) . ": " .
195  self::msg( "internalerror-fatal-exception",
196  "Fatal exception of type $1",
197  get_class( $e ),
198  $logId,
200  ) ) . "</div>\n" .
201  "<!-- " . wordwrap( self::getShowBacktraceError( $e ), 50 ) . " -->";
202  }
203 
204  return $html;
205  }
206 
216  private static function msg( $key, $fallback, ...$params ) {
217  global $wgSitename;
218 
219  // FIXME: Keep logic in sync with MWException::msg.
220  try {
221  $res = wfMessage( $key, ...$params )->text();
222  } catch ( Exception $e ) {
223  $res = wfMsgReplaceArgs( $fallback, $params );
224  // If an exception happens inside message rendering,
225  // {{SITENAME}} sometimes won't be replaced.
226  $res = strtr( $res, [
227  '{{SITENAME}}' => $wgSitename,
228  ] );
229  }
230  return $res;
231  }
232 
237  private static function getText( Throwable $e ) {
239 
240  if ( $wgShowExceptionDetails ) {
241  return MWExceptionHandler::getLogMessage( $e ) .
242  "\nBacktrace:\n" .
244  } else {
245  return self::getShowBacktraceError( $e ) . "\n";
246  }
247  }
248 
253  private static function getShowBacktraceError( Throwable $e ) {
254  $var = '$wgShowExceptionDetails = true;';
255  return "Set $var at the bottom of LocalSettings.php to show detailed debugging information.";
256  }
257 
261  private static function isCommandLine() {
262  return !empty( $GLOBALS['wgCommandLineMode'] );
263  }
264 
268  private static function header( $header ) {
269  if ( !headers_sent() ) {
270  header( $header );
271  }
272  }
273 
277  private static function statusHeader( $code ) {
278  if ( !headers_sent() ) {
279  HttpStatus::header( $code );
280  }
281  }
282 
290  private static function printError( $message ) {
291  // NOTE: STDERR may not be available, especially if php-cgi is used from the
292  // command line (T17602). Try to produce meaningful output anyway. Using
293  // echo may corrupt output to STDOUT though.
294  if ( defined( 'STDERR' ) ) {
295  fwrite( STDERR, $message );
296  } else {
297  echo $message;
298  }
299  }
300 
304  private static function reportOutageHTML( Throwable $e ) {
306 
307  $sorry = htmlspecialchars( self::msg(
308  'dberr-problems',
309  'Sorry! This site is experiencing technical difficulties.'
310  ) );
311  $again = htmlspecialchars( self::msg(
312  'dberr-again',
313  'Try waiting a few minutes and reloading.'
314  ) );
315 
316  if ( $wgShowHostnames ) {
317  $info = str_replace(
318  '$1',
319  Html::element( 'span', [ 'dir' => 'ltr' ], $e->getMessage() ),
320  htmlspecialchars( self::msg( 'dberr-info', '($1)' ) )
321  );
322  } else {
323  $info = htmlspecialchars( self::msg(
324  'dberr-info-hidden',
325  '(Cannot access the database)'
326  ) );
327  }
328 
329  MediaWikiServices::getInstance()->getMessageCache()->disable(); // no DB access
330  $html = "<!DOCTYPE html>\n" .
331  '<html><head>' .
332  '<title>' .
333  htmlspecialchars( $wgSitename ) .
334  '</title>' .
335  '<style>body { font-family: sans-serif; margin: 0; padding: 0.5em 2em; }</style>' .
336  "</head><body><h1>$sorry</h1><p>$again</p><p><small>$info</small></p>";
337 
338  if ( $wgShowExceptionDetails ) {
339  $html .= '<p>Backtrace:</p><pre>' .
340  htmlspecialchars( $e->getTraceAsString() ) . '</pre>';
341  }
342 
343  $html .= '</body></html>';
344  echo $html;
345  }
346 }
Message\newFromSpecifier
static newFromSpecifier( $value)
Transform a MessageSpecifier or a primitive value used interchangeably with specifiers (a message key...
Definition: Message.php:423
$wgMimeType
$wgMimeType
The default Content-Type header.
Definition: DefaultSettings.php:3387
MWExceptionRenderer\getHTML
static getHTML(Throwable $e)
If $wgShowExceptionDetails is true, return a HTML message with a backtrace to the error,...
Definition: MWExceptionRenderer.php:180
MWExceptionRenderer\getText
static getText(Throwable $e)
Definition: MWExceptionRenderer.php:237
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:163
MWExceptionRenderer\msg
static msg( $key, $fallback,... $params)
Get a message from i18n.
Definition: MWExceptionRenderer.php:216
MWExceptionHandler\getPublicLogMessage
static getPublicLogMessage(Throwable $e)
Definition: MWExceptionHandler.php:513
MWExceptionRenderer\reportOutageHTML
static reportOutageHTML(Throwable $e)
Definition: MWExceptionRenderer.php:304
$fallback
$fallback
Definition: MessagesAb.php:11
MessageSpecifier
Stable for implementing.
Definition: MessageSpecifier.php:24
$wgShowHostnames
$wgShowHostnames
Expose backend server host names through the API and various HTML comments.
Definition: DefaultSettings.php:6777
MWExceptionRenderer\reportHTML
static reportHTML(Throwable $e)
Output the throwable report using HTML.
Definition: MWExceptionRenderer.php:133
wfMsgReplaceArgs
wfMsgReplaceArgs( $message, $args)
Replace message parameter keys on the given formatted output.
Definition: GlobalFunctions.php:1255
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1220
$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:364
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:455
MWExceptionRenderer\output
static output(Throwable $e, $mode, Throwable $eNew=null)
Definition: MWExceptionRenderer.php:40
MWExceptionRenderer\isCommandLine
static isCommandLine()
Definition: MWExceptionRenderer.php:261
MWExceptionRenderer\AS_PRETTY
const AS_PRETTY
Definition: MWExceptionRenderer.php:33
Wikimedia\Rdbms\DBReadOnlyError
@newable
Definition: DBReadOnlyError.php:28
MWExceptionRenderer\useOutputPage
static useOutputPage(Throwable $e)
Definition: MWExceptionRenderer.php:106
$header
$header
Definition: updateCredits.php:41
MWExceptionRenderer\header
static header( $header)
Definition: MWExceptionRenderer.php:268
$wgSitename
$wgSitename
Name of the site.
Definition: DefaultSettings.php:80
MWExceptionRenderer\getShowBacktraceError
static getShowBacktraceError(Throwable $e)
Definition: MWExceptionRenderer.php:253
MWExceptionRenderer\printError
static printError( $message)
Print a message, if possible to STDERR.
Definition: MWExceptionRenderer.php:290
RequestContext\getMain
static getMain()
Get the RequestContext object associated with the main request.
Definition: RequestContext.php:454
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 unique request ID.
Definition: WebRequest.php:327
$wgShowExceptionDetails
$wgShowExceptionDetails
If set to true, uncaught exceptions will print the exception message and a complete stack trace to ou...
Definition: DefaultSettings.php:6749
getTitle
getTitle()
Definition: RevisionSearchResultTrait.php:81
MWExceptionRenderer\statusHeader
static statusHeader( $code)
Definition: MWExceptionRenderer.php:277
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:231
$wgOut
$wgOut
Definition: Setup.php:781
MWExceptionRenderer
Class to expose exceptions to the client (API bots, users, admins using CLI scripts)
Definition: MWExceptionRenderer.php:31
MWExceptionHandler\getLogMessage
static getLogMessage(Throwable $e)
Get a message formatting the throwable message and its origin.
Definition: MWExceptionHandler.php:474
$GLOBALS
$GLOBALS['IP']
Definition: ComposerHookHandler.php:6
MWExceptionRenderer\AS_RAW
const AS_RAW
Definition: MWExceptionRenderer.php:32