Merge "API: Allow for format modules that cannot handle errors"
[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 * Install an exception handler for MediaWiki exception types.
28 */
29 public static function installHandler() {
30 set_exception_handler( array( 'MWExceptionHandler', 'handle' ) );
31 }
32
33 /**
34 * Report an exception to the user
35 */
36 protected static function report( Exception $e ) {
37 global $wgShowExceptionDetails;
38
39 $cmdLine = MWException::isCommandLine();
40
41 if ( $e instanceof MWException ) {
42 try {
43 // Try and show the exception prettily, with the normal skin infrastructure
44 $e->report();
45 } catch ( Exception $e2 ) {
46 // Exception occurred from within exception handler
47 // Show a simpler error message for the original exception,
48 // don't try to invoke report()
49 $message = "MediaWiki internal error.\n\n";
50
51 if ( $wgShowExceptionDetails ) {
52 $message .= 'Original exception: ' . self::getLogMessage( $e ) .
53 "\nBacktrace:\n" . self::getRedactedTraceAsString( $e ) .
54 "\n\nException caught inside exception handler: " . self::getLogMessage( $e2 ) .
55 "\nBacktrace:\n" . self::getRedactedTraceAsString( $e2 );
56 } else {
57 $message .= "Exception caught inside exception handler.\n\n" .
58 "Set \$wgShowExceptionDetails = true; at the bottom of LocalSettings.php " .
59 "to show detailed debugging information.";
60 }
61
62 $message .= "\n";
63
64 if ( $cmdLine ) {
65 self::printError( $message );
66 } else {
67 echo nl2br( htmlspecialchars( $message ) ) . "\n";
68 }
69 }
70 } else {
71 $message = "Unexpected non-MediaWiki exception encountered, of type \"" .
72 get_class( $e ) . "\"";
73
74 if ( $wgShowExceptionDetails ) {
75 $message .= "\n" . MWExceptionHandler::getLogMessage( $e ) . "\nBacktrace:\n" .
76 self::getRedactedTraceAsString( $e ) . "\n";
77 }
78
79 if ( $cmdLine ) {
80 self::printError( $message );
81 } else {
82 echo nl2br( htmlspecialchars( $message ) ) . "\n";
83 }
84 }
85 }
86
87 /**
88 * Print a message, if possible to STDERR.
89 * Use this in command line mode only (see isCommandLine)
90 *
91 * @param string $message Failure text
92 */
93 public static function printError( $message ) {
94 # NOTE: STDERR may not be available, especially if php-cgi is used from the
95 # command line (bug #15602). Try to produce meaningful output anyway. Using
96 # echo may corrupt output to STDOUT though.
97 if ( defined( 'STDERR' ) ) {
98 fwrite( STDERR, $message );
99 } else {
100 echo $message;
101 }
102 }
103
104 /**
105 * If there are any open database transactions, roll them back and log
106 * the stack trace of the exception that should have been caught so the
107 * transaction could be aborted properly.
108 * @since 1.23
109 * @param Exception $e
110 */
111 public static function rollbackMasterChangesAndLog( Exception $e ) {
112 $factory = wfGetLBFactory();
113 if ( $factory->hasMasterChanges() ) {
114 wfDebugLog( 'Bug56269',
115 'Exception thrown with an uncommited database transaction: ' .
116 MWExceptionHandler::getLogMessage( $e ) . "\n" .
117 $e->getTraceAsString()
118 );
119 $factory->rollbackMasterChanges();
120 }
121 }
122
123 /**
124 * Exception handler which simulates the appropriate catch() handling:
125 *
126 * try {
127 * ...
128 * } catch ( MWException $e ) {
129 * $e->report();
130 * } catch ( Exception $e ) {
131 * echo $e->__toString();
132 * }
133 */
134 public static function handle( $e ) {
135 global $wgFullyInitialised;
136
137 self::rollbackMasterChangesAndLog( $e );
138
139 self::report( $e );
140
141 // Final cleanup
142 if ( $wgFullyInitialised ) {
143 try {
144 // uses $wgRequest, hence the $wgFullyInitialised condition
145 wfLogProfilingData();
146 } catch ( Exception $e ) {
147 }
148 }
149
150 // Exit value should be nonzero for the benefit of shell jobs
151 exit( 1 );
152 }
153
154 /**
155 * Generate a string representation of an exception's stack trace
156 *
157 * Like Exception::getTraceAsString, but replaces argument values with
158 * argument type or class name.
159 *
160 * @param Exception $e
161 * @return string
162 */
163 public static function getRedactedTraceAsString( Exception $e ) {
164 $text = '';
165
166 foreach ( self::getRedactedTrace( $e ) as $level => $frame ) {
167 if ( isset( $frame['file'] ) && isset( $frame['line'] ) ) {
168 $text .= "#{$level} {$frame['file']}({$frame['line']}): ";
169 } else {
170 // 'file' and 'line' are unset for calls via call_user_func (bug 55634)
171 // This matches behaviour of Exception::getTraceAsString to instead
172 // display "[internal function]".
173 $text .= "#{$level} [internal function]: ";
174 }
175
176 if ( isset( $frame['class'] ) ) {
177 $text .= $frame['class'] . $frame['type'] . $frame['function'];
178 } else {
179 $text .= $frame['function'];
180 }
181
182 if ( isset( $frame['args'] ) ) {
183 $text .= '(' . implode( ', ', $frame['args'] ) . ")\n";
184 } else {
185 $text .= "()\n";
186 }
187 }
188
189 $level = $level + 1;
190 $text .= "#{$level} {main}";
191
192 return $text;
193 }
194
195 /**
196 * Return a copy of an exception's backtrace as an array.
197 *
198 * Like Exception::getTrace, but replaces each element in each frame's
199 * argument array with the name of its class (if the element is an object)
200 * or its type (if the element is a PHP primitive).
201 *
202 * @since 1.22
203 * @param Exception $e
204 * @return array
205 */
206 public static function getRedactedTrace( Exception $e ) {
207 return array_map( function ( $frame ) {
208 if ( isset( $frame['args'] ) ) {
209 $frame['args'] = array_map( function ( $arg ) {
210 return is_object( $arg ) ? get_class( $arg ) : gettype( $arg );
211 }, $frame['args'] );
212 }
213 return $frame;
214 }, $e->getTrace() );
215 }
216
217 /**
218 * Get the ID for this error.
219 *
220 * The ID is saved so that one can match the one output to the user (when
221 * $wgShowExceptionDetails is set to false), to the entry in the debug log.
222 *
223 * @since 1.22
224 * @param Exception $e
225 * @return string
226 */
227 public static function getLogId( Exception $e ) {
228 if ( !isset( $e->_mwLogId ) ) {
229 $e->_mwLogId = wfRandomString( 8 );
230 }
231 return $e->_mwLogId;
232 }
233
234 /**
235 * If the exception occurred in the course of responding to a request,
236 * returns the requested URL. Otherwise, returns false.
237 *
238 * @since 1.23
239 * @return string|bool
240 */
241 public static function getURL() {
242 global $wgRequest;
243 if ( !isset( $wgRequest ) || $wgRequest instanceof FauxRequest ) {
244 return false;
245 }
246 return $wgRequest->getRequestURL();
247 }
248
249 /**
250 * Return the requested URL and point to file and line number from which the
251 * exception occurred.
252 *
253 * @since 1.22
254 * @param Exception $e
255 * @return string
256 */
257 public static function getLogMessage( Exception $e ) {
258 $id = self::getLogId( $e );
259 $file = $e->getFile();
260 $line = $e->getLine();
261 $message = $e->getMessage();
262 $url = self::getURL() ?: '[no req]';
263
264 return "[$id] $url Exception from line $line of $file: $message";
265 }
266
267 /**
268 * Serialize an Exception object to JSON.
269 *
270 * The JSON object will have keys 'id', 'file', 'line', 'message', and
271 * 'url'. These keys map to string values, with the exception of 'line',
272 * which is a number, and 'url', which may be either a string URL or or
273 * null if the exception did not occur in the context of serving a web
274 * request.
275 *
276 * If $wgLogExceptionBacktrace is true, it will also have a 'backtrace'
277 * key, mapped to the array return value of Exception::getTrace, but with
278 * each element in each frame's "args" array (if set) replaced with the
279 * argument's class name (if the argument is an object) or type name (if
280 * the argument is a PHP primitive).
281 *
282 * @par Sample JSON record ($wgLogExceptionBacktrace = false):
283 * @code
284 * {
285 * "id": "c41fb419",
286 * "file": "/var/www/mediawiki/includes/cache/MessageCache.php",
287 * "line": 704,
288 * "message": "Non-string key given",
289 * "url": "/wiki/Main_Page"
290 * }
291 * @endcode
292 *
293 * @par Sample JSON record ($wgLogExceptionBacktrace = true):
294 * @code
295 * {
296 * "id": "dc457938",
297 * "file": "/vagrant/mediawiki/includes/cache/MessageCache.php",
298 * "line": 704,
299 * "message": "Non-string key given",
300 * "url": "/wiki/Main_Page",
301 * "backtrace": [{
302 * "file": "/vagrant/mediawiki/extensions/VisualEditor/VisualEditor.hooks.php",
303 * "line": 80,
304 * "function": "get",
305 * "class": "MessageCache",
306 * "type": "->",
307 * "args": ["array"]
308 * }]
309 * }
310 * @endcode
311 *
312 * @since 1.23
313 * @param Exception $e
314 * @param bool $pretty Add non-significant whitespace to improve readability (default: false).
315 * @param int $escaping Bitfield consisting of FormatJson::.*_OK class constants.
316 * @return string|bool: JSON string if successful; false upon failure
317 */
318 public static function jsonSerializeException( Exception $e, $pretty = false, $escaping = 0 ) {
319 global $wgLogExceptionBacktrace;
320
321 $exceptionData = array(
322 'id' => self::getLogId( $e ),
323 'file' => $e->getFile(),
324 'line' => $e->getLine(),
325 'message' => $e->getMessage(),
326 );
327
328 // Because MediaWiki is first and foremost a web application, we set a
329 // 'url' key unconditionally, but set it to null if the exception does
330 // not occur in the context of a web request, as a way of making that
331 // fact visible and explicit.
332 $exceptionData['url'] = self::getURL() ?: null;
333
334 if ( $wgLogExceptionBacktrace ) {
335 // Argument values may not be serializable, so redact them.
336 $exceptionData['backtrace'] = self::getRedactedTrace( $e );
337 }
338
339 return FormatJson::encode( $exceptionData, $pretty, $escaping );
340 }
341
342 /**
343 * Log an exception to the exception log (if enabled).
344 *
345 * This method must not assume the exception is an MWException,
346 * it is also used to handle PHP errors or errors from other libraries.
347 *
348 * @since 1.22
349 * @param Exception $e
350 */
351 public static function logException( Exception $e ) {
352 global $wgLogExceptionBacktrace;
353
354 if ( !( $e instanceof MWException ) || $e->isLoggable() ) {
355 $log = self::getLogMessage( $e );
356 if ( $wgLogExceptionBacktrace ) {
357 wfDebugLog( 'exception', $log . "\n" . $e->getTraceAsString() );
358 } else {
359 wfDebugLog( 'exception', $log );
360 }
361
362 $json = self::jsonSerializeException( $e, false, FormatJson::ALL_OK );
363 if ( $json !== false ) {
364 wfDebugLog( 'exception-json', $json, 'private' );
365 }
366 }
367
368 }
369
370 }