X-Git-Url: http://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2FException.php;h=008be15a383ddb527d4f6e460237f2af9b988ce1;hb=3195d2ab7ff55e2633ddcfa86188f61f1fd26184;hp=a91f8657a0127635e7e5a1992a368078a9ebbc26;hpb=22280a1e3b8bd8ad33bac75225a6c71d19780d36;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/Exception.php b/includes/Exception.php index a91f8657a0..008be15a38 100644 --- a/includes/Exception.php +++ b/includes/Exception.php @@ -30,7 +30,6 @@ * @ingroup Exception */ class MWException extends Exception { - /** * Should the exception use $wgOut to output the error? * @@ -43,6 +42,16 @@ class MWException extends Exception { !empty( $GLOBALS['wgTitle'] ); } + /** + * Whether to log this exception in the exception debug log. + * + * @since 1.23 + * @return boolean + */ + function isLoggable() { + return true; + } + /** * Can the extension use the Message class/wfMessage to get i18n-ed messages? * @@ -74,7 +83,9 @@ class MWException extends Exception { return null; // Just silently ignore } - if ( !array_key_exists( $name, $wgExceptionHooks ) || !is_array( $wgExceptionHooks[$name] ) ) { + if ( !array_key_exists( $name, $wgExceptionHooks ) || + !is_array( $wgExceptionHooks[$name] ) + ) { return null; } @@ -82,7 +93,11 @@ class MWException extends Exception { $callargs = array_merge( array( $this ), $args ); foreach ( $hooks as $hook ) { - if ( is_string( $hook ) || ( is_array( $hook ) && count( $hook ) >= 2 && is_string( $hook[0] ) ) ) { // 'function' or array( 'class', hook' ) + if ( + is_string( $hook ) || + ( is_array( $hook ) && count( $hook ) >= 2 && is_string( $hook[0] ) ) + ) { + // 'function' or array( 'class', hook' ) $result = call_user_func_array( $hook, $callargs ); } else { $result = null; @@ -125,8 +140,8 @@ class MWException extends Exception { global $wgShowExceptionDetails; if ( $wgShowExceptionDetails ) { - return '

' . nl2br( htmlspecialchars( $this->getMessage() ) ) . - '

Backtrace:

' . nl2br( htmlspecialchars( MWExceptionHandler::formatRedactedTrace( $this ) ) ) . + return '

' . nl2br( htmlspecialchars( MWExceptionHandler::getLogMessage( $this ) ) ) . + '

Backtrace:

' . nl2br( htmlspecialchars( MWExceptionHandler::getRedactedTraceAsString( $this ) ) ) . "

