MWExceptionHandler.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748
  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. use MediaWiki\Logger\LoggerFactory;
  21. use MediaWiki\MediaWikiServices;
  22. use Psr\Log\LogLevel;
  23. use Wikimedia\Rdbms\DBError;
  24. /**
  25. * Handler class for MWExceptions
  26. * @ingroup Exception
  27. */
  28. class MWExceptionHandler {
  29. const CAUGHT_BY_HANDLER = 'mwe_handler'; // error reported by this exception handler
  30. const CAUGHT_BY_OTHER = 'other'; // error reported by direct logException() call
  31. /**
  32. * @var string $reservedMemory
  33. */
  34. protected static $reservedMemory;
  35. /**
  36. * Error types that, if unhandled, are fatal to the request.
  37. *
  38. * On PHP 7, these error types may be thrown as Error objects, which
  39. * implement Throwable (but not Exception).
  40. *
  41. * On HHVM, these invoke the set_error_handler callback, similar to how
  42. * (non-fatal) warnings and notices are reported, except that after this
  43. * handler runs for fatal error tpyes, script execution stops!
  44. *
  45. * The user will be shown an HTTP 500 Internal Server Error.
  46. * As such, these should be sent to MediaWiki's "fatal" or "exception"
  47. * channel. Normally, the error handler logs them to the "error" channel.
  48. *
  49. * @var array $fatalErrorTypes
  50. */
  51. protected static $fatalErrorTypes = [
  52. E_ERROR,
  53. E_PARSE,
  54. E_CORE_ERROR,
  55. E_COMPILE_ERROR,
  56. E_USER_ERROR,
  57. // E.g. "Catchable fatal error: Argument X must be Y, null given"
  58. E_RECOVERABLE_ERROR,
  59. // HHVM's FATAL_ERROR constant
  60. 16777217,
  61. ];
  62. /**
  63. * @var bool $handledFatalCallback
  64. */
  65. protected static $handledFatalCallback = false;
  66. /**
  67. * Install handlers with PHP.
  68. */
  69. public static function installHandler() {
  70. // This catches:
  71. // * Exception objects that were explicitly thrown but not
  72. // caught anywhere in the application. This is rare given those
  73. // would normally be caught at a high-level like MediaWiki::run (index.php),
  74. // api.php, or ResourceLoader::respond (load.php). These high-level
  75. // catch clauses would then call MWExceptionHandler::logException
  76. // or MWExceptionHandler::handleException.
  77. // If they are not caught, then they are handled here.
  78. // * Error objects (on PHP 7+), for issues that would historically
  79. // cause fatal errors but may now be caught as Throwable (not Exception).
  80. // Same as previous case, but more common to bubble to here instead of
  81. // caught locally because they tend to not be safe to recover from.
  82. // (e.g. argument TypeErorr, devision by zero, etc.)
  83. set_exception_handler( 'MWExceptionHandler::handleUncaughtException' );
  84. // This catches:
  85. // * Non-fatal errors (e.g. PHP Notice, PHP Warning, PHP Error) that do not
  86. // interrupt execution in any way. We log these in the background and then
  87. // continue execution.
  88. // * Fatal errors (on HHVM in PHP5 mode) where PHP 7 would throw Throwable.
  89. set_error_handler( 'MWExceptionHandler::handleError' );
  90. // This catches:
  91. // * Fatal error for which no Throwable is thrown (PHP 7), and no Error emitted (HHVM).
  92. // This includes Out-Of-Memory and Timeout fatals.
  93. //
  94. // Reserve 16k of memory so we can report OOM fatals
  95. self::$reservedMemory = str_repeat( ' ', 16384 );
  96. register_shutdown_function( 'MWExceptionHandler::handleFatalError' );
  97. }
  98. /**
  99. * Report an exception to the user
  100. * @param Exception|Throwable $e
  101. */
  102. protected static function report( $e ) {
  103. try {
  104. // Try and show the exception prettily, with the normal skin infrastructure
  105. if ( $e instanceof MWException ) {
  106. // Delegate to MWException until all subclasses are handled by
  107. // MWExceptionRenderer and MWException::report() has been
  108. // removed.
  109. $e->report();
  110. } else {
  111. MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_PRETTY );
  112. }
  113. } catch ( Exception $e2 ) {
  114. // Exception occurred from within exception handler
  115. // Show a simpler message for the original exception,
  116. // don't try to invoke report()
  117. MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_RAW, $e2 );
  118. }
  119. }
  120. /**
  121. * Roll back any open database transactions and log the stack trace of the exception
  122. *
  123. * This method is used to attempt to recover from exceptions
  124. *
  125. * @since 1.23
  126. * @param Exception|Throwable $e
  127. */
  128. public static function rollbackMasterChangesAndLog( $e ) {
  129. $services = MediaWikiServices::getInstance();
  130. if ( !$services->isServiceDisabled( 'DBLoadBalancerFactory' ) ) {
  131. // Rollback DBs to avoid transaction notices. This might fail
  132. // to rollback some databases due to connection issues or exceptions.
  133. // However, any sane DB driver will rollback implicitly anyway.
  134. try {
  135. $services->getDBLoadBalancerFactory()->rollbackMasterChanges( __METHOD__ );
  136. } catch ( DBError $e2 ) {
  137. // If the DB is unreacheable, rollback() will throw an error
  138. // and the error report() method might need messages from the DB,
  139. // which would result in an exception loop. PHP may escalate such
  140. // errors to "Exception thrown without a stack frame" fatals, but
  141. // it's better to be explicit here.
  142. self::logException( $e2, self::CAUGHT_BY_HANDLER );
  143. }
  144. }
  145. self::logException( $e, self::CAUGHT_BY_HANDLER );
  146. }
  147. /**
  148. * Callback to use with PHP's set_exception_handler.
  149. *
  150. * @since 1.31
  151. * @param Exception|Throwable $e
  152. */
  153. public static function handleUncaughtException( $e ) {
  154. self::handleException( $e );
  155. // Make sure we don't claim success on exit for CLI scripts (T177414)
  156. if ( wfIsCLI() ) {
  157. register_shutdown_function(
  158. function () {
  159. exit( 255 );
  160. }
  161. );
  162. }
  163. }
  164. /**
  165. * Exception handler which simulates the appropriate catch() handling:
  166. *
  167. * try {
  168. * ...
  169. * } catch ( Exception $e ) {
  170. * $e->report();
  171. * } catch ( Exception $e ) {
  172. * echo $e->__toString();
  173. * }
  174. *
  175. * @since 1.25
  176. * @param Exception|Throwable $e
  177. */
  178. public static function handleException( $e ) {
  179. self::rollbackMasterChangesAndLog( $e );
  180. self::report( $e );
  181. }
  182. /**
  183. * Handler for set_error_handler() callback notifications.
  184. *
  185. * Receive a callback from the interpreter for a raised error, create an
  186. * ErrorException, and log the exception to the 'error' logging
  187. * channel(s). If the raised error is a fatal error type (only under HHVM)
  188. * delegate to handleFatalError() instead.
  189. *
  190. * @since 1.25
  191. *
  192. * @param int $level Error level raised
  193. * @param string $message
  194. * @param string|null $file
  195. * @param int|null $line
  196. * @return bool
  197. *
  198. * @see logError()
  199. */
  200. public static function handleError(
  201. $level, $message, $file = null, $line = null
  202. ) {
  203. global $wgPropagateErrors;
  204. if ( in_array( $level, self::$fatalErrorTypes ) ) {
  205. return self::handleFatalError( ...func_get_args() );
  206. }
  207. // Map PHP error constant to a PSR-3 severity level.
  208. // Avoid use of "DEBUG" or "INFO" levels, unless the
  209. // error should evade error monitoring and alerts.
  210. //
  211. // To decide the log level, ask yourself: "Has the
  212. // program's behaviour diverged from what the written
  213. // code expected?"
  214. //
  215. // For example, use of a deprecated method or violating a strict standard
  216. // has no impact on functional behaviour (Warning). On the other hand,
  217. // accessing an undefined variable makes behaviour diverge from what the
  218. // author intended/expected. PHP recovers from an undefined variables by
  219. // yielding null and continuing execution, but it remains a change in
  220. // behaviour given the null was not part of the code and is likely not
  221. // accounted for.
  222. switch ( $level ) {
  223. case E_WARNING:
  224. case E_CORE_WARNING:
  225. case E_COMPILE_WARNING:
  226. $levelName = 'Warning';
  227. $severity = LogLevel::ERROR;
  228. break;
  229. case E_NOTICE:
  230. $levelName = 'Notice';
  231. $severity = LogLevel::ERROR;
  232. break;
  233. case E_USER_NOTICE:
  234. // Used by wfWarn(), MWDebug::warning()
  235. $levelName = 'Notice';
  236. $severity = LogLevel::WARNING;
  237. break;
  238. case E_USER_WARNING:
  239. // Used by wfWarn(), MWDebug::warning()
  240. $levelName = 'Warning';
  241. $severity = LogLevel::WARNING;
  242. break;
  243. case E_STRICT:
  244. $levelName = 'Strict Standards';
  245. $severity = LogLevel::WARNING;
  246. break;
  247. case E_DEPRECATED:
  248. case E_USER_DEPRECATED:
  249. $levelName = 'Deprecated';
  250. $severity = LogLevel::WARNING;
  251. break;
  252. default:
  253. $levelName = 'Unknown error';
  254. $severity = LogLevel::ERROR;
  255. break;
  256. }
  257. $e = new ErrorException( "PHP $levelName: $message", 0, $level, $file, $line );
  258. self::logError( $e, 'error', $severity );
  259. // If $wgPropagateErrors is true return false so PHP shows/logs the error normally.
  260. // Ignore $wgPropagateErrors if track_errors is set
  261. // (which means someone is counting on regular PHP error handling behavior).
  262. return !( $wgPropagateErrors || ini_get( 'track_errors' ) );
  263. }
  264. /**
  265. * Dual purpose callback used as both a set_error_handler() callback and
  266. * a registered shutdown function. Receive a callback from the interpreter
  267. * for a raised error or system shutdown, check for a fatal error, and log
  268. * to the 'fatal' logging channel.
  269. *
  270. * Special handling is included for missing class errors as they may
  271. * indicate that the user needs to install 3rd-party libraries via
  272. * Composer or other means.
  273. *
  274. * @since 1.25
  275. *
  276. * @param int|null $level Error level raised
  277. * @param string|null $message Error message
  278. * @param string|null $file File that error was raised in
  279. * @param int|null $line Line number error was raised at
  280. * @param array|null $context Active symbol table point of error
  281. * @param array|null $trace Backtrace at point of error (undocumented HHVM
  282. * feature)
  283. * @return bool Always returns false
  284. */
  285. public static function handleFatalError(
  286. $level = null, $message = null, $file = null, $line = null,
  287. $context = null, $trace = null
  288. ) {
  289. // Free reserved memory so that we have space to process OOM
  290. // errors
  291. self::$reservedMemory = null;
  292. if ( $level === null ) {
  293. // Called as a shutdown handler, get data from error_get_last()
  294. if ( static::$handledFatalCallback ) {
  295. // Already called once (probably as an error handler callback
  296. // under HHVM) so don't log again.
  297. return false;
  298. }
  299. $lastError = error_get_last();
  300. if ( $lastError !== null ) {
  301. $level = $lastError['type'];
  302. $message = $lastError['message'];
  303. $file = $lastError['file'];
  304. $line = $lastError['line'];
  305. } else {
  306. $level = 0;
  307. $message = '';
  308. }
  309. }
  310. if ( !in_array( $level, self::$fatalErrorTypes ) ) {
  311. // Only interested in fatal errors, others should have been
  312. // handled by MWExceptionHandler::handleError
  313. return false;
  314. }
  315. $url = WebRequest::getGlobalRequestURL();
  316. $msgParts = [
  317. '[{exception_id}] {exception_url} PHP Fatal Error',
  318. ( $line || $file ) ? ' from' : '',
  319. $line ? " line $line" : '',
  320. ( $line && $file ) ? ' of' : '',
  321. $file ? " $file" : '',
  322. ": $message",
  323. ];
  324. $msg = implode( '', $msgParts );
  325. // Look at message to see if this is a class not found failure
  326. // HHVM: Class undefined: foo
  327. // PHP7: Class 'foo' not found
  328. if ( preg_match( "/Class (undefined: \w+|'\w+' not found)/", $message ) ) {
  329. // phpcs:disable Generic.Files.LineLength
  330. $msg = <<<TXT
  331. {$msg}
  332. MediaWiki or an installed extension requires this class but it is not embedded directly in MediaWiki's git repository and must be installed separately by the end user.
  333. Please see <a href="https://www.mediawiki.org/wiki/Download_from_Git#Fetch_external_libraries">mediawiki.org</a> for help on installing the required components.
  334. TXT;
  335. // phpcs:enable
  336. }
  337. // We can't just create an exception and log it as it is likely that
  338. // the interpreter has unwound the stack already. If that is true the
  339. // stacktrace we would get would be functionally empty. If however we
  340. // have been called as an error handler callback *and* HHVM is in use
  341. // we will have been provided with a useful stacktrace that we can
  342. // log.
  343. $trace = $trace ?: debug_backtrace();
  344. $logger = LoggerFactory::getInstance( 'fatal' );
  345. $logger->error( $msg, [
  346. 'fatal_exception' => [
  347. 'class' => ErrorException::class,
  348. 'message' => "PHP Fatal Error: {$message}",
  349. 'code' => $level,
  350. 'file' => $file,
  351. 'line' => $line,
  352. 'trace' => self::prettyPrintTrace( self::redactTrace( $trace ) ),
  353. ],
  354. 'exception_id' => WebRequest::getRequestId(),
  355. 'exception_url' => $url,
  356. 'caught_by' => self::CAUGHT_BY_HANDLER
  357. ] );
  358. // Remember call so we don't double process via HHVM's fatal
  359. // notifications and the shutdown hook behavior
  360. static::$handledFatalCallback = true;
  361. return false;
  362. }
  363. /**
  364. * Generate a string representation of an exception's stack trace
  365. *
  366. * Like Exception::getTraceAsString, but replaces argument values with
  367. * argument type or class name.
  368. *
  369. * @param Exception|Throwable $e
  370. * @return string
  371. * @see prettyPrintTrace()
  372. */
  373. public static function getRedactedTraceAsString( $e ) {
  374. return self::prettyPrintTrace( self::getRedactedTrace( $e ) );
  375. }
  376. /**
  377. * Generate a string representation of a stacktrace.
  378. *
  379. * @param array $trace
  380. * @param string $pad Constant padding to add to each line of trace
  381. * @return string
  382. * @since 1.26
  383. */
  384. public static function prettyPrintTrace( array $trace, $pad = '' ) {
  385. $text = '';
  386. $level = 0;
  387. foreach ( $trace as $level => $frame ) {
  388. if ( isset( $frame['file'] ) && isset( $frame['line'] ) ) {
  389. $text .= "{$pad}#{$level} {$frame['file']}({$frame['line']}): ";
  390. } else {
  391. // 'file' and 'line' are unset for calls via call_user_func
  392. // (T57634) This matches behaviour of
  393. // Exception::getTraceAsString to instead display "[internal
  394. // function]".
  395. $text .= "{$pad}#{$level} [internal function]: ";
  396. }
  397. if ( isset( $frame['class'] ) && isset( $frame['type'] ) && isset( $frame['function'] ) ) {
  398. $text .= $frame['class'] . $frame['type'] . $frame['function'];
  399. } elseif ( isset( $frame['function'] ) ) {
  400. $text .= $frame['function'];
  401. } else {
  402. $text .= 'NO_FUNCTION_GIVEN';
  403. }
  404. if ( isset( $frame['args'] ) ) {
  405. $text .= '(' . implode( ', ', $frame['args'] ) . ")\n";
  406. } else {
  407. $text .= "()\n";
  408. }
  409. }
  410. $level = $level + 1;
  411. $text .= "{$pad}#{$level} {main}";
  412. return $text;
  413. }
  414. /**
  415. * Return a copy of an exception's backtrace as an array.
  416. *
  417. * Like Exception::getTrace, but replaces each element in each frame's
  418. * argument array with the name of its class (if the element is an object)
  419. * or its type (if the element is a PHP primitive).
  420. *
  421. * @since 1.22
  422. * @param Exception|Throwable $e
  423. * @return array
  424. */
  425. public static function getRedactedTrace( $e ) {
  426. return static::redactTrace( $e->getTrace() );
  427. }
  428. /**
  429. * Redact a stacktrace generated by Exception::getTrace(),
  430. * debug_backtrace() or similar means. Replaces each element in each
  431. * frame's argument array with the name of its class (if the element is an
  432. * object) or its type (if the element is a PHP primitive).
  433. *
  434. * @since 1.26
  435. * @param array $trace Stacktrace
  436. * @return array Stacktrace with arugment values converted to data types
  437. */
  438. public static function redactTrace( array $trace ) {
  439. return array_map( function ( $frame ) {
  440. if ( isset( $frame['args'] ) ) {
  441. $frame['args'] = array_map( function ( $arg ) {
  442. return is_object( $arg ) ? get_class( $arg ) : gettype( $arg );
  443. }, $frame['args'] );
  444. }
  445. return $frame;
  446. }, $trace );
  447. }
  448. /**
  449. * If the exception occurred in the course of responding to a request,
  450. * returns the requested URL. Otherwise, returns false.
  451. *
  452. * @since 1.23
  453. * @return string|false
  454. */
  455. public static function getURL() {
  456. global $wgRequest;
  457. if ( !isset( $wgRequest ) || $wgRequest instanceof FauxRequest ) {
  458. return false;
  459. }
  460. return $wgRequest->getRequestURL();
  461. }
  462. /**
  463. * Get a message formatting the exception message and its origin.
  464. *
  465. * @since 1.22
  466. * @param Exception|Throwable $e
  467. * @return string
  468. */
  469. public static function getLogMessage( $e ) {
  470. $id = WebRequest::getRequestId();
  471. $type = get_class( $e );
  472. $file = $e->getFile();
  473. $line = $e->getLine();
  474. $message = $e->getMessage();
  475. $url = self::getURL() ?: '[no req]';
  476. return "[$id] $url $type from line $line of $file: $message";
  477. }
  478. /**
  479. * Get a normalised message for formatting with PSR-3 log event context.
  480. *
  481. * Must be used together with `getLogContext()` to be useful.
  482. *
  483. * @since 1.30
  484. * @param Exception|Throwable $e
  485. * @return string
  486. */
  487. public static function getLogNormalMessage( $e ) {
  488. $type = get_class( $e );
  489. $file = $e->getFile();
  490. $line = $e->getLine();
  491. $message = $e->getMessage();
  492. return "[{exception_id}] {exception_url} $type from line $line of $file: $message";
  493. }
  494. /**
  495. * @param Exception|Throwable $e
  496. * @return string
  497. */
  498. public static function getPublicLogMessage( $e ) {
  499. $reqId = WebRequest::getRequestId();
  500. $type = get_class( $e );
  501. return '[' . $reqId . '] '
  502. . gmdate( 'Y-m-d H:i:s' ) . ': '
  503. . 'Fatal exception of type "' . $type . '"';
  504. }
  505. /**
  506. * Get a PSR-3 log event context from an Exception.
  507. *
  508. * Creates a structured array containing information about the provided
  509. * exception that can be used to augment a log message sent to a PSR-3
  510. * logger.
  511. *
  512. * @param Exception|Throwable $e
  513. * @param string $catcher CAUGHT_BY_* class constant indicating what caught the error
  514. * @return array
  515. */
  516. public static function getLogContext( $e, $catcher = self::CAUGHT_BY_OTHER ) {
  517. return [
  518. 'exception' => $e,
  519. 'exception_id' => WebRequest::getRequestId(),
  520. 'exception_url' => self::getURL() ?: '[no req]',
  521. 'caught_by' => $catcher
  522. ];
  523. }
  524. /**
  525. * Get a structured representation of an Exception.
  526. *
  527. * Returns an array of structured data (class, message, code, file,
  528. * backtrace) derived from the given exception. The backtrace information
  529. * will be redacted as per getRedactedTraceAsArray().
  530. *
  531. * @param Exception|Throwable $e
  532. * @param string $catcher CAUGHT_BY_* class constant indicating what caught the error
  533. * @return array
  534. * @since 1.26
  535. */
  536. public static function getStructuredExceptionData( $e, $catcher = self::CAUGHT_BY_OTHER ) {
  537. global $wgLogExceptionBacktrace;
  538. $data = [
  539. 'id' => WebRequest::getRequestId(),
  540. 'type' => get_class( $e ),
  541. 'file' => $e->getFile(),
  542. 'line' => $e->getLine(),
  543. 'message' => $e->getMessage(),
  544. 'code' => $e->getCode(),
  545. 'url' => self::getURL() ?: null,
  546. 'caught_by' => $catcher
  547. ];
  548. if ( $e instanceof ErrorException &&
  549. ( error_reporting() & $e->getSeverity() ) === 0
  550. ) {
  551. // Flag surpressed errors
  552. $data['suppressed'] = true;
  553. }
  554. if ( $wgLogExceptionBacktrace ) {
  555. $data['backtrace'] = self::getRedactedTrace( $e );
  556. }
  557. $previous = $e->getPrevious();
  558. if ( $previous !== null ) {
  559. $data['previous'] = self::getStructuredExceptionData( $previous, $catcher );
  560. }
  561. return $data;
  562. }
  563. /**
  564. * Serialize an Exception object to JSON.
  565. *
  566. * The JSON object will have keys 'id', 'file', 'line', 'message', and
  567. * 'url'. These keys map to string values, with the exception of 'line',
  568. * which is a number, and 'url', which may be either a string URL or or
  569. * null if the exception did not occur in the context of serving a web
  570. * request.
  571. *
  572. * If $wgLogExceptionBacktrace is true, it will also have a 'backtrace'
  573. * key, mapped to the array return value of Exception::getTrace, but with
  574. * each element in each frame's "args" array (if set) replaced with the
  575. * argument's class name (if the argument is an object) or type name (if
  576. * the argument is a PHP primitive).
  577. *
  578. * @par Sample JSON record ($wgLogExceptionBacktrace = false):
  579. * @code
  580. * {
  581. * "id": "c41fb419",
  582. * "type": "MWException",
  583. * "file": "/var/www/mediawiki/includes/cache/MessageCache.php",
  584. * "line": 704,
  585. * "message": "Non-string key given",
  586. * "url": "/wiki/Main_Page"
  587. * }
  588. * @endcode
  589. *
  590. * @par Sample JSON record ($wgLogExceptionBacktrace = true):
  591. * @code
  592. * {
  593. * "id": "dc457938",
  594. * "type": "MWException",
  595. * "file": "/vagrant/mediawiki/includes/cache/MessageCache.php",
  596. * "line": 704,
  597. * "message": "Non-string key given",
  598. * "url": "/wiki/Main_Page",
  599. * "backtrace": [{
  600. * "file": "/vagrant/mediawiki/extensions/VisualEditor/VisualEditor.hooks.php",
  601. * "line": 80,
  602. * "function": "get",
  603. * "class": "MessageCache",
  604. * "type": "->",
  605. * "args": ["array"]
  606. * }]
  607. * }
  608. * @endcode
  609. *
  610. * @since 1.23
  611. * @param Exception|Throwable $e
  612. * @param bool $pretty Add non-significant whitespace to improve readability (default: false).
  613. * @param int $escaping Bitfield consisting of FormatJson::.*_OK class constants.
  614. * @param string $catcher CAUGHT_BY_* class constant indicating what caught the error
  615. * @return string|false JSON string if successful; false upon failure
  616. */
  617. public static function jsonSerializeException(
  618. $e, $pretty = false, $escaping = 0, $catcher = self::CAUGHT_BY_OTHER
  619. ) {
  620. return FormatJson::encode(
  621. self::getStructuredExceptionData( $e, $catcher ),
  622. $pretty,
  623. $escaping
  624. );
  625. }
  626. /**
  627. * Log an exception to the exception log (if enabled).
  628. *
  629. * This method must not assume the exception is an MWException,
  630. * it is also used to handle PHP exceptions or exceptions from other libraries.
  631. *
  632. * @param Exception|Throwable $e
  633. * @param string $catcher CAUGHT_BY_* class constant indicating what caught the error
  634. * @param array $extraData (since 1.34) Additional data to log
  635. * @since 1.22
  636. */
  637. public static function logException( $e, $catcher = self::CAUGHT_BY_OTHER, $extraData = [] ) {
  638. if ( !( $e instanceof MWException ) || $e->isLoggable() ) {
  639. $logger = LoggerFactory::getInstance( 'exception' );
  640. $context = self::getLogContext( $e, $catcher );
  641. if ( $extraData ) {
  642. $context['extraData'] = $extraData;
  643. }
  644. $logger->error(
  645. self::getLogNormalMessage( $e ),
  646. $context
  647. );
  648. $json = self::jsonSerializeException( $e, false, FormatJson::ALL_OK, $catcher );
  649. if ( $json !== false ) {
  650. $logger = LoggerFactory::getInstance( 'exception-json' );
  651. $logger->error( $json, [ 'private' => true ] );
  652. }
  653. Hooks::run( 'LogException', [ $e, false ] );
  654. }
  655. }
  656. /**
  657. * Log an exception that wasn't thrown but made to wrap an error.
  658. *
  659. * @since 1.25
  660. * @param ErrorException $e
  661. * @param string $channel
  662. * @param string $level
  663. */
  664. protected static function logError(
  665. ErrorException $e, $channel, $level = LogLevel::ERROR
  666. ) {
  667. $catcher = self::CAUGHT_BY_HANDLER;
  668. // The set_error_handler callback is independent from error_reporting.
  669. // Filter out unwanted errors manually (e.g. when
  670. // Wikimedia\suppressWarnings is active).
  671. $suppressed = ( error_reporting() & $e->getSeverity() ) === 0;
  672. if ( !$suppressed ) {
  673. $logger = LoggerFactory::getInstance( $channel );
  674. $logger->log(
  675. $level,
  676. self::getLogNormalMessage( $e ),
  677. self::getLogContext( $e, $catcher )
  678. );
  679. }
  680. // Include all errors in the json log (surpressed errors will be flagged)
  681. $json = self::jsonSerializeException( $e, false, FormatJson::ALL_OK, $catcher );
  682. if ( $json !== false ) {
  683. $logger = LoggerFactory::getInstance( "{$channel}-json" );
  684. $logger->log( $level, $json, [ 'private' => true ] );
  685. }
  686. Hooks::run( 'LogException', [ $e, $suppressed ] );
  687. }
  688. }