Merge "Inject cache as constructor param of SiteSQLStore"
[lhc/web/wiklou.git] / includes / exception / MWExceptionHandler.php
1 <?php
2 /**
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.
7 *
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.
12 *
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
17 *
18 * @file
19 */
20
21 /**
22 * Handler class for MWExceptions
23 * @ingroup Exception
24 */
25 class MWExceptionHandler {
26
27 /**
28 * Install handlers with PHP.
29 */
30 public static function installHandler() {
31 set_exception_handler( array( 'MWExceptionHandler', 'handleException' ) );
32 set_error_handler( array( 'MWExceptionHandler', 'handleError' ) );
33 }
34
35 /**
36 * Report an exception to the user
37 * @param Exception $e
38 */
39 protected static function report( Exception $e ) {
40 global $wgShowExceptionDetails;
41
42 $cmdLine = MWException::isCommandLine();
43
44 if ( $e instanceof MWException ) {
45 try {
46 // Try and show the exception prettily, with the normal skin infrastructure
47 $e->report();
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";
53
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 );
59 } else {
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.";
63 }
64
65 $message .= "\n";
66
67 if ( $cmdLine ) {
68 self::printError( $message );
69 } else {
70 echo nl2br( htmlspecialchars( $message ) ) . "\n";
71 }
72 }
73 } else {
74 $message = "Unexpected non-MediaWiki exception encountered, of type \"" .
75 get_class( $e ) . "\"";
76
77 if ( $wgShowExceptionDetails ) {
78 $message .= "\n" . MWExceptionHandler::getLogMessage( $e ) . "\nBacktrace:\n" .
79 self::getRedactedTraceAsString( $e ) . "\n";
80 }
81
82 if ( $cmdLine ) {
83 self::printError( $message );
84 } else {
85 echo nl2br( htmlspecialchars( $message ) ) . "\n";
86 }
87
88 }
89 }
90
91 /**
92 * Print a message, if possible to STDERR.
93 * Use this in command line mode only (see isCommandLine)
94 *
95 * @param string $message Failure text
96 */
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 );
103 } else {
104 echo $message;
105 }
106 }
107
108 /**
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.
112 *
113 * @since 1.23
114 * @param Exception $e
115 */
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()
123 );
124 $factory->rollbackMasterChanges();
125 }
126 }
127
128 /**
129 * Exception handler which simulates the appropriate catch() handling:
130 *
131 * try {
132 * ...
133 * } catch ( MWException $e ) {
134 * $e->report();
135 * } catch ( Exception $e ) {
136 * echo $e->__toString();
137 * }
138 *
139 * @since 1.25
140 * @param Exception $e
141 */
142 public static function handleException( $e ) {
143 global $wgFullyInitialised;
144
145 self::rollbackMasterChangesAndLog( $e );
146 self::logException( $e );
147 self::report( $e );
148
149 // Final cleanup
150 if ( $wgFullyInitialised ) {
151 try {
152 // uses $wgRequest, hence the $wgFullyInitialised condition
153 wfLogProfilingData();
154 } catch ( Exception $e ) {
155 }
156 }
157
158 // Exit value should be nonzero for the benefit of shell jobs
159 exit( 1 );
160 }
161
162 /**
163 * @since 1.25
164 * @param int $level Error level raised
165 * @param string $message
166 * @param string $file
167 * @param int $line
168 */
169 public static function handleError( $level, $message, $file = null, $line = null ) {
170 // Map error constant to error name (reverse-engineer PHP error reporting)
171 switch ( $level ) {
172 case E_ERROR:
173 case E_CORE_ERROR:
174 case E_COMPILE_ERROR:
175 case E_USER_ERROR:
176 case E_RECOVERABLE_ERROR:
177 case E_PARSE:
178 $levelName = 'Error';
179 break;
180 case E_WARNING:
181 case E_CORE_WARNING:
182 case E_COMPILE_WARNING:
183 case E_USER_WARNING:
184 $levelName = 'Warning';
185 break;
186 case E_NOTICE:
187 case E_USER_NOTICE:
188 $levelName = 'Notice';
189 break;
190 case E_STRICT:
191 $levelName = 'Strict Standards';
192 break;
193 case E_DEPRECATED:
194 case E_USER_DEPRECATED:
195 $levelName = 'Deprecated';
196 break;
197 default:
198 $levelName = 'Unknown error';
199 break;
200 }
201
202 $e = new ErrorException( "PHP $levelName: $message", 0, $level, $file, $line );
203 self::logError( $e );
204
205 // This handler is for logging only. Return false will instruct PHP
206 // to continue regular handling.
207 return false;
208 }
209
210 /**
211 * Generate a string representation of an exception's stack trace
212 *
213 * Like Exception::getTraceAsString, but replaces argument values with
214 * argument type or class name.
215 *
216 * @param Exception $e
217 * @return string
218 */
219 public static function getRedactedTraceAsString( Exception $e ) {
220 $text = '';
221
222 foreach ( self::getRedactedTrace( $e ) as $level => $frame ) {
223 if ( isset( $frame['file'] ) && isset( $frame['line'] ) ) {
224 $text .= "#{$level} {$frame['file']}({$frame['line']}): ";
225 } else {
226 // 'file' and 'line' are unset for calls via call_user_func (bug 55634)
227 // This matches behaviour of Exception::getTraceAsString to instead
228 // display "[internal function]".
229 $text .= "#{$level} [internal function]: ";
230 }
231
232 if ( isset( $frame['class'] ) ) {
233 $text .= $frame['class'] . $frame['type'] . $frame['function'];
234 } else {
235 $text .= $frame['function'];
236 }
237
238 if ( isset( $frame['args'] ) ) {
239 $text .= '(' . implode( ', ', $frame['args'] ) . ")\n";
240 } else {
241 $text .= "()\n";
242 }
243 }
244
245 $level = $level + 1;
246 $text .= "#{$level} {main}";
247
248 return $text;
249 }
250
251 /**
252 * Return a copy of an exception's backtrace as an array.
253 *
254 * Like Exception::getTrace, but replaces each element in each frame's
255 * argument array with the name of its class (if the element is an object)
256 * or its type (if the element is a PHP primitive).
257 *
258 * @since 1.22
259 * @param Exception $e
260 * @return array
261 */
262 public static function getRedactedTrace( Exception $e ) {
263 return array_map( function ( $frame ) {
264 if ( isset( $frame['args'] ) ) {
265 $frame['args'] = array_map( function ( $arg ) {
266 return is_object( $arg ) ? get_class( $arg ) : gettype( $arg );
267 }, $frame['args'] );
268 }
269 return $frame;
270 }, $e->getTrace() );
271 }
272
273 /**
274 * Get the ID for this exception.
275 *
276 * The ID is saved so that one can match the one output to the user (when
277 * $wgShowExceptionDetails is set to false), to the entry in the debug log.
278 *
279 * @since 1.22
280 * @param Exception $e
281 * @return string
282 */
283 public static function getLogId( Exception $e ) {
284 if ( !isset( $e->_mwLogId ) ) {
285 $e->_mwLogId = wfRandomString( 8 );
286 }
287 return $e->_mwLogId;
288 }
289
290 /**
291 * If the exception occurred in the course of responding to a request,
292 * returns the requested URL. Otherwise, returns false.
293 *
294 * @since 1.23
295 * @return string|bool
296 */
297 public static function getURL() {
298 global $wgRequest;
299 if ( !isset( $wgRequest ) || $wgRequest instanceof FauxRequest ) {
300 return false;
301 }
302 return $wgRequest->getRequestURL();
303 }
304
305 /**
306 * Get a message formatting the exception message and its origin.
307 *
308 * @since 1.22
309 * @param Exception $e
310 * @return string
311 */
312 public static function getLogMessage( Exception $e ) {
313 $id = self::getLogId( $e );
314 $type = get_class( $e );
315 $file = $e->getFile();
316 $line = $e->getLine();
317 $message = $e->getMessage();
318 $url = self::getURL() ?: '[no req]';
319
320 return "[$id] $url $type from line $line of $file: $message";
321 }
322
323 /**
324 * Serialize an Exception object to JSON.
325 *
326 * The JSON object will have keys 'id', 'file', 'line', 'message', and
327 * 'url'. These keys map to string values, with the exception of 'line',
328 * which is a number, and 'url', which may be either a string URL or or
329 * null if the exception did not occur in the context of serving a web
330 * request.
331 *
332 * If $wgLogExceptionBacktrace is true, it will also have a 'backtrace'
333 * key, mapped to the array return value of Exception::getTrace, but with
334 * each element in each frame's "args" array (if set) replaced with the
335 * argument's class name (if the argument is an object) or type name (if
336 * the argument is a PHP primitive).
337 *
338 * @par Sample JSON record ($wgLogExceptionBacktrace = false):
339 * @code
340 * {
341 * "id": "c41fb419",
342 * "type": "MWException",
343 * "file": "/var/www/mediawiki/includes/cache/MessageCache.php",
344 * "line": 704,
345 * "message": "Non-string key given",
346 * "url": "/wiki/Main_Page"
347 * }
348 * @endcode
349 *
350 * @par Sample JSON record ($wgLogExceptionBacktrace = true):
351 * @code
352 * {
353 * "id": "dc457938",
354 * "type": "MWException",
355 * "file": "/vagrant/mediawiki/includes/cache/MessageCache.php",
356 * "line": 704,
357 * "message": "Non-string key given",
358 * "url": "/wiki/Main_Page",
359 * "backtrace": [{
360 * "file": "/vagrant/mediawiki/extensions/VisualEditor/VisualEditor.hooks.php",
361 * "line": 80,
362 * "function": "get",
363 * "class": "MessageCache",
364 * "type": "->",
365 * "args": ["array"]
366 * }]
367 * }
368 * @endcode
369 *
370 * @since 1.23
371 * @param Exception $e
372 * @param bool $pretty Add non-significant whitespace to improve readability (default: false).
373 * @param int $escaping Bitfield consisting of FormatJson::.*_OK class constants.
374 * @return string|bool JSON string if successful; false upon failure
375 */
376 public static function jsonSerializeException( Exception $e, $pretty = false, $escaping = 0 ) {
377 global $wgLogExceptionBacktrace;
378
379 $exceptionData = array(
380 'id' => self::getLogId( $e ),
381 'type' => get_class( $e ),
382 'file' => $e->getFile(),
383 'line' => $e->getLine(),
384 'message' => $e->getMessage(),
385 );
386
387 // Because MediaWiki is first and foremost a web application, we set a
388 // 'url' key unconditionally, but set it to null if the exception does
389 // not occur in the context of a web request, as a way of making that
390 // fact visible and explicit.
391 $exceptionData['url'] = self::getURL() ?: null;
392
393 if ( $wgLogExceptionBacktrace ) {
394 // Argument values may not be serializable, so redact them.
395 $exceptionData['backtrace'] = self::getRedactedTrace( $e );
396 }
397
398 return FormatJson::encode( $exceptionData, $pretty, $escaping );
399 }
400
401 /**
402 * Log an exception to the exception log (if enabled).
403 *
404 * This method must not assume the exception is an MWException,
405 * it is also used to handle PHP exceptions or exceptions from other libraries.
406 *
407 * @since 1.22
408 * @param Exception $e
409 */
410 public static function logException( Exception $e ) {
411 global $wgLogExceptionBacktrace;
412
413 if ( !( $e instanceof MWException ) || $e->isLoggable() ) {
414 $log = self::getLogMessage( $e );
415 if ( $wgLogExceptionBacktrace ) {
416 wfDebugLog( 'exception', $log . "\n" . $e->getTraceAsString() );
417 } else {
418 wfDebugLog( 'exception', $log );
419 }
420
421 $json = self::jsonSerializeException( $e, false, FormatJson::ALL_OK );
422 if ( $json !== false ) {
423 wfDebugLog( 'exception-json', $json, 'private' );
424 }
425 }
426 }
427
428 /**
429 * Log an exception that wasn't thrown but made to wrap an error.
430 *
431 * @since 1.25
432 * @param Exception $e
433 */
434 protected static function logError( Exception $e ) {
435 global $wgLogExceptionBacktrace;
436
437 $log = self::getLogMessage( $e );
438 if ( $wgLogExceptionBacktrace ) {
439 wfDebugLog( 'error', $log . "\n" . $e->getTraceAsString() );
440 } else {
441 wfDebugLog( 'error', $log );
442 }
443 }
444 }