summaryrefslogtreecommitdiff
path: root/includes/exception
diff options
context:
space:
mode:
Diffstat (limited to 'includes/exception')
-rw-r--r--includes/exception/HttpError.php42
-rw-r--r--includes/exception/MWException.php23
-rw-r--r--includes/exception/MWExceptionHandler.php201
-rw-r--r--includes/exception/TimestampException.php7
-rw-r--r--includes/exception/UserNotLoggedIn.php10
5 files changed, 243 insertions, 40 deletions
diff --git a/includes/exception/HttpError.php b/includes/exception/HttpError.php
index 6ab6e039..b81c5731 100644
--- a/includes/exception/HttpError.php
+++ b/includes/exception/HttpError.php
@@ -18,6 +18,8 @@
* @file
*/
+use MediaWiki\Logger\LoggerFactory;
+
/**
* Show an error that looks like an HTTP server error.
* Replacement for wfHttpError().
@@ -43,6 +45,19 @@ class HttpError extends MWException {
}
/**
+ * We don't want the default exception logging as we got our own logging set
+ * up in self::report.
+ *
+ * @see MWException::isLoggable
+ *
+ * @since 1.24
+ * @return bool
+ */
+ public function isLoggable() {
+ return false;
+ }
+
+ /**
* Returns the HTTP status code supplied to the constructor.
*
* @return int
@@ -52,11 +67,13 @@ class HttpError extends MWException {
}
/**
- * Report the HTTP error.
+ * Report and log the HTTP error.
* Sends the appropriate HTTP status code and outputs an
* HTML page with an error message.
*/
public function report() {
+ $this->doLog();
+
$httpMessage = HttpStatus::getMessage( $this->httpCode );
header( "Status: {$this->httpCode} {$httpMessage}", true, $this->httpCode );
@@ -65,6 +82,29 @@ class HttpError extends MWException {
print $this->getHTML();
}
+ private function doLog() {
+ $logger = LoggerFactory::getInstance( 'HttpError' );
+ $content = $this->content;
+
+ if ( $content instanceof Message ) {
+ $content = $content->text();
+ }
+
+ $context = array(
+ 'file' => $this->getFile(),
+ 'line' => $this->getLine(),
+ 'http_code' => $this->httpCode,
+ );
+
+ $logMsg = "$content ({http_code}) from {file}:{line}";
+
+ if ( $this->getStatusCode() < 500 ) {
+ $logger->info( $logMsg, $context );
+ } else {
+ $logger->error( $logMsg, $context );
+ }
+ }
+
/**
* Returns HTML for reporting the HTTP error.
* This will be a minimal but complete HTML document.
diff --git a/includes/exception/MWException.php b/includes/exception/MWException.php
index 074128f8..478fead1 100644
--- a/includes/exception/MWException.php
+++ b/includes/exception/MWException.php
@@ -117,10 +117,12 @@ class MWException extends Exception {
$args = array_slice( func_get_args(), 2 );
if ( $this->useMessageCache() ) {
- return wfMessage( $key, $args )->text();
- } else {
- return wfMsgReplaceArgs( $fallback, $args );
+ try {
+ return wfMessage( $key, $args )->text();
+ } catch ( Exception $e ) {
+ }
}
+ return wfMsgReplaceArgs( $fallback, $args );
}
/**
@@ -139,10 +141,17 @@ class MWException extends Exception {
nl2br( htmlspecialchars( MWExceptionHandler::getRedactedTraceAsString( $this ) ) ) .
"</p>\n";
} else {
+ $logId = MWExceptionHandler::getLogId( $this );
+ $type = get_class( $this );
return "<div class=\"errorbox\">" .
- '[' . MWExceptionHandler::getLogId( $this ) . '] ' .
- gmdate( 'Y-m-d H:i:s' ) .
- ": Fatal exception of type " . get_class( $this ) . "</div>\n" .
+ '[' . $logId . '] ' .
+ gmdate( 'Y-m-d H:i:s' ) . ": " .
+ $this->msg( "internalerror-fatal-exception",
+ "Fatal exception of type $1",
+ $type,
+ $logId,
+ MWExceptionHandler::getURL( $this )
+ ) . "</div>\n" .
"<!-- Set \$wgShowExceptionDetails = true; " .
"at the bottom of LocalSettings.php to show detailed " .
"debugging information. -->";
@@ -222,8 +231,6 @@ class MWException extends Exception {
public function report() {
global $wgMimeType;
- MWExceptionHandler::logException( $this );
-
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 ) );
diff --git a/includes/exception/MWExceptionHandler.php b/includes/exception/MWExceptionHandler.php
index 71917e13..c50b6c8c 100644
--- a/includes/exception/MWExceptionHandler.php
+++ b/includes/exception/MWExceptionHandler.php
@@ -23,11 +23,25 @@
* @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";
@@ -69,8 +83,7 @@ class MWExceptionHandler {
}
}
} else {
- $message = "Unexpected non-MediaWiki exception encountered, of type \"" .
- get_class( $e ) . "\"";
+ $message = "Exception encountered, of type \"" . get_class( $e ) . "\"";
if ( $wgShowExceptionDetails ) {
$message .= "\n" . MWExceptionHandler::getLogMessage( $e ) . "\nBacktrace:\n" .
@@ -82,6 +95,7 @@ class MWExceptionHandler {
} else {
echo nl2br( htmlspecialchars( $message ) ) . "\n";
}
+
}
}
@@ -106,6 +120,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
*/
@@ -126,34 +141,132 @@ class MWExceptionHandler {
*
* try {
* ...
- * } catch ( MWException $e ) {
+ * } catch ( Exception $e ) {
* $e->report();
* } catch ( Exception $e ) {
* echo $e->__toString();
* }
+ *
+ * @since 1.25
* @param Exception $e
*/
- public static function handle( $e ) {
- global $wgFullyInitialised;
-
- self::rollbackMasterChangesAndLog( $e );
+ public static function handleException( Exception $e ) {
+ try {
+ // Rollback DBs to avoid transaction notices. This may fail
+ // to rollback some DB due to connection issues or exceptions.
+ // However, any sane DB driver will rollback implicitly anyway.
+ self::rollbackMasterChangesAndLog( $e );
+ } catch ( DBError $e2 ) {
+ // If the DB is unreacheable, rollback() will throw an error
+ // and the error report() method might need messages from the DB,
+ // which would result in an exception loop. PHP may escalate such
+ // errors to "Exception thrown without a stack frame" fatals, but
+ // it's better to be explicit here.
+ self::logException( $e2 );
+ }
+ self::logException( $e );
self::report( $e );
- // Final cleanup
- if ( $wgFullyInitialised ) {
- try {
- // uses $wgRequest, hence the $wgFullyInitialised condition
- wfLogProfilingData();
- } catch ( Exception $e ) {
- }
- }
-
// Exit value should be nonzero for the benefit of shell jobs
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)
+ $channel = 'error';
+ 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';
+ $channel = 'fatal';
+ 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';
+ $channel = 'fatal';
+ break;
+ default:
+ $levelName = 'Unknown error';
+ break;
+ }
+
+ $e = new ErrorException( "PHP $levelName: $message", 0, $level, $file, $line );
+ self::logError( $e, $channel );
+
+ // 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']
+ ) ) {
+ // @codingStandardsIgnoreStart Generic.Files.LineLength.TooLong
+ $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;
+ // @codingStandardsIgnoreEnd
+ }
+ $e = new ErrorException( $msg, 0, $lastError['type'] );
+ self::logError( $e, 'fatal' );
+ }
+ }
+
+ /**
* Generate a string representation of an exception's stack trace
*
* Like Exception::getTraceAsString, but replaces argument values with
@@ -217,7 +330,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.
@@ -238,7 +351,7 @@ class MWExceptionHandler {
* returns the requested URL. Otherwise, returns false.
*
* @since 1.23
- * @return string|bool
+ * @return string|false
*/
public static function getURL() {
global $wgRequest;
@@ -249,8 +362,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
@@ -258,12 +370,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";
}
/**
@@ -285,6 +398,7 @@ class MWExceptionHandler {
* @code
* {
* "id": "c41fb419",
+ * "type": "MWException",
* "file": "/var/www/mediawiki/includes/cache/MessageCache.php",
* "line": 704,
* "message": "Non-string key given",
@@ -296,6 +410,7 @@ class MWExceptionHandler {
* @code
* {
* "id": "dc457938",
+ * "type": "MWException",
* "file": "/vagrant/mediawiki/includes/cache/MessageCache.php",
* "line": 704,
* "message": "Non-string key given",
@@ -315,18 +430,24 @@ class MWExceptionHandler {
* @param Exception $e
* @param bool $pretty Add non-significant whitespace to improve readability (default: false).
* @param int $escaping Bitfield consisting of FormatJson::.*_OK class constants.
- * @return string|bool JSON string if successful; false upon failure
+ * @return string|false JSON string if successful; false upon failure
*/
public static function jsonSerializeException( Exception $e, $pretty = false, $escaping = 0 ) {
global $wgLogExceptionBacktrace;
$exceptionData = array(
'id' => self::getLogId( $e ),
+ 'type' => get_class( $e ),
'file' => $e->getFile(),
'line' => $e->getLine(),
'message' => $e->getMessage(),
);
+ if ( $e instanceof ErrorException && ( error_reporting() & $e->getSeverity() ) === 0 ) {
+ // Flag surpressed errors
+ $exceptionData['suppressed'] = true;
+ }
+
// Because MediaWiki is first and foremost a web application, we set a
// 'url' key unconditionally, but set it to null if the exception does
// not occur in the context of a web request, as a way of making that
@@ -345,7 +466,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
@@ -366,7 +487,33 @@ class MWExceptionHandler {
wfDebugLog( 'exception-json', $json, 'private' );
}
}
-
}
+ /**
+ * Log an exception that wasn't thrown but made to wrap an error.
+ *
+ * @since 1.25
+ * @param ErrorException $e
+ * @param string $channel
+ */
+ protected static function logError( ErrorException $e, $channel ) {
+ global $wgLogExceptionBacktrace;
+
+ // The set_error_handler callback is independent from error_reporting.
+ // Filter out unwanted errors manually (e.g. when wfSuppressWarnings is active).
+ if ( ( error_reporting() & $e->getSeverity() ) !== 0 ) {
+ $log = self::getLogMessage( $e );
+ if ( $wgLogExceptionBacktrace ) {
+ wfDebugLog( $channel, $log . "\n" . $e->getTraceAsString() );
+ } else {
+ wfDebugLog( $channel, $log );
+ }
+ }
+
+ // Include all errors in the json log (surpressed errors will be flagged)
+ $json = self::jsonSerializeException( $e, false, FormatJson::ALL_OK );
+ if ( $json !== false ) {
+ wfDebugLog( "$channel-json", $json, 'private' );
+ }
+ }
}
diff --git a/includes/exception/TimestampException.php b/includes/exception/TimestampException.php
new file mode 100644
index 00000000..b9c0c35c
--- /dev/null
+++ b/includes/exception/TimestampException.php
@@ -0,0 +1,7 @@
+<?php
+
+/**
+ * @since 1.20
+ */
+class TimestampException extends MWException {
+}
diff --git a/includes/exception/UserNotLoggedIn.php b/includes/exception/UserNotLoggedIn.php
index 03ba0b20..02fca3d8 100644
--- a/includes/exception/UserNotLoggedIn.php
+++ b/includes/exception/UserNotLoggedIn.php
@@ -25,8 +25,9 @@
* 'exception-nologin' as a title and 'exception-nologin-text' for the message.
*
* @note In order for this exception to redirect, the error message passed to the
- * constructor has to be explicitly added to LoginForm::validErrorMessages. Otherwise,
- * the user will just be shown the message rather than redirected.
+ * constructor has to be explicitly added to LoginForm::validErrorMessages or with
+ * the LoginFormValidErrorMessages hook. Otherwise, the user will just be shown the message
+ * rather than redirected.
*
* @par Example:
* @code
@@ -52,7 +53,8 @@
class UserNotLoggedIn extends ErrorPageError {
/**
- * @note The value of the $reasonMsg parameter must be put into LoginForm::validErrorMessages
+ * @note The value of the $reasonMsg parameter must be put into LoginForm::validErrorMessages or
+ * set with the LoginFormValidErrorMessages Hook.
* if you want the user to be automatically redirected to the login form.
*
* @param string $reasonMsg A message key containing the reason for the error.
@@ -77,7 +79,7 @@ class UserNotLoggedIn extends ErrorPageError {
public function report() {
// If an unsupported message is used, don't try redirecting to Special:Userlogin,
// since the message may not be compatible.
- if ( !in_array( $this->msg, LoginForm::$validErrorMessages ) ) {
+ if ( !in_array( $this->msg, LoginForm::getValidErrorMessages() ) ) {
parent::report();
}