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