Exception: Clean up html document for error pages
[lhc/web/wiklou.git] / includes / db / DatabaseError.php
index 331b8ae..937bea0 100644 (file)
@@ -37,30 +37,18 @@ class DBError extends MWException {
         * @param $db DatabaseBase object which threw the error
         * @param string $error A simple error message to be used for debugging
         */
-       function __construct( DatabaseBase &$db, $error ) {
+       function __construct( DatabaseBase $db = null, $error ) {
                $this->db = $db;
                parent::__construct( $error );
        }
 
-       /**
-        * @param $html string
-        * @return string
-        */
-       protected function getContentMessage( $html ) {
-               if ( $html ) {
-                       return nl2br( htmlspecialchars( $this->getMessage() ) );
-               } else {
-                       return $this->getMessage();
-               }
-       }
-
        /**
         * @return string
         */
        function getText() {
                global $wgShowDBErrorBacktrace;
 
-               $s = $this->getContentMessage( false ) . "\n";
+               $s = $this->getTextContent() . "\n";
 
                if ( $wgShowDBErrorBacktrace ) {
                        $s .= "Backtrace:\n" . $this->getTraceAsString() . "\n";
@@ -75,14 +63,28 @@ class DBError extends MWException {
        function getHTML() {
                global $wgShowDBErrorBacktrace;
 
-               $s = $this->getContentMessage( true );
+               $s = $this->getHTMLContent();
 
                if ( $wgShowDBErrorBacktrace ) {
-                       $s .= '<p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) );
+                       $s .= '<p>Backtrace:</p><pre>' . htmlspecialchars( $this->getTraceAsString() ) . '</pre>';
                }
 
                return $s;
        }
+
+       /**
+        * @return string
+        */
+       protected function getTextContent() {
+               return $this->getMessage();
+       }
+
+       /**
+        * @return string
+        */
+       protected function getHTMLContent() {
+               return '<p>' . nl2br( htmlspecialchars( $this->getMessage() ) ) . '</p>';
+       }
 }
 
 /**
@@ -91,16 +93,17 @@ class DBError extends MWException {
 class DBConnectionError extends DBError {
        public $error;
 
-       function __construct( DatabaseBase &$db, $error = 'unknown error' ) {
+       function __construct( DatabaseBase $db = null, $error = 'unknown error' ) {
                $msg = 'DB connection error';
 
                if ( trim( $error ) != '' ) {
                        $msg .= ": $error";
+               } elseif ( $db ) {
+                       $error = $this->db->getServer();
                }
 
-               $this->error = $error;
-
                parent::__construct( $db, $msg );
+               $this->error = $error;
        }
 
        /**
@@ -133,67 +136,71 @@ class DBConnectionError extends DBError {
         * @return bool
         */
        function getLogMessage() {
-               # Don't send to the exception log
+               // Don't send to the exception log
                return false;
        }
 
-       /**
-        * @return string
-        */
-       function getPageTitle() {
-               global $wgSitename;
-               return htmlspecialchars( $this->msg( 'dberr-header', "$wgSitename has a problem" ) );
-       }
-
        /**
         * @return string
         */
        function getHTML() {
-               global $wgShowDBErrorBacktrace;
+               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.' ) );
-               $info = htmlspecialchars( $this->msg( 'dberr-info', '(Can\'t contact the database server: $1)' ) );
-
-               # No database access
-               MessageCache::singleton()->disable();
 
-               if ( trim( $this->error ) == '' ) {
-                       $this->error = $this->db->getProperty( 'mServer' );
+               if ( $wgShowHostnames || $wgShowSQLErrors ) {
+                       $info = str_replace(
+                               '$1', Html::element( 'span', array( 'dir' => 'ltr' ), $this->error ),
+                               htmlspecialchars( $this->msg( 'dberr-info', '(Cannot contact the database server: $1)' ) )
+                       );
+               } else {
+                       $info = htmlspecialchars( $this->msg( 'dberr-info-hidden', '(Cannot contact the database server)' ) );
                }
 
-               $this->error = Html::element( 'span', array( 'dir' => 'ltr' ), $this->error );
+               # No database access
+               MessageCache::singleton()->disable();
 
-               $noconnect = "<h1>$sorry</h1><p>$again</p><p><small>$info</small></p>";
-               $text = str_replace( '$1', $this->error, $noconnect );
+               $html = "<h1>$sorry</h1><p>$again</p><p><small>$info</small></p>";
 
                if ( $wgShowDBErrorBacktrace ) {
-                       $text .= '<p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) );
+                       $html .= '<p>Backtrace:</p><pre>' . htmlspecialchars( $this->getTraceAsString() ) . '</pre>';
                }
 
-               $extra = $this->searchForm();
+               $html .= '<hr />';
+               $html .= $this->searchForm();
 
-               return "$text<hr />$extra";
+               return $html;
+       }
+
+       protected function getTextContent() {
+               global $wgShowHostnames, $wgShowSQLErrors;
+
+               if ( $wgShowHostnames || $wgShowSQLErrors ) {
+                       return $this->getMessage();
+               } else {
+                       return 'DB connection error';
+               }
        }
 
        public function reportHTML() {
                global $wgUseFileCache;
 
-               # Check whether we can serve a file-cached copy of the page with the error underneath
+               // 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?
+                               // Cached version on file system?
                                if ( $cache !== null ) {
-                                       # Hack: extend the body for error messages
+                                       // Hack: extend the body for error messages
                                        $cache = str_replace( array( '</html>', '</body>' ), '', $cache );
-                                       # Add cache notice...
-                                       $cache .= '<div style="color:red;font-size:150%;font-weight:bold;">'.
+                                       // 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. ' ) ) .
+                                                       '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
+                                       // Output cached page with notices on bottom and re-close body
                                        echo "{$cache}<hr />{$this->getHTML()}</body></html>";
                                        return;
                                }
@@ -202,7 +209,7 @@ class DBConnectionError extends DBError {
                        }
                }
 
-               # We can't, cough and die in the usual fashion
+               // We can't, cough and die in the usual fashion
                parent::reportHTML();
        }
 
@@ -223,8 +230,8 @@ class DBConnectionError extends DBError {
 
                $trygoogle = <<<EOT
 <div style="margin: 1.5em">$usegoogle<br />
-<small>$outofdate</small></div>
-<!-- SiteSearch Google -->
+<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" />
@@ -233,12 +240,11 @@ class DBConnectionError extends DBError {
 
        <input type="text" name="q" size="31" maxlength="255" value="$search" />
        <input type="submit" name="btnG" value="$googlesearch" />
-  <div>
-       <input type="radio" name="sitesearch" id="gwiki" value="$server" checked="checked" /><label for="gwiki">$sitename</label>
-       <input type="radio" name="sitesearch" id="gWWW" value="" /><label for="gWWW">WWW</label>
-  </div>
+       <p>
+               <label><input type="radio" name="sitesearch" value="$server" checked="checked" />$sitename</label>
+               <label><input type="radio" name="sitesearch" value="" />WWW</label>
+       </p>
 </form>
-<!-- SiteSearch Google -->
 EOT;
                return $trygoogle;
        }
@@ -250,15 +256,17 @@ EOT;
                global $wgTitle, $wgOut, $wgRequest;
 
                if ( $wgOut->isDisabled() ) {
-                       return ''; // Done already?
+                       // Done already?
+                       return '';
                }
 
-               if ( $wgTitle ) { // use $wgTitle if we managed to set it
+               if ( $wgTitle ) {
+                       // use $wgTitle if we managed to set it
                        $t = $wgTitle->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.
+                       // 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( ' ', '_', $wgRequest->getVal( 'title' ) );
                        if ( $t == '' ) { // fallback to main page
                                $t = Title::newFromText(
@@ -288,7 +296,7 @@ class DBQueryError extends DBError {
         * @param $sql string
         * @param $fname string
         */
-       function __construct( DatabaseBase &$db, $error, $errno, $sql, $fname ) {
+       function __construct( DatabaseBase $db, $error, $errno, $sql, $fname ) {
                $message = "A database error has occurred. Did you forget to run maintenance/update.php after upgrading?  See: https://www.mediawiki.org/wiki/Manual:Upgrading#Run_the_update_script\n" .
                        "Query: $sql\n" .
                        "Function: $fname\n" .
@@ -302,55 +310,107 @@ class DBQueryError extends DBError {
        }
 
        /**
-        * @param $html string
+        * @return bool
+        */
+       function getLogMessage() {
+               # Don't send to the exception log
+               return false;
+       }
+
+       /**
+        * @return String
+        */
+       function getPageTitle() {
+               return $this->msg( 'databaseerror', 'Database error' );
+       }
+
+       /**
         * @return string
         */
-       function getContentMessage( $html ) {
-               if ( $this->useMessageCache() ) {
-                       if ( $html ) {
-                               $msg = 'dberrortext';
-                               $sql = htmlspecialchars( $this->getSQL() );
-                               $fname = htmlspecialchars( $this->fname );
-                               $error = htmlspecialchars( $this->error );
-                       } else {
-                               $msg = 'dberrortextcl';
-                               $sql = $this->getSQL();
-                               $fname = $this->fname;
-                               $error = $this->error;
+       protected function getHTMLContent() {
+               $key = 'databaseerror-text';
+               $s = Html::element( 'p', array(), $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', array(),
+                                               $this->msg( $key, $this->getFallbackMessage( $key ) )
+                                       )
+                               );
                        }
-                       return wfMessage( $msg )->rawParams( $sql, $fname, $this->errno, $error )->text();
-               } else {
-                       return parent::getContentMessage( $html );
+                       $s .= '</ul>';
                }
+
+               return $s;
        }
 
        /**
-        * @return String
+        * @return string
         */
-       function getSQL() {
-               global $wgShowSQLErrors;
+       protected function getTextContent() {
+               $key = 'databaseerror-textcl';
+               $s = $this->msg( $key, $this->getFallbackMessage( $key ) ) . "\n";
 
-               if ( !$wgShowSQLErrors ) {
-                       return $this->msg( 'sqlhidden', 'SQL hidden' );
-               } else {
-                       return $this->sql;
+               foreach ( $this->getTechnicalDetails() as $key => $detail ) {
+                       $s .= $this->msg( $key, $this->getFallbackMessage( $key ), $detail[2] ) . "\n";
                }
+
+               return $s;
        }
 
        /**
-        * @return bool
+        * 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.
         */
-       function getLogMessage() {
-               # Don't send to the exception log
-               return false;
+       protected function getTechnicalDetails() {
+               global $wgShowHostnames, $wgShowSQLErrors;
+
+               $attribs = array( 'dir' => 'ltr' );
+               $details = array();
+
+               if ( $wgShowSQLErrors ) {
+                       $details['databaseerror-query'] = array(
+                               'div', array( 'class' => 'mw-code' ) + $attribs, $this->sql );
+               }
+
+               if ( $wgShowHostnames || $wgShowSQLErrors ) {
+                       $errorMessage = $this->errno . ' ' . $this->error;
+                       $details['databaseerror-function'] = array( 'code', $attribs, $this->fname );
+                       $details['databaseerror-error'] = array( 'samp', $attribs, $errorMessage );
+               }
+
+               return $details;
        }
 
        /**
-        * @return String
+        * @param string $key Message key
+        * @return string: English message text
         */
-       function getPageTitle() {
-               return $this->msg( 'databaseerror', 'Database error' );
+       private function getFallbackMessage( $key ) {
+               $messages = array(
+                       '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];
        }
+
 }
 
 /**