MWExceptionRenderer.php 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  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\DBReadOnlyError;
  22. use Wikimedia\Rdbms\DBExpectedError;
  23. /**
  24. * Class to expose exceptions to the client (API bots, users, admins using CLI scripts)
  25. * @since 1.28
  26. */
  27. class MWExceptionRenderer {
  28. const AS_RAW = 1; // show as text
  29. const AS_PRETTY = 2; // show as HTML
  30. /**
  31. * @param Exception|Throwable $e Original exception
  32. * @param int $mode MWExceptionExposer::AS_* constant
  33. * @param Exception|Throwable|null $eNew New exception from attempting to show the first
  34. */
  35. public static function output( $e, $mode, $eNew = null ) {
  36. global $wgMimeType, $wgShowExceptionDetails;
  37. if ( defined( 'MW_API' ) ) {
  38. // Unhandled API exception, we can't be sure that format printer is alive
  39. self::header( 'MediaWiki-API-Error: internal_api_error_' . get_class( $e ) );
  40. wfHttpError( 500, 'Internal Server Error', self::getText( $e ) );
  41. } elseif ( self::isCommandLine() ) {
  42. self::printError( self::getText( $e ) );
  43. } elseif ( $mode === self::AS_PRETTY ) {
  44. self::statusHeader( 500 );
  45. self::header( "Content-Type: $wgMimeType; charset=utf-8" );
  46. if ( $e instanceof DBConnectionError ) {
  47. self::reportOutageHTML( $e );
  48. } else {
  49. self::reportHTML( $e );
  50. }
  51. } else {
  52. self::statusHeader( 500 );
  53. self::header( "Content-Type: $wgMimeType; charset=utf-8" );
  54. if ( $eNew ) {
  55. $message = "MediaWiki internal error.\n\n";
  56. if ( $wgShowExceptionDetails ) {
  57. $message .= 'Original exception: ' .
  58. MWExceptionHandler::getLogMessage( $e ) .
  59. "\nBacktrace:\n" . MWExceptionHandler::getRedactedTraceAsString( $e ) .
  60. "\n\nException caught inside exception handler: " .
  61. MWExceptionHandler::getLogMessage( $eNew ) .
  62. "\nBacktrace:\n" . MWExceptionHandler::getRedactedTraceAsString( $eNew );
  63. } else {
  64. $message .= 'Original exception: ' .
  65. MWExceptionHandler::getPublicLogMessage( $e );
  66. $message .= "\n\nException caught inside exception handler.\n\n" .
  67. self::getShowBacktraceError( $e );
  68. }
  69. $message .= "\n";
  70. } elseif ( $wgShowExceptionDetails ) {
  71. $message = MWExceptionHandler::getLogMessage( $e ) .
  72. "\nBacktrace:\n" .
  73. MWExceptionHandler::getRedactedTraceAsString( $e ) . "\n";
  74. } else {
  75. $message = MWExceptionHandler::getPublicLogMessage( $e );
  76. }
  77. echo nl2br( htmlspecialchars( $message ) ) . "\n";
  78. }
  79. }
  80. /**
  81. * @param Exception|Throwable $e
  82. * @return bool Should the exception use $wgOut to output the error?
  83. */
  84. private static function useOutputPage( $e ) {
  85. // Can the extension use the Message class/wfMessage to get i18n-ed messages?
  86. foreach ( $e->getTrace() as $frame ) {
  87. if ( isset( $frame['class'] ) && $frame['class'] === LocalisationCache::class ) {
  88. return false;
  89. }
  90. }
  91. // Don't even bother with OutputPage if there's no Title context set,
  92. // (e.g. we're in RL code on load.php) - the Skin system (and probably
  93. // most of MediaWiki) won't work.
  94. return (
  95. !empty( $GLOBALS['wgFullyInitialised'] ) &&
  96. !empty( $GLOBALS['wgOut'] ) &&
  97. RequestContext::getMain()->getTitle() &&
  98. !defined( 'MEDIAWIKI_INSTALL' )
  99. );
  100. }
  101. /**
  102. * Output the exception report using HTML
  103. *
  104. * @param Exception|Throwable $e
  105. */
  106. private static function reportHTML( $e ) {
  107. global $wgOut, $wgSitename;
  108. if ( self::useOutputPage( $e ) ) {
  109. if ( $e instanceof MWException ) {
  110. $wgOut->prepareErrorPage( $e->getPageTitle() );
  111. } elseif ( $e instanceof DBReadOnlyError ) {
  112. $wgOut->prepareErrorPage( self::msg( 'readonly', 'Database is locked' ) );
  113. } elseif ( $e instanceof DBExpectedError ) {
  114. $wgOut->prepareErrorPage( self::msg( 'databaseerror', 'Database error' ) );
  115. } else {
  116. $wgOut->prepareErrorPage( self::msg( 'internalerror', 'Internal error' ) );
  117. }
  118. // Show any custom GUI message before the details
  119. if ( $e instanceof MessageSpecifier ) {
  120. $wgOut->addHTML( Html::element( 'p', [], Message::newFromSpecifier( $e )->text() ) );
  121. }
  122. $wgOut->addHTML( self::getHTML( $e ) );
  123. $wgOut->output();
  124. } else {
  125. self::header( 'Content-Type: text/html; charset=utf-8' );
  126. $pageTitle = self::msg( 'internalerror', 'Internal error' );
  127. echo "<!DOCTYPE html>\n" .
  128. '<html><head>' .
  129. // Mimick OutputPage::setPageTitle behaviour
  130. '<title>' .
  131. htmlspecialchars( self::msg( 'pagetitle', "$1 - $wgSitename", $pageTitle ) ) .
  132. '</title>' .
  133. '<style>body { font-family: sans-serif; margin: 0; padding: 0.5em 2em; }</style>' .
  134. "</head><body>\n";
  135. echo self::getHTML( $e );
  136. echo "</body></html>\n";
  137. }
  138. }
  139. /**
  140. * If $wgShowExceptionDetails is true, return a HTML message with a
  141. * backtrace to the error, otherwise show a message to ask to set it to true
  142. * to show that information.
  143. *
  144. * @param Exception|Throwable $e
  145. * @return string Html to output
  146. */
  147. public static function getHTML( $e ) {
  148. global $wgShowExceptionDetails;
  149. if ( $wgShowExceptionDetails ) {
  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. * @param mixed ...$params To pass to wfMessage()
  178. * @return string Message with arguments replaced
  179. */
  180. private static function msg( $key, $fallback, ...$params ) {
  181. global $wgSitename;
  182. // FIXME: Keep logic in sync with MWException::msg.
  183. try {
  184. $res = wfMessage( $key, ...$params )->text();
  185. } catch ( Exception $e ) {
  186. $res = wfMsgReplaceArgs( $fallback, $params );
  187. // If an exception happens inside message rendering,
  188. // {{SITENAME}} sometimes won't be replaced.
  189. $res = strtr( $res, [
  190. '{{SITENAME}}' => $wgSitename,
  191. ] );
  192. }
  193. return $res;
  194. }
  195. /**
  196. * @param Exception|Throwable $e
  197. * @return string
  198. */
  199. private static function getText( $e ) {
  200. global $wgShowExceptionDetails;
  201. if ( $wgShowExceptionDetails ) {
  202. return MWExceptionHandler::getLogMessage( $e ) .
  203. "\nBacktrace:\n" .
  204. MWExceptionHandler::getRedactedTraceAsString( $e ) . "\n";
  205. } else {
  206. return self::getShowBacktraceError( $e ) . "\n";
  207. }
  208. }
  209. /**
  210. * @param Exception|Throwable $e
  211. * @return string
  212. */
  213. private static function getShowBacktraceError( $e ) {
  214. $var = '$wgShowExceptionDetails = true;';
  215. return "Set $var at the bottom of LocalSettings.php to show detailed debugging information.";
  216. }
  217. /**
  218. * @return bool
  219. */
  220. private static function isCommandLine() {
  221. return !empty( $GLOBALS['wgCommandLineMode'] );
  222. }
  223. /**
  224. * @param string $header
  225. */
  226. private static function header( $header ) {
  227. if ( !headers_sent() ) {
  228. header( $header );
  229. }
  230. }
  231. /**
  232. * @param int $code
  233. */
  234. private static function statusHeader( $code ) {
  235. if ( !headers_sent() ) {
  236. HttpStatus::header( $code );
  237. }
  238. }
  239. /**
  240. * Print a message, if possible to STDERR.
  241. * Use this in command line mode only (see isCommandLine)
  242. *
  243. * @suppress SecurityCheck-XSS
  244. * @param string $message Failure text
  245. */
  246. private static function printError( $message ) {
  247. // NOTE: STDERR may not be available, especially if php-cgi is used from the
  248. // command line (T17602). Try to produce meaningful output anyway. Using
  249. // echo may corrupt output to STDOUT though.
  250. if ( defined( 'STDERR' ) ) {
  251. fwrite( STDERR, $message );
  252. } else {
  253. echo $message;
  254. }
  255. }
  256. /**
  257. * @param Exception|Throwable $e
  258. */
  259. private static function reportOutageHTML( $e ) {
  260. global $wgShowExceptionDetails, $wgShowHostnames, $wgSitename;
  261. $sorry = htmlspecialchars( self::msg(
  262. 'dberr-problems',
  263. 'Sorry! This site is experiencing technical difficulties.'
  264. ) );
  265. $again = htmlspecialchars( self::msg(
  266. 'dberr-again',
  267. 'Try waiting a few minutes and reloading.'
  268. ) );
  269. if ( $wgShowHostnames ) {
  270. $info = str_replace(
  271. '$1',
  272. Html::element( 'span', [ 'dir' => 'ltr' ], $e->getMessage() ),
  273. htmlspecialchars( self::msg( 'dberr-info', '($1)' ) )
  274. );
  275. } else {
  276. $info = htmlspecialchars( self::msg(
  277. 'dberr-info-hidden',
  278. '(Cannot access the database)'
  279. ) );
  280. }
  281. MessageCache::singleton()->disable(); // no DB access
  282. $html = "<!DOCTYPE html>\n" .
  283. '<html><head>' .
  284. '<title>' .
  285. htmlspecialchars( $wgSitename ) .
  286. '</title>' .
  287. '<style>body { font-family: sans-serif; margin: 0; padding: 0.5em 2em; }</style>' .
  288. "</head><body><h1>$sorry</h1><p>$again</p><p><small>$info</small></p>";
  289. if ( $wgShowExceptionDetails ) {
  290. $html .= '<p>Backtrace:</p><pre>' .
  291. htmlspecialchars( $e->getTraceAsString() ) . '</pre>';
  292. }
  293. $html .= '</body></html>';
  294. echo $html;
  295. }
  296. }