X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=blobdiff_plain;f=includes%2Fexception%2FMWExceptionHandler.php;h=d25f1a82694fa20d760b3b2dbf6058cc5b2c24c2;hp=def653fb300633edeb7d3a2cf7b53108897a0ef6;hb=59ebff658ce912c1b0e7ef8d8f9bfec5a4e17b39;hpb=180c2020cc8d703b8f6e9a8ad3988ca431560250 diff --git a/includes/exception/MWExceptionHandler.php b/includes/exception/MWExceptionHandler.php index def653fb30..d25f1a8269 100644 --- a/includes/exception/MWExceptionHandler.php +++ b/includes/exception/MWExceptionHandler.php @@ -26,31 +26,39 @@ use MediaWiki\Logger\LoggerFactory; */ class MWExceptionHandler { + /** + * @var string $reservedMemory + */ protected static $reservedMemory; + /** + * @var array $fatalErrorTypes + */ protected static $fatalErrorTypes = array( E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, /* HHVM's FATAL_ERROR level */ 16777217, ); + /** + * @var bool $handledFatalCallback + */ + protected static $handledFatalCallback = false; /** * Install handlers with PHP. */ public static function installHandler() { - set_exception_handler( array( 'MWExceptionHandler', 'handleException' ) ); - set_error_handler( array( 'MWExceptionHandler', 'handleError' ) ); + set_exception_handler( 'MWExceptionHandler::handleException' ); + set_error_handler( 'MWExceptionHandler::handleError' ); // Reserve 16k of memory so we can report OOM fatals self::$reservedMemory = str_repeat( ' ', 16384 ); - register_shutdown_function( - array( 'MWExceptionHandler', 'handleFatalError' ) - ); + register_shutdown_function( 'MWExceptionHandler::handleFatalError' ); } /** * Report an exception to the user - * @param Exception $e + * @param Exception|Throwable $e */ - protected static function report( Exception $e ) { + protected static function report( $e ) { global $wgShowExceptionDetails; $cmdLine = MWException::isCommandLine(); @@ -124,9 +132,9 @@ class MWExceptionHandler { * transaction could be aborted properly. * * @since 1.23 - * @param Exception $e + * @param Exception|Throwable $e */ - public static function rollbackMasterChangesAndLog( Exception $e ) { + public static function rollbackMasterChangesAndLog( $e ) { $factory = wfGetLBFactory(); if ( $factory->hasMasterChanges() ) { $logger = LoggerFactory::getInstance( 'Bug56269' ); @@ -151,9 +159,9 @@ class MWExceptionHandler { * } * * @since 1.25 - * @param Exception $e + * @param Exception|Throwable $e */ - public static function handleException( Exception $e ) { + public static function handleException( $e ) { try { // Rollback DBs to avoid transaction notices. This may fail // to rollback some DB due to connection issues or exceptions. @@ -176,24 +184,36 @@ class MWExceptionHandler { } /** + * Handler for set_error_handler() callback notifications. + * + * Receive a callback from the interpreter for a raised error, create an + * ErrorException, and log the exception to the 'error' logging + * channel(s). If the raised error is a fatal error type (only under HHVM) + * delegate to handleFatalError() instead. + * * @since 1.25 + * * @param int $level Error level raised * @param string $message * @param string $file * @param int $line + * + * @see logError() */ - public static function handleError( $level, $message, $file = null, $line = null ) { - // Map error constant to error name (reverse-engineer PHP error reporting) - $channel = 'error'; + public static function handleError( + $level, $message, $file = null, $line = null + ) { + if ( in_array( $level, self::$fatalErrorTypes ) ) { + return call_user_func_array( + 'MWExceptionHandler::handleFatalError', func_get_args() + ); + } + + // Map error constant to error name (reverse-engineer PHP error + // reporting) switch ( $level ) { - case E_ERROR: - case E_CORE_ERROR: - case E_COMPILE_ERROR: - case E_USER_ERROR: case E_RECOVERABLE_ERROR: - case E_PARSE: $levelName = 'Error'; - $channel = 'fatal'; break; case E_WARNING: case E_CORE_WARNING: @@ -212,17 +232,13 @@ class MWExceptionHandler { case E_USER_DEPRECATED: $levelName = 'Deprecated'; break; - case /* HHVM's FATAL_ERROR */ 16777217: - $levelName = 'Fatal'; - $channel = 'fatal'; - break; default: $levelName = 'Unknown error'; break; } $e = new ErrorException( "PHP $levelName: $message", 0, $level, $file, $line ); - self::logError( $e, $channel ); + self::logError( $e, 'error' ); // This handler is for logging only. Return false will instruct PHP // to continue regular handling. @@ -231,42 +247,101 @@ class MWExceptionHandler { /** - * Look for a fatal error as the cause of the request termination and log - * as an exception. + * Dual purpose callback used as both a set_error_handler() callback and + * a registered shutdown function. Receive a callback from the interpreter + * for a raised error or system shutdown, check for a fatal error, and log + * to the 'fatal' logging channel. * * Special handling is included for missing class errors as they may * indicate that the user needs to install 3rd-party libraries via * Composer or other means. * * @since 1.25 + * + * @param int $level Error level raised + * @param string $message Error message + * @param string $file File that error was raised in + * @param int $line Line number error was raised at + * @param array $context Active symbol table point of error + * @param array $trace Backtrace at point of error (undocumented HHVM + * feature) + * @return bool Always returns false */ - public static function handleFatalError() { + public static function handleFatalError( + $level = null, $message = null, $file = null, $line = null, + $context = null, $trace = null + ) { + // Free reserved memory so that we have space to process OOM + // errors self::$reservedMemory = null; - $lastError = error_get_last(); - if ( $lastError && - isset( $lastError['type'] ) && - in_array( $lastError['type'], self::$fatalErrorTypes ) - ) { - $msg = "Fatal Error: {$lastError['message']}"; - // HHVM: Class undefined: foo - // PHP5: Class 'foo' not found - if ( preg_match( "/Class (undefined: \w+|'\w+' not found)/", - $lastError['message'] - ) ) { - // @codingStandardsIgnoreStart Generic.Files.LineLength.TooLong - $msg = <<mediawiki.org for help on installing the required components. TXT; - // @codingStandardsIgnoreEnd - } - $e = new ErrorException( $msg, 0, $lastError['type'] ); - self::logError( $e, 'fatal' ); + // @codingStandardsIgnoreEnd } + + // We can't just create an exception and log it as it is likely that + // the interpreter has unwound the stack already. If that is true the + // stacktrace we would get would be functionally empty. If however we + // have been called as an error handler callback *and* HHVM is in use + // we will have been provided with a useful stacktrace that we can + // log. + $trace = $trace ?: debug_backtrace(); + $logger = LoggerFactory::getInstance( 'fatal' ); + $logger->error( $msg, array( + 'exception' => array( + 'class' => 'ErrorException', + 'message' => "PHP Fatal Error: {$message}", + 'code' => $level, + 'file' => $file, + 'line' => $line, + 'trace' => static::redactTrace( $trace ), + ), + 'exception_id' => wfRandomString( 8 ), + ) ); + + // Remember call so we don't double process via HHVM's fatal + // notifications and the shutdown hook behavior + static::$handledFatalCallback = true; + return false; } /** @@ -275,34 +350,34 @@ TXT; * Like Exception::getTraceAsString, but replaces argument values with * argument type or class name. * - * @param Exception $e + * @param Exception|Throwable $e * @return string + * @see prettyPrintTrace() */ - public static function getRedactedTraceAsString( Exception $e ) { - return self::prettyPrintRedactedTrace( - self::getRedactedTrace( $e ) - ); + public static function getRedactedTraceAsString( $e ) { + return self::prettyPrintTrace( self::getRedactedTrace( $e ) ); } /** - * Generate a string representation of a structured stack trace generated - * by getRedactedTrace(). + * Generate a string representation of a stacktrace. * * @param array $trace + * @param string $pad Constant padding to add to each line of trace * @return string * @since 1.26 */ - public static function prettyPrintRedactedTrace( array $trace ) { + public static function prettyPrintTrace( array $trace, $pad = '' ) { $text = ''; foreach ( $trace as $level => $frame ) { if ( isset( $frame['file'] ) && isset( $frame['line'] ) ) { - $text .= "#{$level} {$frame['file']}({$frame['line']}): "; + $text .= "{$pad}#{$level} {$frame['file']}({$frame['line']}): "; } else { - // 'file' and 'line' are unset for calls via call_user_func (bug 55634) - // This matches behaviour of Exception::getTraceAsString to instead - // display "[internal function]". - $text .= "#{$level} [internal function]: "; + // 'file' and 'line' are unset for calls via call_user_func + // (bug 55634) This matches behaviour of + // Exception::getTraceAsString to instead display "[internal + // function]". + $text .= "{$pad}#{$level} [internal function]: "; } if ( isset( $frame['class'] ) ) { @@ -319,7 +394,7 @@ TXT; } $level = $level + 1; - $text .= "#{$level} {main}"; + $text .= "{$pad}#{$level} {main}"; return $text; } @@ -332,10 +407,24 @@ TXT; * or its type (if the element is a PHP primitive). * * @since 1.22 - * @param Exception $e + * @param Exception|Throwable $e * @return array */ - public static function getRedactedTrace( Exception $e ) { + public static function getRedactedTrace( $e ) { + return static::redactTrace( $e->getTrace() ); + } + + /** + * Redact a stacktrace generated by Exception::getTrace(), + * debug_backtrace() or similar means. Replaces each element in each + * frame's argument array with the name of its class (if the element is an + * object) or its type (if the element is a PHP primitive). + * + * @since 1.26 + * @param array $trace Stacktrace + * @return array Stacktrace with arugment values converted to data types + */ + public static function redactTrace( array $trace ) { return array_map( function ( $frame ) { if ( isset( $frame['args'] ) ) { $frame['args'] = array_map( function ( $arg ) { @@ -343,7 +432,7 @@ TXT; }, $frame['args'] ); } return $frame; - }, $e->getTrace() ); + }, $trace ); } /** @@ -353,10 +442,10 @@ TXT; * $wgShowExceptionDetails is set to false), to the entry in the debug log. * * @since 1.22 - * @param Exception $e + * @param Exception|Throwable $e * @return string */ - public static function getLogId( Exception $e ) { + public static function getLogId( $e ) { if ( !isset( $e->_mwLogId ) ) { $e->_mwLogId = wfRandomString( 8 ); } @@ -382,10 +471,10 @@ TXT; * Get a message formatting the exception message and its origin. * * @since 1.22 - * @param Exception $e + * @param Exception|Throwable $e * @return string */ - public static function getLogMessage( Exception $e ) { + public static function getLogMessage( $e ) { $id = self::getLogId( $e ); $type = get_class( $e ); $file = $e->getFile(); @@ -403,12 +492,13 @@ TXT; * exception that can be used to augment a log message sent to a PSR-3 * logger. * - * @param Exception $e + * @param Exception|Throwable $e * @return array */ - public static function getLogContext( Exception $e ) { + public static function getLogContext( $e ) { return array( 'exception' => $e, + 'exception_id' => static::getLogId( $e ), ); } @@ -419,11 +509,11 @@ TXT; * backtrace) derived from the given exception. The backtrace information * will be redacted as per getRedactedTraceAsArray(). * - * @param Exception $e + * @param Exception|Throwable $e * @return array * @since 1.26 */ - public static function getStructuredExceptionData( Exception $e ) { + public static function getStructuredExceptionData( $e ) { global $wgLogExceptionBacktrace; $data = array( 'id' => self::getLogId( $e ), @@ -502,12 +592,12 @@ TXT; * @endcode * * @since 1.23 - * @param Exception $e + * @param Exception|Throwable $e * @param bool $pretty Add non-significant whitespace to improve readability (default: false). * @param int $escaping Bitfield consisting of FormatJson::.*_OK class constants. * @return string|false JSON string if successful; false upon failure */ - public static function jsonSerializeException( Exception $e, $pretty = false, $escaping = 0 ) { + public static function jsonSerializeException( $e, $pretty = false, $escaping = 0 ) { $data = self::getStructuredExceptionData( $e ); return FormatJson::encode( $data, $pretty, $escaping ); } @@ -519,9 +609,9 @@ TXT; * it is also used to handle PHP exceptions or exceptions from other libraries. * * @since 1.22 - * @param Exception $e + * @param Exception|Throwable $e */ - public static function logException( Exception $e ) { + public static function logException( $e ) { if ( !( $e instanceof MWException ) || $e->isLoggable() ) { $logger = LoggerFactory::getInstance( 'exception' ); $logger->error( @@ -548,7 +638,8 @@ TXT; */ protected static function logError( ErrorException $e, $channel ) { // The set_error_handler callback is independent from error_reporting. - // Filter out unwanted errors manually (e.g. when MediaWiki\suppressWarnings is active). + // Filter out unwanted errors manually (e.g. when + // MediaWiki\suppressWarnings is active). $suppressed = ( error_reporting() & $e->getSeverity() ) === 0; if ( !$suppressed ) { $logger = LoggerFactory::getInstance( $channel );