MediaWiki REL1_30
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::handleException' );
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
128 public static function handleException( $e ) {
129 self::rollbackMasterChangesAndLog( $e );
130 self::report( $e );
131 }
132
151 public static function handleError(
152 $level, $message, $file = null, $line = null
153 ) {
154 if ( in_array( $level, self::$fatalErrorTypes ) ) {
155 return call_user_func_array(
156 'MWExceptionHandler::handleFatalError', func_get_args()
157 );
158 }
159
160 // Map error constant to error name (reverse-engineer PHP error
161 // reporting)
162 switch ( $level ) {
163 case E_RECOVERABLE_ERROR:
164 $levelName = 'Error';
165 $severity = LogLevel::ERROR;
166 break;
167 case E_WARNING:
168 case E_CORE_WARNING:
169 case E_COMPILE_WARNING:
170 case E_USER_WARNING:
171 $levelName = 'Warning';
172 $severity = LogLevel::WARNING;
173 break;
174 case E_NOTICE:
175 case E_USER_NOTICE:
176 $levelName = 'Notice';
177 $severity = LogLevel::INFO;
178 break;
179 case E_STRICT:
180 $levelName = 'Strict Standards';
181 $severity = LogLevel::DEBUG;
182 break;
183 case E_DEPRECATED:
184 case E_USER_DEPRECATED:
185 $levelName = 'Deprecated';
186 $severity = LogLevel::INFO;
187 break;
188 default:
189 $levelName = 'Unknown error';
190 $severity = LogLevel::ERROR;
191 break;
192 }
193
194 $e = new ErrorException( "PHP $levelName: $message", 0, $level, $file, $line );
195 self::logError( $e, 'error', $severity );
196
197 // This handler is for logging only. Return false will instruct PHP
198 // to continue regular handling.
199 return false;
200 }
201
223 public static function handleFatalError(
224 $level = null, $message = null, $file = null, $line = null,
225 $context = null, $trace = null
226 ) {
227 // Free reserved memory so that we have space to process OOM
228 // errors
229 self::$reservedMemory = null;
230
231 if ( $level === null ) {
232 // Called as a shutdown handler, get data from error_get_last()
233 if ( static::$handledFatalCallback ) {
234 // Already called once (probably as an error handler callback
235 // under HHVM) so don't log again.
236 return false;
237 }
238
239 $lastError = error_get_last();
240 if ( $lastError !== null ) {
241 $level = $lastError['type'];
242 $message = $lastError['message'];
243 $file = $lastError['file'];
244 $line = $lastError['line'];
245 } else {
246 $level = 0;
247 $message = '';
248 }
249 }
250
251 if ( !in_array( $level, self::$fatalErrorTypes ) ) {
252 // Only interested in fatal errors, others should have been
253 // handled by MWExceptionHandler::handleError
254 return false;
255 }
256
257 $msg = "[{exception_id}] PHP Fatal Error: {$message}";
258
259 // Look at message to see if this is a class not found failure
260 // HHVM: Class undefined: foo
261 // PHP5: Class 'foo' not found
262 if ( preg_match( "/Class (undefined: \w+|'\w+' not found)/", $msg ) ) {
263 // @codingStandardsIgnoreStart Generic.Files.LineLength.TooLong
264 $msg = <<<TXT
265{$msg}
266
267MediaWiki 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.
268
269Please see <a href="https://www.mediawiki.org/wiki/Download_from_Git#Fetch_external_libraries">mediawiki.org</a> for help on installing the required components.
270TXT;
271 // @codingStandardsIgnoreEnd
272 }
273
274 // We can't just create an exception and log it as it is likely that
275 // the interpreter has unwound the stack already. If that is true the
276 // stacktrace we would get would be functionally empty. If however we
277 // have been called as an error handler callback *and* HHVM is in use
278 // we will have been provided with a useful stacktrace that we can
279 // log.
280 $trace = $trace ?: debug_backtrace();
281 $logger = LoggerFactory::getInstance( 'fatal' );
282 $logger->error( $msg, [
283 'fatal_exception' => [
284 'class' => 'ErrorException',
285 'message' => "PHP Fatal Error: {$message}",
286 'code' => $level,
287 'file' => $file,
288 'line' => $line,
289 'trace' => static::redactTrace( $trace ),
290 ],
291 'exception_id' => wfRandomString( 8 ),
292 'caught_by' => self::CAUGHT_BY_HANDLER
293 ] );
294
295 // Remember call so we don't double process via HHVM's fatal
296 // notifications and the shutdown hook behavior
297 static::$handledFatalCallback = true;
298 return false;
299 }
300
311 public static function getRedactedTraceAsString( $e ) {
312 return self::prettyPrintTrace( self::getRedactedTrace( $e ) );
313 }
314
323 public static function prettyPrintTrace( array $trace, $pad = '' ) {
324 $text = '';
325
326 $level = 0;
327 foreach ( $trace as $level => $frame ) {
328 if ( isset( $frame['file'] ) && isset( $frame['line'] ) ) {
329 $text .= "{$pad}#{$level} {$frame['file']}({$frame['line']}): ";
330 } else {
331 // 'file' and 'line' are unset for calls via call_user_func
332 // (T57634) This matches behaviour of
333 // Exception::getTraceAsString to instead display "[internal
334 // function]".
335 $text .= "{$pad}#{$level} [internal function]: ";
336 }
337
338 if ( isset( $frame['class'] ) && isset( $frame['type'] ) && isset( $frame['function'] ) ) {
339 $text .= $frame['class'] . $frame['type'] . $frame['function'];
340 } elseif ( isset( $frame['function'] ) ) {
341 $text .= $frame['function'];
342 } else {
343 $text .= 'NO_FUNCTION_GIVEN';
344 }
345
346 if ( isset( $frame['args'] ) ) {
347 $text .= '(' . implode( ', ', $frame['args'] ) . ")\n";
348 } else {
349 $text .= "()\n";
350 }
351 }
352
353 $level = $level + 1;
354 $text .= "{$pad}#{$level} {main}";
355
356 return $text;
357 }
358
370 public static function getRedactedTrace( $e ) {
371 return static::redactTrace( $e->getTrace() );
372 }
373
384 public static function redactTrace( array $trace ) {
385 return array_map( function ( $frame ) {
386 if ( isset( $frame['args'] ) ) {
387 $frame['args'] = array_map( function ( $arg ) {
388 return is_object( $arg ) ? get_class( $arg ) : gettype( $arg );
389 }, $frame['args'] );
390 }
391 return $frame;
392 }, $trace );
393 }
394
406 public static function getLogId( $e ) {
407 wfDeprecated( __METHOD__, '1.27' );
408 return WebRequest::getRequestId();
409 }
410
418 public static function getURL() {
419 global $wgRequest;
420 if ( !isset( $wgRequest ) || $wgRequest instanceof FauxRequest ) {
421 return false;
422 }
423 return $wgRequest->getRequestURL();
424 }
425
433 public static function getLogMessage( $e ) {
434 $id = WebRequest::getRequestId();
435 $type = get_class( $e );
436 $file = $e->getFile();
437 $line = $e->getLine();
438 $message = $e->getMessage();
439 $url = self::getURL() ?: '[no req]';
440
441 return "[$id] $url $type from line $line of $file: $message";
442 }
443
453 public static function getLogNormalMessage( $e ) {
454 $type = get_class( $e );
455 $file = $e->getFile();
456 $line = $e->getLine();
457 $message = $e->getMessage();
458
459 return "[{exception_id}] {exception_url} $type from line $line of $file: $message";
460 }
461
466 public static function getPublicLogMessage( $e ) {
467 $reqId = WebRequest::getRequestId();
468 $type = get_class( $e );
469 return '[' . $reqId . '] '
470 . gmdate( 'Y-m-d H:i:s' ) . ': '
471 . 'Fatal exception of type "' . $type . '"';
472 }
473
485 public static function getLogContext( $e, $catcher = self::CAUGHT_BY_OTHER ) {
486 return [
487 'exception' => $e,
488 'exception_id' => WebRequest::getRequestId(),
489 'exception_url' => self::getURL() ?: '[no req]',
490 'caught_by' => $catcher
491 ];
492 }
493
506 public static function getStructuredExceptionData( $e, $catcher = self::CAUGHT_BY_OTHER ) {
508
509 $data = [
510 'id' => WebRequest::getRequestId(),
511 'type' => get_class( $e ),
512 'file' => $e->getFile(),
513 'line' => $e->getLine(),
514 'message' => $e->getMessage(),
515 'code' => $e->getCode(),
516 'url' => self::getURL() ?: null,
517 'caught_by' => $catcher
518 ];
519
520 if ( $e instanceof ErrorException &&
521 ( error_reporting() & $e->getSeverity() ) === 0
522 ) {
523 // Flag surpressed errors
524 $data['suppressed'] = true;
525 }
526
528 $data['backtrace'] = self::getRedactedTrace( $e );
529 }
530
531 $previous = $e->getPrevious();
532 if ( $previous !== null ) {
533 $data['previous'] = self::getStructuredExceptionData( $previous, $catcher );
534 }
535
536 return $data;
537 }
538
593 public static function jsonSerializeException(
594 $e, $pretty = false, $escaping = 0, $catcher = self::CAUGHT_BY_OTHER
595 ) {
596 return FormatJson::encode(
597 self::getStructuredExceptionData( $e, $catcher ),
598 $pretty,
599 $escaping
600 );
601 }
602
613 public static function logException( $e, $catcher = self::CAUGHT_BY_OTHER ) {
614 if ( !( $e instanceof MWException ) || $e->isLoggable() ) {
615 $logger = LoggerFactory::getInstance( 'exception' );
616 $logger->error(
617 self::getLogNormalMessage( $e ),
618 self::getLogContext( $e, $catcher )
619 );
620
621 $json = self::jsonSerializeException( $e, false, FormatJson::ALL_OK, $catcher );
622 if ( $json !== false ) {
623 $logger = LoggerFactory::getInstance( 'exception-json' );
624 $logger->error( $json, [ 'private' => true ] );
625 }
626
627 Hooks::run( 'LogException', [ $e, false ] );
628 }
629 }
630
639 protected static function logError(
640 ErrorException $e, $channel, $level = LogLevel::ERROR
641 ) {
642 $catcher = self::CAUGHT_BY_HANDLER;
643 // The set_error_handler callback is independent from error_reporting.
644 // Filter out unwanted errors manually (e.g. when
645 // MediaWiki\suppressWarnings is active).
646 $suppressed = ( error_reporting() & $e->getSeverity() ) === 0;
647 if ( !$suppressed ) {
648 $logger = LoggerFactory::getInstance( $channel );
649 $logger->log(
650 $level,
651 self::getLogNormalMessage( $e ),
652 self::getLogContext( $e, $catcher )
653 );
654 }
655
656 // Include all errors in the json log (surpressed errors will be flagged)
657 $json = self::jsonSerializeException( $e, false, FormatJson::ALL_OK, $catcher );
658 if ( $json !== false ) {
659 $logger = LoggerFactory::getInstance( "{$channel}-json" );
660 $logger->log( $level, $json, [ 'private' => true ] );
661 }
662
663 Hooks::run( 'LogException', [ $e, $suppressed ] );
664 }
665}
shown</td >< td > a href
$wgLogExceptionBacktrace
If true, send the exception backtrace to the error log.
wfRandomString( $length=32)
Get a random string containing a number of pseudo-random hex characters.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
if(! $wgDBerrorLogTZ) $wgRequest
Definition Setup.php:662
$line
Definition cdb.php:58
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 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:2158
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:2780
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:2243
returning false will NOT prevent logging $e
Definition hooks.txt:2146
A helper class for throttling authentication attempts.