Add MWExceptionRenderer class and decouple DBError
authorAaron Schulz <aschulz@wikimedia.org>
Wed, 14 Sep 2016 03:57:54 +0000 (20:57 -0700)
committerAaron Schulz <aschulz@wikimedia.org>
Wed, 14 Sep 2016 18:53:55 +0000 (11:53 -0700)
* 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

autoload.php
includes/db/DatabaseError.php
includes/db/IDatabase.php
includes/db/loadbalancer/ILoadBalancer.php
includes/exception/MWException.php
includes/exception/MWExceptionHandler.php
includes/exception/MWExceptionRenderer.php [new file with mode: 0644]

index a71d943..5141c35 100644 (file)
@@ -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',
index cfae74f..2242c5a 100644 (file)
  * 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 .= '<p>Backtrace:</p><pre>' . htmlspecialchars( $this->getTraceAsString() ) . '</pre>';
-               }
-
-               return $s;
-       }
-
-       function getPageTitle() {
-               return $this->msg( 'databaseerror', 'Database error' );
-       }
-
-       /**
-        * @return string
-        */
-       protected function getTextContent() {
-               return $this->getMessage();
-       }
-
-       /**
-        * @return string
-        */
-       protected function getHTMLContent() {
-               return '<p>' . nl2br( htmlspecialchars( $this->getTextContent() ) ) . '</p>';
-       }
 }
 
 /**
  * @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 = "<h1>$sorry</h1><p>$again</p><p><small>$info</small></p>";
-
-               if ( $wgShowDBErrorBacktrace ) {
-                       $html .= '<p>Backtrace:</p><pre>' . htmlspecialchars( $this->getTraceAsString() ) . '</pre>';
-               }
-
-               $html .= '<hr />';
-               $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( [ '</html>', '</body>' ], '', $cache );
-                                       // Add cache notice...
-                                       $cache .= '<div style="border:1px solid #ffd0d0;padding:1em;">' .
-                                               htmlspecialchars( $this->msg( 'dberr-cachederror',
-                                                       'This is a cached copy of the requested page, and may not be up to date.' ) ) .
-                                               '</div>';
-
-                                       // Output cached page with notices on bottom and re-close body
-                                       echo "{$cache}<hr />{$this->getHTML()}</body></html>";
-
-                                       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 = <<<EOT
-<div style="margin: 1.5em">$usegoogle<br />
-<small>$outofdate</small>
-</div>
-<form method="get" action="//www.google.com/search" id="googlesearch">
-       <input type="hidden" name="domains" value="$server" />
-       <input type="hidden" name="num" value="50" />
-       <input type="hidden" name="ie" value="UTF-8" />
-       <input type="hidden" name="oe" value="UTF-8" />
-
-       <input type="text" name="q" size="31" maxlength="255" value="$search" />
-       <input type="submit" name="btnG" value="$googlesearch" />
-       <p>
-               <label><input type="radio" name="sitesearch" value="$server" checked="checked" />$sitename</label>
-               <label><input type="radio" name="sitesearch" value="" />WWW</label>
-       </p>
-</form>
-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 .= '<ul>';
-                       foreach ( $details as $key => $detail ) {
-                               $s .= str_replace(
-                                       '$1', call_user_func_array( 'Html::element', $detail ),
-                                       Html::element( 'li', [],
-                                               $this->msg( $key, $this->getFallbackMessage( $key ) )
-                                       )
-                               );
-                       }
-                       $s .= '</ul>';
-               }
-
-               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 {
-}
index f312357..e2d7436 100644 (file)
@@ -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
  */
index 9313ccd..94e0f2b 100644 (file)
@@ -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
  */
