useMessageCache() && !empty( $GLOBALS['wgFullyInitialised'] ) && !empty( $GLOBALS['wgOut'] ); } /** * 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 bool */ function useMessageCache() { global $wgLang; foreach ( $this->getTrace() as $frame ) { if ( isset( $frame['class'] ) && $frame['class'] === 'LocalisationCache' ) { return false; } } return $wgLang instanceof Language; } /** * Run hook to allow extensions to modify the text of the exception * * @param string $name class name of the exception * @param array $args arguments to pass to the callback functions * @return string|null string to output or null if any hook has been called */ function runHooks( $name, $args = array() ) { global $wgExceptionHooks; if ( !isset( $wgExceptionHooks ) || !is_array( $wgExceptionHooks ) ) { return null; // Just silently ignore } if ( !array_key_exists( $name, $wgExceptionHooks ) || !is_array( $wgExceptionHooks[$name] ) ) { return null; } $hooks = $wgExceptionHooks[$name]; $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' ) $result = call_user_func_array( $hook, $callargs ); } else { $result = null; } if ( is_string( $result ) ) { return $result; } } return null; } /** * Get a message from i18n * * @param string $key message name * @param string $fallback default message if the message cache can't be * called by the exception * The function also has other parameters that are arguments for the message * @return string message with arguments replaced */ function msg( $key, $fallback /*[, params...] */ ) { $args = array_slice( func_get_args(), 2 ); if ( $this->useMessageCache() ) { return wfMessage( $key, $args )->plain(); } else { return wfMsgReplaceArgs( $fallback, $args ); } } /** * If $wgShowExceptionDetails is true, return a HTML message with a * backtrace to the error, otherwise show a message to ask to set it to true * to show that information. * * @return string html to output */ function getHTML() { global $wgShowExceptionDetails; if ( $wgShowExceptionDetails ) { return '
' . nl2br( htmlspecialchars( MWExceptionHandler::getLogMessage( $this ) ) ) . '
Backtrace:
' . nl2br( htmlspecialchars( MWExceptionHandler::getRedactedTraceAsString( $this ) ) ) . "
\n"; } else { return "$content
\n"; } } /** * 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 */ 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; } } /** * Exception handler which simulates the appropriate catch() handling: * * try { * ... * } catch ( MWException $e ) { * $e->report(); * } catch ( Exception $e ) { * echo $e->__toString(); * } */ public static function handle( $e ) { global $wgFullyInitialised; 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() . "\n" ); } else { wfDebugLog( 'exception', $log ); } $json = self::jsonSerializeException( $e, false, FormatJson::ALL_OK ); if ( $json !== false ) { wfDebugLog( 'exception-json', $json, false ); } } } }