From: Aaron Schulz Date: Wed, 14 Sep 2016 03:57:54 +0000 (-0700) Subject: Add MWExceptionRenderer class and decouple DBError X-Git-Tag: 1.31.0-rc.0~5581^2 X-Git-Url: http://git.cyclocoop.org/%7B%24admin_url%7Dmes_infos.php?a=commitdiff_plain;h=00bee029718f3215396e984d04b9450bc3872503;p=lhc%2Fweb%2Fwiklou.git Add MWExceptionRenderer class and decouple DBError * This handles the work of showing exceptions so that MWException does not have too. * Simplify the DBError classes to regular Exception classes. Lots of pointless prettification has been removed, but DBConnectionError still gets the usual special treatment of a fallback page and Google form. * Remove hacky file cache fallback code that probably did not work. * Make MWExceptionHandler::report() wrap MWExceptionExposer::output(). * Make MWException::runHooks() wrap MWExceptionExposer::runHooks(). Change-Id: I5dfdc84e94ddac65417226cf7c84513ebb9f9faa --- diff --git a/autoload.php b/autoload.php index a71d9432e7..5141c35c88 100644 --- a/autoload.php +++ b/autoload.php @@ -767,6 +767,7 @@ $wgAutoloadLocalClasses = [ 'MWDocGen' => __DIR__ . '/maintenance/mwdocgen.php', 'MWException' => __DIR__ . '/includes/exception/MWException.php', 'MWExceptionHandler' => __DIR__ . '/includes/exception/MWExceptionHandler.php', + 'MWExceptionRenderer' => __DIR__ . '/includes/exception/MWExceptionRenderer.php', 'MWGrants' => __DIR__ . '/includes/utils/MWGrants.php', 'MWHttpRequest' => __DIR__ . '/includes/HttpFunctions.php', 'MWMemcached' => __DIR__ . '/includes/compat/MemcachedClientCompat.php', diff --git a/includes/db/DatabaseError.php b/includes/db/DatabaseError.php index cfae74fdfd..2242c5af53 100644 --- a/includes/db/DatabaseError.php +++ b/includes/db/DatabaseError.php @@ -25,16 +25,16 @@ * Database error base class * @ingroup Database */ -class DBError extends MWException { - /** @var DatabaseBase */ +class DBError extends Exception { + /** @var IDatabase */ public $db; /** * Construct a database error - * @param DatabaseBase $db Object which threw the error + * @param IDatabase $db Object which threw the error * @param string $error A simple error message to be used for debugging */ - function __construct( DatabaseBase $db = null, $error ) { + function __construct( IDatabase $db = null, $error ) { $this->db = $db; parent::__construct( $error ); } @@ -48,274 +48,23 @@ class DBError extends MWException { * @since 1.23 */ class DBExpectedError extends DBError { - /** - * @return string - */ - function getText() { - global $wgShowDBErrorBacktrace; - - $s = $this->getTextContent() . "\n"; - - if ( $wgShowDBErrorBacktrace ) { - $s .= "Backtrace:\n" . $this->getTraceAsString() . "\n"; - } - - return $s; - } - - /** - * @return string - */ - function getHTML() { - global $wgShowDBErrorBacktrace; - - $s = $this->getHTMLContent(); - - if ( $wgShowDBErrorBacktrace ) { - $s .= '

Backtrace:

' . htmlspecialchars( $this->getTraceAsString() ) . '
'; - } - - return $s; - } - - function getPageTitle() { - return $this->msg( 'databaseerror', 'Database error' ); - } - - /** - * @return string - */ - protected function getTextContent() { - return $this->getMessage(); - } - - /** - * @return string - */ - protected function getHTMLContent() { - return '

' . nl2br( htmlspecialchars( $this->getTextContent() ) ) . '