index 0a174fe..5496cb6 100644 (file)
@@ -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 );
        }
 
        /**
index 9c83d3c..8359846 100644 (file)
@@ -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 (file)
index 0000000..f455191
--- /dev/null
@@ -0,0 +1,402 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Aaron Schulz
+ */
+
+/**
+ * Class to expose exceptions to the client (API bots, users, admins using CLI scripts)
+ * @since 1.28
+ */
+class MWExceptionRenderer {
+       const AS_RAW = 1; // show as text
+       const AS_PRETTY = 2; // show as HTML
+
+       /**
+        * @param Exception $e Original exception
+        * @param integer $mode MWExceptionExposer::AS_* constant
+        * @param Exception|null $eNew New exception from attempting to show the first
+        */
+       public static function output( Exception $e, $mode, Exception $eNew = null ) {
+               global $wgMimeType;
+
+               if ( $e instanceof DBConnectionError ) {
+                       self::reportOutageHTML( $e );
+                       return;
+               }
+
+               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( $e ) );
+                       wfHttpError( 500, 'Internal Server Error', self::getText( $e ) );
+               } elseif ( self::isCommandLine() ) {
+                       self::printError( self::getText( $e ) );
+               } elseif ( $mode === self::AS_PRETTY ) {
+                       self::statusHeader( 500 );
+                       self::header( "Content-Type: $wgMimeType; charset=utf-8" );
+                       self::reportHTML( $e );
+               } else {
+                       if ( $eNew ) {
+                               $message = "MediaWiki internal error.\n\n";
+                               if ( self::showBackTrace( $e ) ) {
+                                       $message .= 'Original exception: ' .
+                                               MWExceptionHandler::getLogMessage( $e ) .
+                                               "\nBacktrace:\n" . MWExceptionHandler::getRedactedTraceAsString( $e ) .
+                                               "\n\nException caught inside exception handler: " .
+                                                       MWExceptionHandler::getLogMessage( $eNew ) .
+                                               "\nBacktrace:\n" . MWExceptionHandler::getRedactedTraceAsString( $eNew );
+                               } 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";
+                       } else {
+                               if ( self::showBackTrace( $e ) ) {
+                                       $message = MWExceptionHandler::getLogMessage( $e ) .
+                                               "\nBacktrace:\n" .
+                                               MWExceptionHandler::getRedactedTraceAsString( $e ) . "\n";
+                               } else {
+                                       $message = MWExceptionHandler::getPublicLogMessage( $e );
+                               }
+                       }
+                       if ( self::isCommandLine() ) {
+                               self::printError( $message );
+                       } else {
+                               echo nl2br( htmlspecialchars( $message ) ) . "\n";
+                       }
+               }
+       }
+
+       /**
+        * Run hook to allow extensions to modify the text of the exception
+        *
+        * Called by MWException for b/c
+        *
+        * @param Exception $e
+        * @param string $name Class name of the exception
+        * @param array $args Arguments to pass to the callback functions
+        * @return string|null String to output or null if any hook has been called
+        */
+       public static function runHooks( Exception $e, $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( [ $e ], $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;
+       }
+
+       /**
+        * @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 "<!DOCTYPE html>\n" .
+                               '<html><head>' .
+                               // Mimick OutputPage::setPageTitle behaviour
+                               '<title>' .
+                               htmlspecialchars( self::msg( 'pagetitle', "$1 - $wgSitename", $pageTitle ) ) .
+                               '</title>' .
+                               '<style>body { font-family: sans-serif; margin: 0; padding: 0.5em 2em; }</style>' .
+                               "</head><body>\n";
+
+                       $hookResult = self::runHooks( $e, get_class( $e ) . 'Raw' );
+                       if ( $hookResult ) {
+                               echo $hookResult;
+                       } else {
+                               echo self::getHTML( $e );
+                       }
+
+                       echo "</body></html>\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 '<p>' .
+                               nl2br( htmlspecialchars( MWExceptionHandler::getLogMessage( $e ) ) ) .
+                               '</p><p>Backtrace:</p><p>' .
+                               nl2br( htmlspecialchars( MWExceptionHandler::getRedactedTraceAsString( $e ) ) ) .
+                               "</p>\n";
+               } else {
+                       $logId = WebRequest::getRequestId();
+                       return "<div class=\"errorbox\">" .
+                               '[' . $logId . '] ' .
+                               gmdate( 'Y-m-d H:i:s' ) . ": " .
+                               self::msg( "internalerror-fatal-exception",
+                                       "Fatal exception of type $1",
+                                       get_class( $e ),
+                                       $logId,
+                                       MWExceptionHandler::getURL()
+                               ) . "</div>\n" .
+                       "<!-- Set \$wgShowExceptionDetails = true; " .
+                       "at the bottom of LocalSettings.php to show detailed " .
+                       "debugging information. -->";
+               }
+       }
+
+       /**
+        * 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 = "<h1>$sorry</h1><p>$again</p><p><small>$info</small></p>";
+
+               if ( $wgShowDBErrorBacktrace ) {
+                       $html .= '<p>Backtrace:</p><pre>' .
+                               htmlspecialchars( $e->getTraceAsString() ) . '</pre>';
+               }
+
+               $html .= '<hr />';
+               $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 = <<<EOT
+<div style="margin: 1.5em">$usegoogle<br />
+<small>$outofdate</small>
+</div>
+<form method="get" action="//www.google.com/search" id="googlesearch">
+       <input type="hidden" name="domains" value="$server" />
+       <input type="hidden" name="num" value="50" />
+       <input type="hidden" name="ie" value="UTF-8" />
+       <input type="hidden" name="oe" value="UTF-8" />
+       <input type="text" name="q" size="31" maxlength="255" value="$search" />
+       <input type="submit" name="btnG" value="$googlesearch" />
+       <p>
+               <label><input type="radio" name="sitesearch" value="$server" checked="checked" />$sitename</label>
+               <label><input type="radio" name="sitesearch" value="" />WWW</label>
+       </p>
+</form>
+EOT;
+               return $trygoogle;
+       }
+}