summaryrefslogtreecommitdiff
path: root/includes/exception/MWExceptionHandler.php
diff options
context:
space:
mode:
Diffstat (limited to 'includes/exception/MWExceptionHandler.php')
-rw-r--r--includes/exception/MWExceptionHandler.php372
1 files changed, 372 insertions, 0 deletions
diff --git a/includes/exception/MWExceptionHandler.php b/includes/exception/MWExceptionHandler.php
new file mode 100644
index 00000000..71917e13
--- /dev/null
+++ b/includes/exception/MWExceptionHandler.php
@@ -0,0 +1,372 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Handler class for MWExceptions
+ * @ingroup Exception
+ */
+class MWExceptionHandler {
+ /**
+ * Install an exception handler for MediaWiki exception types.
+ */
+ public static function installHandler() {
+ set_exception_handler( array( 'MWExceptionHandler', 'handle' ) );
+ }
+
+ /**
+ * Report an exception to the user
+ * @param Exception $e
+ */
+ protected static function report( Exception $e ) {
+ global $wgShowExceptionDetails;
+
+ $cmdLine = MWException::isCommandLine();
+
+ if ( $e instanceof MWException ) {
+ try {
+ // Try and show the exception prettily, with the normal skin infrastructure
+ $e->report();
+ } catch ( Exception $e2 ) {
+ // Exception occurred from within exception handler
+ // Show a simpler error message for the original exception,
+ // don't try to invoke report()
+ $message = "MediaWiki internal error.\n\n";
+
+ if ( $wgShowExceptionDetails ) {
+ $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 " .
+ "to show detailed debugging information.";
+ }
+
+ $message .= "\n";
+
+ if ( $cmdLine ) {
+ self::printError( $message );
+ } else {
+ echo nl2br( htmlspecialchars( $message ) ) . "\n";
+ }
+ }
+ } else {
+ $message = "Unexpected non-MediaWiki exception encountered, of type \"" .
+ get_class( $e ) . "\"";
+
+ if ( $wgShowExceptionDetails ) {
+ $message .= "\n" . MWExceptionHandler::getLogMessage( $e ) . "\nBacktrace:\n" .
+ self::getRedactedTraceAsString( $e ) . "\n";
+ }
+
+ if ( $cmdLine ) {
+ self::printError( $message );
+ } else {
+ echo nl2br( htmlspecialchars( $message ) ) . "\n";
+ }
+ }
+ }
+
+ /**
+ * Print a message, if possible to STDERR.
+ * Use this in command line mode only (see isCommandLine)
+ *
+ * @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.
+ if ( defined( 'STDERR' ) ) {
+ fwrite( STDERR, $message );
+ } else {
+ echo $message;
+ }
+ }
+
+ /**
+ * If there are any open database transactions, roll them back and log
+ * the stack trace of the exception that should have been caught so the
+ * transaction could be aborted properly.
+ * @since 1.23
+ * @param Exception $e
+ */
+ public static function rollbackMasterChangesAndLog( Exception $e ) {
+ $factory = wfGetLBFactory();
+ if ( $factory->hasMasterChanges() ) {
+ wfDebugLog( 'Bug56269',
+ 'Exception thrown with an uncommited database transaction: ' .
+ MWExceptionHandler::getLogMessage( $e ) . "\n" .
+ $e->getTraceAsString()
+ );
+ $factory->rollbackMasterChanges();
+ }
+ }
+
+ /**
+ * Exception handler which simulates the appropriate catch() handling:
+ *
+ * try {
+ * ...
+ * } catch ( MWException $e ) {
+ * $e->report();
+ * } catch ( Exception $e ) {
+ * echo $e->__toString();
+ * }
+ * @param Exception $e
+ */
+ public static function handle( $e ) {
+ global $wgFullyInitialised;
+
+ self::rollbackMasterChangesAndLog( $e );
+
+ self::report( $e );
+
+ // Final cleanup
+ if ( $wgFullyInitialised ) {
+ try {
+ // uses $wgRequest, hence the $wgFullyInitialised condition
+ wfLogProfilingData();
+ } catch ( Exception $e ) {
+ }
+ }
+
+ // Exit value should be nonzero for the benefit of shell jobs
+ exit( 1 );
+ }
+
+ /**
+ * 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 getRedactedTraceAsString( Exception $e ) {
+ $text = '';
+
+ 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]".
+ $text .= "#{$level} [internal function]: ";
+ }
+
+ if ( isset( $frame['class'] ) ) {
+ $text .= $frame['class'] . $frame['type'] . $frame['function'];
+ } else {
+ $text .= $frame['function'];
+ }
+
+ if ( isset( $frame['args'] ) ) {
+ $text .= '(' . implode( ', ', $frame['args'] ) . ")\n";
+ } else {
+ $text .= "()\n";
+ }
+ }
+
+ $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.
+ *
+ * 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 $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() );
+ } else {
+ wfDebugLog( 'exception', $log );
+ }
+
+ $json = self::jsonSerializeException( $e, false, FormatJson::ALL_OK );
+ if ( $json !== false ) {
+ wfDebugLog( 'exception-json', $json, 'private' );
+ }
+ }
+
+ }
+
+}