3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
22 * Handler class for MWExceptions
25 class MWExceptionHandler
{
28 * Install handlers with PHP.
30 public static function installHandler() {
31 set_exception_handler( array( 'MWExceptionHandler', 'handleException' ) );
32 set_error_handler( array( 'MWExceptionHandler', 'handleError' ) );
36 * Report an exception to the user
39 protected static function report( Exception
$e ) {
40 global $wgShowExceptionDetails;
42 $cmdLine = MWException
::isCommandLine();
44 if ( $e instanceof MWException
) {
46 // Try and show the exception prettily, with the normal skin infrastructure
48 } catch ( Exception
$e2 ) {
49 // Exception occurred from within exception handler
50 // Show a simpler message for the original exception,
51 // don't try to invoke report()
52 $message = "MediaWiki internal error.\n\n";
54 if ( $wgShowExceptionDetails ) {
55 $message .= 'Original exception: ' . self
::getLogMessage( $e ) .
56 "\nBacktrace:\n" . self
::getRedactedTraceAsString( $e ) .
57 "\n\nException caught inside exception handler: " . self
::getLogMessage( $e2 ) .
58 "\nBacktrace:\n" . self
::getRedactedTraceAsString( $e2 );
60 $message .= "Exception caught inside exception handler.\n\n" .
61 "Set \$wgShowExceptionDetails = true; at the bottom of LocalSettings.php " .
62 "to show detailed debugging information.";
68 self
::printError( $message );
70 echo nl2br( htmlspecialchars( $message ) ) . "\n";
74 $message = "Unexpected non-MediaWiki exception encountered, of type \"" .
75 get_class( $e ) . "\"";
77 if ( $wgShowExceptionDetails ) {
78 $message .= "\n" . MWExceptionHandler
::getLogMessage( $e ) . "\nBacktrace:\n" .
79 self
::getRedactedTraceAsString( $e ) . "\n";
83 self
::printError( $message );
85 echo nl2br( htmlspecialchars( $message ) ) . "\n";
92 * Print a message, if possible to STDERR.
93 * Use this in command line mode only (see isCommandLine)
95 * @param string $message Failure text
97 public static function printError( $message ) {
98 # NOTE: STDERR may not be available, especially if php-cgi is used from the
99 # command line (bug #15602). Try to produce meaningful output anyway. Using
100 # echo may corrupt output to STDOUT though.
101 if ( defined( 'STDERR' ) ) {
102 fwrite( STDERR
, $message );
109 * If there are any open database transactions, roll them back and log
110 * the stack trace of the exception that should have been caught so the
111 * transaction could be aborted properly.
114 * @param Exception $e
116 public static function rollbackMasterChangesAndLog( Exception
$e ) {
117 $factory = wfGetLBFactory();
118 if ( $factory->hasMasterChanges() ) {
119 wfDebugLog( 'Bug56269',
120 'Exception thrown with an uncommited database transaction: ' .
121 MWExceptionHandler
::getLogMessage( $e ) . "\n" .
122 $e->getTraceAsString()
124 $factory->rollbackMasterChanges();
129 * Exception handler which simulates the appropriate catch() handling:
133 * } catch ( MWException $e ) {
135 * } catch ( Exception $e ) {
136 * echo $e->__toString();
140 * @param Exception $e
142 public static function handleException( $e ) {
143 global $wgFullyInitialised;
145 self
::rollbackMasterChangesAndLog( $e );
146 self
::logException( $e );
150 if ( $wgFullyInitialised ) {
152 // uses $wgRequest, hence the $wgFullyInitialised condition
153 wfLogProfilingData();
154 } catch ( Exception
$e ) {
158 // Exit value should be nonzero for the benefit of shell jobs
164 * @param int $level Error level raised
165 * @param string $message
166 * @param string $file
169 public static function handleError( $level, $message, $file = null, $line = null ) {
170 $e = new ErrorException( $message, 0, $level, $file, $line );
171 self
::logError( $e );
173 // This handler is for logging only. Return false will instruct PHP
174 // to continue regular handling.
179 * Generate a string representation of an exception's stack trace
181 * Like Exception::getTraceAsString, but replaces argument values with
182 * argument type or class name.
184 * @param Exception $e
187 public static function getRedactedTraceAsString( Exception
$e ) {
190 foreach ( self
::getRedactedTrace( $e ) as $level => $frame ) {
191 if ( isset( $frame['file'] ) && isset( $frame['line'] ) ) {
192 $text .= "#{$level} {$frame['file']}({$frame['line']}): ";
194 // 'file' and 'line' are unset for calls via call_user_func (bug 55634)
195 // This matches behaviour of Exception::getTraceAsString to instead
196 // display "[internal function]".
197 $text .= "#{$level} [internal function]: ";
200 if ( isset( $frame['class'] ) ) {
201 $text .= $frame['class'] . $frame['type'] . $frame['function'];
203 $text .= $frame['function'];
206 if ( isset( $frame['args'] ) ) {
207 $text .= '(' . implode( ', ', $frame['args'] ) . ")\n";
214 $text .= "#{$level} {main}";
220 * Return a copy of an exception's backtrace as an array.
222 * Like Exception::getTrace, but replaces each element in each frame's
223 * argument array with the name of its class (if the element is an object)
224 * or its type (if the element is a PHP primitive).
227 * @param Exception $e
230 public static function getRedactedTrace( Exception
$e ) {
231 return array_map( function ( $frame ) {
232 if ( isset( $frame['args'] ) ) {
233 $frame['args'] = array_map( function ( $arg ) {
234 return is_object( $arg ) ?
get_class( $arg ) : gettype( $arg );
242 * Get the ID for this exception.
244 * The ID is saved so that one can match the one output to the user (when
245 * $wgShowExceptionDetails is set to false), to the entry in the debug log.
248 * @param Exception $e
251 public static function getLogId( Exception
$e ) {
252 if ( !isset( $e->_mwLogId
) ) {
253 $e->_mwLogId
= wfRandomString( 8 );
259 * If the exception occurred in the course of responding to a request,
260 * returns the requested URL. Otherwise, returns false.
263 * @return string|bool
265 public static function getURL() {
267 if ( !isset( $wgRequest ) ||
$wgRequest instanceof FauxRequest
) {
270 return $wgRequest->getRequestURL();
274 * Get a message formatting the exception message and its origin.
277 * @param Exception $e
280 public static function getLogMessage( Exception
$e ) {
281 $id = self
::getLogId( $e );
282 $type = get_class( $e );
283 $file = $e->getFile();
284 $line = $e->getLine();
285 $message = $e->getMessage();
286 $url = self
::getURL() ?
: '[no req]';
288 return "[$id] $url $type from line $line of $file: $message";
292 * Serialize an Exception object to JSON.
294 * The JSON object will have keys 'id', 'file', 'line', 'message', and
295 * 'url'. These keys map to string values, with the exception of 'line',
296 * which is a number, and 'url', which may be either a string URL or or
297 * null if the exception did not occur in the context of serving a web
300 * If $wgLogExceptionBacktrace is true, it will also have a 'backtrace'
301 * key, mapped to the array return value of Exception::getTrace, but with
302 * each element in each frame's "args" array (if set) replaced with the
303 * argument's class name (if the argument is an object) or type name (if
304 * the argument is a PHP primitive).
306 * @par Sample JSON record ($wgLogExceptionBacktrace = false):
310 * "type": "MWException",
311 * "file": "/var/www/mediawiki/includes/cache/MessageCache.php",
313 * "message": "Non-string key given",
314 * "url": "/wiki/Main_Page"
318 * @par Sample JSON record ($wgLogExceptionBacktrace = true):
322 * "type": "MWException",
323 * "file": "/vagrant/mediawiki/includes/cache/MessageCache.php",
325 * "message": "Non-string key given",
326 * "url": "/wiki/Main_Page",
328 * "file": "/vagrant/mediawiki/extensions/VisualEditor/VisualEditor.hooks.php",
331 * "class": "MessageCache",
339 * @param Exception $e
340 * @param bool $pretty Add non-significant whitespace to improve readability (default: false).
341 * @param int $escaping Bitfield consisting of FormatJson::.*_OK class constants.
342 * @return string|bool JSON string if successful; false upon failure
344 public static function jsonSerializeException( Exception
$e, $pretty = false, $escaping = 0 ) {
345 global $wgLogExceptionBacktrace;
347 $exceptionData = array(
348 'id' => self
::getLogId( $e ),
349 'type' => get_class( $e ),
350 'file' => $e->getFile(),
351 'line' => $e->getLine(),
352 'message' => $e->getMessage(),
355 // Because MediaWiki is first and foremost a web application, we set a
356 // 'url' key unconditionally, but set it to null if the exception does
357 // not occur in the context of a web request, as a way of making that
358 // fact visible and explicit.
359 $exceptionData['url'] = self
::getURL() ?
: null;
361 if ( $wgLogExceptionBacktrace ) {
362 // Argument values may not be serializable, so redact them.
363 $exceptionData['backtrace'] = self
::getRedactedTrace( $e );
366 return FormatJson
::encode( $exceptionData, $pretty, $escaping );
370 * Log an exception to the exception log (if enabled).
372 * This method must not assume the exception is an MWException,
373 * it is also used to handle PHP exceptions or exceptions from other libraries.
376 * @param Exception $e
378 public static function logException( Exception
$e ) {
379 global $wgLogExceptionBacktrace;
381 if ( !( $e instanceof MWException
) ||
$e->isLoggable() ) {
382 $log = self
::getLogMessage( $e );
383 if ( $wgLogExceptionBacktrace ) {
384 wfDebugLog( 'exception', $log . "\n" . $e->getTraceAsString() );
386 wfDebugLog( 'exception', $log );
389 $json = self
::jsonSerializeException( $e, false, FormatJson
::ALL_OK
);
390 if ( $json !== false ) {
391 wfDebugLog( 'exception-json', $json, 'private' );
397 * Log an exception that wasn't thrown but made to wrap an error.
400 * @param Exception $e
402 protected static function logError( Exception
$e ) {
403 global $wgLogExceptionBacktrace;
405 $log = self
::getLogMessage( $e );
406 if ( $wgLogExceptionBacktrace ) {
407 wfDebugLog( 'error', $log . "\n" . $e->getTraceAsString() );
409 wfDebugLog( 'error', $log );