MWExceptionRenderer.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  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 Wikimedia\Rdbms\DBConnectionError;
  21. use Wikimedia\Rdbms\DBError;
  22. use Wikimedia\Rdbms\DBReadOnlyError;
  23. use Wikimedia\Rdbms\DBExpectedError;
  24. /**
  25. * Class to expose exceptions to the client (API bots, users, admins using CLI scripts)
  26. * @since 1.28
  27. */
  28. class MWExceptionRenderer {
  29. const AS_RAW = 1; // show as text
  30. const AS_PRETTY = 2; // show as HTML
  31. /**
  32. * @param Exception|Throwable $e Original exception
  33. * @param int $mode MWExceptionExposer::AS_* constant
  34. * @param Exception|Throwable|null $eNew New exception from attempting to show the first
  35. */
  36. public static function output( $e, $mode, $eNew = null ) {
  37. global $wgMimeType;
  38. if ( defined( 'MW_API' ) ) {
  39. // Unhandled API exception, we can't be sure that format printer is alive
  40. self::header( 'MediaWiki-API-Error: internal_api_error_' . get_class( $e ) );
  41. wfHttpError( 500, 'Internal Server Error', self::getText( $e ) );
  42. } elseif ( self::isCommandLine() ) {
  43. self::printError( self::getText( $e ) );
  44. } elseif ( $mode === self::AS_PRETTY ) {
  45. self::statusHeader( 500 );
  46. if ( $e instanceof DBConnectionError ) {
  47. self::reportOutageHTML( $e );
  48. } else {
  49. self::header( "Content-Type: $wgMimeType; charset=utf-8" );
  50. self::reportHTML( $e );
  51. }
  52. } else {
  53. if ( $eNew ) {
  54. $message = "MediaWiki internal error.\n\n";
  55. if ( self::showBackTrace( $e ) ) {
  56. $message .= 'Original exception: ' .
  57. MWExceptionHandler::getLogMessage( $e ) .
  58. "\nBacktrace:\n" . MWExceptionHandler::getRedactedTraceAsString( $e ) .
  59. "\n\nException caught inside exception handler: " .
  60. MWExceptionHandler::getLogMessage( $eNew ) .
  61. "\nBacktrace:\n" . MWExceptionHandler::getRedactedTraceAsString( $eNew );
  62. } else {
  63. $message .= 'Original exception: ' .
  64. MWExceptionHandler::getPublicLogMessage( $e );
  65. $message .= "\n\nException caught inside exception handler.\n\n" .
  66. self::getShowBacktraceError( $e );
  67. }
  68. $message .= "\n";
  69. } else {
  70. if ( self::showBackTrace( $e ) ) {
  71. $message = MWExceptionHandler::getLogMessage( $e ) .
  72. "\nBacktrace:\n" .
  73. MWExceptionHandler::getRedactedTraceAsString( $e ) . "\n";
  74. } else {
  75. $message = MWExceptionHandler::getPublicLogMessage( $e );
  76. }
  77. }
  78. echo nl2br( htmlspecialchars( $message ) ) . "\n";
  79. }
  80. }
  81. /**
  82. * @param Exception|Throwable $e
  83. * @return bool Should the exception use $wgOut to output the error?
  84. */
  85. private static function useOutputPage( $e ) {
  86. // Can the extension use the Message class/wfMessage to get i18n-ed messages?
  87. foreach ( $e->getTrace() as $frame ) {
  88. if ( isset( $frame['class'] ) && $frame['class'] === LocalisationCache::class ) {
  89. return false;
  90. }
  91. }
  92. // Don't even bother with OutputPage if there's no Title context set,
  93. // (e.g. we're in RL code on load.php) - the Skin system (and probably
  94. // most of MediaWiki) won't work.
  95. return (
  96. !empty( $GLOBALS['wgFullyInitialised'] ) &&
  97. !empty( $GLOBALS['wgOut'] ) &&
  98. RequestContext::getMain()->getTitle() &&
  99. !defined( 'MEDIAWIKI_INSTALL' )
  100. );
  101. }
  102. /**
  103. * Output the exception report using HTML
  104. *
  105. * @param Exception|Throwable $e
  106. */
  107. private static function reportHTML( $e ) {
  108. global $wgOut, $wgSitename;
  109. if ( self::useOutputPage( $e ) ) {
  110. if ( $e instanceof MWException ) {
  111. $wgOut->prepareErrorPage( $e->getPageTitle() );
  112. } elseif ( $e instanceof DBReadOnlyError ) {
  113. $wgOut->prepareErrorPage( self::msg( 'readonly', 'Database is locked' ) );
  114. } elseif ( $e instanceof DBExpectedError ) {
  115. $wgOut->prepareErrorPage( self::msg( 'databaseerror', 'Database error' ) );
  116. } else {
  117. $wgOut->prepareErrorPage( self::msg( 'internalerror', 'Internal error' ) );
  118. }
  119. // Show any custom GUI message before the details
  120. if ( $e instanceof MessageSpecifier ) {
  121. $wgOut->addHTML( Html::element( 'p', [], Message::newFromSpecifier( $e )->text() ) );
  122. }
  123. $wgOut->addHTML( self::getHTML( $e ) );
  124. $wgOut->output();
  125. } else {
  126. self::header( 'Content-Type: text/html; charset=utf-8' );
  127. $pageTitle = self::msg( 'internalerror', 'Internal error' );
  128. echo "<!DOCTYPE html>\n" .
  129. '<html><head>' .
  130. // Mimick OutputPage::setPageTitle behaviour
  131. '<title>' .
  132. htmlspecialchars( self::msg( 'pagetitle', "$1 - $wgSitename", $pageTitle ) ) .
  133. '</title>' .
  134. '<style>body { font-family: sans-serif; margin: 0; padding: 0.5em 2em; }</style>' .
  135. "</head><body>\n";
  136. echo self::getHTML( $e );
  137. echo "</body></html>\n";
  138. }
  139. }
  140. /**
  141. * If $wgShowExceptionDetails is true, return a HTML message with a
  142. * backtrace to the error, otherwise show a message to ask to set it to true
  143. * to show that information.
  144. *
  145. * @param Exception|Throwable $e
  146. * @return string Html to output
  147. */
  148. public static function getHTML( $e ) {
  149. if ( self::showBackTrace( $e ) ) {
  150. $html = "<div class=\"errorbox mw-content-ltr\"><p>" .
  151. nl2br( htmlspecialchars( MWExceptionHandler::getLogMessage( $e ) ) ) .
  152. '</p><p>Backtrace:</p><p>' .
  153. nl2br( htmlspecialchars( MWExceptionHandler::getRedactedTraceAsString( $e ) ) ) .
  154. "</p></div>\n";
  155. } else {
  156. $logId = WebRequest::getRequestId();
  157. $html = "<div class=\"errorbox mw-content-ltr\">" .
  158. htmlspecialchars(
  159. '[' . $logId . '] ' .
  160. gmdate( 'Y-m-d H:i:s' ) . ": " .
  161. self::msg( "internalerror-fatal-exception",
  162. "Fatal exception of type $1",
  163. get_class( $e ),
  164. $logId,
  165. MWExceptionHandler::getURL()
  166. ) ) . "</div>\n" .
  167. "<!-- " . wordwrap( self::getShowBacktraceError( $e ), 50 ) . " -->";
  168. }
  169. return $html;
  170. }
  171. /**
  172. * Get a message from i18n
  173. *
  174. * @param string $key Message name
  175. * @param string $fallback Default message if the message cache can't be
  176. * called by the exception
  177. * The function also has other parameters that are arguments for the message
  178. * @return string Message with arguments replaced
  179. */
  180. private static function msg( $key, $fallback /*[, params...] */ ) {
  181. $args = array_slice( func_get_args(), 2 );
  182. try {
  183. return wfMessage( $key, $args )->text();
  184. } catch ( Exception $e ) {
  185. return wfMsgReplaceArgs( $fallback, $args );
  186. }
  187. }
  188. /**
  189. * @param Exception|Throwable $e
  190. * @return string
  191. */
  192. private static function getText( $e ) {
  193. if ( self::showBackTrace( $e ) ) {
  194. return MWExceptionHandler::getLogMessage( $e ) .
  195. "\nBacktrace:\n" .
  196. MWExceptionHandler::getRedactedTraceAsString( $e ) . "\n";
  197. } else {
  198. return self::getShowBacktraceError( $e ) . "\n";
  199. }
  200. }
  201. /**
  202. * @param Exception|Throwable $e
  203. * @return bool
  204. */
  205. private static function showBackTrace( $e ) {
  206. global $wgShowExceptionDetails, $wgShowDBErrorBacktrace;
  207. return (
  208. $wgShowExceptionDetails &&
  209. ( !( $e instanceof DBError ) || $wgShowDBErrorBacktrace )
  210. );
  211. }
  212. /**
  213. * @param Exception|Throwable $e
  214. * @return string
  215. */
  216. private static function getShowBacktraceError( $e ) {
  217. global $wgShowExceptionDetails, $wgShowDBErrorBacktrace;
  218. $vars = [];
  219. if ( !$wgShowExceptionDetails ) {
  220. $vars[] = '$wgShowExceptionDetails = true;';
  221. }
  222. if ( $e instanceof DBError && !$wgShowDBErrorBacktrace ) {
  223. $vars[] = '$wgShowDBErrorBacktrace = true;';
  224. }
  225. $vars = implode( ' and ', $vars );
  226. return "Set $vars at the bottom of LocalSettings.php to show detailed debugging information.";
  227. }
  228. /**
  229. * @return bool
  230. */
  231. private static function isCommandLine() {
  232. return !empty( $GLOBALS['wgCommandLineMode'] );
  233. }
  234. /**
  235. * @param string $header
  236. */
  237. private static function header( $header ) {
  238. if ( !headers_sent() ) {
  239. header( $header );
  240. }
  241. }
  242. /**
  243. * @param int $code
  244. */
  245. private static function statusHeader( $code ) {
  246. if ( !headers_sent() ) {
  247. HttpStatus::header( $code );
  248. }
  249. }
  250. /**
  251. * Print a message, if possible to STDERR.
  252. * Use this in command line mode only (see isCommandLine)
  253. *
  254. * @param string $message Failure text
  255. */
  256. private static function printError( $message ) {
  257. // NOTE: STDERR may not be available, especially if php-cgi is used from the
  258. // command line (bug #15602). Try to produce meaningful output anyway. Using
  259. // echo may corrupt output to STDOUT though.
  260. if ( defined( 'STDERR' ) ) {
  261. fwrite( STDERR, $message );
  262. } else {
  263. echo $message;
  264. }
  265. }
  266. /**
  267. * @param Exception|Throwable $e
  268. */
  269. private static function reportOutageHTML( $e ) {
  270. global $wgShowDBErrorBacktrace, $wgShowHostnames, $wgShowSQLErrors;
  271. $sorry = htmlspecialchars( self::msg(
  272. 'dberr-problems',
  273. 'Sorry! This site is experiencing technical difficulties.'
  274. ) );
  275. $again = htmlspecialchars( self::msg(
  276. 'dberr-again',
  277. 'Try waiting a few minutes and reloading.'
  278. ) );
  279. if ( $wgShowHostnames || $wgShowSQLErrors ) {
  280. $info = str_replace(
  281. '$1',
  282. Html::element( 'span', [ 'dir' => 'ltr' ], $e->getMessage() ),
  283. htmlspecialchars( self::msg( 'dberr-info', '($1)' ) )
  284. );
  285. } else {
  286. $info = htmlspecialchars( self::msg(
  287. 'dberr-info-hidden',
  288. '(Cannot access the database)'
  289. ) );
  290. }
  291. MessageCache::singleton()->disable(); // no DB access
  292. $html = "<h1>$sorry</h1><p>$again</p><p><small>$info</small></p>";
  293. if ( $wgShowDBErrorBacktrace ) {
  294. $html .= '<p>Backtrace:</p><pre>' .
  295. htmlspecialchars( $e->getTraceAsString() ) . '</pre>';
  296. }
  297. $html .= '<hr />';
  298. $html .= self::googleSearchForm();
  299. echo $html;
  300. }
  301. /**
  302. * @return string
  303. */
  304. private static function googleSearchForm() {
  305. global $wgSitename, $wgCanonicalServer, $wgRequest;
  306. $usegoogle = htmlspecialchars( self::msg(
  307. 'dberr-usegoogle',
  308. 'You can try searching via Google in the meantime.'
  309. ) );
  310. $outofdate = htmlspecialchars( self::msg(
  311. 'dberr-outofdate',
  312. 'Note that their indexes of our content may be out of date.'
  313. ) );
  314. $googlesearch = htmlspecialchars( self::msg( 'searchbutton', 'Search' ) );
  315. $search = htmlspecialchars( $wgRequest->getVal( 'search' ) );
  316. $server = htmlspecialchars( $wgCanonicalServer );
  317. $sitename = htmlspecialchars( $wgSitename );
  318. $trygoogle = <<<EOT
  319. <div style="margin: 1.5em">$usegoogle<br />
  320. <small>$outofdate</small>
  321. </div>
  322. <form method="get" action="//www.google.com/search" id="googlesearch">
  323. <input type="hidden" name="domains" value="$server" />
  324. <input type="hidden" name="num" value="50" />
  325. <input type="hidden" name="ie" value="UTF-8" />
  326. <input type="hidden" name="oe" value="UTF-8" />
  327. <input type="text" name="q" size="31" maxlength="255" value="$search" />
  328. <input type="submit" name="btnG" value="$googlesearch" />
  329. <p>
  330. <label><input type="radio" name="sitesearch" value="$server" checked="checked" />$sitename</label>
  331. <label><input type="radio" name="sitesearch" value="" />WWW</label>
  332. </p>
  333. </form>
  334. EOT;
  335. return $trygoogle;
  336. }
  337. }