'; - } } /** * @ingroup Database */ class DBConnectionError extends DBExpectedError { - /** @var string Error text */ - public $error; - /** - * @param DatabaseBase $db Object throwing the error + * @param IDatabase $db Object throwing the error * @param string $error Error text */ - function __construct( DatabaseBase $db = null, $error = 'unknown error' ) { - $msg = 'DB connection error'; - + function __construct( IDatabase $db = null, $error = 'unknown error' ) { + $msg = 'Cannot access the database'; if ( trim( $error ) != '' ) { $msg .= ": $error"; - } elseif ( $db ) { - $error = $this->db->getServer(); } parent::__construct( $db, $msg ); - $this->error = $error; - } - - /** - * @return bool - */ - function useOutputPage() { - // Not likely to work - return false; - } - - /** - * @param string $key - * @param string $fallback Unescaped alternative error text in case the - * message cache cannot be used. Can contain parameters as in regular - * messages, that should be passed as additional parameters. - * @return string Unprocessed plain error text with parameters replaced - */ - function msg( $key, $fallback /*[, params...] */ ) { - $args = array_slice( func_get_args(), 2 ); - - if ( $this->useMessageCache() ) { - return wfMessage( $key, $args )->useDatabase( false )->text(); - } else { - return wfMsgReplaceArgs( $fallback, $args ); - } - } - - /** - * @return bool - */ - function isLoggable() { - // Don't send to the exception log, already in dberror log - return false; - } - - /** - * @return string Safe HTML - */ - function getHTML() { - global $wgShowDBErrorBacktrace, $wgShowHostnames, $wgShowSQLErrors; - - $sorry = htmlspecialchars( $this->msg( - 'dberr-problems', - 'Sorry! This site is experiencing technical difficulties.' - ) ); - $again = htmlspecialchars( $this->msg( - 'dberr-again', - 'Try waiting a few minutes and reloading.' - ) ); - - if ( $wgShowHostnames || $wgShowSQLErrors ) { - $info = str_replace( - '$1', Html::element( 'span', [ 'dir' => 'ltr' ], $this->error ), - htmlspecialchars( $this->msg( 'dberr-info', '(Cannot access the database: $1)' ) ) - ); - } else { - $info = htmlspecialchars( $this->msg( - 'dberr-info-hidden', - '(Cannot access the database)' - ) ); - } - - # No database access - MessageCache::singleton()->disable(); - - $html = "

$sorry

$again

$info

"; - - if ( $wgShowDBErrorBacktrace ) { - $html .= '

Backtrace:

' . htmlspecialchars( $this->getTraceAsString() ) . '
'; - } - - $html .= '
'; - $html .= $this->searchForm(); - - return $html; - } - - protected function getTextContent() { - global $wgShowHostnames, $wgShowSQLErrors; - - if ( $wgShowHostnames || $wgShowSQLErrors ) { - return $this->getMessage(); - } else { - return 'DB connection error'; - } - } - - /** - * Output the exception report using HTML. - * - * @return void - */ - public function reportHTML() { - global $wgUseFileCache; - - // Check whether we can serve a file-cached copy of the page with the error underneath - if ( $wgUseFileCache ) { - try { - $cache = $this->fileCachedPage(); - // Cached version on file system? - if ( $cache !== null ) { - // Hack: extend the body for error messages - $cache = str_replace( [ '', '' ], '', $cache ); - // Add cache notice... - $cache .= '
' . - htmlspecialchars( $this->msg( 'dberr-cachederror', - 'This is a cached copy of the requested page, and may not be up to date.' ) ) . - '
'; - - // Output cached page with notices on bottom and re-close body - echo "{$cache}
{$this->getHTML()}"; - - return; - } - } catch ( Exception $e ) { - // Do nothing, just use the default page - } - } - - // We can't, cough and die in the usual fashion - parent::reportHTML(); - } - - /** - * @return string - */ - function searchForm() { - global $wgSitename, $wgCanonicalServer, $wgRequest; - - $usegoogle = htmlspecialchars( $this->msg( - 'dberr-usegoogle', - 'You can try searching via Google in the meantime.' - ) ); - $outofdate = htmlspecialchars( $this->msg( - 'dberr-outofdate', - 'Note that their indexes of our content may be out of date.' - ) ); - $googlesearch = htmlspecialchars( $this->msg( 'searchbutton', 'Search' ) ); - - $search = htmlspecialchars( $wgRequest->getVal( 'search' ) ); - - $server = htmlspecialchars( $wgCanonicalServer ); - $sitename = htmlspecialchars( $wgSitename ); - - $trygoogle = <<$usegoogle
-$outofdate - -
- - - - - - - -

- - -

-
-EOT; - - return $trygoogle; - } - - /** - * @return string - */ - private function fileCachedPage() { - $context = RequestContext::getMain(); - - if ( $context->getOutput()->isDisabled() ) { - // Done already? - return ''; - } - - if ( $context->getTitle() ) { - // Use the main context's title if we managed to set it - $t = $context->getTitle()->getPrefixedDBkey(); - } else { - // Fallback to the raw title URL param. We can't use the Title - // class is it may hit the interwiki table and give a DB error. - // We may get a cache miss due to not sanitizing the title though. - $t = str_replace( ' ', '_', $context->getRequest()->getVal( 'title' ) ); - if ( $t == '' ) { // fallback to main page - $t = Title::newFromText( - $this->msg( 'mainpage', 'Main Page' ) )->getPrefixedDBkey(); - } - } - - $cache = new HTMLFileCache( $t, 'view' ); - if ( $cache->isCached() ) { - return $cache->fetchText(); - } else { - return ''; - } } } @@ -323,17 +72,24 @@ EOT; * @ingroup Database */ class DBQueryError extends DBExpectedError { - public $error, $errno, $sql, $fname; + /** @var string */ + public $error; + /** @var integer */ + public $errno; + /** @var string */ + public $sql; + /** @var string */ + public $fname; /** - * @param DatabaseBase $db + * @param IDatabase $db * @param string $error * @param int|string $errno * @param string $sql * @param string $fname */ - function __construct( DatabaseBase $db, $error, $errno, $sql, $fname ) { - if ( $db->wasConnectionError( $errno ) ) { + function __construct( IDatabase $db, $error, $errno, $sql, $fname ) { + if ( $db instanceof DatabaseBase && $db->wasConnectionError( $errno ) ) { $message = "A connection error occured. \n" . "Query: $sql\n" . "Function: $fname\n" . @@ -353,122 +109,31 @@ class DBQueryError extends DBExpectedError { $this->sql = $sql; $this->fname = $fname; } +} - /** - * @return string - */ - function getPageTitle() { - return $this->msg( 'databaseerror', 'Database error' ); - } - - /** - * @return string - */ - protected function getHTMLContent() { - $key = 'databaseerror-text'; - $s = Html::element( 'p', [], $this->msg( $key, $this->getFallbackMessage( $key ) ) ); - - $details = $this->getTechnicalDetails(); - if ( $details ) { - $s .= ''; - } - - return $s; - } - - /** - * @return string - */ - protected function getTextContent() { - $key = 'databaseerror-textcl'; - $s = $this->msg( $key, $this->getFallbackMessage( $key ) ) . "\n"; - - foreach ( $this->getTechnicalDetails() as $key => $detail ) { - $s .= $this->msg( $key, $this->getFallbackMessage( $key ), $detail[2] ) . "\n"; - } - - return $s; - } - - /** - * Make a list of technical details that can be shown to the user. This information can - * aid in debugging yet may be useful to an attacker trying to exploit a security weakness - * in the software or server configuration. - * - * Thus no such details are shown by default, though if $wgShowHostnames is true, only the - * full SQL query is hidden; in fact, the error message often does contain a hostname, and - * sites using this option probably don't care much about "security by obscurity". Of course, - * if $wgShowSQLErrors is true, the SQL query *is* shown. - * - * @return array Keys are message keys; values are arrays of arguments for Html::element(). - * Array will be empty if users are not allowed to see any of these details at all. - */ - protected function getTechnicalDetails() { - global $wgShowHostnames, $wgShowSQLErrors; - - $attribs = [ 'dir' => 'ltr' ]; - $details = []; - - if ( $wgShowSQLErrors ) { - $details['databaseerror-query'] = [ - 'div', [ 'class' => 'mw-code' ] + $attribs, $this->sql ]; - } - - if ( $wgShowHostnames || $wgShowSQLErrors ) { - $errorMessage = $this->errno . ' ' . $this->error; - $details['databaseerror-function'] = [ 'code', $attribs, $this->fname ]; - $details['databaseerror-error'] = [ 'samp', $attribs, $errorMessage ]; - } - - return $details; - } - - /** - * @param string $key Message key - * @return string English message text - */ - private function getFallbackMessage( $key ) { - $messages = [ - 'databaseerror-text' => 'A database query error has occurred. -This may indicate a bug in the software.', - 'databaseerror-textcl' => 'A database query error has occurred.', - 'databaseerror-query' => 'Query: $1', - 'databaseerror-function' => 'Function: $1', - 'databaseerror-error' => 'Error: $1', - ]; - - return $messages[$key]; - } +/** + * @ingroup Database + */ +class DBReadOnlyError extends DBExpectedError { } /** * @ingroup Database */ -class DBUnexpectedError extends DBError { +class DBTransactionError extends DBExpectedError { } /** + * Exception class for replica DB wait timeouts * @ingroup Database */ -class DBReadOnlyError extends DBExpectedError { - function getPageTitle() { - return $this->msg( 'readonly', 'Database is locked' ); - } +class DBReplicationWaitError extends DBExpectedError { } /** * @ingroup Database */ -class DBTransactionError extends DBExpectedError { +class DBUnexpectedError extends DBError { } /** @@ -482,9 +147,3 @@ class DBAccessError extends DBUnexpectedError { } } -/** - * Exception class for replica DB wait timeouts - * @ingroup Database - */ -class DBReplicationWaitError extends DBUnexpectedError { -} diff --git a/includes/db/IDatabase.php b/includes/db/IDatabase.php index f312357fe3..e2d743617e 100644 --- a/includes/db/IDatabase.php +++ b/includes/db/IDatabase.php @@ -28,7 +28,6 @@ /** * Basic database interface for live and lazy-loaded DB handles * - * @todo: loosen up DB classes from MWException * @note: IDatabase and DBConnRef should be updated to reflect any changes * @ingroup Database */ diff --git a/includes/db/loadbalancer/ILoadBalancer.php b/includes/db/loadbalancer/ILoadBalancer.php index 9313ccd37c..94e0f2be53 100644 --- a/includes/db/loadbalancer/ILoadBalancer.php +++ b/includes/db/loadbalancer/ILoadBalancer.php @@ -25,7 +25,6 @@ /** * Interface for database load balancing object that manages IDatabase handles * - * @todo: loosen up DB classes from MWException * @since 1.28 * @ingroup Database */ diff --git a/includes/exception/MWException.php b/includes/exception/MWException.php index 0a174fe471..5496cb63d0 100644 --- a/includes/exception/MWException.php +++ b/includes/exception/MWException.php @@ -71,37 +71,7 @@ class MWException extends Exception { * @return string|null String to output or null if any hook has been called */ public function runHooks( $name, $args = [] ) { - global $wgExceptionHooks; - - if ( !isset( $wgExceptionHooks ) || !is_array( $wgExceptionHooks ) ) { - return null; // Just silently ignore - } - - if ( !array_key_exists( $name, $wgExceptionHooks ) || - !is_array( $wgExceptionHooks[$name] ) - ) { - return null; - } - - $hooks = $wgExceptionHooks[$name]; - $callargs = array_merge( [ $this ], $args ); - - foreach ( $hooks as $hook ) { - if ( - is_string( $hook ) || - ( is_array( $hook ) && count( $hook ) >= 2 && is_string( $hook[0] ) ) - ) { - // 'function' or [ 'class', 'hook' ] - $result = call_user_func_array( $hook, $callargs ); - } else { - $result = null; - } - - if ( is_string( $result ) ) { - return $result; - } - } - return null; + return MWExceptionRenderer::runHooks( $this, $name, $args ); } /** @@ -229,20 +199,7 @@ class MWException extends Exception { * It will be either HTML or plain text based on isCommandLine(). */ public function report() { - global $wgMimeType; - - if ( defined( 'MW_API' ) ) { - // Unhandled API exception, we can't be sure that format printer is alive - self::header( 'MediaWiki-API-Error: internal_api_error_' . get_class( $this ) ); - wfHttpError( 500, 'Internal Server Error', $this->getText() ); - } elseif ( self::isCommandLine() ) { - MWExceptionHandler::printError( $this->getText() ); - } else { - self::statusHeader( 500 ); - self::header( "Content-Type: $wgMimeType; charset=utf-8" ); - - $this->reportHTML(); - } + MWExceptionRenderer::output( $this, MWExceptionRenderer::AS_PRETTY ); } /** diff --git a/includes/exception/MWExceptionHandler.php b/includes/exception/MWExceptionHandler.php index 9c83d3c365..8359846a42 100644 --- a/includes/exception/MWExceptionHandler.php +++ b/includes/exception/MWExceptionHandler.php @@ -60,71 +60,14 @@ class MWExceptionHandler { * @param Exception|Throwable $e */ protected static function report( $e ) { - global $wgShowExceptionDetails; - - $cmdLine = MWException::isCommandLine(); - - if ( $e instanceof MWException ) { - try { - // Try and show the exception prettily, with the normal skin infrastructure - $e->report(); - } catch ( Exception $e2 ) { - // Exception occurred from within exception handler - // Show a simpler message for the original exception, - // don't try to invoke report() - $message = "MediaWiki internal error.\n\n"; - - if ( $wgShowExceptionDetails ) { - $message .= 'Original exception: ' . self::getLogMessage( $e ) . - "\nBacktrace:\n" . self::getRedactedTraceAsString( $e ) . - "\n\nException caught inside exception handler: " . self::getLogMessage( $e2 ) . - "\nBacktrace:\n" . self::getRedactedTraceAsString( $e2 ); - } else { - $message .= "Exception caught inside exception handler.\n\n" . - "Set \$wgShowExceptionDetails = true; at the bottom of LocalSettings.php " . - "to show detailed debugging information."; - } - - $message .= "\n"; - - if ( $cmdLine ) { - self::printError( $message ); - } else { - echo nl2br( htmlspecialchars( $message ) ) . "\n"; - } - } - } else { - if ( !$wgShowExceptionDetails ) { - $message = self::getPublicLogMessage( $e ); - } else { - $message = self::getLogMessage( $e ) . - "\nBacktrace:\n" . - self::getRedactedTraceAsString( $e ) . "\n"; - } - - if ( $cmdLine ) { - self::printError( $message ); - } else { - echo nl2br( htmlspecialchars( $message ) ) . "\n"; - } - - } - } - - /** - * Print a message, if possible to STDERR. - * Use this in command line mode only (see isCommandLine) - * - * @param string $message Failure text - */ - public static function printError( $message ) { - # NOTE: STDERR may not be available, especially if php-cgi is used from the - # command line (bug #15602). Try to produce meaningful output anyway. Using - # echo may corrupt output to STDOUT though. - if ( defined( 'STDERR' ) ) { - fwrite( STDERR, $message ); - } else { - echo $message; + try { + // Try and show the exception prettily, with the normal skin infrastructure + MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_PRETTY ); + } catch ( Exception $e2 ) { + // Exception occurred from within exception handler + // Show a simpler message for the original exception, + // don't try to invoke report() + MWExceptionRenderer::output( $e, MWExceptionRenderer::AS_PRETTY, $e2 ); } } diff --git a/includes/exception/MWExceptionRenderer.php b/includes/exception/MWExceptionRenderer.php new file mode 100644 index 0000000000..f455191c48 --- /dev/null +++ b/includes/exception/MWExceptionRenderer.php @@ -0,0 +1,402 @@ += 2 && is_string( $hook[0] ) ) + ) { + // 'function' or [ 'class', 'hook' ] + $result = call_user_func_array( $hook, $callargs ); + } else { + $result = null; + } + + if ( is_string( $result ) ) { + return $result; + } + } + + return null; + } + + /** + * @param Exception $e + * @return bool Should the exception use $wgOut to output the error? + */ + private static function useOutputPage( Exception $e ) { + // Can the extension use the Message class/wfMessage to get i18n-ed messages? + $useMessageCache = ( $GLOBALS['wgLang'] instanceof Language ); + foreach ( $e->getTrace() as $frame ) { + if ( isset( $frame['class'] ) && $frame['class'] === 'LocalisationCache' ) { + $useMessageCache = false; + } + } + + return ( + $useMessageCache && + !empty( $GLOBALS['wgFullyInitialised'] ) && + !empty( $GLOBALS['wgOut'] ) && + !defined( 'MEDIAWIKI_INSTALL' ) + ); + } + + /** + * Output the exception report using HTML + * + * @param Exception $e + */ + private static function reportHTML( Exception $e ) { + global $wgOut, $wgSitename; + + if ( self::useOutputPage( $e ) ) { + if ( $e instanceof MWException ) { + $wgOut->prepareErrorPage( $e->getPageTitle() ); + } elseif ( $e instanceof DBReadOnlyError ) { + $wgOut->prepareErrorPage( self::msg( 'readonly', 'Database is locked' ) ); + } elseif ( $e instanceof DBExpectedError ) { + $wgOut->prepareErrorPage( self::msg( 'databaseerror', 'Database error' ) ); + } else { + $wgOut->prepareErrorPage( self::msg( 'internalerror', 'Internal error' ) ); + } + + $hookResult = self::runHooks( $e, get_class( $e ) ); + if ( $hookResult ) { + $wgOut->addHTML( $hookResult ); + } else { + $wgOut->addHTML( self::getHTML( $e ) ); + } + + $wgOut->output(); + } else { + self::header( 'Content-Type: text/html; charset=utf-8' ); + $pageTitle = self::msg( 'internalerror', 'Internal error' ); + echo "\n" . + '' . + // Mimick OutputPage::setPageTitle behaviour + '' . + htmlspecialchars( self::msg( 'pagetitle', "$1 - $wgSitename", $pageTitle ) ) . + '' . + '' . + "\n"; + + $hookResult = self::runHooks( $e, get_class( $e ) . 'Raw' ); + if ( $hookResult ) { + echo $hookResult; + } else { + echo self::getHTML( $e ); + } + + echo "\n"; + } + } + + /** + * If $wgShowExceptionDetails is true, return a HTML message with a + * backtrace to the error, otherwise show a message to ask to set it to true + * to show that information. + * + * @param Exception $e + * @return string Html to output + */ + private static function getHTML( Exception $e ) { + if ( self::showBackTrace( $e ) ) { + return '

' . + nl2br( htmlspecialchars( MWExceptionHandler::getLogMessage( $e ) ) ) . + '

Backtrace:

' . + nl2br( htmlspecialchars( MWExceptionHandler::getRedactedTraceAsString( $e ) ) ) . + "

\n"; + } else { + $logId = WebRequest::getRequestId(); + return "
" . + '[' . $logId . '] ' . + gmdate( 'Y-m-d H:i:s' ) . ": " . + self::msg( "internalerror-fatal-exception", + "Fatal exception of type $1", + get_class( $e ), + $logId, + MWExceptionHandler::getURL() + ) . "
\n" . + ""; + } + } + + /** + * Get a message from i18n + * + * @param string $key Message name + * @param string $fallback Default message if the message cache can't be + * called by the exception + * The function also has other parameters that are arguments for the message + * @return string Message with arguments replaced + */ + private static function msg( $key, $fallback /*[, params...] */ ) { + $args = array_slice( func_get_args(), 2 ); + try { + return wfMessage( $key, $args )->text(); + } catch ( Exception $e ) { + return wfMsgReplaceArgs( $fallback, $args ); + } + } + + /** + * @param Exception $e + * @return string + */ + private function getText( Exception $e ) { + if ( self::showBackTrace( $e ) ) { + return MWExceptionHandler::getLogMessage( $e ) . + "\nBacktrace:\n" . + MWExceptionHandler::getRedactedTraceAsString( $e ) . "\n"; + } else { + return "Set \$wgShowExceptionDetails = true; " . + "in LocalSettings.php to show detailed debugging information.\n"; + } + } + + /** + * @param Exception $e + * @return bool + */ + private static function showBackTrace( Exception $e ) { + global $wgShowExceptionDetails, $wgShowDBErrorBacktrace; + + return ( + $wgShowExceptionDetails && + ( !( $e instanceof DBError ) || $wgShowDBErrorBacktrace ) + ); + } + + /** + * @return bool + */ + private static function isCommandLine() { + return !empty( $GLOBALS['wgCommandLineMode'] ); + } + + /** + * @param string $header + */ + private static function header( $header ) { + if ( !headers_sent() ) { + header( $header ); + } + } + + /** + * @param integer $code + */ + private static function statusHeader( $code ) { + if ( !headers_sent() ) { + HttpStatus::header( $code ); + } + } + + /** + * Print a message, if possible to STDERR. + * Use this in command line mode only (see isCommandLine) + * + * @param string $message Failure text + */ + private static function printError( $message ) { + // NOTE: STDERR may not be available, especially if php-cgi is used from the + // command line (bug #15602). Try to produce meaningful output anyway. Using + // echo may corrupt output to STDOUT though. + if ( defined( 'STDERR' ) ) { + fwrite( STDERR, $message ); + } else { + echo $message; + } + } + + /** + * @param Exception $e + */ + private static function reportOutageHTML( Exception $e ) { + global $wgShowDBErrorBacktrace, $wgShowHostnames, $wgShowSQLErrors; + + $sorry = htmlspecialchars( self::msg( + 'dberr-problems', + 'Sorry! This site is experiencing technical difficulties.' + ) ); + $again = htmlspecialchars( self::msg( + 'dberr-again', + 'Try waiting a few minutes and reloading.' + ) ); + + if ( $wgShowHostnames || $wgShowSQLErrors ) { + $info = str_replace( + '$1', + Html::element( 'span', [ 'dir' => 'ltr' ], htmlspecialchars( $e->getMessage() ) ), + htmlspecialchars( self::msg( 'dberr-info', '($1)' ) ) + ); + } else { + $info = htmlspecialchars( self::msg( + 'dberr-info-hidden', + '(Cannot access the database)' + ) ); + } + + MessageCache::singleton()->disable(); // no DB access + + $html = "

$sorry

$again

$info

"; + + if ( $wgShowDBErrorBacktrace ) { + $html .= '

Backtrace:

' .
+				htmlspecialchars( $e->getTraceAsString() ) . '
'; + } + + $html .= '
'; + $html .= self::googleSearchForm(); + + echo $html; + } + + /** + * @return string + */ + private static function googleSearchForm() { + global $wgSitename, $wgCanonicalServer, $wgRequest; + + $usegoogle = htmlspecialchars( self::msg( + 'dberr-usegoogle', + 'You can try searching via Google in the meantime.' + ) ); + $outofdate = htmlspecialchars( self::msg( + 'dberr-outofdate', + 'Note that their indexes of our content may be out of date.' + ) ); + $googlesearch = htmlspecialchars( self::msg( 'searchbutton', 'Search' ) ); + $search = htmlspecialchars( $wgRequest->getVal( 'search' ) ); + $server = htmlspecialchars( $wgCanonicalServer ); + $sitename = htmlspecialchars( $wgSitename ); + $trygoogle = <<$usegoogle
+$outofdate + +
+ + + + + + +

+ + +

+
+EOT; + return $trygoogle; + } +}