\n"; } else { return "
" . @@ -150,8 +165,8 @@ class MWException extends Exception { global $wgShowExceptionDetails; if ( $wgShowExceptionDetails ) { - return $this->getMessage() . - "\nBacktrace:\n" . MWExceptionHandler::formatRedactedTrace( $this ) . "\n"; + return MWExceptionHandler::getLogMessage( $this ) . + "\nBacktrace:\n" . MWExceptionHandler::getRedactedTraceAsString( $this ) . "\n"; } else { return "Set \$wgShowExceptionDetails = true; " . "in LocalSettings.php to show detailed debugging information.\n"; @@ -164,7 +179,8 @@ class MWException extends Exception { * @return string */ function getPageTitle() { - return $this->msg( 'internalerror', "Internal error" ); + global $wgSitename; + return $this->msg( 'pagetitle', "$1 - $wgSitename", $this->msg( 'internalerror', 'Internal error' ) ); } /** @@ -209,13 +225,14 @@ class MWException extends Exception { $wgOut->output(); } else { - header( "Content-Type: text/html; charset=utf-8" ); - echo "\n" . + header( 'Content-Type: text/html; charset=utf-8' ); + echo "\n" . '' . '' . htmlspecialchars( $this->getPageTitle() ) . '' . + '' . "\n"; - $hookResult = $this->runHooks( get_class( $this ) . "Raw" ); + $hookResult = $this->runHooks( get_class( $this ) . 'Raw' ); if ( $hookResult ) { echo $hookResult; } else { @@ -242,8 +259,8 @@ class MWException extends Exception { } elseif ( self::isCommandLine() ) { MWExceptionHandler::printError( $this->getText() ); } else { - header( "HTTP/1.1 500 MediaWiki exception" ); - header( "Status: 500 MediaWiki exception", true ); + header( 'HTTP/1.1 500 MediaWiki exception' ); + header( 'Status: 500 MediaWiki exception', true ); header( "Content-Type: $wgMimeType; charset=utf-8", true ); $this->reportHTML(); @@ -480,11 +497,11 @@ class UserBlockedError extends ErrorPageError { class UserNotLoggedIn extends ErrorPageError { /** - * @param $reasonMsg A message key containing the reason for the error. + * @param string $reasonMsg A message key containing the reason for the error. * Optional, default: 'exception-nologin-text' - * @param $titleMsg A message key to set the page title. + * @param string $titleMsg A message key to set the page title. * Optional, default: 'exception-nologin' - * @param $params Parameters to wfMessage(). + * @param array $params Parameters to wfMessage(). * Optional, default: null */ public function __construct( @@ -601,8 +618,10 @@ class MWExceptionHandler { $message = "MediaWiki internal error.\n\n"; if ( $wgShowExceptionDetails ) { - $message .= 'Original exception: ' . self::formatRedactedTrace( $e ) . "\n\n" . - 'Exception caught inside exception handler: ' . $e2->__toString(); + $message .= 'Original exception: ' . self::getLogMessage( $e ) . + "\nBacktrace:\n" . self::getRedactedTraceAsString( $e ) . + "\n\nException caught inside exception handler: " . self::getLogMessage( $e2 ) . + "\nBacktrace:\n" . self::getRedactedTraceAsString( $e2 ); } else { $message .= "Exception caught inside exception handler.\n\n" . "Set \$wgShowExceptionDetails = true; at the bottom of LocalSettings.php " . @@ -618,10 +637,12 @@ class MWExceptionHandler { } } } else { - $message = "Unexpected non-MediaWiki exception encountered, of type \"" . get_class( $e ) . "\""; + $message = "Unexpected non-MediaWiki exception encountered, of type \"" . + get_class( $e ) . "\""; if ( $wgShowExceptionDetails ) { - $message .= "\nexception '" . get_class( $e ) . "' in " . $e->getFile() . ":" . $e->getLine() . "\nStack trace:\n" . self::formatRedactedTrace( $e ) . "\n"; + $message .= "\n" . MWExceptionHandler::getLogMessage( $e ) . "\nBacktrace:\n" . + self::getRedactedTraceAsString( $e ) . "\n"; } if ( $cmdLine ) { @@ -639,8 +660,9 @@ class MWExceptionHandler { * @param string $message Failure text */ public static function printError( $message ) { - # NOTE: STDERR may not be available, especially if php-cgi is used from the command line (bug #15602). - # Try to produce meaningful output anyway. Using echo may corrupt output to STDOUT though. + # NOTE: STDERR may not be available, especially if php-cgi is used from the + # command line (bug #15602). Try to produce meaningful output anyway. Using + # echo may corrupt output to STDOUT though. if ( defined( 'STDERR' ) ) { fwrite( STDERR, $message ); } else { @@ -678,69 +700,67 @@ class MWExceptionHandler { } /** - * Get the stack trace from the exception as a string, redacting certain function arguments in the process - * @param Exception $e The exception - * @return string The stack trace as a string + * Generate a string representation of an exception's stack trace + * + * Like Exception::getTraceAsString, but replaces argument values with + * argument type or class name. + * + * @param Exception $e + * @return string */ - public static function formatRedactedTrace( Exception $e ) { - global $wgRedactedFunctionArguments; - $finalExceptionText = ''; + public static function getRedactedTraceAsString( Exception $e ) { + $text = ''; - // Unique value to indicate redacted parameters - $redacted = new stdClass(); - - foreach ( $e->getTrace() as $i => $call ) { - $checkFor = array(); - if ( isset( $call['class'] ) ) { - $checkFor[] = $call['class'] . '::' . $call['function']; - foreach ( class_parents( $call['class'] ) as $parent ) { - $checkFor[] = $parent . '::' . $call['function']; - } - } else { - $checkFor[] = $call['function']; - } - - foreach ( $checkFor as $check ) { - if ( isset( $wgRedactedFunctionArguments[$check] ) ) { - foreach ( (array)$wgRedactedFunctionArguments[$check] as $argNo ) { - $call['args'][$argNo] = $redacted; - } - } - } - - if ( isset( $call['file'] ) && isset( $call['line'] ) ) { - $finalExceptionText .= "#{$i} {$call['file']}({$call['line']}): "; + foreach ( self::getRedactedTrace( $e ) as $level => $frame ) { + if ( isset( $frame['file'] ) && isset( $frame['line'] ) ) { + $text .= "#{$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]". - $finalExceptionText .= "#{$i} [internal function]: "; + $text .= "#{$level} [internal function]: "; } - if ( isset( $call['class'] ) ) { - $finalExceptionText .= $call['class'] . $call['type'] . $call['function']; + if ( isset( $frame['class'] ) ) { + $text .= $frame['class'] . $frame['type'] . $frame['function']; } else { - $finalExceptionText .= $call['function']; + $text .= $frame['function']; } - $args = array(); - if ( isset( $call['args'] ) ) { - foreach ( $call['args'] as $arg ) { - if ( $arg === $redacted ) { - $args[] = 'REDACTED'; - } elseif ( is_object( $arg ) ) { - $args[] = 'Object(' . get_class( $arg ) . ')'; - } elseif( is_array( $arg ) ) { - $args[] = 'Array'; - } else { - $args[] = var_export( $arg, true ); - } - } + + if ( isset( $frame['args'] ) ) { + $text .= '(' . implode( ', ', $frame['args'] ) . ")\n"; + } else { + $text .= "()\n"; } - $finalExceptionText .= '(' . implode( ', ', $args ) . ")\n"; } - return $finalExceptionText . '#' . ( $i + 1 ) . ' {main}'; + + $level = $level + 1; + $text .= "#{$level} {main}"; + + return $text; } + /** + * Return a copy of an exception's backtrace as an array. + * + * Like Exception::getTrace, but 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.22 + * @param Exception $e + * @return array + */ + public static function getRedactedTrace( Exception $e ) { + return array_map( function ( $frame ) { + if ( isset( $frame['args'] ) ) { + $frame['args'] = array_map( function ( $arg ) { + return is_object( $arg ) ? get_class( $arg ) : gettype( $arg ); + }, $frame['args'] ); + } + return $frame; + }, $e->getTrace() ); + } /** * Get the ID for this error. @@ -759,6 +779,21 @@ class MWExceptionHandler { return $e->_mwLogId; } + /** + * If the exception occurred in the course of responding to a request, + * returns the requested URL. Otherwise, returns false. + * + * @since 1.23 + * @return string|bool + */ + public static function getURL() { + global $wgRequest; + if ( !isset( $wgRequest ) || $wgRequest instanceof FauxRequest ) { + return false; + } + return $wgRequest->getRequestURL(); + } + /** * Return the requested URL and point to file and line number from which the * exception occurred. @@ -768,23 +803,88 @@ class MWExceptionHandler { * @return string */ public static function getLogMessage( Exception $e ) { - global $wgRequest; - $id = self::getLogId( $e ); $file = $e->getFile(); $line = $e->getLine(); $message = $e->getMessage(); + $url = self::getURL() ?: '[no req]'; - if ( isset( $wgRequest ) && !$wgRequest instanceof FauxRequest ) { - $url = $wgRequest->getRequestURL(); - if ( !$url ) { - $url = '[no URL]'; - } - } else { - $url = '[no req]'; + return "[$id] $url Exception from line $line of $file: $message"; + } + + /** + * Serialize an Exception object to JSON. + * + * The JSON object will have keys 'id', 'file', 'line', 'message', and + * 'url'. These keys map to string values, with the exception of 'line', + * which is a number, and 'url', which may be either a string URL or or + * null if the exception did not occur in the context of serving a web + * request. + * + * If $wgLogExceptionBacktrace is true, it will also have a 'backtrace' + * key, mapped to the array return value of Exception::getTrace, but with + * each element in each frame's "args" array (if set) replaced with the + * argument's class name (if the argument is an object) or type name (if + * the argument is a PHP primitive). + * + * @par Sample JSON record ($wgLogExceptionBacktrace = false): + * @code + * { + * "id": "c41fb419", + * "file": "/var/www/mediawiki/includes/cache/MessageCache.php", + * "line": 704, + * "message": "Non-string key given", + * "url": "/wiki/Main_Page" + * } + * @endcode + * + * @par Sample JSON record ($wgLogExceptionBacktrace = true): + * @code + * { + * "id": "dc457938", + * "file": "/vagrant/mediawiki/includes/cache/MessageCache.php", + * "line": 704, + * "message": "Non-string key given", + * "url": "/wiki/Main_Page", + * "backtrace": [{ + * "file": "/vagrant/mediawiki/extensions/VisualEditor/VisualEditor.hooks.php", + * "line": 80, + * "function": "get", + * "class": "MessageCache", + * "type": "->", + * "args": ["array"] + * }] + * } + * @endcode + * + * @since 1.23 + * @param Exception $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|bool: JSON string if successful; false upon failure + */ + public static function jsonSerializeException( Exception $e, $pretty = false, $escaping = 0 ) { + global $wgLogExceptionBacktrace; + + $exceptionData = array( + 'id' => self::getLogId( $e ), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'message' => $e->getMessage(), + ); + + // Because MediaWiki is first and foremost a web application, we set a + // 'url' key unconditionally, but set it to null if the exception does + // not occur in the context of a web request, as a way of making that + // fact visible and explicit. + $exceptionData['url'] = self::getURL() ?: null; + + if ( $wgLogExceptionBacktrace ) { + // Argument values may not be serializable, so redact them. + $exceptionData['backtrace'] = self::getRedactedTrace( $e ); } - return "[$id] $url Exception from line $line of $file: $message"; + return FormatJson::encode( $exceptionData, $pretty, $escaping ); } /** @@ -799,14 +899,20 @@ class MWExceptionHandler { public static function logException( Exception $e ) { global $wgLogExceptionBacktrace; - $log = self::getLogMessage( $e ); - if ( $log ) { + if ( !( $e instanceof MWException ) || $e->isLoggable() ) { + $log = self::getLogMessage( $e ); if ( $wgLogExceptionBacktrace ) { - wfDebugLog( 'exception', $log . "\n" . self::formatRedactedTrace( $e ) . "\n" ); + wfDebugLog( 'exception', $log . "\n" . $e->getTraceAsString() . "\n" ); } else { wfDebugLog( 'exception', $log ); } + + $json = self::jsonSerializeException( $e, false, FormatJson::ALL_OK ); + if ( $json !== false ) { + wfDebugLog( 'exception-json', $json, false ); + } } + } }