MediaWiki REL1_31
MWExceptionHandler.php
Go to the documentation of this file.
1<?php
23use Psr\Log\LogLevel;
25
31 const CAUGHT_BY_HANDLER = 'mwe_handler'; // error reported by this exception handler
32 const CAUGHT_BY_OTHER = 'other'; // error reported by direct logException() call
33
37 protected static $reservedMemory;
41 protected static $fatalErrorTypes = [
42 E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR,
43 /* HHVM's FATAL_ERROR level */ 16777217,
44 ];
48 protected static $handledFatalCallback = false;
49
53 public static function installHandler() {
54 set_exception_handler( 'MWExceptionHandler::handleUncaughtException' );
55 set_error_handler( 'MWExceptionHandler::handleError' );
56
57 // Reserve 16k of memory so we can report OOM fatals
58 self::$reservedMemory = str_repeat( ' ', 16384 );
59 register_shutdown_function( 'MWExceptionHandler::handleFatalError' );
60 }
61
66 protected static function report( $e ) {
67 try {
68 // Try and show the exception prettily, with the normal skin infrastructure
69 if ( $e instanceof MWException ) {
70 // Delegate to MWException until all subclasses are handled by
71 // MWExceptionRenderer and MWException::report() has been
72 // removed.
73 $e->report();
74 } else {
76 }
77 } catch ( Exception $e2 ) {
78 // Exception occurred from within exception handler
79 // Show a simpler message for the original exception,
80 // don't try to invoke report()
82 }
83 }
84
93 public static function rollbackMasterChangesAndLog( $e ) {
94 $services = MediaWikiServices::getInstance();
95 if ( !$services->isServiceDisabled( 'DBLoadBalancerFactory' ) ) {
96 // Rollback DBs to avoid transaction notices. This might fail
97 // to rollback some databases due to connection issues or exceptions.
98 // However, any sane DB driver will rollback implicitly anyway.
99 try {
100 $services->getDBLoadBalancerFactory()->rollbackMasterChanges( __METHOD__ );
101 } catch ( DBError $e2 ) {
102 // If the DB is unreacheable, rollback() will throw an error
103 // and the error report() method might need messages from the DB,
104 // which would result in an exception loop. PHP may escalate such
105 // errors to "Exception thrown without a stack frame" fatals, but
106 // it's better to be explicit here.
107 self::logException( $e2, self::CAUGHT_BY_HANDLER );
108 }
109 }
110
111 self::logException( $e, self::CAUGHT_BY_HANDLER );
112 }
113
120 public static function handleUncaughtException( $e ) {
121 self::handleException( $e );
122
123 // Make sure we don't claim success on exit for CLI scripts (T177414)
124 if ( wfIsCLI() ) {
125 register_shutdown_function(
126 function () {
127 exit( 255 );
128 }
129 );
130 }
131 }
132
147 public static function handleException( $e ) {
148 self::rollbackMasterChangesAndLog( $e );
149 self::report( $e );
150 }
151
170 public static function handleError(
171 $level, $message, $file = null, $line = null
172 ) {
173 global $wgPropagateErrors;
174
175 if ( in_array( $level, self::$fatalErrorTypes ) ) {
176 return call_user_func_array(
177 'MWExceptionHandler::handleFatalError', func_get_args()
178 );
179 }
180
181 // Map error constant to error name (reverse-engineer PHP error
182 // reporting)
183 switch ( $level ) {
184 case E_RECOVERABLE_ERROR:
185 $levelName = 'Error';
186 $severity = LogLevel::ERROR;
187 break;
188 case E_WARNING:
189 case E_CORE_WARNING:
190 case E_COMPILE_WARNING:
191 case E_USER_WARNING:
192 $levelName = 'Warning';
193 $severity = LogLevel::WARNING;
194 break;
195 case E_NOTICE:
196 case E_USER_NOTICE:
197 $levelName = 'Notice';
198 $severity = LogLevel::INFO;
199 break;
200 case E_STRICT:
201 $levelName = 'Strict Standards';
202 $severity = LogLevel::DEBUG;
203 break;
204 case E_DEPRECATED:
205 case E_USER_DEPRECATED:
206 $levelName = 'Deprecated';
207 $severity = LogLevel::INFO;
208 break;
209 default:
210 $levelName = 'Unknown error';
211 $severity = LogLevel::ERROR;
212 break;
213 }
214
215 $e = new ErrorException( "PHP $levelName: $message", 0, $level, $file, $line );
216 self::logError( $e, 'error', $severity );
217
218 // If $wgPropagateErrors is true return false so PHP shows/logs the error normally.
219 // Ignore $wgPropagateErrors if the error should break execution, or track_errors is set
220 // (which means someone is counting on regular PHP error handling behavior).
221 return !( $wgPropagateErrors || $level == E_RECOVERABLE_ERROR || ini_get( 'track_errors' ) );
222 }
223
245 public static function handleFatalError(
246 $level = null, $message = null, $file = null, $line = null,
247 $context = null, $trace = null
248 ) {
249 // Free reserved memory so that we have space to process OOM
250 // errors
251 self::$reservedMemory = null;
252
253 if ( $level === null ) {
254 // Called as a shutdown handler, get data from error_get_last()
255 if ( static::$handledFatalCallback ) {
256 // Already called once (probably as an error handler callback
257 // under HHVM) so don't log again.
258 return false;
259 }
260
261 $lastError = error_get_last();
262 if ( $lastError !== null ) {
263 $level = $lastError['type'];
264 $message = $lastError['message'];
265 $file = $lastError['file'];
266 $line = $lastError['line'];
267 } else {
268 $level = 0;
269 $message = '';
270 }
271 }
272
273 if ( !in_array( $level, self::$fatalErrorTypes ) ) {
274 // Only interested in fatal errors, others should have been
275 // handled by MWExceptionHandler::handleError
276 return false;
277 }
278
279 $url = WebRequest::getGlobalRequestURL();
280 $msgParts = [
281 '[{exception_id}] {exception_url} PHP Fatal Error',
282 ( $line || $file ) ? ' from' : '',
283 $line ? " line $line" : '',
284 ( $line && $file ) ? ' of' : '',
285 $file ? " $file" : '',
286 ": $message",
287 ];
288 $msg = implode( '', $msgParts );
289
290 // Look at message to see if this is a class not found failure
291 // HHVM: Class undefined: foo
292 // PHP5: Class 'foo' not found
293 if ( preg_match( "/Class (undefined: \w+|'\w+' not found)/", $message ) ) {
294 // phpcs:disable Generic.Files.LineLength
295 $msg = <<<TXT
296{$msg}
297
298MediaWiki or an installed extension requires this class but it is not embedded directly in MediaWiki's git repository and must be installed separately by the end user.
299
300Please see <a href="https://www.mediawiki.org/wiki/Download_from_Git#Fetch_external_libraries">mediawiki.org</a> for help on installing the required components.
301TXT;
302 // phpcs:enable
303 }
304
305 // We can't just create an exception and log it as it is likely that
306 // the interpreter has unwound the stack already. If that is true the
307 // stacktrace we would get would be functionally empty. If however we
308 // have been called as an error handler callback *and* HHVM is in use
309 // we will have been provided with a useful stacktrace that we can
310 // log.
311 $trace = $trace ?: debug_backtrace();
312 $logger = LoggerFactory::getInstance( 'fatal' );
313 $logger->error( $msg, [
314 'fatal_exception' => [
315 'class' => ErrorException::class,
316 'message' => "PHP Fatal Error: {$message}",
317 'code' => $level,
318 'file' => $file,
319 'line' => $line,
320 'trace' => self::prettyPrintTrace( self::redactTrace( $trace ) ),
321 ],
322 'exception_id' => WebRequest::getRequestId(),
323 'exception_url' => $url,
324 'caught_by' => self::CAUGHT_BY_HANDLER
325 ] );
326
327 // Remember call so we don't double process via HHVM's fatal
328 // notifications and the shutdown hook behavior
329 static::$handledFatalCallback = true;
330 return false;
331 }
332
343 public static function getRedactedTraceAsString( $e ) {
344 return self::prettyPrintTrace( self::getRedactedTrace( $e ) );
345 }
346
355 public static function prettyPrintTrace( array $trace, $pad = '' ) {
356 $text = '';
357
358 $level = 0;
359 foreach ( $trace as $level => $frame ) {
360 if ( isset( $frame['file'] ) && isset( $frame['line'] ) ) {
361 $text .= "{$pad}#{$level} {$frame['file']}({$frame['line']}): ";
362 } else {
363 // 'file' and 'line' are unset for calls via call_user_func
364 // (T57634) This matches behaviour of
365 // Exception::getTraceAsString to instead display "[internal
366 // function]".
367 $text .= "{$pad}#{$level} [internal function]: ";
368 }
369
370 if ( isset( $frame['class'] ) && isset( $frame['type'] ) && isset( $frame['function'] ) ) {
371 $text .= $frame['class'] . $frame['type'] . $frame['function'];
372 } elseif ( isset( $frame['function'] ) ) {
373 $text .= $frame['function'];
374 } else {
375 $text .= 'NO_FUNCTION_GIVEN';
376 }
377
378 if ( isset( $frame['args'] ) ) {
379 $text .= '(' . implode( ', ', $frame['args'] ) . ")\n";
380 } else {
381 $text .= "()\n";
382 }
383 }
384
385 $level = $level + 1;
386 $text .= "{$pad}#{$level} {main}";
387
388 return $text;
389 }
390
402 public static function getRedactedTrace( $e ) {
403 return static::redactTrace( $e->getTrace() );
404 }
405
416 public static function redactTrace( array $trace ) {
417 return array_map( function ( $frame ) {
418 if ( isset( $frame['args'] ) ) {
419 $frame['args'] = array_map( function ( $arg ) {
420 return is_object( $arg ) ? get_class( $arg ) : gettype( $arg );
421 }, $frame['args'] );
422 }
423 return $frame;
424 }, $trace );
425 }
426
438 public static function getLogId( $e ) {
439 wfDeprecated( __METHOD__, '1.27' );
440 return WebRequest::getRequestId();
441 }
442
450 public static function getURL() {
451 global $wgRequest;
452 if ( !isset( $wgRequest ) || $wgRequest instanceof FauxRequest ) {
453 return false;
454 }
455 return $wgRequest->getRequestURL();
456 }
457
465 public static function getLogMessage( $e ) {
466 $id = WebRequest::getRequestId();
467 $type = get_class( $e );
468 $file = $e->getFile();
469 $line = $e->getLine();
470 $message = $e->getMessage();
471 $url = self::getURL() ?: '[no req]';
472
473 return "[$id] $url $type from line $line of $file: $message";
474 }
475
485 public static function getLogNormalMessage( $e ) {
486 $type = get_class( $e );
487 $file = $e->getFile();
488 $line = $e->getLine();
489 $message = $e->getMessage();
490
491 return "[{exception_id}] {exception_url} $type from line $line of $file: $message";
492 }
493
498 public static function getPublicLogMessage( $e ) {
499 $reqId = WebRequest::getRequestId();
500 $type = get_class( $e );
501 return '[' . $reqId . '] '
502 . gmdate( 'Y-m-d H:i:s' ) . ': '
503 . 'Fatal exception of type "' . $type . '"';
504 }
505
517 public static function getLogContext( $e, $catcher = self::CAUGHT_BY_OTHER ) {
518 return [
519 'exception' => $e,
520 'exception_id' => WebRequest::getRequestId(),
521 'exception_url' => self::getURL() ?: '[no req]',
522 'caught_by' => $catcher
523 ];
524 }
525
538 public static function getStructuredExceptionData( $e, $catcher = self::CAUGHT_BY_OTHER ) {
540
541 $data = [
542 'id' => WebRequest::getRequestId(),
543 'type' => get_class( $e ),
544 'file' => $e->getFile(),
545 'line' => $e->getLine(),
546 'message' => $e->getMessage(),
547 'code' => $e->getCode(),
548 'url' => self::getURL() ?: null,
549 'caught_by' => $catcher
550 ];
551
552 if ( $e instanceof ErrorException &&
553 ( error_reporting() & $e->getSeverity() ) === 0
554 ) {
555 // Flag surpressed errors
556 $data['suppressed'] = true;
557 }
558
560 $data['backtrace'] = self::getRedactedTrace( $e );
561 }
562
563 $previous = $e->getPrevious();
564 if ( $previous !== null ) {
565 $data['previous'] = self::getStructuredExceptionData( $previous, $catcher );
566 }
567
568 return $data;
569 }
570
625 public static function jsonSerializeException(
626 $e, $pretty = false, $escaping = 0, $catcher = self::CAUGHT_BY_OTHER
627 ) {
628 return FormatJson::encode(
629 self::getStructuredExceptionData( $e, $catcher ),
630 $pretty,
631 $escaping
632 );
633 }
634
645 public static function logException( $e, $catcher = self::CAUGHT_BY_OTHER ) {
646 if ( !( $e instanceof MWException ) || $e->isLoggable() ) {
647 $logger = LoggerFactory::getInstance( 'exception' );
648 $logger->error(
649 self::getLogNormalMessage( $e ),
650 self::getLogContext( $e, $catcher )
651 );
652
653 $json = self::jsonSerializeException( $e, false, FormatJson::ALL_OK, $catcher );
654 if ( $json !== false ) {
655 $logger = LoggerFactory::getInstance( 'exception-json' );
656 $logger->error( $json, [ 'private' => true ] );
657 }
658
659 Hooks::run( 'LogException', [ $e, false ] );
660 }
661 }
662
671 protected static function logError(
672 ErrorException $e, $channel, $level = LogLevel::ERROR
673 ) {
674 $catcher = self::CAUGHT_BY_HANDLER;
675 // The set_error_handler callback is independent from error_reporting.
676 // Filter out unwanted errors manually (e.g. when
677 // Wikimedia\suppressWarnings is active).
678 $suppressed = ( error_reporting() & $e->getSeverity() ) === 0;
679 if ( !$suppressed ) {
680 $logger = LoggerFactory::getInstance( $channel );
681 $logger->log(
682 $level,
683 self::getLogNormalMessage( $e ),
684 self::getLogContext( $e, $catcher )
685 );
686 }
687
688 // Include all errors in the json log (surpressed errors will be flagged)
689 $json = self::jsonSerializeException( $e, false, FormatJson::ALL_OK, $catcher );
690 if ( $json !== false ) {
691 $logger = LoggerFactory::getInstance( "{$channel}-json" );
692 $logger->log( $level, $json, [ 'private' => true ] );
693 }
694
695 Hooks::run( 'LogException', [ $e, $suppressed ] );
696 }
697}
shown</td >< td > a href
$wgLogExceptionBacktrace
If true, send the exception backtrace to the error log.
$wgPropagateErrors
If true, the MediaWiki error handler passes errors/warnings to the default error handler after loggin...
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
wfIsCLI()
Check if we are running from the commandline.
if(! $wgDBerrorLogTZ) $wgRequest
Definition Setup.php:737
$line
Definition cdb.php:59
WebRequest clone which takes values from a provided array.
Handler class for MWExceptions.
static handleError( $level, $message, $file=null, $line=null)
Handler for set_error_handler() callback notifications.
static getRedactedTraceAsString( $e)
Generate a string representation of an exception's stack trace.
static handleFatalError( $level=null, $message=null, $file=null, $line=null, $context=null, $trace=null)
Dual purpose callback used as both a set_error_handler() callback and a registered shutdown function.
static jsonSerializeException( $e, $pretty=false, $escaping=0, $catcher=self::CAUGHT_BY_OTHER)
Serialize an Exception object to JSON.
static getLogMessage( $e)
Get a message formatting the exception message and its origin.
static prettyPrintTrace(array $trace, $pad='')
Generate a string representation of a stacktrace.
static getLogNormalMessage( $e)
Get a normalised message for formatting with PSR-3 log event context.
static logError(ErrorException $e, $channel, $level=LogLevel::ERROR)
Log an exception that wasn't thrown but made to wrap an error.
static rollbackMasterChangesAndLog( $e)
Roll back any open database transactions and log the stack trace of the exception.
static handleUncaughtException( $e)
Callback to use with PHP's set_exception_handler.
static report( $e)
Report an exception to the user.
static getStructuredExceptionData( $e, $catcher=self::CAUGHT_BY_OTHER)
Get a structured representation of an Exception.
static redactTrace(array $trace)
Redact a stacktrace generated by Exception::getTrace(), debug_backtrace() or similar means.
static logException( $e, $catcher=self::CAUGHT_BY_OTHER)
Log an exception to the exception log (if enabled).
static installHandler()
Install handlers with PHP.
static getRedactedTrace( $e)
Return a copy of an exception's backtrace as an array.
static getLogId( $e)
Get the ID for this exception.
static getURL()
If the exception occurred in the course of responding to a request, returns the requested URL.
static getLogContext( $e, $catcher=self::CAUGHT_BY_OTHER)
Get a PSR-3 log event context from an Exception.
static handleException( $e)
Exception handler which simulates the appropriate catch() handling:
static output( $e, $mode, $eNew=null)
MediaWiki exception.
PSR-3 logger instance factory.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Database error base class.
Definition DBError.php:30
returning false will NOT prevent logging a wrapping ErrorException $suppressed
Definition hooks.txt:2188
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on and they can depend only on the ResourceLoaderContext $context
Definition hooks.txt:2811
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title after the basic globals have been set but before ordinary actions take place or wrap services the preferred way to define a new service is the $wgServiceWiringFiles array $services
Definition hooks.txt:2273
returning false will NOT prevent logging $e
Definition hooks.txt:2176
A helper class for throttling authentication attempts.