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