* @ingroup Exception
*/
class MWException extends Exception {
- var $logId;
-
/**
* Should the exception use $wgOut to output the error?
*
!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?
*
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;
}
$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;
global $wgShowExceptionDetails;
if ( $wgShowExceptionDetails ) {
- return '<p>' . nl2br( htmlspecialchars( $this->getMessage() ) ) .
- '</p><p>Backtrace:</p><p>' . nl2br( htmlspecialchars( MWExceptionHandler::formatRedactedTrace( $this ) ) ) .
+ return '<p>' . nl2br( htmlspecialchars( MWExceptionHandler::getLogMessage( $this ) ) ) .
+ '</p><p>Backtrace:</p><p>' . nl2br( htmlspecialchars( MWExceptionHandler::getRedactedTraceAsString( $this ) ) ) .
"</p>\n";
} else {
return "<div class=\"errorbox\">" .
- '[' . $this->getLogId() . '] ' .
+ '[' . MWExceptionHandler::getLogId( $this ) . '] ' .
gmdate( 'Y-m-d H:i:s' ) .
": Fatal exception of type " . get_class( $this ) . "</div>\n" .
"<!-- Set \$wgShowExceptionDetails = true; " .
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";
* @return string
*/
function getPageTitle() {
- return $this->msg( 'internalerror', "Internal error" );
+ global $wgSitename;
+ return $this->msg( 'pagetitle', "$1 - $wgSitename", $this->msg( 'internalerror', 'Internal error' ) );
}
/**
- * Get a random ID for this error.
- * This allows to link the exception to its corresponding log entry when
- * $wgShowExceptionDetails is set to false.
+ * Get a the ID for this error.
*
+ * @since 1.20
+ * @deprecated since 1.22 Use MWExceptionHandler::getLogId instead.
* @return string
*/
function getLogId() {
- if ( $this->logId === null ) {
- $this->logId = wfRandomString( 8 );
- }
- return $this->logId;
+ wfDeprecated( __METHOD__, '1.22' );
+ return MWExceptionHandler::getLogId( $this );
}
/**
* Return the requested URL and point to file and line number from which the
* exception occurred
*
+ * @since 1.8
+ * @deprecated since 1.22 Use MWExceptionHandler::getLogMessage instead.
* @return string
*/
function getLogMessage() {
- global $wgRequest;
-
- $id = $this->getLogId();
- $file = $this->getFile();
- $line = $this->getLine();
- $message = $this->getMessage();
-
- 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";
+ wfDeprecated( __METHOD__, '1.22' );
+ return MWExceptionHandler::getLogMessage( $this );
}
/**
$wgOut->output();
} else {
- header( "Content-Type: text/html; charset=utf-8" );
- echo "<!doctype html>\n" .
+ header( 'Content-Type: text/html; charset=utf-8' );
+ echo "<!DOCTYPE html>\n" .
'<html><head>' .
'<title>' . htmlspecialchars( $this->getPageTitle() ) . '</title>' .
+ '<style>body { font-family: sans-serif; margin: 0; padding: 0.5em 2em; }</style>' .
"</head><body>\n";
- $hookResult = $this->runHooks( get_class( $this ) . "Raw" );
+ $hookResult = $this->runHooks( get_class( $this ) . 'Raw' );
if ( $hookResult ) {
echo $hookResult;
} else {
function report() {
global $wgMimeType;
- $this->logException();
+ MWExceptionHandler::logException( $this );
if ( defined( 'MW_API' ) ) {
// Unhandled API exception, we can't be sure that format printer is alive
} 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();
}
}
- /**
- * Log the error message to the exception log (if enabled)
- */
- function logException() {
- global $wgLogExceptionBacktrace;
-
- $log = $this->getLogMessage();
- if ( $log ) {
- if ( $wgLogExceptionBacktrace ) {
- wfDebugLog( 'exception', $log . "\n" . MWExceptionHandler::formatRedactedTrace( $this ) . "\n" );
- } else {
- wfDebugLog( 'exception', $log );
- }
- }
- }
-
/**
* Check whether we are in command line mode or not to report the exception
* in the correct format.
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(
$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 " .
}
}
} 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 ) {
* @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 {
}
/**
- * 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 = '';
- 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'];
- }
+ foreach ( self::getRedactedTrace( $e ) as $level => $frame ) {
+ if ( isset( $frame['file'] ) && isset( $frame['line'] ) ) {
+ $text .= "#{$level} {$frame['file']}({$frame['line']}): ";
} else {
- $checkFor[] = $call['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 .= "#{$level} [internal function]: ";
}
- foreach ( $checkFor as $check ) {
- if ( isset( $wgRedactedFunctionArguments[$check] ) ) {
- foreach ( (array)$wgRedactedFunctionArguments[$check] as $argNo ) {
- $call['args'][$argNo] = 'REDACTED';
- }
- }
+ if ( isset( $frame['class'] ) ) {
+ $text .= $frame['class'] . $frame['type'] . $frame['function'];
+ } else {
+ $text .= $frame['function'];
}
- $finalExceptionText .= "#{$i} {$call['file']}({$call['line']}): ";
- if ( isset( $call['class'] ) ) {
- $finalExceptionText .= $call['class'] . $call['type'] . $call['function'];
+ if ( isset( $frame['args'] ) ) {
+ $text .= '(' . implode( ', ', $frame['args'] ) . ")\n";
} else {
- $finalExceptionText .= $call['function'];
+ $text .= "()\n";
}
- $args = array();
- foreach ( $call['args'] as $arg ) {
- if ( is_object( $arg ) ) {
- $args[] = 'Object(' . get_class( $arg ) . ')';
- } elseif( is_array( $arg ) ) {
- $args[] = 'Array';
- } else {
- $args[] = var_export( $arg, true );
- }
+ }
+
+ $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'] );
}
- $finalExceptionText .= '(' . implode( ', ', $args ) . ")\n";
+ return $frame;
+ }, $e->getTrace() );
+ }
+
+ /**
+ * Get the ID for this error.
+ *
+ * The ID is saved so that one can match the one output to the user (when
+ * $wgShowExceptionDetails is set to false), to the entry in the debug log.
+ *
+ * @since 1.22
+ * @param Exception $e
+ * @return string
+ */
+ public static function getLogId( Exception $e ) {
+ if ( !isset( $e->_mwLogId ) ) {
+ $e->_mwLogId = wfRandomString( 8 );
}
- return $finalExceptionText . '#' . ( $i + 1 ) . ' {main}';
+ 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.
+ *
+ * @since 1.22
+ * @param Exception $e
+ * @return string
+ */
+ public static function getLogMessage( Exception $e ) {
+ $id = self::getLogId( $e );
+ $file = $e->getFile();
+ $line = $e->getLine();
+ $message = $e->getMessage();
+ $url = self::getURL() ?: '[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 FormatJson::encode( $exceptionData, $pretty, $escaping );
+ }
+
+ /**
+ * Log an exception to the exception log (if enabled).
+ *
+ * This method must not assume the exception is an MWException,
+ * it is also used to handle PHP errors or errors from other libraries.
+ *
+ * @since 1.22
+ * @param Exception $e
+ */
+ public static function logException( Exception $e ) {
+ global $wgLogExceptionBacktrace;
+
+ if ( !( $e instanceof MWException ) || $e->isLoggable() ) {
+ $log = self::getLogMessage( $e );
+ if ( $wgLogExceptionBacktrace ) {
+ 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 );
+ }
+ }
+
+ }
+
}