diff options
author | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
---|---|---|
committer | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
commit | fc7369835258467bf97eb64f184b93691f9a9fd5 (patch) | |
tree | daabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/includes/exception |
first commit
Diffstat (limited to 'www/wiki/includes/exception')
19 files changed, 2066 insertions, 0 deletions
diff --git a/www/wiki/includes/exception/BadRequestError.php b/www/wiki/includes/exception/BadRequestError.php new file mode 100644 index 00000000..5fcf0e62 --- /dev/null +++ b/www/wiki/includes/exception/BadRequestError.php @@ -0,0 +1,34 @@ +<?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 + */ + +/** + * An error page that emits an HTTP 400 Bad Request status code. + * + * @since 1.28 + * @ingroup Exception + */ +class BadRequestError extends ErrorPageError { + + public function report() { + global $wgOut; + $wgOut->setStatusCode( 400 ); + parent::report(); + } +} diff --git a/www/wiki/includes/exception/BadTitleError.php b/www/wiki/includes/exception/BadTitleError.php new file mode 100644 index 00000000..40c18a42 --- /dev/null +++ b/www/wiki/includes/exception/BadTitleError.php @@ -0,0 +1,49 @@ +<?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 + */ + +/** + * Show an error page on a badtitle. + * + * Uses BadRequestError to emit a 400 HTTP error code to ensure caching proxies and + * mobile browsers know not to cache it as valid content. (T35646) + * + * @since 1.19 + * @ingroup Exception + */ +class BadTitleError extends BadRequestError { + /** + * @param string|Message|MalformedTitleException $msg A message key (default: 'badtitletext'), or + * a MalformedTitleException to figure out things from + * @param array $params Parameter to wfMessage() + */ + public function __construct( $msg = 'badtitletext', $params = [] ) { + if ( $msg instanceof MalformedTitleException ) { + $errorMessage = $msg->getErrorMessage(); + if ( !$errorMessage ) { + parent::__construct( 'badtitle', 'badtitletext', [] ); + } else { + $errorMessageParams = $msg->getErrorMessageParameters(); + parent::__construct( 'badtitle', $errorMessage, $errorMessageParams ); + } + } else { + parent::__construct( 'badtitle', $msg, $params ); + } + } +} diff --git a/www/wiki/includes/exception/CannotCreateActorException.php b/www/wiki/includes/exception/CannotCreateActorException.php new file mode 100644 index 00000000..7c7ccfc4 --- /dev/null +++ b/www/wiki/includes/exception/CannotCreateActorException.php @@ -0,0 +1,29 @@ +<?php +/** + * Exception thrown when some operation failed + * + * 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 + * + * @since 1.31 + */ + +/** + * Exception thrown when an actor can't be created. + */ +class CannotCreateActorException extends RuntimeException { +} diff --git a/www/wiki/includes/exception/ErrorPageError.php b/www/wiki/includes/exception/ErrorPageError.php new file mode 100644 index 00000000..4b181267 --- /dev/null +++ b/www/wiki/includes/exception/ErrorPageError.php @@ -0,0 +1,72 @@ +<?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 + */ + +/** + * An error page which can definitely be safely rendered using the OutputPage. + * + * @since 1.7 + * @ingroup Exception + */ +class ErrorPageError extends MWException implements ILocalizedException { + public $title, $msg, $params; + + /** + * Note: these arguments are keys into wfMessage(), not text! + * + * @param string|Message $title Message key (string) for page title, or a Message object + * @param string|Message $msg Message key (string) for error text, or a Message object + * @param array $params Array with parameters to wfMessage() + */ + public function __construct( $title, $msg, $params = [] ) { + $this->title = $title; + $this->msg = $msg; + $this->params = $params; + + // T46111: Messages in the log files should be in English and not + // customized by the local wiki. So get the default English version for + // passing to the parent constructor. Our overridden report() below + // makes sure that the page shown to the user is not forced to English. + $enMsg = $this->getMessageObject(); + $enMsg->inLanguage( 'en' )->useDatabase( false ); + parent::__construct( $enMsg->text() ); + } + + /** + * Return a Message object for this exception + * @since 1.29 + * @return Message + */ + public function getMessageObject() { + if ( $this->msg instanceof Message ) { + return clone $this->msg; + } + return wfMessage( $this->msg, $this->params ); + } + + public function report() { + if ( self::isCommandLine() || defined( 'MW_API' ) ) { + parent::report(); + } else { + global $wgOut; + $wgOut->showErrorPage( $this->title, $this->msg, $this->params ); + $wgOut->output(); + } + } +} diff --git a/www/wiki/includes/exception/FatalError.php b/www/wiki/includes/exception/FatalError.php new file mode 100644 index 00000000..a7d672fa --- /dev/null +++ b/www/wiki/includes/exception/FatalError.php @@ -0,0 +1,43 @@ +<?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 + */ + +/** + * Exception class which takes an HTML error message, and does not + * produce a backtrace. Replacement for OutputPage::fatalError(). + * + * @since 1.7 + * @ingroup Exception + */ +class FatalError extends MWException { + + /** + * @return string + */ + public function getHTML() { + return $this->getMessage(); + } + + /** + * @return string + */ + public function getText() { + return $this->getMessage(); + } +} diff --git a/www/wiki/includes/exception/HttpError.php b/www/wiki/includes/exception/HttpError.php new file mode 100644 index 00000000..f464d8af --- /dev/null +++ b/www/wiki/includes/exception/HttpError.php @@ -0,0 +1,129 @@ +<?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 + */ + +use MediaWiki\Logger\LoggerFactory; + +/** + * Show an error that looks like an HTTP server error. + * Replacement for wfHttpError(). + * + * @since 1.19 + * @ingroup Exception + */ +class HttpError extends MWException { + private $httpCode, $header, $content; + + /** + * @param int $httpCode HTTP status code to send to the client + * @param string|Message $content Content of the message + * @param string|Message|null $header Content of the header (\<title\> and \<h1\>) + */ + public function __construct( $httpCode, $content, $header = null ) { + parent::__construct( $content ); + $this->httpCode = (int)$httpCode; + $this->header = $header; + $this->content = $content; + } + + /** + * 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 + */ + public function getStatusCode() { + return $this->httpCode; + } + + /** + * 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(); + + HttpStatus::header( $this->httpCode ); + header( 'Content-type: text/html; charset=utf-8' ); + + print $this->getHTML(); + } + + private function doLog() { + $logger = LoggerFactory::getInstance( 'HttpError' ); + $content = $this->content; + + if ( $content instanceof Message ) { + $content = $content->text(); + } + + $context = [ + '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. + * + * @return string HTML + */ + public function getHTML() { + if ( $this->header === null ) { + $titleHtml = htmlspecialchars( HttpStatus::getMessage( $this->httpCode ) ); + } elseif ( $this->header instanceof Message ) { + $titleHtml = $this->header->escaped(); + } else { + $titleHtml = htmlspecialchars( $this->header ); + } + + if ( $this->content instanceof Message ) { + $contentHtml = $this->content->escaped(); + } else { + $contentHtml = nl2br( htmlspecialchars( $this->content ) ); + } + + return "<!DOCTYPE html>\n" . + "<html><head><title>$titleHtml</title></head>\n" . + "<body><h1>$titleHtml</h1><p>$contentHtml</p></body></html>\n"; + } +} diff --git a/www/wiki/includes/exception/LocalizedException.php b/www/wiki/includes/exception/LocalizedException.php new file mode 100644 index 00000000..f5f8c84e --- /dev/null +++ b/www/wiki/includes/exception/LocalizedException.php @@ -0,0 +1,66 @@ +<?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 + */ + +/** + * Interface for MediaWiki-localized exceptions + * + * @since 1.29 + * @ingroup Exception + */ +interface ILocalizedException { + /** + * Return a Message object for this exception + * @return Message + */ + public function getMessageObject(); +} + +/** + * Basic localized exception. + * + * @since 1.29 + * @ingroup Exception + * @note Don't use this in a situation where MessageCache is not functional. + */ +class LocalizedException extends Exception implements ILocalizedException { + /** @var string|array|MessageSpecifier */ + protected $messageSpec; + + /** + * @param string|array|MessageSpecifier $messageSpec See Message::newFromSpecifier + * @param int $code + * @param Exception|Throwable $previous The previous exception used for the exception chaining. + */ + public function __construct( $messageSpec, $code = 0, $previous = null ) { + $this->messageSpec = $messageSpec; + + // Exception->getMessage() should be in plain English, not localized. + // So fetch the English version of the message, without local + // customizations, and make a basic attempt to turn markup into text. + $msg = $this->getMessageObject()->inLanguage( 'en' )->useDatabase( false )->text(); + $msg = preg_replace( '!</?(var|kbd|samp|code)>!', '"', $msg ); + $msg = Sanitizer::stripAllTags( $msg ); + parent::__construct( $msg, $code, $previous ); + } + + public function getMessageObject() { + return Message::newFromSpecifier( $this->messageSpec ); + } +} diff --git a/www/wiki/includes/exception/MWContentSerializationException.php b/www/wiki/includes/exception/MWContentSerializationException.php new file mode 100644 index 00000000..500cf7ce --- /dev/null +++ b/www/wiki/includes/exception/MWContentSerializationException.php @@ -0,0 +1,8 @@ +<?php +/** + * Exception representing a failure to serialize or unserialize a content object. + * + * @ingroup Content + */ +class MWContentSerializationException extends MWException { +} diff --git a/www/wiki/includes/exception/MWException.php b/www/wiki/includes/exception/MWException.php new file mode 100644 index 00000000..b3e9422b --- /dev/null +++ b/www/wiki/includes/exception/MWException.php @@ -0,0 +1,230 @@ +<?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 + */ + +/** + * MediaWiki exception + * + * @ingroup Exception + */ +class MWException extends Exception { + /** + * Should the exception use $wgOut to output the error? + * + * @return bool + */ + public function useOutputPage() { + return $this->useMessageCache() && + !empty( $GLOBALS['wgFullyInitialised'] ) && + !empty( $GLOBALS['wgOut'] ) && + !defined( 'MEDIAWIKI_INSTALL' ); + } + + /** + * Whether to log this exception in the exception debug log. + * + * @since 1.23 + * @return bool + */ + public function isLoggable() { + return true; + } + + /** + * Can the extension use the Message class/wfMessage to get i18n-ed messages? + * + * @return bool + */ + public function useMessageCache() { + global $wgLang; + + foreach ( $this->getTrace() as $frame ) { + if ( isset( $frame['class'] ) && $frame['class'] === LocalisationCache::class ) { + return false; + } + } + + return $wgLang instanceof Language; + } + + /** + * 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 + */ + public function msg( $key, $fallback /*[, params...] */ ) { + $args = array_slice( func_get_args(), 2 ); + + if ( $this->useMessageCache() ) { + try { + return wfMessage( $key, $args )->text(); + } catch ( Exception $e ) { + } + } + return wfMsgReplaceArgs( $fallback, $args ); + } + + /** + * 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. + * + * @return string Html to output + */ + public function getHTML() { + global $wgShowExceptionDetails; + + if ( $wgShowExceptionDetails ) { + return '<p>' . nl2br( htmlspecialchars( MWExceptionHandler::getLogMessage( $this ) ) ) . + '</p><p>Backtrace:</p><p>' . + nl2br( htmlspecialchars( MWExceptionHandler::getRedactedTraceAsString( $this ) ) ) . + "</p>\n"; + } else { + $logId = WebRequest::getRequestId(); + $type = static::class; + return Html::errorBox( + htmlspecialchars( + '[' . $logId . '] ' . + gmdate( 'Y-m-d H:i:s' ) . ": " . + $this->msg( "internalerror-fatal-exception", + "Fatal exception of type $1", + $type, + $logId, + MWExceptionHandler::getURL( $this ) + ) + ) ) . + "<!-- Set \$wgShowExceptionDetails = true; " . + "at the bottom of LocalSettings.php to show detailed " . + "debugging information. -->"; + } + } + + /** + * Get the text to display when reporting the error on the command line. + * If $wgShowExceptionDetails is true, return a text message with a + * backtrace to the error. + * + * @return string + */ + public function getText() { + global $wgShowExceptionDetails; + + if ( $wgShowExceptionDetails ) { + return MWExceptionHandler::getLogMessage( $this ) . + "\nBacktrace:\n" . MWExceptionHandler::getRedactedTraceAsString( $this ) . "\n"; + } else { + return "Set \$wgShowExceptionDetails = true; " . + "in LocalSettings.php to show detailed debugging information.\n"; + } + } + + /** + * Return the title of the page when reporting this error in a HTTP response. + * + * @return string + */ + public function getPageTitle() { + return $this->msg( 'internalerror', 'Internal error' ); + } + + /** + * Output the exception report using HTML. + */ + public function reportHTML() { + global $wgOut, $wgSitename; + if ( $this->useOutputPage() ) { + $wgOut->prepareErrorPage( $this->getPageTitle() ); + + $wgOut->addHTML( $this->getHTML() ); + + $wgOut->output(); + } else { + self::header( 'Content-Type: text/html; charset=utf-8' ); + echo "<!DOCTYPE html>\n" . + '<html><head>' . + // Mimick OutputPage::setPageTitle behaviour + '<title>' . + htmlspecialchars( $this->msg( 'pagetitle', "$1 - $wgSitename", $this->getPageTitle() ) ) . + '</title>' . + '<style>body { font-family: sans-serif; margin: 0; padding: 0.5em 2em; }</style>' . + "</head><body>\n"; + + echo $this->getHTML(); + + echo "</body></html>\n"; + } + } + + /** + * Output a report about the exception and takes care of formatting. + * 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_' . static::class ); + wfHttpError( 500, 'Internal Server Error', $this->getText() ); + } elseif ( self::isCommandLine() ) { + $message = $this->getText(); + // T17602: STDERR may not be available + if ( !defined( 'MW_PHPUNIT_TEST' ) && defined( 'STDERR' ) ) { + fwrite( STDERR, $message ); + } else { + echo $message; + } + } else { + self::statusHeader( 500 ); + self::header( "Content-Type: $wgMimeType; charset=utf-8" ); + + $this->reportHTML(); + } + } + + /** + * Check whether we are in command line mode or not to report the exception + * in the correct format. + * + * @return bool + */ + public static function isCommandLine() { + return !empty( $GLOBALS['wgCommandLineMode'] ); + } + + /** + * Send a header, if we haven't already sent them. We shouldn't, + * but sometimes we might in a weird case like Export + * @param string $header + */ + private static function header( $header ) { + if ( !headers_sent() ) { + header( $header ); + } + } + private static function statusHeader( $code ) { + if ( !headers_sent() ) { + HttpStatus::header( $code ); + } + } +} diff --git a/www/wiki/includes/exception/MWExceptionHandler.php b/www/wiki/includes/exception/MWExceptionHandler.php new file mode 100644 index 00000000..79f0a233 --- /dev/null +++ b/www/wiki/includes/exception/MWExceptionHandler.php @@ -0,0 +1,697 @@ +<?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 + */ + +use MediaWiki\Logger\LoggerFactory; +use MediaWiki\MediaWikiServices; +use Psr\Log\LogLevel; +use Wikimedia\Rdbms\DBError; + +/** + * Handler class for MWExceptions + * @ingroup Exception + */ +class MWExceptionHandler { + const CAUGHT_BY_HANDLER = 'mwe_handler'; // error reported by this exception handler + const CAUGHT_BY_OTHER = 'other'; // error reported by direct logException() call + + /** + * @var string $reservedMemory + */ + protected static $reservedMemory; + /** + * @var array $fatalErrorTypes + */ + protected static $fatalErrorTypes = [ + E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, + /* HHVM's FATAL_ERROR level */ 16777217, + ]; + /** + * @var bool $handledFatalCallback + */ + protected static $handledFatalCallback = false; + + /** + * Install handlers with PHP. + */ + public static function installHandler() { + set_exception_handler( 'MWExceptionHandler::handleUncaughtException' ); + set_error_handler( 'MWExceptionHandler::handleError' ); + + // Reserve 16k of memory so we can report OOM fatals + self::$reservedMemory = str_repeat( ' ', 16384 ); + register_shutdown_function( 'MWExceptionHandler::handleFatalError' ); + } + + /** + * Report an exception to the user + * @param Exception|Throwable $e + */ + protected static function report( $e ) { + try { + // Try and show the exception prettily, with the normal skin infrastructure + if ( $e instanceof MWException ) { + // Delegate to MWException until all subclasses are handled by + // MWExceptionRenderer and MWException::report() has been + // removed. + $e->report(); + } else { + 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_RAW, $e2 ); + } + } + + /** + * Roll back any open database transactions and log the stack trace of the exception + * + * This method is used to attempt to recover from exceptions + * + * @since 1.23 + * @param Exception|Throwable $e + */ + public static function rollbackMasterChangesAndLog( $e ) { + $services = MediaWikiServices::getInstance(); + if ( !$services->isServiceDisabled( 'DBLoadBalancerFactory' ) ) { + // Rollback DBs to avoid transaction notices. This might fail + // to rollback some databases due to connection issues or exceptions. + // However, any sane DB driver will rollback implicitly anyway. + try { + $services->getDBLoadBalancerFactory()->rollbackMasterChanges( __METHOD__ ); + } 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::CAUGHT_BY_HANDLER ); + } + } + + self::logException( $e, self::CAUGHT_BY_HANDLER ); + } + + /** + * Callback to use with PHP's set_exception_handler. + * + * @since 1.31 + * @param Exception|Throwable $e + */ + public static function handleUncaughtException( $e ) { + self::handleException( $e ); + + // Make sure we don't claim success on exit for CLI scripts (T177414) + if ( wfIsCLI() ) { + register_shutdown_function( + function () { + exit( 255 ); + } + ); + } + } + + /** + * Exception handler which simulates the appropriate catch() handling: + * + * try { + * ... + * } catch ( Exception $e ) { + * $e->report(); + * } catch ( Exception $e ) { + * echo $e->__toString(); + * } + * + * @since 1.25 + * @param Exception|Throwable $e + */ + public static function handleException( $e ) { + self::rollbackMasterChangesAndLog( $e ); + self::report( $e ); + } + + /** + * Handler for set_error_handler() callback notifications. + * + * Receive a callback from the interpreter for a raised error, create an + * ErrorException, and log the exception to the 'error' logging + * channel(s). If the raised error is a fatal error type (only under HHVM) + * delegate to handleFatalError() instead. + * + * @since 1.25 + * + * @param int $level Error level raised + * @param string $message + * @param string $file + * @param int $line + * @return bool + * + * @see logError() + */ + public static function handleError( + $level, $message, $file = null, $line = null + ) { + global $wgPropagateErrors; + + if ( in_array( $level, self::$fatalErrorTypes ) ) { + return call_user_func_array( + 'MWExceptionHandler::handleFatalError', func_get_args() + ); + } + + // Map error constant to error name (reverse-engineer PHP error + // reporting) + switch ( $level ) { + case E_RECOVERABLE_ERROR: + $levelName = 'Error'; + $severity = LogLevel::ERROR; + break; + case E_WARNING: + case E_CORE_WARNING: + case E_COMPILE_WARNING: + case E_USER_WARNING: + $levelName = 'Warning'; + $severity = LogLevel::WARNING; + break; + case E_NOTICE: + case E_USER_NOTICE: + $levelName = 'Notice'; + $severity = LogLevel::INFO; + break; + case E_STRICT: + $levelName = 'Strict Standards'; + $severity = LogLevel::DEBUG; + break; + case E_DEPRECATED: + case E_USER_DEPRECATED: + $levelName = 'Deprecated'; + $severity = LogLevel::INFO; + break; + default: + $levelName = 'Unknown error'; + $severity = LogLevel::ERROR; + break; + } + + $e = new ErrorException( "PHP $levelName: $message", 0, $level, $file, $line ); + self::logError( $e, 'error', $severity ); + + // If $wgPropagateErrors is true return false so PHP shows/logs the error normally. + // Ignore $wgPropagateErrors if the error should break execution, or track_errors is set + // (which means someone is counting on regular PHP error handling behavior). + return !( $wgPropagateErrors || $level == E_RECOVERABLE_ERROR || ini_get( 'track_errors' ) ); + } + + /** + * Dual purpose callback used as both a set_error_handler() callback and + * a registered shutdown function. Receive a callback from the interpreter + * for a raised error or system shutdown, check for a fatal error, and log + * to the 'fatal' logging channel. + * + * 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 + * + * @param int $level Error level raised + * @param string $message Error message + * @param string $file File that error was raised in + * @param int $line Line number error was raised at + * @param array $context Active symbol table point of error + * @param array $trace Backtrace at point of error (undocumented HHVM + * feature) + * @return bool Always returns false + */ + public static function handleFatalError( + $level = null, $message = null, $file = null, $line = null, + $context = null, $trace = null + ) { + // Free reserved memory so that we have space to process OOM + // errors + self::$reservedMemory = null; + + if ( $level === null ) { + // Called as a shutdown handler, get data from error_get_last() + if ( static::$handledFatalCallback ) { + // Already called once (probably as an error handler callback + // under HHVM) so don't log again. + return false; + } + + $lastError = error_get_last(); + if ( $lastError !== null ) { + $level = $lastError['type']; + $message = $lastError['message']; + $file = $lastError['file']; + $line = $lastError['line']; + } else { + $level = 0; + $message = ''; + } + } + + if ( !in_array( $level, self::$fatalErrorTypes ) ) { + // Only interested in fatal errors, others should have been + // handled by MWExceptionHandler::handleError + return false; + } + + $url = WebRequest::getGlobalRequestURL(); + $msgParts = [ + '[{exception_id}] {exception_url} PHP Fatal Error', + ( $line || $file ) ? ' from' : '', + $line ? " line $line" : '', + ( $line && $file ) ? ' of' : '', + $file ? " $file" : '', + ": $message", + ]; + $msg = implode( '', $msgParts ); + + // Look at message to see if this is a class not found failure + // HHVM: Class undefined: foo + // PHP5: Class 'foo' not found + if ( preg_match( "/Class (undefined: \w+|'\w+' not found)/", $message ) ) { + // phpcs:disable Generic.Files.LineLength + $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; + // phpcs:enable + } + + // We can't just create an exception and log it as it is likely that + // the interpreter has unwound the stack already. If that is true the + // stacktrace we would get would be functionally empty. If however we + // have been called as an error handler callback *and* HHVM is in use + // we will have been provided with a useful stacktrace that we can + // log. + $trace = $trace ?: debug_backtrace(); + $logger = LoggerFactory::getInstance( 'fatal' ); + $logger->error( $msg, [ + 'fatal_exception' => [ + 'class' => ErrorException::class, + 'message' => "PHP Fatal Error: {$message}", + 'code' => $level, + 'file' => $file, + 'line' => $line, + 'trace' => self::prettyPrintTrace( self::redactTrace( $trace ) ), + ], + 'exception_id' => WebRequest::getRequestId(), + 'exception_url' => $url, + 'caught_by' => self::CAUGHT_BY_HANDLER + ] ); + + // Remember call so we don't double process via HHVM's fatal + // notifications and the shutdown hook behavior + static::$handledFatalCallback = true; + return false; + } + + /** + * Generate a string representation of an exception's stack trace + * + * Like Exception::getTraceAsString, but replaces argument values with + * argument type or class name. + * + * @param Exception|Throwable $e + * @return string + * @see prettyPrintTrace() + */ + public static function getRedactedTraceAsString( $e ) { + return self::prettyPrintTrace( self::getRedactedTrace( $e ) ); + } + + /** + * Generate a string representation of a stacktrace. + * + * @param array $trace + * @param string $pad Constant padding to add to each line of trace + * @return string + * @since 1.26 + */ + public static function prettyPrintTrace( array $trace, $pad = '' ) { + $text = ''; + + $level = 0; + foreach ( $trace as $level => $frame ) { + if ( isset( $frame['file'] ) && isset( $frame['line'] ) ) { + $text .= "{$pad}#{$level} {$frame['file']}({$frame['line']}): "; + } else { + // 'file' and 'line' are unset for calls via call_user_func + // (T57634) This matches behaviour of + // Exception::getTraceAsString to instead display "[internal + // function]". + $text .= "{$pad}#{$level} [internal function]: "; + } + + if ( isset( $frame['class'] ) && isset( $frame['type'] ) && isset( $frame['function'] ) ) { + $text .= $frame['class'] . $frame['type'] . $frame['function']; + } elseif ( isset( $frame['function'] ) ) { + $text .= $frame['function']; + } else { + $text .= 'NO_FUNCTION_GIVEN'; + } + + if ( isset( $frame['args'] ) ) { + $text .= '(' . implode( ', ', $frame['args'] ) . ")\n"; + } else { + $text .= "()\n"; + } + } + + $level = $level + 1; + $text .= "{$pad}#{$level} {main}"; + + return $text; + } + + /** + * Return a copy of an exception's backtrace as an array. + * + * Like Exception::getTrace, but replaces each element in each frame's + * argument array with the name of its class (if the element is an object) + * or its type (if the element is a PHP primitive). + * + * @since 1.22 + * @param Exception|Throwable $e + * @return array + */ + public static function getRedactedTrace( $e ) { + return static::redactTrace( $e->getTrace() ); + } + + /** + * Redact a stacktrace generated by Exception::getTrace(), + * debug_backtrace() or similar means. Replaces each element in each + * frame's argument array with the name of its class (if the element is an + * object) or its type (if the element is a PHP primitive). + * + * @since 1.26 + * @param array $trace Stacktrace + * @return array Stacktrace with arugment values converted to data types + */ + public static function redactTrace( array $trace ) { + return array_map( function ( $frame ) { + if ( isset( $frame['args'] ) ) { + $frame['args'] = array_map( function ( $arg ) { + return is_object( $arg ) ? get_class( $arg ) : gettype( $arg ); + }, $frame['args'] ); + } + return $frame; + }, $trace ); + } + + /** + * 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. + * + * @since 1.22 + * @deprecated since 1.27: Exception IDs are synonymous with request IDs. + * @param Exception|Throwable $e + * @return string + */ + public static function getLogId( $e ) { + wfDeprecated( __METHOD__, '1.27' ); + return WebRequest::getRequestId(); + } + + /** + * If the exception occurred in the course of responding to a request, + * returns the requested URL. Otherwise, returns false. + * + * @since 1.23 + * @return string|false + */ + public static function getURL() { + global $wgRequest; + if ( !isset( $wgRequest ) || $wgRequest instanceof FauxRequest ) { + return false; + } + return $wgRequest->getRequestURL(); + } + + /** + * Get a message formatting the exception message and its origin. + * + * @since 1.22 + * @param Exception|Throwable $e + * @return string + */ + public static function getLogMessage( $e ) { + $id = WebRequest::getRequestId(); + $type = get_class( $e ); + $file = $e->getFile(); + $line = $e->getLine(); + $message = $e->getMessage(); + $url = self::getURL() ?: '[no req]'; + + return "[$id] $url $type from line $line of $file: $message"; + } + + /** + * Get a normalised message for formatting with PSR-3 log event context. + * + * Must be used together with `getLogContext()` to be useful. + * + * @since 1.30 + * @param Exception|Throwable $e + * @return string + */ + public static function getLogNormalMessage( $e ) { + $type = get_class( $e ); + $file = $e->getFile(); + $line = $e->getLine(); + $message = $e->getMessage(); + + return "[{exception_id}] {exception_url} $type from line $line of $file: $message"; + } + + /** + * @param Exception|Throwable $e + * @return string + */ + public static function getPublicLogMessage( $e ) { + $reqId = WebRequest::getRequestId(); + $type = get_class( $e ); + return '[' . $reqId . '] ' + . gmdate( 'Y-m-d H:i:s' ) . ': ' + . 'Fatal exception of type "' . $type . '"'; + } + + /** + * Get a PSR-3 log event context from an Exception. + * + * Creates a structured array containing information about the provided + * exception that can be used to augment a log message sent to a PSR-3 + * logger. + * + * @param Exception|Throwable $e + * @param string $catcher CAUGHT_BY_* class constant indicating what caught the error + * @return array + */ + public static function getLogContext( $e, $catcher = self::CAUGHT_BY_OTHER ) { + return [ + 'exception' => $e, + 'exception_id' => WebRequest::getRequestId(), + 'exception_url' => self::getURL() ?: '[no req]', + 'caught_by' => $catcher + ]; + } + + /** + * Get a structured representation of an Exception. + * + * Returns an array of structured data (class, message, code, file, + * backtrace) derived from the given exception. The backtrace information + * will be redacted as per getRedactedTraceAsArray(). + * + * @param Exception|Throwable $e + * @param string $catcher CAUGHT_BY_* class constant indicating what caught the error + * @return array + * @since 1.26 + */ + public static function getStructuredExceptionData( $e, $catcher = self::CAUGHT_BY_OTHER ) { + global $wgLogExceptionBacktrace; + + $data = [ + 'id' => WebRequest::getRequestId(), + 'type' => get_class( $e ), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'message' => $e->getMessage(), + 'code' => $e->getCode(), + 'url' => self::getURL() ?: null, + 'caught_by' => $catcher + ]; + + if ( $e instanceof ErrorException && + ( error_reporting() & $e->getSeverity() ) === 0 + ) { + // Flag surpressed errors + $data['suppressed'] = true; + } + + if ( $wgLogExceptionBacktrace ) { + $data['backtrace'] = self::getRedactedTrace( $e ); + } + + $previous = $e->getPrevious(); + if ( $previous !== null ) { + $data['previous'] = self::getStructuredExceptionData( $previous, $catcher ); + } + + return $data; + } + + /** + * Serialize an Exception object to JSON. + * + * The JSON object will have keys 'id', 'file', 'line', 'message', and + * 'url'. These keys map to string values, with the exception of 'line', + * which is a number, and 'url', which may be either a string URL or or + * null if the exception did not occur in the context of serving a web + * request. + * + * If $wgLogExceptionBacktrace is true, it will also have a 'backtrace' + * key, mapped to the array return value of Exception::getTrace, but with + * each element in each frame's "args" array (if set) replaced with the + * argument's class name (if the argument is an object) or type name (if + * the argument is a PHP primitive). + * + * @par Sample JSON record ($wgLogExceptionBacktrace = false): + * @code + * { + * "id": "c41fb419", + * "type": "MWException", + * "file": "/var/www/mediawiki/includes/cache/MessageCache.php", + * "line": 704, + * "message": "Non-string key given", + * "url": "/wiki/Main_Page" + * } + * @endcode + * + * @par Sample JSON record ($wgLogExceptionBacktrace = true): + * @code + * { + * "id": "dc457938", + * "type": "MWException", + * "file": "/vagrant/mediawiki/includes/cache/MessageCache.php", + * "line": 704, + * "message": "Non-string key given", + * "url": "/wiki/Main_Page", + * "backtrace": [{ + * "file": "/vagrant/mediawiki/extensions/VisualEditor/VisualEditor.hooks.php", + * "line": 80, + * "function": "get", + * "class": "MessageCache", + * "type": "->", + * "args": ["array"] + * }] + * } + * @endcode + * + * @since 1.23 + * @param Exception|Throwable $e + * @param bool $pretty Add non-significant whitespace to improve readability (default: false). + * @param int $escaping Bitfield consisting of FormatJson::.*_OK class constants. + * @param string $catcher CAUGHT_BY_* class constant indicating what caught the error + * @return string|false JSON string if successful; false upon failure + */ + public static function jsonSerializeException( + $e, $pretty = false, $escaping = 0, $catcher = self::CAUGHT_BY_OTHER + ) { + return FormatJson::encode( + self::getStructuredExceptionData( $e, $catcher ), + $pretty, + $escaping + ); + } + + /** + * 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 exceptions or exceptions from other libraries. + * + * @since 1.22 + * @param Exception|Throwable $e + * @param string $catcher CAUGHT_BY_* class constant indicating what caught the error + */ + public static function logException( $e, $catcher = self::CAUGHT_BY_OTHER ) { + if ( !( $e instanceof MWException ) || $e->isLoggable() ) { + $logger = LoggerFactory::getInstance( 'exception' ); + $logger->error( + self::getLogNormalMessage( $e ), + self::getLogContext( $e, $catcher ) + ); + + $json = self::jsonSerializeException( $e, false, FormatJson::ALL_OK, $catcher ); + if ( $json !== false ) { + $logger = LoggerFactory::getInstance( 'exception-json' ); + $logger->error( $json, [ 'private' => true ] ); + } + + Hooks::run( 'LogException', [ $e, false ] ); + } + } + + /** + * Log an exception that wasn't thrown but made to wrap an error. + * + * @since 1.25 + * @param ErrorException $e + * @param string $channel + * @param string $level + */ + protected static function logError( + ErrorException $e, $channel, $level = LogLevel::ERROR + ) { + $catcher = self::CAUGHT_BY_HANDLER; + // The set_error_handler callback is independent from error_reporting. + // Filter out unwanted errors manually (e.g. when + // Wikimedia\suppressWarnings is active). + $suppressed = ( error_reporting() & $e->getSeverity() ) === 0; + if ( !$suppressed ) { + $logger = LoggerFactory::getInstance( $channel ); + $logger->log( + $level, + self::getLogNormalMessage( $e ), + self::getLogContext( $e, $catcher ) + ); + } + + // Include all errors in the json log (surpressed errors will be flagged) + $json = self::jsonSerializeException( $e, false, FormatJson::ALL_OK, $catcher ); + if ( $json !== false ) { + $logger = LoggerFactory::getInstance( "{$channel}-json" ); + $logger->log( $level, $json, [ 'private' => true ] ); + } + + Hooks::run( 'LogException', [ $e, $suppressed ] ); + } +} diff --git a/www/wiki/includes/exception/MWExceptionRenderer.php b/www/wiki/includes/exception/MWExceptionRenderer.php new file mode 100644 index 00000000..5d750365 --- /dev/null +++ b/www/wiki/includes/exception/MWExceptionRenderer.php @@ -0,0 +1,338 @@ +<?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 + */ + +use Wikimedia\Rdbms\DBConnectionError; +use Wikimedia\Rdbms\DBError; +use Wikimedia\Rdbms\DBReadOnlyError; +use Wikimedia\Rdbms\DBExpectedError; + +/** + * 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|Throwable $e Original exception + * @param int $mode MWExceptionExposer::AS_* constant + * @param Exception|Throwable|null $eNew New exception from attempting to show the first + */ + public static function output( $e, $mode, $eNew = null ) { + 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( $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" ); + if ( $e instanceof DBConnectionError ) { + self::reportOutageHTML( $e ); + } else { + self::reportHTML( $e ); + } + } else { + self::statusHeader( 500 ); + self::header( "Content-Type: $wgMimeType; charset=utf-8" ); + 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 .= 'Original exception: ' . + MWExceptionHandler::getPublicLogMessage( $e ); + $message .= "\n\nException caught inside exception handler.\n\n" . + self::getShowBacktraceError( $e ); + } + $message .= "\n"; + } else { + if ( self::showBackTrace( $e ) ) { + $message = MWExceptionHandler::getLogMessage( $e ) . + "\nBacktrace:\n" . + MWExceptionHandler::getRedactedTraceAsString( $e ) . "\n"; + } else { + $message = MWExceptionHandler::getPublicLogMessage( $e ); + } + } + echo nl2br( htmlspecialchars( $message ) ) . "\n"; + } + } + + /** + * @param Exception|Throwable $e + * @return bool Should the exception use $wgOut to output the error? + */ + private static function useOutputPage( $e ) { + // Can the extension use the Message class/wfMessage to get i18n-ed messages? + foreach ( $e->getTrace() as $frame ) { + if ( isset( $frame['class'] ) && $frame['class'] === LocalisationCache::class ) { + return false; + } + } + + // Don't even bother with OutputPage if there's no Title context set, + // (e.g. we're in RL code on load.php) - the Skin system (and probably + // most of MediaWiki) won't work. + + return ( + !empty( $GLOBALS['wgFullyInitialised'] ) && + !empty( $GLOBALS['wgOut'] ) && + RequestContext::getMain()->getTitle() && + !defined( 'MEDIAWIKI_INSTALL' ) + ); + } + + /** + * Output the exception report using HTML + * + * @param Exception|Throwable $e + */ + private static function reportHTML( $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' ) ); + } + + // Show any custom GUI message before the details + if ( $e instanceof MessageSpecifier ) { + $wgOut->addHTML( Html::element( 'p', [], Message::newFromSpecifier( $e )->text() ) ); + } + $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"; + + 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|Throwable $e + * @return string Html to output + */ + public static function getHTML( $e ) { + if ( self::showBackTrace( $e ) ) { + $html = "<div class=\"errorbox mw-content-ltr\"><p>" . + nl2br( htmlspecialchars( MWExceptionHandler::getLogMessage( $e ) ) ) . + '</p><p>Backtrace:</p><p>' . + nl2br( htmlspecialchars( MWExceptionHandler::getRedactedTraceAsString( $e ) ) ) . + "</p></div>\n"; + } else { + $logId = WebRequest::getRequestId(); + $html = "<div class=\"errorbox mw-content-ltr\">" . + htmlspecialchars( + '[' . $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" . + "<!-- " . wordwrap( self::getShowBacktraceError( $e ), 50 ) . " -->"; + } + + return $html; + } + + /** + * 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|Throwable $e + * @return string + */ + private static function getText( $e ) { + if ( self::showBackTrace( $e ) ) { + return MWExceptionHandler::getLogMessage( $e ) . + "\nBacktrace:\n" . + MWExceptionHandler::getRedactedTraceAsString( $e ) . "\n"; + } else { + return self::getShowBacktraceError( $e ) . "\n"; + } + } + + /** + * @param Exception|Throwable $e + * @return bool + */ + private static function showBackTrace( $e ) { + global $wgShowExceptionDetails, $wgShowDBErrorBacktrace; + + return ( + $wgShowExceptionDetails && + ( !( $e instanceof DBError ) || $wgShowDBErrorBacktrace ) + ); + } + + /** + * @param Exception|Throwable $e + * @return string + */ + private static function getShowBacktraceError( $e ) { + global $wgShowExceptionDetails, $wgShowDBErrorBacktrace; + $vars = []; + if ( !$wgShowExceptionDetails ) { + $vars[] = '$wgShowExceptionDetails = true;'; + } + if ( $e instanceof DBError && !$wgShowDBErrorBacktrace ) { + $vars[] = '$wgShowDBErrorBacktrace = true;'; + } + $vars = implode( ' and ', $vars ); + return "Set $vars at the bottom of LocalSettings.php to show detailed debugging information."; + } + + /** + * @return bool + */ + private static function isCommandLine() { + return !empty( $GLOBALS['wgCommandLineMode'] ); + } + + /** + * @param string $header + */ + private static function header( $header ) { + if ( !headers_sent() ) { + header( $header ); + } + } + + /** + * @param int $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|Throwable $e + */ + private static function reportOutageHTML( $e ) { + global $wgShowDBErrorBacktrace, $wgShowHostnames, $wgShowSQLErrors, $wgSitename; + + $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' ], $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 = "<!DOCTYPE html>\n" . + '<html><head>' . + '<title>' . + htmlspecialchars( $wgSitename ) . + '</title>' . + '<style>body { font-family: sans-serif; margin: 0; padding: 0.5em 2em; }</style>' . + "</head><body><h1>$sorry</h1><p>$again</p><p><small>$info</small></p>"; + + if ( $wgShowDBErrorBacktrace ) { + $html .= '<p>Backtrace:</p><pre>' . + htmlspecialchars( $e->getTraceAsString() ) . '</pre>'; + } + + $html .= '</body></html>'; + echo $html; + } +} diff --git a/www/wiki/includes/exception/MWUnknownContentModelException.php b/www/wiki/includes/exception/MWUnknownContentModelException.php new file mode 100644 index 00000000..df7111ac --- /dev/null +++ b/www/wiki/includes/exception/MWUnknownContentModelException.php @@ -0,0 +1,25 @@ +<?php +/** + * Exception thrown when an unregistered content model is requested. This error + * can be triggered by user input, so a separate exception class is provided so + * callers can substitute a context-specific, internationalised error message. + * + * @ingroup Content + * @since 1.27 + */ +class MWUnknownContentModelException extends MWException { + /** @var string The name of the unknown content model */ + private $modelId; + + /** @param string $modelId */ + function __construct( $modelId ) { + parent::__construct( "The content model '$modelId' is not registered on this wiki.\n" . + 'See https://www.mediawiki.org/wiki/Content_handlers to find out which extensions ' . + 'handle this content model.' ); + $this->modelId = $modelId; + } + /** @return string */ + public function getModelId() { + return $this->modelId; + } +} diff --git a/www/wiki/includes/exception/PermissionsError.php b/www/wiki/includes/exception/PermissionsError.php new file mode 100644 index 00000000..cc69a762 --- /dev/null +++ b/www/wiki/includes/exception/PermissionsError.php @@ -0,0 +1,72 @@ +<?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 + */ + +/** + * Show an error when a user tries to do something they do not have the necessary + * permissions for. + * + * @since 1.18 + * @ingroup Exception + */ +class PermissionsError extends ErrorPageError { + public $permission, $errors; + + /** + * @param string|null $permission A permission name or null if unknown + * @param array $errors Error message keys or [key, param...] arrays; must not be empty if + * $permission is null + * @throws \InvalidArgumentException + */ + public function __construct( $permission, $errors = [] ) { + global $wgLang; + + if ( $permission === null && !$errors ) { + throw new \InvalidArgumentException( __METHOD__ . + ': $permission and $errors cannot both be empty' ); + } + + $this->permission = $permission; + + if ( !count( $errors ) ) { + $groups = []; + foreach ( User::getGroupsWithPermission( $this->permission ) as $group ) { + $groups[] = UserGroupMembership::getLink( $group, RequestContext::getMain(), 'wiki' ); + } + + if ( $groups ) { + $errors[] = [ 'badaccess-groups', $wgLang->commaList( $groups ), count( $groups ) ]; + } else { + $errors[] = [ 'badaccess-group0' ]; + } + } + + $this->errors = $errors; + + // Give the parent class something to work with + parent::__construct( 'permissionserrors', Message::newFromSpecifier( $errors[0] ) ); + } + + public function report() { + global $wgOut; + + $wgOut->showPermissionsErrorPage( $this->errors, $this->permission ); + $wgOut->output(); + } +} diff --git a/www/wiki/includes/exception/ProcOpenError.php b/www/wiki/includes/exception/ProcOpenError.php new file mode 100644 index 00000000..f00bcd4b --- /dev/null +++ b/www/wiki/includes/exception/ProcOpenError.php @@ -0,0 +1,29 @@ +<?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 + */ + +namespace MediaWiki; + +use Exception; + +class ProcOpenError extends Exception { + public function __construct() { + parent::__construct( 'proc_open() returned error!' ); + } +} diff --git a/www/wiki/includes/exception/ReadOnlyError.php b/www/wiki/includes/exception/ReadOnlyError.php new file mode 100644 index 00000000..de42f056 --- /dev/null +++ b/www/wiki/includes/exception/ReadOnlyError.php @@ -0,0 +1,36 @@ +<?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 + */ + +/** + * Show an error when the wiki is locked/read-only and the user tries to do + * something that requires write access. + * + * @since 1.18 + * @ingroup Exception + */ +class ReadOnlyError extends ErrorPageError { + public function __construct() { + parent::__construct( + 'readonly', + 'readonlytext', + wfReadOnlyReason() ?: [] + ); + } +} diff --git a/www/wiki/includes/exception/ShellDisabledError.php b/www/wiki/includes/exception/ShellDisabledError.php new file mode 100644 index 00000000..203b58dc --- /dev/null +++ b/www/wiki/includes/exception/ShellDisabledError.php @@ -0,0 +1,32 @@ +<?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 + */ + +namespace MediaWiki; + +use Exception; + +/** + * @since 1.30 + */ +class ShellDisabledError extends Exception { + public function __construct() { + parent::__construct( 'Unable to run external programs, proc_open() is disabled' ); + } +} diff --git a/www/wiki/includes/exception/ThrottledError.php b/www/wiki/includes/exception/ThrottledError.php new file mode 100644 index 00000000..bec0d904 --- /dev/null +++ b/www/wiki/includes/exception/ThrottledError.php @@ -0,0 +1,40 @@ +<?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 + */ + +/** + * Show an error when the user hits a rate limit. + * + * @since 1.18 + * @ingroup Exception + */ +class ThrottledError extends ErrorPageError { + public function __construct() { + parent::__construct( + 'actionthrottled', + 'actionthrottledtext' + ); + } + + public function report() { + global $wgOut; + $wgOut->setStatusCode( 429 ); + parent::report(); + } +} diff --git a/www/wiki/includes/exception/UserBlockedError.php b/www/wiki/includes/exception/UserBlockedError.php new file mode 100644 index 00000000..9d19f8b6 --- /dev/null +++ b/www/wiki/includes/exception/UserBlockedError.php @@ -0,0 +1,33 @@ +<?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 + */ + +/** + * Show an error when the user tries to do something whilst blocked. + * + * @since 1.18 + * @ingroup Exception + */ +class UserBlockedError extends ErrorPageError { + public function __construct( Block $block ) { + // @todo FIXME: Implement a more proper way to get context here. + $params = $block->getPermissionsError( RequestContext::getMain() ); + parent::__construct( 'blockedtitle', array_shift( $params ), $params ); + } +} diff --git a/www/wiki/includes/exception/UserNotLoggedIn.php b/www/wiki/includes/exception/UserNotLoggedIn.php new file mode 100644 index 00000000..6086d559 --- /dev/null +++ b/www/wiki/includes/exception/UserNotLoggedIn.php @@ -0,0 +1,104 @@ +<?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 + */ + +/** + * Redirect a user to the login page + * + * This is essentially an ErrorPageError exception which by default uses the + * '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 LoginHelper::validErrorMessages or with + * the LoginFormValidErrorMessages hook. Otherwise, the user will just be shown the message + * rather than redirected. + * + * @par Example: + * @code + * if( $user->isAnon() ) { + * throw new UserNotLoggedIn(); + * } + * @endcode + * + * Note the parameter order differs from ErrorPageError, this allows you to + * simply specify a reason without overriding the default title. + * + * @par Example: + * @code + * if( $user->isAnon() ) { + * throw new UserNotLoggedIn( 'action-require-loggedin' ); + * } + * @endcode + * + * @see T39627 + * @since 1.20 + * @ingroup Exception + */ +class UserNotLoggedIn extends ErrorPageError { + + /** + * @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. + * Optional, default: 'exception-nologin-text' + * @param string $titleMsg A message key to set the page title. + * Optional, default: 'exception-nologin' + * @param array $params Parameters to wfMessage(). + * Optional, default: [] + */ + public function __construct( + $reasonMsg = 'exception-nologin-text', + $titleMsg = 'exception-nologin', + $params = [] + ) { + parent::__construct( $titleMsg, $reasonMsg, $params ); + } + + /** + * Redirect to Special:Userlogin if the specified message is compatible. Otherwise, + * show an error page as usual. + */ + 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, LoginHelper::getValidErrorMessages() ) ) { + parent::report(); + } + + // Message is valid. Redirec to Special:Userlogin + + $context = RequestContext::getMain(); + + $output = $context->getOutput(); + $query = $context->getRequest()->getValues(); + // Title will be overridden by returnto + unset( $query['title'] ); + // Redirect to Special:Userlogin + $output->redirect( SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [ + // Return to this page when the user logs in + 'returnto' => $context->getTitle()->getFullText(), + 'returntoquery' => wfArrayToCgi( $query ), + 'warning' => $this->msg, + ] ) ); + + $output->output(); + } +} |