API: Clean up uncaught exception backtrace output
[lhc/web/wiklou.git] / includes / exception / MWExceptionHandler.php
index 1f1ba9c..83801b6 100644 (file)
  * @ingroup Exception
  */
 class MWExceptionHandler {
+
+       protected static $reservedMemory;
+       protected static $fatalErrorTypes = array(
+               E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR,
+               /* HHVM's FATAL_ERROR level */ 16777217,
+       );
+
        /**
-        * Install an exception handler for MediaWiki exception types.
+        * Install handlers with PHP.
         */
        public static function installHandler() {
-               set_exception_handler( array( 'MWExceptionHandler', 'handle' ) );
+               set_exception_handler( array( 'MWExceptionHandler', 'handleException' ) );
+               set_error_handler( array( 'MWExceptionHandler', 'handleError' ) );
+
+               // Reserve 16k of memory so we can report OOM fatals
+               self::$reservedMemory = str_repeat( ' ', 16384 );
+               register_shutdown_function(
+                       array( 'MWExceptionHandler', 'handleFatalError' )
+               );
        }
 
        /**
@@ -45,7 +59,7 @@ class MWExceptionHandler {
                                $e->report();
                        } catch ( Exception $e2 ) {
                                // Exception occurred from within exception handler
-                               // Show a simpler error message for the original exception,
+                               // Show a simpler message for the original exception,
                                // don't try to invoke report()
                                $message = "MediaWiki internal error.\n\n";
 
@@ -83,7 +97,6 @@ class MWExceptionHandler {
                                echo nl2br( htmlspecialchars( $message ) ) . "\n";
                        }
 
-                       self::logException( $e );
                }
        }
 
@@ -108,6 +121,7 @@ class MWExceptionHandler {
         * If there are any open database transactions, roll them back and log
         * the stack trace of the exception that should have been caught so the
         * transaction could be aborted properly.
+        *
         * @since 1.23
         * @param Exception $e
         */
@@ -133,13 +147,15 @@ class MWExceptionHandler {
         *   } catch ( Exception $e ) {
         *       echo $e->__toString();
         *   }
+        *
+        * @since 1.25
         * @param Exception $e
         */
-       public static function handle( $e ) {
+       public static function handleException( $e ) {
                global $wgFullyInitialised;
 
                self::rollbackMasterChangesAndLog( $e );
-
+               self::logException( $e );
                self::report( $e );
 
                // Final cleanup
@@ -155,6 +171,95 @@ class MWExceptionHandler {
                exit( 1 );
        }
 
+       /**
+        * @since 1.25
+        * @param int $level Error level raised
+        * @param string $message
+        * @param string $file
+        * @param int $line
+        */
+       public static function handleError( $level, $message, $file = null, $line = null ) {
+               // Map error constant to error name (reverse-engineer PHP error reporting)
+               switch ( $level ) {
+                       case E_ERROR:
+                       case E_CORE_ERROR:
+                       case E_COMPILE_ERROR:
+                       case E_USER_ERROR:
+                       case E_RECOVERABLE_ERROR:
+                       case E_PARSE:
+                               $levelName = 'Error';
+                               break;
+                       case E_WARNING:
+                       case E_CORE_WARNING:
+                       case E_COMPILE_WARNING:
+                       case E_USER_WARNING:
+                               $levelName = 'Warning';
+                               break;
+                       case E_NOTICE:
+                       case E_USER_NOTICE:
+                               $levelName = 'Notice';
+                               break;
+                       case E_STRICT:
+                               $levelName = 'Strict Standards';
+                               break;
+                       case E_DEPRECATED:
+                       case E_USER_DEPRECATED:
+                               $levelName = 'Deprecated';
+                               break;
+                       case /* HHVM's FATAL_ERROR */ 16777217:
+                               $levelName = 'Fatal';
+                               break;
+                       default:
+                               $levelName = 'Unknown error';
+                               break;
+               }
+
+               $e = new ErrorException( "PHP $levelName: $message", 0, $level, $file, $line );
+               self::logError( $e );
+
+               // This handler is for logging only. Return false will instruct PHP
+               // to continue regular handling.
+               return false;
+       }
+
+
+       /**
+        * Look for a fatal error as the cause of the request termination and log
+        * as an exception.
+        *
+        * Special handling is included for missing class errors as they may
+        * indicate that the user needs to install 3rd-party libraries via
+        * Composer or other means.
+        *
+        * @since 1.25
+        */
+       public static function handleFatalError() {
+               self::$reservedMemory = null;
+               $lastError = error_get_last();
+
+               if ( $lastError &&
+                       isset( $lastError['type'] ) &&
+                       in_array( $lastError['type'], self::$fatalErrorTypes )
+               ) {
+                       $msg = "Fatal Error: {$lastError['message']}";
+                       // HHVM: Class undefined: foo
+                       // PHP5: Class 'foo' not found
+                       if ( preg_match( "/Class (undefined: \w+|'\w+' not found)/",
+                               $lastError['message']
+                       ) ) {
+                               $msg = <<<TXT
+{$msg}
+
+MediaWiki or an installed extension requires this class but it is not embedded directly in MediaWiki's git repository and must be installed separately by the end user.
+
+Please see <a href="https://www.mediawiki.org/wiki/Download_from_Git#Fetch_external_libraries">mediawiki.org</a> for help on installing the required components.
+TXT;
+                       }
+                       $e = new ErrorException( $msg, 0, $lastError['type'] );
+                       self::logError( $e );
+               }
+       }
+
        /**
         * Generate a string representation of an exception's stack trace
         *
@@ -219,7 +324,7 @@ class MWExceptionHandler {
        }
 
        /**
-        * Get the ID for this error.
+        * Get the ID for this exception.
         *
         * The ID is saved so that one can match the one output to the user (when
         * $wgShowExceptionDetails is set to false), to the entry in the debug log.
@@ -251,8 +356,7 @@ class MWExceptionHandler {
        }
 
        /**
-        * Return the requested URL and point to file and line number from which the
-        * exception occurred.
+        * Get a message formatting the exception message and its origin.
         *
         * @since 1.22
         * @param Exception $e
@@ -260,12 +364,13 @@ class MWExceptionHandler {
         */
        public static function getLogMessage( Exception $e ) {
                $id = self::getLogId( $e );
+               $type = get_class( $e );
                $file = $e->getFile();
                $line = $e->getLine();
                $message = $e->getMessage();
                $url = self::getURL() ?: '[no req]';
 
-               return "[$id] $url   Exception from line $line of $file: $message";
+               return "[$id] $url   $type from line $line of $file: $message";
        }
 
        /**
@@ -287,6 +392,7 @@ class MWExceptionHandler {
         * @code
         *  {
         *    "id": "c41fb419",
+        *    "type": "MWException",
         *    "file": "/var/www/mediawiki/includes/cache/MessageCache.php",
         *    "line": 704,
         *    "message": "Non-string key given",
@@ -298,6 +404,7 @@ class MWExceptionHandler {
         * @code
         *  {
         *    "id": "dc457938",
+        *    "type": "MWException",
         *    "file": "/vagrant/mediawiki/includes/cache/MessageCache.php",
         *    "line": 704,
         *    "message": "Non-string key given",
@@ -324,6 +431,7 @@ class MWExceptionHandler {
 
                $exceptionData = array(
                        'id' => self::getLogId( $e ),
+                       'type' => get_class( $e ),
                        'file' => $e->getFile(),
                        'line' => $e->getLine(),
                        'message' => $e->getMessage(),
@@ -347,7 +455,7 @@ class MWExceptionHandler {
         * Log an exception to the exception log (if enabled).
         *
         * This method must not assume the exception is an MWException,
-        * it is also used to handle PHP errors or errors from other libraries.
+        * it is also used to handle PHP exceptions or exceptions from other libraries.
         *
         * @since 1.22
         * @param Exception $e
@@ -368,7 +476,22 @@ class MWExceptionHandler {
                                wfDebugLog( 'exception-json', $json, 'private' );
                        }
                }
-
        }
 
+       /**
+        * Log an exception that wasn't thrown but made to wrap an error.
+        *
+        * @since 1.25
+        * @param Exception $e
+       */
+       protected static function logError( Exception $e ) {
+               global $wgLogExceptionBacktrace;
+
+               $log = self::getLogMessage( $e );
+               if ( $wgLogExceptionBacktrace ) {
+                       wfDebugLog( 'error', $log . "\n" . $e->getTraceAsString() );
+               } else {
+                       wfDebugLog( 'error', $log );
+               }
+       }
 }