* ApiBase::$messageMap is no longer public. Code attempting to access it will
result in a PHP fatal error.
* $wgUserEmailUseReplyTo is now true by default to work around restrictive DMARC policies.
+* Subpages are now enabled by default in the Template namespace. Set
+ $wgNamespacesWithSubpages[NS_TEMPLATE] to false to keep the old behavior.
=== New features in 1.29 ===
* (T5233) A cookie can now be set when a user is autoblocked, to track that user if
they move to a new IP address. This is disabled by default.
+* Added ILocalizedException interface to standardize the use of localized
+ exceptions, largely so the API can handle them more sensibly.
=== External library changes in 1.29 ===
using the new 'errorformat', 'errorlang', and 'errorsuselocal' parameters.
* API error codes may have changed. Most notably, errors from modules using
parameter prefixes (e.g. all query submodules) will no longer be prefixed.
+* ApiPageSet-using modules will report the 'invalidreason' using the specified
+ 'errorformat'.
* action=emailuser may return a "Warnings" status, and now returns 'warnings' and
'errors' subelements (as applicable) instead of 'message'.
* action=imagerotate returns an 'errors' subelement rather than 'errormessage'.
'CopyJobQueue' => __DIR__ . '/maintenance/copyJobQueue.php',
'CoreParserFunctions' => __DIR__ . '/includes/parser/CoreParserFunctions.php',
'CoreTagHooks' => __DIR__ . '/includes/parser/CoreTagHooks.php',
- 'CoreVersionChecker' => __DIR__ . '/includes/registration/CoreVersionChecker.php',
'CreateAndPromote' => __DIR__ . '/maintenance/createAndPromote.php',
'CreateFileOp' => __DIR__ . '/includes/libs/filebackend/fileop/CreateFileOp.php',
'CreditsAction' => __DIR__ . '/includes/actions/CreditsAction.php',
'ILBFactory' => __DIR__ . '/includes/libs/rdbms/lbfactory/ILBFactory.php',
'ILoadBalancer' => __DIR__ . '/includes/libs/rdbms/loadbalancer/ILoadBalancer.php',
'ILoadMonitor' => __DIR__ . '/includes/libs/rdbms/loadmonitor/ILoadMonitor.php',
+ 'ILocalizedException' => __DIR__ . '/includes/exception/LocalizedException.php',
'IMaintainableDatabase' => __DIR__ . '/includes/libs/rdbms/database/IMaintainableDatabase.php',
'IP' => __DIR__ . '/includes/libs/IP.php',
'IPSet' => __DIR__ . '/includes/compat/IPSetCompat.php',
'LocalSettingsGenerator' => __DIR__ . '/includes/installer/LocalSettingsGenerator.php',
'LocalisationCache' => __DIR__ . '/includes/cache/localisation/LocalisationCache.php',
'LocalisationCacheBulkLoad' => __DIR__ . '/includes/cache/localisation/LocalisationCacheBulkLoad.php',
+ 'LocalizedException' => __DIR__ . '/includes/exception/LocalizedException.php',
'LockManager' => __DIR__ . '/includes/libs/lockmanager/LockManager.php',
'LockManagerGroup' => __DIR__ . '/includes/filebackend/lockmanager/LockManagerGroup.php',
'LogEntry' => __DIR__ . '/includes/logging/LogEntry.php',
'MagicWordArray' => __DIR__ . '/includes/MagicWordArray.php',
'MailAddress' => __DIR__ . '/includes/mail/MailAddress.php',
'MainConfigDependency' => __DIR__ . '/includes/cache/CacheDependency.php',
+ 'MaintainableDBConnRef' => __DIR__ . '/includes/libs/rdbms/database/MaintainableDBConnRef.php',
'Maintenance' => __DIR__ . '/maintenance/Maintenance.php',
'MaintenanceFormatInstallDoc' => __DIR__ . '/maintenance/formatInstallDoc.php',
'MakeTestEdits' => __DIR__ . '/maintenance/makeTestEdits.php',
'UzConverter' => __DIR__ . '/languages/classes/LanguageUz.php',
'VFormHTMLForm' => __DIR__ . '/includes/htmlform/VFormHTMLForm.php',
'ValidateRegistrationFile' => __DIR__ . '/maintenance/validateRegistrationFile.php',
+ 'VersionChecker' => __DIR__ . '/includes/registration/VersionChecker.php',
'ViewAction' => __DIR__ . '/includes/actions/ViewAction.php',
'ViewCLI' => __DIR__ . '/maintenance/view.php',
'VirtualRESTService' => __DIR__ . '/includes/libs/virtualrest/VirtualRESTService.php',
},
"requires": {
"type": "object",
- "description": "Indicates what versions of MediaWiki core are required. This syntax may be extended in the future, for example to check dependencies between other extensions.",
+ "description": "Indicates what versions of MediaWiki core or extensions are required. This syntax may be extended in the future, for example to check dependencies between other services.",
"properties": {
"MediaWiki": {
"type": "string",
"description": "Version constraint string against MediaWiki core."
+ },
+ "extensions": {
+ "type": "object",
+ "description": "Set of version constraint strings against specific extensions."
+ },
+ "skins": {
+ "type": "object",
+ "description": "Set of version constraint strings against specific skins."
}
}
},
* @param Title $title Optional title object for the category represented by
* the given row. May be provided if it is already known, to avoid having
* to re-create a title object later.
- * @return Category
+ * @return Category|false
*/
public static function newFromRow( $row, $title = null ) {
$cat = new self();
NS_FILE_TALK => true,
NS_MEDIAWIKI => true,
NS_MEDIAWIKI_TALK => true,
+ NS_TEMPLATE => true,
NS_TEMPLATE_TALK => true,
NS_HELP => true,
NS_HELP_TALK => true,
/** @var bool Has a summary been preset using GET parameter &summary= ? */
public $hasPresetSummary = false;
- /** @var bool */
+ /** @var Revision|bool */
public $mBaseRevision = false;
/** @var bool */
protected $cache = [];
/**
- * Map of repo URLs to viewer URLs. Access via static method getViewers().
+ * @var array|false Map of repo URLs to viewer URLs. Access via static method getViewers().
*/
private static $viewers = false;
* @param string $url Either fully-qualified or a local path + query
* @param string $defaultProto One of the PROTO_* constants. Determines the
* protocol to use if $url or $wgServer is protocol-relative
- * @return string Fully-qualified URL, current-path-relative URL or false if
+ * @return string|false Fully-qualified URL, current-path-relative URL or false if
* no valid URL can be constructed
*/
function wfExpandUrl( $url, $defaultProto = PROTO_CURRENT ) {
}
/**
- * @return string
+ * @return string|false
*/
function getText() {
if ( isset( self::$blobCache[$this->mOldId] ) ) {
*/
protected function formatListParam( array $params, $listType, $format ) {
if ( !isset( self::$listTypeMap[$listType] ) ) {
- $warning = 'Invalid list type for message "' . $this->getKey() . '": ' .
- htmlspecialchars( serialize( $param ) );
+ $warning = 'Invalid list type for message "' . $this->getKey() . '": '
+ . htmlspecialchars( $listType )
+ . ' (params are ' . htmlspecialchars( serialize( $params ) ) . ')';
trigger_error( $warning, E_USER_WARNING );
$e = new Exception;
wfDebugLog( 'Bug58676', $warning . "\n" . $e->getTraceAsString() );
* if there isn't one. This is used by Skin to determine whether to enable
* JavaScript frame-breaking, for clients that don't support X-Frame-Options.
*
- * @return string
+ * @return string|false
*/
public function getFrameOptions() {
$config = $this->getConfig();
$response->header( "Content-Type: text/html; charset=utf-8" );
if ( $config->get( 'DebugRedirects' ) ) {
$url = htmlspecialchars( $redirect );
- print "<html>\n<head>\n<title>Redirect</title>\n</head>\n<body>\n";
+ print "<!DOCTYPE html>\n<html>\n<head>\n<title>Redirect</title>\n</head>\n<body>\n";
print "<p>Location: <a href=\"$url\">$url</a></p>\n";
print "</body>\n</html>\n";
} else {
* difference between a $1 that was not replaced and a $1 that was part of
* the content a $1 was replaced with.
* @param string $value
- * @return string
+ * @return string|false
*/
public function replace( $value ) {
$this->error = false;
*
* @param string $action
*
- * @return string 14-char timestamp or "infinity", or false if the input was invalid
+ * @return string|false 14-char timestamp or "infinity", or false if the input was invalid
*/
function getExpiry( $action ) {
if ( $this->mExpirySelection[$action] == 'existing' ) {
/**
* Fetch revision's user id without regard for the current user's permissions
*
- * @return string
+ * @return int
* @deprecated since 1.25, use getUser( Revision::RAW )
*/
public function getRawUser() {
* (same as the the wiki $row was loaded from) or false to indicate the local
* wiki (this is the default). Otherwise, it must be a symbolic wiki database
* identifier as understood by the LoadBalancer class.
- * @return string Text the text requested or false on failure
+ * @return string|false Text the text requested or false on failure
*/
public static function getRevisionText( $row, $prefix = 'old_', $wiki = false ) {
* @param string $from
* @param string $to
* @param string|array $in
- * @return string
+ * @return string|array
*/
function doReplace( $from, $to, $in ) {
if ( is_string( $in ) ) {
/**
* Returns the DB name of the distant wiki which owns the object.
*
- * @return string The DB name
+ * @return string|false The DB name
*/
public function getTransWikiID() {
if ( !$this->isExternal() ) {
/**
* Get the namespace text
*
- * @return string Namespace text
+ * @return string|false Namespace text
*/
public function getNsText() {
if ( $this->isExternal() ) {
* Get the last touched timestamp
*
* @param IDatabase $db Optional db
- * @return string Last-touched timestamp
+ * @return string|false Last-touched timestamp
*/
public function getTouched( $db = null ) {
if ( $db === null ) {
* This is for use prior to Setup.php, when no WebRequest object is available.
* At other times, use the non-static function getProtocol().
*
- * @return array
+ * @return string
*/
public static function detectProtocol() {
if ( ( !empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' ) ||
header( 'Content-Type: text/html' );
$encUrl = htmlspecialchars( $url );
echo <<<HTML
+<!DOCTYPE html>
<html>
<head>
<title>Security redirect</title>
* @param string $wikiID Wiki'd id (generally database name)
* @param string $page Page name (must be normalised before calling this function!)
* @param string $text Link's text; optional, default to $page
- * @return string HTML link or false if the wiki was not found
+ * @return string|false HTML link or false if the wiki was not found
*/
public static function makeForeignLink( $wikiID, $page, $text = null ) {
if ( !$text ) {
throw ApiUsageException::newWithMessage( $this, $msg, $code, $data, $httpCode );
}
+ /**
+ * Abort execution with an error derived from an exception
+ *
+ * @since 1.29
+ * @param Exception|Throwable $exception See ApiErrorFormatter::getMessageFromException()
+ * @param array $options See ApiErrorFormatter::getMessageFromException()
+ * @throws ApiUsageException always
+ */
+ public function dieWithException( $exception, array $options = [] ) {
+ $this->dieWithError(
+ $this->getErrorFormatter()->getMessageFromException( $exception, $options )
+ );
+ }
+
/**
* Adds a warning to the output, else dies
*
* "apihelp-{$this->getModulePath()}-description".
*
* @deprecated since 1.25
- * @return Message|string|array
+ * @return Message|string|array|false
*/
protected function getDescription() {
return false;
try {
$content = ContentHandler::makeContent( $text, $this->getTitle() );
} catch ( MWContentSerializationException $ex ) {
- // @todo: Internationalize MWContentSerializationException
- $this->dieWithError(
- [ 'apierror-contentserializationexception', wfEscapeWikiText( $ex->getMessage() ) ],
- 'parseerror'
- );
+ $this->dieWithException( $ex, [
+ 'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
+ ] );
return;
}
} else {
}
}
+ /**
+ * Get an ApiMessage from an exception
+ * @since 1.29
+ * @param Exception|Throwable $exception
+ * @param array $options
+ * - wrap: (string|array|MessageSpecifier) Used to wrap the exception's
+ * message. The exception's message will be added as the final parameter.
+ * - code: (string) Default code
+ * - data: (array) Extra data
+ * @return ApiMessage
+ */
+ public function getMessageFromException( $exception, array $options = [] ) {
+ $options += [ 'code' => null, 'data' => [] ];
+
+ if ( $exception instanceof ILocalizedException ) {
+ $msg = $exception->getMessageObject();
+ $params = [];
+ } else {
+ // Extract code and data from the exception, if applicable
+ if ( $exception instanceof UsageException ) {
+ $data = $exception->getMessageArray();
+ if ( !isset( $options['code'] ) ) {
+ $options['code'] = $data['code'];
+ }
+ unset( $data['code'], $data['info'] );
+ $options['data'] = array_merge( $data['code'], $options['data'] );
+ }
+
+ if ( isset( $options['wrap'] ) ) {
+ $msg = $options['wrap'];
+ } else {
+ $msg = new RawMessage( '$1' );
+ if ( !isset( $options['code'] ) ) {
+ $options['code'] = 'internal_api_error_' . get_class( $exception );
+ }
+ }
+ $params = [ wfEscapeWikiText( $exception->getMessage() ) ];
+ }
+ return ApiMessage::create( $msg, $options['code'], $options['data'] )
+ ->params( $params )
+ ->inLanguage( $this->lang )
+ ->title( $this->getDummyTitle() )
+ ->useDatabase( $this->useDB );
+ }
+
+ /**
+ * Format an exception as an array
+ * @since 1.29
+ * @param Exception|Throwable $exception
+ * @param array $options See self::getMessageFromException(), plus
+ * - format: (string) Format override
+ * @return array
+ */
+ public function formatException( $exception, array $options = [] ) {
+ return $this->formatMessage(
+ $this->getMessageFromException( $exception, $options ),
+ isset( $options['format'] ) ? $options['format'] : null
+ );
+ }
+
/**
* Format a message as an array
* @param Message|array|string $msg Message. See ApiMessage::create().
] + $msg->getApiData();
}
+ /**
+ * Format an exception as an array
+ * @since 1.29
+ * @param Exception|Throwable $exception
+ * @param array $options See parent::formatException(), plus
+ * - bc: (bool) Return only the string, not an array
+ * @return array|string
+ */
+ public function formatException( $exception, array $options = [] ) {
+ $ret = parent::formatException( $exception, $options );
+ return empty( $options['bc'] ) ? $ret : $ret['info'];
+ }
+
protected function addWarningOrError( $tag, $modulePath, $msg ) {
$value = self::stripMarkup( $msg->text() );
try {
$importer->doImport();
} catch ( Exception $e ) {
- $this->dieWithError( [ 'apierror-import-unknownerror', wfEscapeWikiText( $e->getMessage() ) ] );
+ $this->dieWithException( $e, [ 'wrap' => 'apierror-import-unknownerror' ] );
}
$resultData = $reporter->getData();
$params = [
'apierror-exceptioncaught',
WebRequest::getRequestId(),
- wfEscapeWikiText( $e->getMessage() )
+ $e instanceof ILocalizedException
+ ? $e->getMessageObject()
+ : wfEscapeWikiText( $e->getMessage() )
];
}
$messages[] = ApiMessage::create( $params, $code );
$this->mAllPages[0][$title] = $this->mFakePageId;
$this->mInvalidTitles[$this->mFakePageId] = [
'title' => $title,
- 'invalidreason' => $ex->getMessage(),
+ 'invalidreason' => $this->getErrorFormatter()->formatException( $ex, [ 'bc' => true ] ),
];
$this->mFakePageId--;
continue; // There's nothing else we can do
/**
* @param ApiBase $module
- * @return ApiResult
+ * @return array
*/
private function getModuleInfo( $module ) {
$ret = [];
try {
$this->content = ContentHandler::makeContent( $text, $titleObj, $model, $format );
} catch ( MWContentSerializationException $ex ) {
- // @todo: Internationalize MWContentSerializationException
- $this->dieWithError(
- [ 'apierror-contentserializationexception', wfEscapeWikiText( $ex->getMessage() ) ],
- 'parseerror'
- );
+ $this->dieWithException( $ex, [
+ 'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
+ ] );
}
if ( $this->section !== false ) {
* @return bool
*/
public function validateSha1Hash( $hash ) {
- return preg_match( '/^[a-f0-9]{40}$/', $hash );
+ return (bool)preg_match( '/^[a-f0-9]{40}$/', $hash );
}
/**
* @return bool
*/
public function validateSha1Base36Hash( $hash ) {
- return preg_match( '/^[a-z0-9]{31}$/', $hash );
+ return (bool)preg_match( '/^[a-z0-9]{31}$/', $hash );
}
/**
*/
private function validateHexSortkey( $hexSortkey ) {
// A hex sortkey has an unbound number of 2 letter pairs
- return preg_match( '/^(?:[a-fA-F0-9]{2})*$/D', $hexSortkey );
+ return (bool)preg_match( '/^(?:[a-fA-F0-9]{2})*$/D', $hexSortkey );
}
/**
$result->addIndexedTagName( [ 'query', $this->getModuleName() ], $modulePrefix );
}
// @todo Update exception handling here to understand current getFile exceptions
- // @todo Internationalize the exceptions
} catch ( UploadStashFileNotFoundException $e ) {
- $this->dieWithError( [ 'apierror-stashedfilenotfound', wfEscapeWikiText( $e->getMessage() ) ] );
+ $this->dieWithException( $e, [ 'wrap' => 'apierror-stashedfilenotfound' ] );
} catch ( UploadStashBadPathException $e ) {
- $this->dieWithError( [ 'apierror-stashpathinvalid', wfEscapeWikiText( $e->getMessage() ) ] );
+ $this->dieWithException( $e, [ 'wrap' => 'apierror-stashpathinvalid' ] );
}
}
* @param Content $content Edit content
* @param User $user
* @param string $summary Edit summary
- * @return integer ApiStashEdit::ERROR_* constant
+ * @return string ApiStashEdit::ERROR_* constant
* @since 1.25
*/
public static function parseAndStash( WikiPage $page, Content $content, User $user, $summary ) {
if ( $status->isGood() && !$status->getValue() ) {
// Not actually a 'good' status...
- $status->fatal( new ApiRawMessage( 'Invalid stashed file', 'stashfailed' ) );
+ $status->fatal( new ApiMessage( 'apierror-stashinvalidfile', 'stashfailed' ) );
}
} catch ( Exception $e ) {
$debugMessage = 'Stashing temporary file failed: ' . get_class( $e ) . ' ' . $e->getMessage();
wfDebug( __METHOD__ . ' ' . $debugMessage . "\n" );
- $status = Status::newFatal( new ApiRawMessage( $e->getMessage(), 'stashfailed' ) );
+ $status = Status::newFatal( $this->getErrorFormatter()->getMessageFromException(
+ $e, [ 'wrap' => new ApiMessage( 'apierror-stashexception', 'stashfailed' ) ]
+ ) );
}
if ( $status->isGood() ) {
* @param array $verification
*/
protected function checkVerification( array $verification ) {
- // @todo Move them to ApiBase's message map
switch ( $verification['status'] ) {
// Recoverable errors
case UploadBase::MIN_LENGTH_PARTNAME:
/**
* Handles a stash exception, giving a useful error to the user.
- * @todo Internationalize the exceptions
+ * @todo Internationalize the exceptions then get rid of this
* @param Exception $e
* @return StatusValue
*/
protected function handleStashException( $e ) {
- $err = wfEscapeWikiText( $e->getMessage() );
switch ( get_class( $exception ) ) {
case 'UploadStashFileNotFoundException':
- return StatusValue::newFatal( 'apierror-stashedfilenotfound', $err );
+ $wrap = 'apierror-stashedfilenotfound';
+ break;
case 'UploadStashBadPathException':
- return StatusValue::newFatal( 'apierror-stashpathinvalid', $err );
+ $wrap = 'apierror-stashpathinvalid';
+ break;
case 'UploadStashFileException':
- return StatusValue::newFatal( 'apierror-stashfilestorage', $err );
+ $wrap = 'apierror-stashfilestorage';
+ break;
case 'UploadStashZeroLengthFileException':
- return StatusValue::newFatal( 'apierror-stashzerolength', $err );
+ $wrap = 'apierror-stashzerolength';
+ break;
case 'UploadStashNotLoggedInException':
return StatusValue::newFatal( ApiMessage::create(
[ 'apierror-mustbeloggedin', $this->msg( 'action-upload' ) ], 'stashnotloggedin'
) );
case 'UploadStashWrongOwnerException':
- return StatusValue::newFatal( 'apierror-stashwrongowner', $err );
+ $wrap = 'apierror-stashwrongowner';
+ break;
case 'UploadStashNoSuchKeyException':
- return StatusValue::newFatal( 'apierror-stashnosuchfilekey', $err );
+ $wrap = 'apierror-stashnosuchfilekey';
+ break;
default:
- return StatusValue::newFatal( 'uploadstash-exception', get_class( $e ), $err );
+ $wrap = [ 'uploadstash-exception', get_class( $e ) ];
+ break;
}
+ return StatusValue::newFatal(
+ $this->getErrorFormatter()->getMessageFromException( $e, [ 'wrap' => $wrap ] )
+ );
}
/**
* starts throwing ApiUsageException. Eventually UsageException will go away
* and this will (probably) extend MWException directly.
*/
-class ApiUsageException extends UsageException {
+class ApiUsageException extends UsageException implements ILocalizedException {
protected $modulePath;
protected $status;
] + $enMsg->getApiData();
}
+ /**
+ * @inheritdoc
+ */
+ public function getMessageObject() {
+ return $this->status->getMessage();
+ }
+
/**
* @return string
*/
"apierror-specialpage-cantexecute": "You don't have permission to view the results of this special page.",
"apierror-stashedfilenotfound": "Could not find the file in the stash: $1.",
"apierror-stashedit-missingtext": "No stashed text found with the given hash.",
+ "apierror-stashexception": "$1",
"apierror-stashfailed-complete": "Chunked upload is already completed, check status for details.",
"apierror-stashfailed-nosession": "No chunked upload session with this key.",
"apierror-stashfilestorage": "Could not store upload in the stash: $1",
+ "apierror-stashinvalidfile": "Invalid stashed file.",
"apierror-stashnosuchfilekey": "No such filekey: $1.",
"apierror-stashpathinvalid": "File key of improper format or otherwise invalid: $1.",
"apierror-stashwrongowner": "Wrong owner: $1",
"apierror-stashedfilenotfound": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text. Currently this is probably English, hopefully we'll fix that in the future.",
"apierror-stashedit-missingtext": "{{doc-apierror}}",
"apierror-stashfailed-complete": "{{doc-apierror}}",
+ "apierror-stashexception": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text. May be English or localized, may or may not end in punctuation.",
"apierror-stashfailed-nosession": "{{doc-apierror}}",
"apierror-stashfilestorage": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text, which may already end with punctuation. Currently this is probably English, hopefully we'll fix that in the future.",
+ "apierror-stashinvalidfile": "{{doc-apierror}}",
"apierror-stashnosuchfilekey": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text. Currently this is probably English, hopefully we'll fix that in the future.",
"apierror-stashpathinvalid": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text. Currently this is probably English, hopefully we'll fix that in the future.",
"apierror-stashwrongowner": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text, which should already end with punctuation. Currently this is probably English, hopefully we'll fix that in the future.",
/**
* Save and compress text to the cache
* @param string $text
- * @return string Compressed text
+ * @return string|false Compressed text
*/
public function saveText( $text ) {
if ( $this->useGzip() ) {
*/
use MediaWiki\MediaWikiServices;
use Wikimedia\ScopedCallback;
+use MediaWiki\Logger\LoggerFactory;
/**
* MediaWiki message cache structure version.
*/
protected $mCache;
+ /**
+ * @var bool[] Map of (language code => boolean)
+ */
+ protected $mCacheVolatile = [];
+
/**
* Should mean that database cannot be used, but check
* @var bool $mDisable
protected $mExpiry;
/**
- * Message cache has its own parser which it uses to transform
- * messages.
+ * Message cache has its own parser which it uses to transform messages
+ * @var ParserOptions
*/
- protected $mParserOptions, $mParser;
+ protected $mParserOptions;
+ /** @var Parser */
+ protected $mParser;
/**
* Variable for tracking which variables are already loaded
*/
public static function normalizeKey( $key ) {
global $wgContLang;
+
$lckey = strtr( $key, ' ', '_' );
if ( ord( $lckey ) < 128 ) {
$lckey[0] = strtolower( $lckey[0] );
# Hash of the contents is stored in memcache, to detect if data-center cache
# or local cache goes out of date (e.g. due to replace() on some other server)
list( $hash, $hashVolatile ) = $this->getValidationHash( $code );
+ $this->mCacheVolatile[$code] = $hashVolatile;
# Try the local cache and check against the cluster hash key...
$cache = $this->getLocalCache( $code );
$bigConds[] = 'page_len > ' . intval( $wgMaxMsgCacheEntrySize );
# Load titles for all oversized pages in the MediaWiki namespace
- $res = $dbr->select( 'page', 'page_title', $bigConds, __METHOD__ . "($code)-big" );
+ $res = $dbr->select(
+ 'page',
+ [ 'page_title', 'page_latest' ],
+ $bigConds,
+ __METHOD__ . "($code)-big"
+ );
foreach ( $res as $row ) {
$cache[$row->page_title] = '!TOO BIG';
+ // At least include revision ID so page changes are reflected in the hash
+ $cache['EXCESSIVE'][$row->page_title] = $row->page_latest;
}
# Conditions to load the remaining pages with their contents
* Updates cache as necessary when message page is changed
*
* @param string|bool $title Name of the page changed (false if deleted)
- * @param mixed $text New contents of the page.
+ * @param string|bool $text New contents of the page (false if deleted)
*/
public function replace( $title, $text ) {
global $wgMaxMsgCacheEntrySize, $wgContLang, $wgLanguageCode;
// a self-deadlock. This is safe as no reads happen *directly* in this
// method between getReentrantScopedLock() and load() below. There is
// no risk of data "changing under our feet" for replace().
- $cacheKey = wfMemcKey( 'messages', $code );
- $scopedLock = $this->getReentrantScopedLock( $cacheKey );
+ $scopedLock = $this->getReentrantScopedLock( wfMemcKey( 'messages', $code ) );
+ // Load the messages from the master DB to avoid race conditions
$this->load( $code, self::FOR_UPDATE );
- $titleKey = wfMemcKey( 'messages', 'individual', $title );
+ // Load the new value into the process cache...
if ( $text === false ) {
- // Article was deleted
$this->mCache[$code][$title] = '!NONEXISTENT';
- $this->wanCache->delete( $titleKey );
} elseif ( strlen( $text ) > $wgMaxMsgCacheEntrySize ) {
- // Check for size
$this->mCache[$code][$title] = '!TOO BIG';
- $this->wanCache->set( $titleKey, ' ' . $text, $this->mExpiry );
+ // Pre-fill the individual key cache with the known latest message text
+ $key = $this->wanCache->makeKey( 'messages-big', $this->mCache[$code]['HASH'], $title );
+ $this->wanCache->set( $key, " $text", $this->mExpiry );
} else {
$this->mCache[$code][$title] = ' ' . $text;
- $this->wanCache->delete( $titleKey );
}
-
- // Mark this cache as definitely "latest" (non-volatile) so
- // load() calls do try to refresh the cache with replica DB data
+ // Mark this cache as definitely being "latest" (non-volatile) so
+ // load() calls do not try to refresh the cache with replica DB data
$this->mCache[$code]['LATEST'] = time();
// Update caches if the lock was acquired
if ( $scopedLock ) {
$this->saveToCaches( $this->mCache[$code], 'all', $code );
+ } else {
+ LoggerFactory::getInstance( 'MessageCache' )->error(
+ __METHOD__ . ': could not acquire lock to update {title} ({code})',
+ [ 'title' => $title, 'code' => $code ] );
}
ScopedCallback::consume( $scopedLock );
protected function getValidationHash( $code ) {
$curTTL = null;
$value = $this->wanCache->get(
- wfMemcKey( 'messages', $code, 'hash', 'v1' ),
+ $this->wanCache->makeKey( 'messages', $code, 'hash', 'v1' ),
$curTTL,
[ wfMemcKey( 'messages', $code ) ]
);
- if ( !$value ) {
- // No hash found at all; cache must regenerate to be safe
- $hash = false;
- $expired = true;
- } else {
+ if ( $value ) {
$hash = $value['hash'];
- if ( ( time() - $value['latest'] ) < WANObjectCache::HOLDOFF_TTL ) {
- // Cache was recently updated via replace() and should be up-to-date
+ if ( ( time() - $value['latest'] ) < WANObjectCache::TTL_MINUTE ) {
+ // Cache was recently updated via replace() and should be up-to-date.
+ // That method is only called in the primary datacenter and uses FOR_UPDATE.
+ // Also, it is unlikely that the current datacenter is *now* secondary one.
$expired = false;
} else {
// See if the "check" key was bumped after the hash was generated
$expired = ( $curTTL < 0 );
}
+ } else {
+ // No hash found at all; cache must regenerate to be safe
+ $hash = false;
+ $expired = true;
}
return [ $hash, $expired ];
* Set the md5 used to validate the local disk cache
*
* If $cache has a 'LATEST' UNIX timestamp key, then the hash will not
- * be treated as "volatile" by getValidationHash() for the next few seconds
+ * be treated as "volatile" by getValidationHash() for the next few seconds.
+ * This is triggered when $cache is generated using FOR_UPDATE mode.
*
* @param string $code
* @param array $cache Cached messages with a version
*/
protected function setValidationHash( $code, array $cache ) {
$this->wanCache->set(
- wfMemcKey( 'messages', $code, 'hash', 'v1' ),
+ $this->wanCache->makeKey( 'messages', $code, 'hash', 'v1' ),
[
'hash' => $cache['HASH'],
'latest' => isset( $cache['LATEST'] ) ? $cache['LATEST'] : 0
*/
private function getMessageForLang( $lang, $lckey, $useDB, &$alreadyTried ) {
global $wgContLang;
+
$langcode = $lang->getCode();
// Try checking the database for the requested language
*/
private function getMessagePageName( $langcode, $uckey ) {
global $wgLanguageCode;
+
if ( $langcode === $wgLanguageCode ) {
// Messages created in the content language will not have the /lang extension
return $uckey;
if ( isset( $this->mCache[$code][$title] ) ) {
$entry = $this->mCache[$code][$title];
if ( substr( $entry, 0, 1 ) === ' ' ) {
- // The message exists, so make sure a string
- // is returned.
+ // The message exists, so make sure a string is returned.
return (string)substr( $entry, 1 );
} elseif ( $entry === '!NONEXISTENT' ) {
return false;
}
// Try the individual message cache
- $titleKey = wfMemcKey( 'messages', 'individual', $title );
-
- $curTTL = null;
- $entry = $this->wanCache->get(
- $titleKey,
- $curTTL,
- [ wfMemcKey( 'messages', $code ) ]
- );
- $entry = ( $curTTL >= 0 ) ? $entry : false;
+ $titleKey = $this->wanCache->makeKey( 'messages-big', $this->mCache[$code]['HASH'], $title );
+
+ if ( $this->mCacheVolatile[$code] ) {
+ $entry = false;
+ // Make sure that individual keys respect the WAN cache holdoff period too
+ LoggerFactory::getInstance( 'MessageCache' )->debug(
+ __METHOD__ . ': loading volatile key \'{titleKey}\'',
+ [ 'titleKey' => $titleKey, 'code' => $code ] );
+ } else {
+ $entry = $this->wanCache->get( $titleKey );
+ }
- if ( $entry ) {
+ if ( $entry !== false ) {
if ( substr( $entry, 0, 1 ) === ' ' ) {
$this->mCache[$code][$title] = $entry;
// The message exists, so make sure a string is returned
}
}
- // Try loading it from the database
+ // Try loading the message from the database
$dbr = wfGetDB( DB_REPLICA );
$cacheOpts = Database::getCacheSetOptions( $dbr );
// Use newKnownCurrent() to avoid querying revision/user tables
if ( $revision ) {
$content = $revision->getContent();
- if ( !$content ) {
- // A possibly temporary loading failure.
- wfDebugLog(
- 'MessageCache',
- __METHOD__ . ": failed to load message page text for {$title} ($code)"
- );
- $message = null; // no negative caching
- } else {
- // XXX: Is this the right way to turn a Content object into a message?
- // NOTE: $content is typically either WikitextContent, JavaScriptContent or
- // CssContent. MessageContent is *not* used for storing messages, it's
- // only used for wrapping them when needed.
- $message = $content->getWikitextForTransclusion();
-
- if ( $message === false || $message === null ) {
- wfDebugLog(
- 'MessageCache',
- __METHOD__ . ": message content doesn't provide wikitext "
- . "(content model: " . $content->getModel() . ")"
- );
-
- $message = false; // negative caching
- } else {
+ if ( $content ) {
+ $message = $this->getMessageTextFromContent( $content );
+ if ( is_string( $message ) ) {
$this->mCache[$code][$title] = ' ' . $message;
$this->wanCache->set( $titleKey, ' ' . $message, $this->mExpiry, $cacheOpts );
}
+ } else {
+ // A possibly temporary loading failure
+ LoggerFactory::getInstance( 'MessageCache' )->warning(
+ __METHOD__ . ': failed to load message page text for \'{titleKey}\'',
+ [ 'titleKey' => $titleKey, 'code' => $code ] );
+ $message = null; // no negative caching
}
} else {
$message = false; // negative caching
*/
function getParser() {
global $wgParser, $wgParserConf;
+
if ( !$this->mParser && isset( $wgParser ) ) {
# Do some initialisation so that we don't have to do it twice
$wgParser->firstCallInit();
public function parse( $text, $title = null, $linestart = true,
$interface = false, $language = null
) {
+ global $wgTitle;
+
if ( $this->mInParser ) {
return htmlspecialchars( $text );
}
$popts->setTargetLanguage( $language );
if ( !$title || !$title instanceof Title ) {
- global $wgTitle;
wfDebugLog( 'GlobalTitleFail', __METHOD__ . ' called by ' .
wfGetAllCallers( 6 ) . ' with no title set.' );
$title = $wgTitle;
*/
public function getAllMessageKeys( $code ) {
global $wgContLang;
+
$this->load( $code );
if ( !isset( $this->mCache[$code] ) ) {
// Apparently load() failed
$cache = $this->mCache[$code];
unset( $cache['VERSION'] );
unset( $cache['EXPIRY'] );
+ unset( $cache['EXCESSIVE'] );
// Remove any !NONEXISTENT keys
$cache = array_diff( $cache, [ '!NONEXISTENT' ] );
// Keys may appear with a capital first letter. lcfirst them.
return array_map( [ $wgContLang, 'lcfirst' ], array_keys( $cache ) );
}
+
+ /**
+ * Purge message caches when a MediaWiki: page is created, updated, or deleted
+ *
+ * @param Title $title Message page title
+ * @param Content|null $content New content for edit/create, null on deletion
+ * @since 1.29
+ */
+ public function updateMessageOverride( Title $title, Content $content = null ) {
+ global $wgContLang;
+
+ $msgText = $this->getMessageTextFromContent( $content );
+ if ( $msgText === null ) {
+ $msgText = false; // treat as not existing
+ }
+
+ $this->replace( $title->getDBkey(), $msgText );
+
+ if ( $wgContLang->hasVariants() ) {
+ $wgContLang->updateConversionTable( $title );
+ }
+ }
+
+ /**
+ * @param Content|null $content Content or null if the message page does not exist
+ * @return string|bool|null Returns false if $content is null and null on error
+ */
+ private function getMessageTextFromContent( Content $content = null ) {
+ // @TODO: could skip pseudo-messages like js/css here, based on content model
+ if ( $content ) {
+ // Message page exists...
+ // XXX: Is this the right way to turn a Content object into a message?
+ // NOTE: $content is typically either WikitextContent, JavaScriptContent or
+ // CssContent. MessageContent is *not* used for storing messages, it's
+ // only used for wrapping them when needed.
+ $msgText = $content->getWikitextForTransclusion();
+ if ( $msgText === false || $msgText === null ) {
+ // This might be due to some kind of misconfiguration...
+ $msgText = null;
+ LoggerFactory::getInstance( 'MessageCache' )->warning(
+ __METHOD__ . ": message content doesn't provide wikitext "
+ . "(content model: " . $content->getModel() . ")" );
+ }
+ } else {
+ // Message page does not exist...
+ $msgText = false;
+ }
+
+ return $msgText;
+ }
}
* array.
* @param string $code
* @param string $key
- * @return bool|null|string
+ * @return bool|null|string|string[]
*/
public function getSubitemList( $code, $key ) {
if ( in_array( $key, self::$splitKeys ) ) {
$cache = ObjectCache::getLocalServerInstance( CACHE_ANYTHING );
$cacheKey = $cache->makeKey(
'first-letters',
+ get_class( $this ),
$this->locale,
$this->digitTransformLanguage->getCode(),
self::getICUVersion(),
/** @var bool Whether to use temporary tables or not */
private $useTemporaryTables = true;
- /** @var Database */
+ /** @var IMaintainableDatabase */
private $db;
/**
* Constructor
*
- * @param Database $db A database subclass
+ * @param IMaintainableDatabase $db A database subclass
* @param array $tablesToClone An array of tables to clone, unprefixed
* @param string $newTablePrefix Prefix to assign to the tables
* @param string $oldTablePrefix Prefix on current tables, if not $wgDBprefix
* @param bool $dropCurrentTables
*/
- public function __construct( Database $db, array $tablesToClone,
+ public function __construct( IMaintainableDatabase $db, array $tablesToClone,
$newTablePrefix, $oldTablePrefix = '', $dropCurrentTables = true
) {
$this->db = $db;
# Create new table
wfDebug( __METHOD__ . " duplicating $oldTableName to $newTableName\n" );
- $this->db->duplicateTableStructure( $oldTableName, $newTableName, $this->useTemporaryTables );
+ $this->db->duplicateTableStructure(
+ $oldTableName, $newTableName, $this->useTemporaryTables );
}
}
}
/**
- * @return string
+ * @return string|int
*/
public function lastErrno() {
$err = sqlsrv_errors( SQLSRV_ERR_ALL );
*
* @param array|string $table
* @param string $field
- * @return ORAField|ORAResult
+ * @return ORAField|ORAResult|false
*/
private function fieldInfoMulti( $table, $field ) {
$field = strtoupper( $field );
if ( is_nan( $item ) ) {
return 'NaN';
}
- return $item;
+ return (string)$item;
}
if ( is_scalar( $item ) ) {
/**
* @param int $i
- * @return string|null
+ * @return string[]|string|null
*/
public function getClosing( $i = null ) {
if ( $i === null ) {
if ( $link ) {
return "[$link $id]";
} else {
- return $id;
+ return (string)$id;
}
}
* @since 1.7
* @ingroup Exception
*/
-class ErrorPageError extends MWException {
+class ErrorPageError extends MWException implements ILocalizedException {
public $title, $msg, $params;
/**
// 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.
- if ( $msg instanceof Message ) {
- $enMsg = clone $msg;
- } else {
- $enMsg = wfMessage( $msg, $params );
- }
+ $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() {
global $wgOut;
--- /dev/null
+<?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 Exception 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 = html_entity_decode( strip_tags( $msg ), ENT_QUOTES | ENT_HTML5 );
+ parent::__construct( $msg, $code, $previous );
+ }
+
+ public function getMessageObject() {
+ return Message::newFromSpecifier( $this->messageSpec );
+ }
+}
}
$this->errors = $errors;
+
+ // Give the parent class something to work with
+ parent::__construct( 'permissionserrors', Message::newFromSpecifier( $errors[0] ) );
}
public function report() {
* @param string $cluster Cluster name
* @return LoadBalancer
*/
- function getLoadBalancer( $cluster ) {
+ private function getLoadBalancer( $cluster ) {
$wiki = isset( $this->params['wiki'] ) ? $this->params['wiki'] : false;
return wfGetLBFactory()->getExternalLB( $cluster, $wiki );
* Get a replica DB connection for the specified cluster
*
* @param string $cluster Cluster name
- * @return IDatabase
+ * @return DBConnRef
*/
- function getSlave( $cluster ) {
+ public function getSlave( $cluster ) {
global $wgDefaultExternalStore;
$wiki = isset( $this->params['wiki'] ) ? $this->params['wiki'] : false;
* Get a master database connection for the specified cluster
*
* @param string $cluster Cluster name
- * @return IDatabase
+ * @return MaintainableDBConnRef
*/
- function getMaster( $cluster ) {
+ public function getMaster( $cluster ) {
$wiki = isset( $this->params['wiki'] ) ? $this->params['wiki'] : false;
$lb = $this->getLoadBalancer( $cluster );
- $db = $lb->getConnectionRef( DB_MASTER, [], $wiki );
+ $db = $lb->getMaintenanceConnectionRef( DB_MASTER, [], $wiki );
$db->clearFlag( DBO_TRX ); // sanity
return $db;
* @param IDatabase $db
* @return string Table name ('blobs' by default)
*/
- function getTable( $db ) {
+ public function getTable( $db ) {
$table = $db->getLBInfo( 'blobs table' );
if ( is_null( $table ) ) {
$table = 'blobs';
* @param string $id
* @param string $itemID
* @return HistoryBlob|bool Returns false if missing
- * @private
*/
- function fetchBlob( $cluster, $id, $itemID ) {
+ private function fetchBlob( $cluster, $id, $itemID ) {
/**
* One-step cache variable to hold base blobs; operations that
* pull multiple revisions may often pull multiple times from
* @return array A map from the blob_id's requested to their content.
* Unlocated ids are not represented
*/
- function batchFetchBlobs( $cluster, array $ids ) {
+ private function batchFetchBlobs( $cluster, array $ids ) {
$dbr = $this->getSlave( $cluster );
$res = $dbr->select( $this->getTable( $dbr ),
[ 'blob_id', 'blob_text' ], [ 'blob_id' => array_keys( $ids ) ], __METHOD__ );
*/
protected $pathDisclosureProtection = 'simple';
- /** @var bool Public zone URL. */
+ /** @var string|false Public zone URL. */
protected $url;
/** @var string The base thumbnail URL. Defaults to "<url>/thumb". */
* @return bool Whether non-ASCII path characters are allowed
*/
public function backendSupportsUnicodePaths() {
- return ( $this->getBackend()->getFeatures() & FileBackend::ATTR_UNICODE_PATHS );
+ return (bool)( $this->getBackend()->getFeatures() & FileBackend::ATTR_UNICODE_PATHS );
}
/**
* constructor, whereas local repositories use the local Title functions.
*
* @param string $name
- * @return string
+ * @return string|false
*/
public function getDescriptionUrl( $name ) {
$encName = wfUrlencode( $name );
*
* @param string $name Name of image to fetch
* @param string $lang Language to fetch it in, if any.
- * @return string
+ * @return string|false
*/
public function getDescriptionRenderUrl( $name, $lang = null ) {
$query = 'action=render';
*
* @param Title $title
* @param string|bool $time
- * @return File
+ * @return File|false
*/
function newFile( $title, $time = false ) {
if ( $time ) {
/**
* Get the repo instance by its name
* @param string $name
- * @return bool
+ * @return FileRepo|bool
*/
function getRepoByName( $name ) {
if ( !$this->reposInitialised ) {
/** @var string SHA-1 hash of file content */
private $sha1;
- /** @var string Number of pages of a multipage document, or false for
+ /** @var int|false Number of pages of a multipage document, or false for
* documents which aren't multipage documents
*/
private $pageCount;
* Return the user name of the uploader.
*
* @deprecated since 1.23 Use getUser( 'text' ) instead.
- * @return string
+ * @return string|int
*/
public function getUserText() {
wfDeprecated( __METHOD__, '1.23' );
/**
* Return upload description.
*
- * @return string
+ * @return string|int
*/
public function getDescription() {
$this->load();
/** @var string Relative path including trailing slash */
protected $hashPath;
- /** @var string Number of pages of a multipage document, or false for
+ /** @var string|false Number of pages of a multipage document, or false for
* documents which aren't multipage documents
*/
protected $pageCount;
/**
* Get the duration of a media file in seconds
*
- * @return int
+ * @return float|int
*/
public function getLength() {
$handler = $this->getHandler();
*
* @param array $handlerParams
*
- * @return string
+ * @return ThumbnailImage|MediaTransformOutput|bool False on failure
*/
function getUnscaledThumb( $handlerParams = [] ) {
$hp =& $handlerParams;
* Returns the number of pages of a multipage document, or false for
* documents which aren't multipage documents
*
- * @return bool|int
+ * @return string|bool|int
*/
function pageCount() {
if ( !isset( $this->pageCount ) ) {
if ( $srcWidth == 0 ) {
return 0;
} else {
- return round( $srcHeight * $dstWidth / $srcWidth );
+ return (int)round( $srcHeight * $dstWidth / $srcWidth );
}
}
* a good reason. This method skips all caches.
*
* @param string $filePath The path to the file (e.g. From getLocalPathRef() )
- * @return array The width, followed by height, with optionally more things after
+ * @return array|false The width, followed by height, with optionally more things after
*/
function getImageSize( $filePath ) {
if ( !$this->getHandler() ) {
* Get the HTML text of the description page, if available
*
* @param bool|Language $lang Optional language to fetch description in
- * @return string
+ * @return string|false
*/
function getDescriptionText( $lang = false ) {
global $wgLang;
/**
* Get the deletion archive key, "<sha1>.<ext>"
*
- * @return string
+ * @return string|false
*/
function getStorageKey() {
$hash = $this->getSha1();
/**
* @param bool|Language $lang Optional language to fetch description in.
- * @return string
+ * @return string|false
*/
function getDescriptionText( $lang = false ) {
global $wgLang;
* with integer 0 as a value.
*
* @param array $array
- * @return array
+ * @return array|string
*/
public static function forceToStringRecursive( $array ) {
if ( is_array( $array ) ) {
$request = $this->mParent
? $this->mParent->getRequest()
: RequestContext::getMain()->getRequest();
- return preg_match( '/MSIE [1-7]\./i', $request->getHeader( 'User-Agent' ) );
+ return (bool)preg_match( '/MSIE [1-7]\./i', $request->getHeader( 'User-Agent' ) );
}
}
/**
* @param WebRequest $request
*
- * @return string
+ * @return string|array
*/
public function loadDataFromRequest( $request ) {
if ( $this->isSubmitAttempt( $request ) ) {
/**
* @param WebRequest $request
*
- * @return string
+ * @return string|int
*/
public function loadDataFromRequest( $request ) {
$size = $request->getInt( $this->mName );
* @return bool
*/
public static function isValidURI( $uri ) {
- return preg_match(
+ return (bool)preg_match(
'/^https?:\/\/[^\/\s]\S*$/D',
$uri
);
/**
* Determine if LocalSettings.php exists. If it does, return its variables.
*
- * @return array
+ * @return array|false
*/
public static function getExistingLocalSettings() {
global $IP;
/**
* Convert a hex string representing a Unicode code point to that code point.
* @param string $c
- * @return string
+ * @return string|false
*/
protected function unicodeChar( $c ) {
$c = hexdec( $c );
*
* @param string $string
*
- * @return string
+ * @return string|false
*/
public static function escapePhpString( $string ) {
if ( is_array( $string ) || is_object( $string ) ) {
* @note More logic is explained in DefaultSettings.
*
* @param string $prefix Interwiki prefix
- * @return Interwiki
+ * @return Interwiki|false
*/
private function getInterwikiCached( $prefix ) {
$value = $this->getInterwikiCacheEntry( $prefix );
/**
* @param string $name
- * @return string
+ * @return string[]
*/
private function decodeQueueName( $name ) {
list( $type, $wiki ) = explode( '/', $name, 2 );
// The position after the end of the next delimiter
private $endPos;
- // The current token
+ /** @var string|false The current token */
private $current;
/**
/** @var Array (location => (start, end)) */
protected $ring = [];
- /** @var Array (location => (start, end)) */
+ /** @var HashRing|null */
protected $liveRing;
/** @var Array (location => UNIX timestamp) */
protected $ejectionExpiries = [];
/**
* @see FileBackendStore::getFileXAttributes()
* @param array $params
- * @return bool|string
+ * @return array[][]
*/
protected function doGetFileXAttributes( array $params ) {
return [ 'headers' => [], 'metadata' => [] ]; // not supported
* @param array $creds From getAuthentication()
* @param string $container
* @param string $object
- * @return array
+ * @return string
*/
protected function storageUrl( array $creds, $container = null, $object = null ) {
$parts = [ $creds['storage_url'] ];
class DBConnRef implements IDatabase {
/** @var ILoadBalancer */
private $lb;
-
- /** @var IDatabase|null Live connection handle */
+ /** @var Database|null Live connection handle */
private $conn;
-
/** @var array|null N-tuple of (server index, group, DatabaseDomain|string) */
private $params;
const FLD_DOMAIN = 2;
/**
- * @param ILoadBalancer $lb
- * @param IDatabase|array $conn Connection or (server index, group, DatabaseDomain|string)
+ * @param ILoadBalancer $lb Connection manager for $conn
+ * @param Database|array $conn New connection handle or (server index, query groups, domain)
*/
public function __construct( ILoadBalancer $lb, $conn ) {
$this->lb = $lb;
- if ( $conn instanceof IDatabase ) {
+ if ( $conn instanceof Database ) {
$this->conn = $conn; // live handle
} elseif ( count( $conn ) >= 3 && $conn[self::FLD_DOMAIN] !== false ) {
$this->params = $conn;
* Clean up the connection when out of scope
*/
function __destruct() {
- if ( $this->conn !== null ) {
+ if ( $this->conn ) {
$this->lb->reuseConnection( $this->conn );
}
}
return $this->mTrxLevel && ( $this->mTrxAtomicLevels || !$this->mTrxAutomatic );
}
- /**
- * Creates a new table with structure copied from existing table
- * Note that unlike most database abstraction functions, this function does not
- * automatically append database prefix, because it works at a lower
- * abstraction level.
- * The table names passed to this function shall not be quoted (this
- * function calls addIdentifierQuotes when needed).
- *
- * @param string $oldName Name of table whose structure should be copied
- * @param string $newName Name of table to be created
- * @param bool $temporary Whether the new table should be temporary
- * @param string $fname Calling function name
- * @throws RuntimeException
- * @return bool True if operation was successful
- */
- public function duplicateTableStructure( $oldName, $newName, $temporary = false,
- $fname = __METHOD__
+ public function duplicateTableStructure(
+ $oldName, $newName, $temporary = false, $fname = __METHOD__
) {
throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
}
* @return array
*/
public function listViews( $prefix = null, $fname = __METHOD__ );
+
+ /**
+ * Creates a new table with structure copied from existing table
+ *
+ * Note that unlike most database abstraction functions, this function does not
+ * automatically append database prefix, because it works at a lower abstraction level.
+ * The table names passed to this function shall not be quoted (this function calls
+ * addIdentifierQuotes() when needed).
+ *
+ * @param string $oldName Name of table whose structure should be copied
+ * @param string $newName Name of table to be created
+ * @param bool $temporary Whether the new table should be temporary
+ * @param string $fname Calling function name
+ * @return bool True if operation was successful
+ * @throws RuntimeException
+ */
+ public function duplicateTableStructure(
+ $oldName, $newName, $temporary = false, $fname = __METHOD__
+ );
}
--- /dev/null
+<?php
+/**
+ * Helper class to handle automatically marking connections as reusable (via RAII pattern)
+ * as well handling deferring the actual network connection until the handle is used
+ *
+ * @note: proxy methods are defined explicity to avoid interface errors
+ * @ingroup Database
+ * @since 1.29
+ */
+class MaintainableDBConnRef extends DBConnRef implements IMaintainableDatabase {
+ public function tableName( $name, $format = 'quoted' ) {
+ return $this->__call( __FUNCTION__, func_get_args() );
+ }
+
+ public function tableNames() {
+ return $this->__call( __FUNCTION__, func_get_args() );
+ }
+
+ public function tableNamesN() {
+ return $this->__call( __FUNCTION__, func_get_args() );
+ }
+
+ public function sourceFile(
+ $filename,
+ callable $lineCallback = null,
+ callable $resultCallback = null,
+ $fname = false,
+ callable $inputCallback = null
+ ) {
+ return $this->__call( __FUNCTION__, func_get_args() );
+ }
+
+ public function sourceStream(
+ $fp,
+ callable $lineCallback = null,
+ callable $resultCallback = null,
+ $fname = __METHOD__,
+ callable $inputCallback = null
+ ) {
+ return $this->__call( __FUNCTION__, func_get_args() );
+ }
+
+ public function dropTable( $tableName, $fName = __METHOD__ ) {
+ return $this->__call( __FUNCTION__, func_get_args() );
+ }
+
+ public function deadlockLoop() {
+ return $this->__call( __FUNCTION__, func_get_args() );
+ }
+
+ public function listViews( $prefix = null, $fname = __METHOD__ ) {
+ return $this->__call( __FUNCTION__, func_get_args() );
+ }
+
+ public function textFieldSize( $table, $field ) {
+ return $this->__call( __FUNCTION__, func_get_args() );
+ }
+
+ public function streamStatementEnd( &$sql, &$newLine ) {
+ return $this->__call( __FUNCTION__, func_get_args() );
+ }
+
+ public function duplicateTableStructure(
+ $oldName, $newName, $temporary = false, $fname = __METHOD__
+ ) {
+ return $this->__call( __FUNCTION__, func_get_args() );
+ }
+}
* @ingroup Database
* @since 1.23
*/
-class DBExpectedError extends DBError implements MessageSpecifier {
+class DBExpectedError extends DBError implements MessageSpecifier, ILocalizedException {
/** @var string[] Message parameters */
protected $params;
public function getParams() {
return $this->params;
}
+
+ /**
+ * @inheritdoc
+ * @since 1.29
+ */
+ public function getMessageObject() {
+ return Message::newFromSpecifier( $this );
+ }
}
/**
* Get the index of the reader connection, which may be a replica DB
+ *
* This takes into account load ratios and lag times. It should
- * always return a consistent index during a given invocation
+ * always return a consistent index during a given invocation.
*
* Side effect: opens connections to databases
* @param string|bool $group Query group, or false for the generic reader
/**
* Set the master wait position
- * If a DB_REPLICA connection has been opened already, waits
- * Otherwise sets a variable telling it to wait if such a connection is opened
+ *
+ * If a DB_REPLICA connection has been opened already, then wait immediately.
+ * Otherwise sets a variable telling it to wait if such a connection is opened.
+ *
* @param DBMasterPos $pos
*/
public function waitFor( $pos );
/**
* Set the master wait position and wait for ALL replica DBs to catch up to it
+ *
* @param DBMasterPos $pos
* @param int $timeout Max seconds to wait; default is mWaitTimeout
* @return bool Success (able to connect and no timeouts reached)
/**
* Get any open connection to a given server index, local or foreign
- * Returns false if there is no connection open
*
- * @param int $i Server index
- * @return IDatabase|bool False on failure
+ * @param int $i Server index or DB_MASTER/DB_REPLICA
+ * @return Database|bool False if no such connection is open
*/
public function getAnyOpenConnection( $i );
/**
* Get a connection by index
- * This is the main entry point for this class.
*
- * @param int $i Server index
+ * @param int $i Server index or DB_MASTER/DB_REPLICA
* @param array|string|bool $groups Query group(s), or false for the generic reader
* @param string|bool $domain Domain ID, or false for the current domain
*
* @throws DBError
- * @return IDatabase
+ * @return Database
*/
public function getConnection( $i, $groups = [], $domain = false );
/**
- * Mark a foreign connection as being available for reuse under a different
- * DB name or prefix. This mechanism is reference-counted, and must be called
- * the same number of times as getConnection() to work.
+ * Mark a foreign connection as being available for reuse under a different DB domain
+ *
+ * This mechanism is reference-counted, and must be called the same number of times
+ * as getConnection() to work.
*
* @param IDatabase $conn
* @throws InvalidArgumentException
/**
* Get a database connection handle reference
*
- * The handle's methods wrap simply wrap those of a IDatabase handle
+ * The handle's methods simply wrap those of a Database handle
*
- * @see LoadBalancer::getConnection() for parameter information
+ * @see ILoadBalancer::getConnection() for parameter information
*
- * @param int $db
+ * @param int $i Server index or DB_MASTER/DB_REPLICA
* @param array|string|bool $groups Query group(s), or false for the generic reader
* @param string|bool $domain Domain ID, or false for the current domain
* @return DBConnRef
*/
- public function getConnectionRef( $db, $groups = [], $domain = false );
+ public function getConnectionRef( $i, $groups = [], $domain = false );
/**
* Get a database connection handle reference without connecting yet
*
- * The handle's methods wrap simply wrap those of a IDatabase handle
+ * The handle's methods simply wrap those of a Database handle
*
- * @see LoadBalancer::getConnection() for parameter information
+ * @see ILoadBalancer::getConnection() for parameter information
*
- * @param int $db
+ * @param int $i Server index or DB_MASTER/DB_REPLICA
* @param array|string|bool $groups Query group(s), or false for the generic reader
* @param string|bool $domain Domain ID, or false for the current domain
* @return DBConnRef
*/
- public function getLazyConnectionRef( $db, $groups = [], $domain = false );
+ public function getLazyConnectionRef( $i, $groups = [], $domain = false );
+
+ /**
+ * Get a maintenance database connection handle reference for migrations and schema changes
+ *
+ * The handle's methods simply wrap those of a Database handle
+ *
+ * @see ILoadBalancer::getConnection() for parameter information
+ *
+ * @param int $db Server index or DB_MASTER/DB_REPLICA
+ * @param array|string|bool $groups Query group(s), or false for the generic reader
+ * @param string|bool $domain Domain ID, or false for the current domain
+ * @return MaintainableDBConnRef
+ */
+ public function getMaintenanceConnectionRef( $db, $groups = [], $domain = false );
/**
* Open a connection to the server given by the specified index
*
* @note If disable() was called on this LoadBalancer, this method will throw a DBAccessError.
*
- * @param int $i Server index
+ * @param int $i Server index or DB_MASTER/DB_REPLICA
* @param string|bool $domain Domain ID, or false for the current domain
- * @return IDatabase|bool Returns false on errors
+ * @return Database|bool Returns false on errors
* @throws DBAccessError
*/
public function openConnection( $i, $domain = false );
return new DBConnRef( $this, [ $db, $groups, $domain ] );
}
+ public function getMaintenanceConnectionRef( $db, $groups = [], $domain = false ) {
+ $domain = ( $domain !== false ) ? $domain : $this->localDomain;
+
+ return new MaintainableDBConnRef( $this, $this->getConnection( $db, $groups, $domain ) );
+ }
+
/**
* @see ILoadBalancer::openConnection()
*
}
if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
- // @todo move this logic to MessageCache
- if ( $this->exists() ) {
- // NOTE: use transclusion text for messages.
- // This is consistent with MessageCache::getMsgFromNamespace()
-
- $content = $this->getContent();
- $text = $content === null ? null : $content->getWikitextForTransclusion();
-
- if ( $text === null ) {
- $text = false;
- }
- } else {
- $text = false;
- }
-
- MessageCache::singleton()->replace( $this->mTitle->getDBkey(), $text );
+ $messageCache = MessageCache::singleton();
+ $messageCache->updateMessageOverride( $this->mTitle, $this->getContent() );
}
return true;
* - 'no-change': don't update the article count, ever
*/
public function doEditUpdates( Revision $revision, User $user, array $options = [] ) {
- global $wgRCWatchCategoryMembership, $wgContLang;
+ global $wgRCWatchCategoryMembership;
$options += [
'changed' => true,
}
if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
- // XXX: could skip pseudo-messages like js/css here, based on content model.
- $msgtext = $content ? $content->getWikitextForTransclusion() : null;
- if ( $msgtext === false || $msgtext === null ) {
- $msgtext = '';
- }
-
- MessageCache::singleton()->replace( $shortTitle, $msgtext );
-
- if ( $wgContLang->hasVariants() ) {
- $wgContLang->updateConversionTable( $this->mTitle );
- }
+ MessageCache::singleton()->updateMessageOverride( $this->mTitle, $content );
}
if ( $options['created'] ) {
* @param Title $title
*/
public static function onArticleDelete( Title $title ) {
- global $wgContLang;
-
// Update existence markers on article/talk tabs...
$other = $title->getOtherPage();
// Messages
if ( $title->getNamespace() == NS_MEDIAWIKI ) {
- MessageCache::singleton()->replace( $title->getDBkey(), false );
-
- if ( $wgContLang->hasVariants() ) {
- $wgContLang->updateConversionTable( $title );
- }
+ MessageCache::singleton()->updateMessageOverride( $title, null );
}
// Images
'names' => [ 2 => null ],
'min' => 2,
'max' => 2,
- ]
+ ],
+ '-{' => [
+ 'end' => '}-',
+ 'names' => [ 1 => null ],
+ 'min' => 1,
+ 'max' => 1,
+ ],
];
/**
* @return string
*/
public function preprocessToXml( $text, $flags = 0 ) {
+ global $wgDisableLangConversion;
+
$forInclusion = $flags & Parser::PTD_FOR_INCLUSION;
$xmlishElements = $this->parser->getStripList();
$stack = new PPDStack;
$searchBase = "[{<\n"; # }
+ if ( !$wgDisableLangConversion ) {
+ $searchBase .= '-';
+ }
+
// For fast reverse searches
$revText = strrev( $text );
$lengthText = strlen( $text );
break;
}
} else {
- $curChar = $text[$i];
+ $curChar = $curTwoChar = $text[$i];
+ if ( ( $i + 1 ) < $lengthText ) {
+ $curTwoChar .= $text[$i + 1];
+ }
if ( $curChar == '|' ) {
$found = 'pipe';
} elseif ( $curChar == '=' ) {
} else {
$found = 'line-start';
}
+ } elseif ( $curTwoChar == $currentClosing ) {
+ $found = 'close';
+ $curChar = $curTwoChar;
} elseif ( $curChar == $currentClosing ) {
$found = 'close';
+ } elseif ( isset( $this->rules[$curTwoChar] ) ) {
+ $curChar = $curTwoChar;
+ $found = 'open';
+ $rule = $this->rules[$curChar];
} elseif ( isset( $this->rules[$curChar] ) ) {
$found = 'open';
$rule = $this->rules[$curChar];
+ } elseif ( $curChar == '-' ) {
+ $found = 'dash';
} else {
# Some versions of PHP have a strcspn which stops on null characters
# Ignore and continue
// input pointer.
} elseif ( $found == 'open' ) {
# count opening brace characters
- $count = strspn( $text, $curChar, $i );
+ $curLen = strlen( $curChar );
+ $count = ( $curLen > 1 ) ? 1 : strspn( $text, $curChar, $i );
# we need to add to stack only if opening brace count is enough for one of the rules
if ( $count >= $rule['min'] ) {
# Add literal brace(s)
$accum .= htmlspecialchars( str_repeat( $curChar, $count ) );
}
- $i += $count;
+ $i += $curLen * $count;
} elseif ( $found == 'close' ) {
$piece = $stack->top;
# lets check if there are enough characters for closing brace
$maxCount = $piece->count;
- $count = strspn( $text, $curChar, $i, $maxCount );
+ $curLen = strlen( $curChar );
+ $count = ( $curLen > 1 ) ? 1 : strspn( $text, $curChar, $i, $maxCount );
# check for maximum matching characters (if there are 5 closing
# characters, we will probably need only 3 - depending on the rules)
# No matching element found in callback array
# Output a literal closing brace and continue
$accum .= htmlspecialchars( str_repeat( $curChar, $count ) );
- $i += $count;
+ $i += $curLen * $count;
continue;
}
$name = $rule['names'][$matchingCount];
}
# Advance input pointer
- $i += $matchingCount;
+ $i += $curLen * $matchingCount;
# Unwind the stack
$stack->pop();
$stack->getCurrentPart()->eqpos = strlen( $accum );
$accum .= '=';
++$i;
+ } elseif ( $found == 'dash' ) {
+ $accum .= '-';
+ ++$i;
}
}
* @return PPNode_Hash_Tree
*/
public function preprocessToObj( $text, $flags = 0 ) {
+ global $wgDisableLangConversion;
+
$tree = $this->cacheGetTree( $text, $flags );
if ( $tree !== false ) {
$store = json_decode( $tree );
$stack = new PPDStack_Hash;
$searchBase = "[{<\n";
+ if ( !$wgDisableLangConversion ) {
+ $searchBase .= '-';
+ }
+
// For fast reverse searches
$revText = strrev( $text );
$lengthText = strlen( $text );
break;
}
} else {
- $curChar = $text[$i];
+ $curChar = $curTwoChar = $text[$i];
+ if ( ( $i + 1 ) < $lengthText ) {
+ $curTwoChar .= $text[$i + 1];
+ }
if ( $curChar == '|' ) {
$found = 'pipe';
} elseif ( $curChar == '=' ) {
} else {
$found = 'line-start';
}
+ } elseif ( $curTwoChar == $currentClosing ) {
+ $found = 'close';
+ $curChar = $curTwoChar;
} elseif ( $curChar == $currentClosing ) {
$found = 'close';
+ } elseif ( isset( $this->rules[$curTwoChar] ) ) {
+ $curChar = $curTwoChar;
+ $found = 'open';
+ $rule = $this->rules[$curChar];
} elseif ( isset( $this->rules[$curChar] ) ) {
$found = 'open';
$rule = $this->rules[$curChar];
+ } elseif ( $curChar == '-' ) {
+ $found = 'dash';
} else {
# Some versions of PHP have a strcspn which stops on null characters
# Ignore and continue
// input pointer.
} elseif ( $found == 'open' ) {
# count opening brace characters
- $count = strspn( $text, $curChar, $i );
+ $curLen = strlen( $curChar );
+ $count = ( $curLen > 1 ) ? 1 : strspn( $text, $curChar, $i );
# we need to add to stack only if opening brace count is enough for one of the rules
if ( $count >= $rule['min'] ) {
# Add literal brace(s)
self::addLiteral( $accum, str_repeat( $curChar, $count ) );
}
- $i += $count;
+ $i += $curLen * $count;
} elseif ( $found == 'close' ) {
$piece = $stack->top;
# lets check if there are enough characters for closing brace
$maxCount = $piece->count;
- $count = strspn( $text, $curChar, $i, $maxCount );
+ $curLen = strlen( $curChar );
+ $count = ( $curLen > 1 ) ? 1 : strspn( $text, $curChar, $i, $maxCount );
# check for maximum matching characters (if there are 5 closing
# characters, we will probably need only 3 - depending on the rules)
# No matching element found in callback array
# Output a literal closing brace and continue
self::addLiteral( $accum, str_repeat( $curChar, $count ) );
- $i += $count;
+ $i += $curLen * $count;
continue;
}
$name = $rule['names'][$matchingCount];
}
# Advance input pointer
- $i += $matchingCount;
+ $i += $curLen * $matchingCount;
# Unwind the stack
$stack->pop();
$accum[] = [ 'equals', [ '=' ] ];
$stack->getCurrentPart()->eqpos = count( $accum ) - 1;
++$i;
+ } elseif ( $found == 'dash' ) {
+ self::addLiteral( $accum, '-' );
+ ++$i;
}
}
+++ /dev/null
-<?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
- */
-
-use Composer\Semver\VersionParser;
-use Composer\Semver\Constraint\Constraint;
-
-/**
- * @since 1.26
- */
-class CoreVersionChecker {
-
- /**
- * @var Constraint|bool representing $wgVersion
- */
- private $coreVersion = false;
-
- /**
- * @var VersionParser
- */
- private $versionParser;
-
- /**
- * @param string $coreVersion Current version of core
- */
- public function __construct( $coreVersion ) {
- $this->versionParser = new VersionParser();
- try {
- $this->coreVersion = new Constraint(
- '==',
- $this->versionParser->normalize( $coreVersion )
- );
- } catch ( UnexpectedValueException $e ) {
- // Non-parsable version, don't fatal.
- }
- }
-
- /**
- * Check that the provided constraint is compatible with the current version of core
- *
- * @param string $constraint Something like ">= 1.26"
- * @return bool
- */
- public function check( $constraint ) {
- if ( $this->coreVersion === false ) {
- // Couldn't parse the core version, so we can't check anything
- return true;
- }
-
- return $this->versionParser->parseConstraints( $constraint )
- ->matches( $this->coreVersion );
- }
-}
}
public function getRequirements( array $info ) {
- $requirements = [];
- $key = ExtensionRegistry::MEDIAWIKI_CORE;
- if ( isset( $info['requires'][$key] ) ) {
- $requirements[$key] = $info['requires'][$key];
- }
-
- return $requirements;
+ return isset( $info['requires'] ) ? $info['requires'] : [];
}
protected function extractHooks( array $info ) {
/**
* Bump whenever the registration cache needs resetting
*/
- const CACHE_VERSION = 4;
+ const CACHE_VERSION = 5;
/**
* Special key that defines the merge strategy
$autoloadClasses = [];
$autoloaderPaths = [];
$processor = new ExtensionProcessor();
+ $versionChecker = new VersionChecker( $wgVersion );
+ $extDependencies = [];
$incompatible = [];
- $coreVersionParser = new CoreVersionChecker( $wgVersion );
foreach ( $queue as $path => $mtime ) {
$json = file_get_contents( $path );
if ( $json === false ) {
throw new Exception( "$path is not a valid JSON file." );
}
- // Check any constraints against MediaWiki core
- $requires = $processor->getRequirements( $info );
- if ( isset( $requires[self::MEDIAWIKI_CORE] )
- && !$coreVersionParser->check( $requires[self::MEDIAWIKI_CORE] )
- ) {
- // Doesn't match, mark it as incompatible.
- $incompatible[] = "{$info['name']} is not compatible with the current "
- . "MediaWiki core (version {$wgVersion}), it requires: " . $requires[self::MEDIAWIKI_CORE]
- . '.';
- continue;
- }
-
if ( !isset( $info['manifest_version'] ) ) {
// For backwards-compatability, assume a version of 1
$info['manifest_version'] = 1;
}
$version = $info['manifest_version'];
if ( $version < self::OLDEST_MANIFEST_VERSION || $version > self::MANIFEST_VERSION ) {
- throw new Exception( "$path: unsupported manifest_version: {$version}" );
+ $incompatible[] = "$path: unsupported manifest_version: {$version}";
}
$autoload = $this->processAutoLoader( dirname( $path ), $info );
$GLOBALS['wgAutoloadClasses'] += $autoload;
$autoloadClasses += $autoload;
+ // get all requirements/dependencies for this extension
+ $requires = $processor->getRequirements( $info );
+
+ // validate the information needed and add the requirements
+ if ( is_array( $requires ) && $requires && isset( $info['name'] ) ) {
+ $extDependencies[$info['name']] = $requires;
+ }
+
// Get extra paths for later inclusion
$autoloaderPaths = array_merge( $autoloaderPaths,
$processor->getExtraAutoloaderPaths( dirname( $path ), $info ) );
// Compatible, read and extract info
$processor->extractInfo( $path, $info, $version );
}
+ $data = $processor->getExtractedInfo();
+
+ // check for incompatible extensions
+ $incompatible = array_merge(
+ $incompatible,
+ $versionChecker
+ ->setLoadedExtensionsAndSkins( $data['credits'] )
+ ->checkArray( $extDependencies )
+ );
+
if ( $incompatible ) {
if ( count( $incompatible ) === 1 ) {
throw new Exception( $incompatible[0] );
throw new Exception( implode( "\n", $incompatible ) );
}
}
- $data = $processor->getExtractedInfo();
+
// Need to set this so we can += to it later
$data['globals']['wgAutoloadClasses'] = [];
$data['autoload'] = $autoloadClasses;
--- /dev/null
+<?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
+ *
+ * @author Legoktm
+ * @author Florian Schmidt
+ */
+
+use Composer\Semver\VersionParser;
+use Composer\Semver\Constraint\Constraint;
+
+/**
+ * Provides functions to check a set of extensions with dependencies against
+ * a set of loaded extensions and given version information.
+ *
+ * @since 1.29
+ */
+class VersionChecker {
+ /**
+ * @var Constraint|bool representing $wgVersion
+ */
+ private $coreVersion = false;
+
+ /**
+ * @var array Loaded extensions
+ */
+ private $loaded = [];
+
+ /**
+ * @var VersionParser
+ */
+ private $versionParser;
+
+ /**
+ * @param string $coreVersion Current version of core
+ */
+ public function __construct( $coreVersion ) {
+ $this->versionParser = new VersionParser();
+ $this->setCoreVersion( $coreVersion );
+ }
+
+ /**
+ * Set an array with credits of all loaded extensions and skins.
+ *
+ * @param array $credits An array of installed extensions with credits of them
+ * @return VersionChecker $this
+ */
+ public function setLoadedExtensionsAndSkins( array $credits ) {
+ $this->loaded = $credits;
+
+ return $this;
+ }
+
+ /**
+ * Set MediaWiki core version.
+ *
+ * @param string $coreVersion Current version of core
+ */
+ private function setCoreVersion( $coreVersion ) {
+ try {
+ $this->coreVersion = new Constraint(
+ '==',
+ $this->versionParser->normalize( $coreVersion )
+ );
+ $this->coreVersion->setPrettyString( $coreVersion );
+ } catch ( UnexpectedValueException $e ) {
+ // Non-parsable version, don't fatal.
+ }
+ }
+
+ /**
+ * Check all given dependencies if they are compatible with the named
+ * installed extensions in the $credits array.
+ *
+ * Example $extDependencies:
+ * {
+ * 'FooBar' => {
+ * 'MediaWiki' => '>= 1.25.0',
+ * 'extensions' => {
+ * 'FooBaz' => '>= 1.25.0'
+ * },
+ * 'skins' => {
+ * 'BazBar' => '>= 1.0.0'
+ * }
+ * }
+ * }
+ *
+ * @param array $extDependencies All extensions that depend on other ones
+ * @return array
+ */
+ public function checkArray( array $extDependencies ) {
+ $errors = [];
+ foreach ( $extDependencies as $extension => $dependencies ) {
+ foreach ( $dependencies as $dependencyType => $values ) {
+ switch ( $dependencyType ) {
+ case ExtensionRegistry::MEDIAWIKI_CORE:
+ $mwError = $this->handleMediaWikiDependency( $values, $extension );
+ if ( $mwError !== false ) {
+ $errors[] = $mwError;
+ }
+ break;
+ case 'extensions':
+ case 'skin':
+ foreach ( $values as $dependency => $constraint ) {
+ $extError = $this->handleExtensionDependency( $dependency, $constraint, $extension );
+ if ( $extError !== false ) {
+ $errors[] = $extError;
+ }
+ }
+ break;
+ default:
+ throw new UnexpectedValueException( 'Dependency type ' . $dependencyType .
+ ' unknown in ' . $extension );
+ }
+ }
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Handle a dependency to MediaWiki core. It will check, if a MediaWiki version constraint was
+ * set with self::setCoreVersion before this call (if not, it will return an empty array) and
+ * checks the version constraint given against it.
+ *
+ * @param string $constraint The required version constraint for this dependency
+ * @param string $checkedExt The Extension, which depends on this dependency
+ * @return bool|string false if no error, or a string with the message
+ */
+ private function handleMediaWikiDependency( $constraint, $checkedExt ) {
+ if ( $this->coreVersion === false ) {
+ // Couldn't parse the core version, so we can't check anything
+ return false;
+ }
+
+ // if the installed and required version are compatible, return an empty array
+ if ( $this->versionParser->parseConstraints( $constraint )
+ ->matches( $this->coreVersion ) ) {
+ return false;
+ }
+ // otherwise mark this as incompatible.
+ return "{$checkedExt} is not compatible with the current "
+ . "MediaWiki core (version {$this->coreVersion->getPrettyString()}), it requires: "
+ . "$constraint.";
+ }
+
+ /**
+ * Handle a dependency to another extension.
+ *
+ * @param string $dependencyName The name of the dependency
+ * @param string $constraint The required version constraint for this dependency
+ * @param string $checkedExt The Extension, which depends on this dependency
+ * @return bool|string false for no errors, or a string message
+ */
+ private function handleExtensionDependency( $dependencyName, $constraint, $checkedExt ) {
+ // Check if the dependency is even installed
+ if ( !isset( $this->loaded[$dependencyName] ) ) {
+ return "{$checkedExt} requires {$dependencyName} to be installed.";
+ }
+ // Check if the dependency has specified a version
+ if ( !isset( $this->loaded[$dependencyName]['version'] ) ) {
+ // If we depend upon any version, and none is set, that's fine.
+ if ( $constraint === '*' ) {
+ wfDebug( "{$dependencyName} does not expose it's version, but {$checkedExt}
+ mentions it with constraint '*'. Assume it's ok so." );
+ return false;
+ } else {
+ // Otherwise, mark it as incompatible.
+ return "{$dependencyName} does not expose it's version, but {$checkedExt}
+ requires: {$constraint}.";
+ }
+ } else {
+ // Try to get a constraint for the dependency version
+ try {
+ $installedVersion = new Constraint(
+ '==',
+ $this->versionParser->normalize( $this->loaded[$dependencyName]['version'] )
+ );
+ } catch ( UnexpectedValueException $e ) {
+ // Non-parsable version, output an error message that the version
+ // string is invalid
+ return "$dependencyName does not have a valid version string.";
+ }
+ // Check if the constraint actually matches...
+ if (
+ !$this->versionParser->parseConstraints( $constraint )->matches( $installedVersion )
+ ) {
+ return "{$checkedExt} is not compatible with the current "
+ . "installed version of {$dependencyName} "
+ . "({$this->loaded[$dependencyName]['version']}), "
+ . "it requires: " . $constraint . '.';
+ }
+ }
+
+ return false;
+ }
+}
return Wikimedia\base_convert( $hash, 16, 36, 7 );
}
+ /**
+ * Add an error to the 'errors' array and log it.
+ *
+ * Should only be called from within respond().
+ *
+ * @since 1.29
+ * @param Exception $e
+ * @param string $msg
+ * @param array $context
+ */
+ protected function outputErrorAndLog( Exception $e, $msg, array $context = [] ) {
+ MWExceptionHandler::logException( $e );
+ $this->logger->warning(
+ $msg,
+ $context + [ 'exception' => $e ]
+ );
+ $this->errors[] = self::formatExceptionNoComment( $e );
+ }
+
/**
* Helper method to get and combine versions of multiple modules.
*
return '';
}
$hashes = array_map( function ( $module ) use ( $context ) {
- return $this->getModule( $module )->getVersionHash( $context );
+ try {
+ return $this->getModule( $module )->getVersionHash( $context );
+ } catch ( Exception $e ) {
+ // If modules fail to compute a version, do still consider the versions
+ // of other modules - don't set an empty string E-Tag for the whole request.
+ // See also T152266 and StartupModule::getModuleRegistrations().
+ $this->outputErrorAndLog( $e,
+ 'Calculating version for "{module}" failed: {exception}',
+ [
+ 'module' => $module,
+ ]
+ );
+ return '';
+ }
}, $moduleNames );
return self::makeHash( implode( '', $hashes ) );
}
// Preload for getCombinedVersion() and for batch makeModuleResponse()
$this->preloadModuleInfo( array_keys( $modules ), $context );
} catch ( Exception $e ) {
- MWExceptionHandler::logException( $e );
- $this->logger->warning( 'Preloading module info failed: {exception}', [
- 'exception' => $e
- ] );
- $this->errors[] = self::formatExceptionNoComment( $e );
+ $this->outputErrorAndLog( $e, 'Preloading module info failed: {exception}' );
}
// Combine versions to propagate cache invalidation
try {
$versionHash = $this->getCombinedVersion( $context, array_keys( $modules ) );
} catch ( Exception $e ) {
- MWExceptionHandler::logException( $e );
- $this->logger->warning( 'Calculating version hash failed: {exception}', [
- 'exception' => $e
- ] );
- $this->errors[] = self::formatExceptionNoComment( $e );
+ $this->outputErrorAndLog( $e, 'Calculating version hash failed: {exception}' );
}
// See RFC 2616 § 3.11 Entity Tags
return MWExceptionHandler::getPublicLogMessage( $e );
}
- return MWExceptionHandler::getLogMessage( $e );
+ return MWExceptionHandler::getLogMessage( $e ) .
+ "\nBacktrace:\n" .
+ MWExceptionHandler::getRedactedTraceAsString( $e );
}
/**
$out .= $strContent;
} catch ( Exception $e ) {
- MWExceptionHandler::logException( $e );
- $this->logger->warning( 'Generating module package failed: {exception}', [
- 'exception' => $e
- ] );
- $this->errors[] = self::formatExceptionNoComment( $e );
+ $this->outputErrorAndLog( $e, 'Generating module package failed: {exception}' );
// Respond to client with error-state instead of module implementation
$states[$name] = 'error';
* @return string JavaScript code for registering all modules with the client loader
*/
public function getModuleRegistrations( ResourceLoaderContext $context ) {
-
$resourceLoader = $context->getResourceLoader();
$target = $context->getRequest()->getVal( 'target', 'desktop' );
// Bypass target filter if this request is Special:JavaScriptTest.
$byPassTargetFilter = $this->getConfig()->get( 'EnableJavaScriptTest' ) && $target === 'test';
$out = '';
+ $states = [];
$registryData = [];
// Get registry data
continue;
}
- $versionHash = $module->getVersionHash( $context );
- if ( strlen( $versionHash ) !== 7 ) {
+ try {
+ $versionHash = $module->getVersionHash( $context );
+ } catch ( Exception $e ) {
+ // See also T152266 and ResourceLoader::getCombinedVersion()
+ MWExceptionHandler::logException( $e );
+ $context->getLogger()->warning(
+ 'Calculating version for "{module}" failed: {exception}',
+ [
+ 'module' => $name,
+ 'exception' => $e,
+ ]
+ );
+ $versionHash = '';
+ $states[$name] = 'error';
+ }
+
+ if ( $versionHash !== '' && strlen( $versionHash ) !== 7 ) {
$context->getLogger()->warning(
"Module '{module}' produced an invalid version hash: '{version}'.",
[
// Register modules
$out .= "\n" . ResourceLoader::makeLoaderRegisterScript( $registrations );
+ if ( $states ) {
+ $out .= "\n" . ResourceLoader::makeLoaderStateScript( $states );
+ }
+
return $out;
}
* @author Brian Wolff
*/
-use stdClass;
-
/**
* @ingroup SpecialPage
*/
* MalformedTitleException is thrown when a TitleParser is unable to parse a title string.
* @since 1.23
*/
-class MalformedTitleException extends Exception {
+class MalformedTitleException extends Exception implements ILocalizedException {
private $titleText = null;
private $errorMessage = null;
private $errorMessageParameters = [];
public function getErrorMessageParameters() {
return $this->errorMessageParameters;
}
+
+ /**
+ * @since 1.29
+ * @return Message
+ */
+ public function getMessageObject() {
+ return wfMessage( $this->getErrorMessage(), $this->getErrorMessageParameters() );
+ }
}
public $dateFormatStrings = [];
public $mExtendedSpecialPageAliases;
- protected $namespaceNames, $mNamespaceIds, $namespaceAliases;
+ /** @var array|null */
+ protected $namespaceNames;
+ protected $mNamespaceIds, $namespaceAliases;
/**
* ReplacementArray object caches
if ( is_null( $this->namespaceNames ) ) {
global $wgMetaNamespace, $wgMetaNamespaceTalk, $wgExtraNamespaces;
- $this->namespaceNames = self::$dataCache->getItem( $this->mCode, 'namespaceNames' );
$validNamespaces = MWNamespace::getCanonicalNamespaces();
- /** @suppress PhanTypeInvalidLeftOperand */
- $this->namespaceNames = $wgExtraNamespaces + $this->namespaceNames + $validNamespaces;
+ $this->namespaceNames = $wgExtraNamespaces +
+ self::$dataCache->getItem( $this->mCode, 'namespaceNames' );
+ $this->namespaceNames += $validNamespaces;
$this->namespaceNames[NS_PROJECT] = $wgMetaNamespace;
if ( $wgMetaNamespaceTalk ) {
/**
* @param string $key
- * @return array|null
+ * @return string|null
*/
public function getMessage( $key ) {
return self::$dataCache->getSubitem( $this->mCode, 'messages', $key );
"wlshowhidemine": "my edits",
"wlshowhidecategorization": "page categorization",
"watchlist-options": "Watchlist options",
- "watchlist-mark-all-visited": "Are you sure you want to reset unseen watchlist changes by marking all pages as visited?",
"watching": "Watching...",
"unwatching": "Unwatching...",
"watcherrortext": "An error occurred while changing your watchlist settings for \"$1\".",
"wlshowhidemine": "Option text in [[Special:Watchlist]]. Cf. {{msg-mw|rcshowhidemine}}.",
"wlshowhidecategorization": "Option text in [[Special:Watchlist]]. Cf. {{msg-mw|rcshowhidecategorization}}.",
"watchlist-options": "Legend of the fieldset of [[Special:Watchlist]]\n\nSee also:\n* {{msg-mw|Watchlist-details|watchlist header}}\n* {{msg-mw|Wlheader-enotif|watchlist header}}\n* {{msg-mw|enotif reset|Submit button text}}",
- "watchlist-mark-all-visited": "Dialog text in [[Special:Watchlist]] displayed for confirming whether the user wants to reset unseen watchlist changes by marking all pages as visited.",
"watching": "Text displayed when clicked on the watch tab: {{msg-mw|Watch}}. It means the wiki is adding that page to your watchlist.",
"unwatching": "Text displayed when clicked on the unwatch tab: {{msg-mw|Unwatch}}. It means the wiki is removing that page from your watchlist.",
"watcherrortext": "When a user clicked the watch/unwatch tab and the action did not succeed, this message is displayed.\n\nThis message is used raw and should not contain wikitext.\n\nParameters:\n* $1 - ...\nSee also:\n* {{msg-mw|Addedwatchtext}}",
],
'mediawiki.special.watchlist' => [
'scripts' => 'resources/src/mediawiki.special/mediawiki.special.watchlist.js',
- 'messages' => 'watchlist-mark-all-visited',
'dependencies' => [
'mediawiki.api',
- 'mediawiki.jqueryMsg',
- 'oojs-ui-windows',
+ 'oojs-ui-core',
'user.options',
]
],
*/
( function ( mw, $, OO ) {
$( function () {
- var $resetForm = $( '#mw-watchlist-resetbutton' ),
- $progressBar = new OO.ui.ProgressBarWidget( { progress: false } ).$element;
-
- $progressBar.css( {
- visibility: 'hidden',
- position: 'absolute',
- width: '100%'
- } );
- $resetForm.append( $progressBar );
+ var $progressBar, $resetForm = $( '#mw-watchlist-resetbutton' );
// If the user wants to reset their watchlist, use an API call to do so (no reload required)
// Adapted from a user script by User:NQ of English Wikipedia
// (User:NQ/WatchlistResetConfirm.js)
$resetForm.submit( function ( event ) {
- event.preventDefault();
+ var $button = $resetForm.find( 'input[name=mw-watchlist-reset-submit]' );
- OO.ui.confirm( mw.msg( 'watchlist-mark-all-visited' ) ).done( function ( confirmed ) {
- var $button;
-
- if ( confirmed ) {
- // Disable reset button to prevent multiple requests and show progress bar
- $button = $resetForm.find( 'input[name=mw-watchlist-reset-submit]' ).prop( 'disabled', true );
- $progressBar.css( 'visibility', 'visible' );
-
- // Use action=setnotificationtimestamp to mark all as visited,
- // then set all watchlist lines accordingly
- new mw.Api().postWithToken( 'csrf', {
- formatversion: 2,
- action: 'setnotificationtimestamp',
- entirewatchlist: true
- } ).done( function () {
- $button.css( 'visibility', 'hidden' );
- $progressBar.css( 'visibility', 'hidden' );
- $( '.mw-changeslist-line-watched' )
- .removeClass( 'mw-changeslist-line-watched' )
- .addClass( 'mw-changeslist-line-not-watched' );
- } ).fail( function () {
- // On error, fall back to server-side reset
- // First remove this submit listener and then re-submit the form
- $resetForm.off( 'submit' ).submit();
- } );
+ event.preventDefault();
- }
+ // Disable reset button to prevent multiple concurrent requests
+ $button.prop( 'disabled', true );
+
+ // Show progress bar
+ if ( $progressBar ) {
+ $progressBar.css( 'visibility', 'visible' );
+ } else {
+ $progressBar = new OO.ui.ProgressBarWidget( { progress: false } ).$element;
+ $progressBar.css( {
+ position: 'absolute',
+ width: '100%'
+ } );
+ $resetForm.append( $progressBar );
+ }
+
+ // Use action=setnotificationtimestamp to mark all as visited,
+ // then set all watchlist lines accordingly
+ new mw.Api().postWithToken( 'csrf', {
+ formatversion: 2,
+ action: 'setnotificationtimestamp',
+ entirewatchlist: true
+ } ).done( function () {
+ // Enable button again
+ $button.prop( 'disabled', false );
+ // Hide the button because further clicks can not generate any visual changes
+ $button.css( 'visibility', 'hidden' );
+ $progressBar.css( 'visibility', 'hidden' );
+ $( '.mw-changeslist-line-watched' )
+ .removeClass( 'mw-changeslist-line-watched' )
+ .addClass( 'mw-changeslist-line-not-watched' );
+ } ).fail( function () {
+ // On error, fall back to server-side reset
+ // First remove this submit listener and then re-submit the form
+ $resetForm.off( 'submit' ).submit();
} );
} );
</p>
!! end
+!! test
+T146304: Don't break template parsing if language converter markup is in the parameter.
+!! options
+language=sr variant=sr-ec
+!! wikitext
+{{echo|-{R|foo}-}}
+!! html/php
+<p>foo
+</p>
+!! end
+
# FIXME: This test is currently broken in the PHP parser (bug 52661)
!! test
Don't break image parsing if language converter markup is in the caption.
* Clones all tables in the given database (whatever database that connection has
* open), to versions with the test prefix.
*
- * @param Database $db Database to use
+ * @param IMaintainableDatabase $db Database to use
* @param string $prefix Prefix to use for test tables
* @return bool True if tables were cloned, false if only the prefix was changed
*/
- protected static function setupDatabaseWithTestPrefix( Database $db, $prefix ) {
+ protected static function setupDatabaseWithTestPrefix( IMaintainableDatabase $db, $prefix ) {
$tablesCloned = self::listTables( $db );
$dbClone = new CloneDatabase( $db, $tablesCloned, $prefix );
$dbClone->useTemporaryTables( self::$useTemporaryTables );
list( $proto, $cluster ) = explode( '://', $url, 2 );
// Avoid getMaster() because setupDatabaseWithTestPrefix()
// requires Database instead of plain DBConnRef/IDatabase
- $lb = $externalStoreDB->getLoadBalancer( $cluster );
- $dbw = $lb->getConnection( DB_MASTER );
- $dbws[] = $dbw;
+ $dbws[] = $externalStoreDB->getMaster( $cluster );
}
}
/**
* @since 1.18
*
- * @param Database $db
+ * @param IMaintainableDatabase $db
*
* @return array
*/
- public static function listTables( Database $db ) {
+ public static function listTables( IMaintainableDatabase $db ) {
$prefix = $db->tablePrefix();
$tables = $db->listTables( $prefix, __METHOD__ );
if ( isset( PHPUnitMaintClass::$additionalOptions[$offset] ) ) {
return PHPUnitMaintClass::$additionalOptions[$offset];
}
+
+ return null;
}
/**
* @param array|string $options Language code or options array
* - string 'lang' Language code
* - string 'dir' Language direction (ltr or rtl)
+ * - string 'modules' Pipe-separated list of module names
+ * - string|null 'only' "scripts" (unwrapped script), "styles" (stylesheet), or null
+ * (mw.loader.implement).
* @return ResourceLoaderContext
*/
- protected function getResourceLoaderContext( $options = [] ) {
+ protected function getResourceLoaderContext( $options = [], ResourceLoader $rl = null ) {
if ( is_string( $options ) ) {
// Back-compat for extension tests
$options = [ 'lang' => $options ];
$options += [
'lang' => 'en',
'dir' => 'ltr',
+ 'modules' => 'startup',
+ 'only' => 'scripts',
];
- $resourceLoader = new ResourceLoader();
+ $resourceLoader = $rl ?: new ResourceLoader();
$request = new FauxRequest( [
'lang' => $options['lang'],
- 'modules' => 'startup',
- 'only' => 'scripts',
+ 'modules' => $options['modules'],
+ 'only' => $options['only'],
'skin' => 'vector',
'target' => 'phpunit',
] );
public function __construct( Config $config = null, LoggerInterface $logger = null ) {
$this->setLogger( $logger ?: new NullLogger() );
$this->config = $config ?: MediaWikiServices::getInstance()->getMainConfig();
+ // Source "local" is required by StartupModule
+ $this->addSource( 'local', $this->config->get( 'LoadScript' ) );
$this->setMessageBlobStore( new MessageBlobStore( $this, $this->getLogger() ) );
}
+
+ public function getErrors() {
+ return $this->errors;
+ }
}
+++ /dev/null
-<?php
-
-/**
- * @covers CoreVersionChecker
- */
-class CoreVersionCheckerTest extends PHPUnit_Framework_TestCase {
- /**
- * @dataProvider provideCheck
- */
- public function testCheck( $coreVersion, $constraint, $expected ) {
- $checker = new CoreVersionChecker( $coreVersion );
- $this->assertEquals( $expected, $checker->check( $constraint ) );
- }
-
- public static function provideCheck() {
- return [
- // [ $wgVersion, constraint, expected ]
- [ '1.25alpha', '>= 1.26', false ],
- [ '1.25.0', '>= 1.26', false ],
- [ '1.26alpha', '>= 1.26', true ],
- [ '1.26alpha', '>= 1.26.0', true ],
- [ '1.26alpha', '>= 1.26.0-stable', false ],
- [ '1.26.0', '>= 1.26.0-stable', true ],
- [ '1.26.1', '>= 1.26.0-stable', true ],
- [ '1.27.1', '>= 1.26.0-stable', true ],
- [ '1.26alpha', '>= 1.26.1', false ],
- [ '1.26alpha', '>= 1.26alpha', true ],
- [ '1.26alpha', '>= 1.25', true ],
- [ '1.26.0-alpha.14', '>= 1.26.0-alpha.15', false ],
- [ '1.26.0-alpha.14', '>= 1.26.0-alpha.10', true ],
- [ '1.26.1', '>= 1.26.2, <=1.26.0', false ],
- [ '1.26.1', '^1.26.2', false ],
- // Accept anything for un-parsable version strings
- [ '1.26mwf14', '== 1.25alpha', true ],
- [ 'totallyinvalid', '== 1.0', true ],
- ];
- }
-}
--- /dev/null
+<?php
+
+/**
+ * @covers VersionChecker
+ */
+class VersionCheckerTest extends PHPUnit_Framework_TestCase {
+ /**
+ * @dataProvider provideCheck
+ */
+ public function testCheck( $coreVersion, $constraint, $expected ) {
+ $checker = new VersionChecker( $coreVersion );
+ $this->assertEquals( $expected, !(bool)$checker->checkArray( [
+ 'FakeExtension' => [
+ 'MediaWiki' => $constraint,
+ ],
+ ] )
+ );
+ }
+
+ public static function provideCheck() {
+ return [
+ // [ $wgVersion, constraint, expected ]
+ [ '1.25alpha', '>= 1.26', false ],
+ [ '1.25.0', '>= 1.26', false ],
+ [ '1.26alpha', '>= 1.26', true ],
+ [ '1.26alpha', '>= 1.26.0', true ],
+ [ '1.26alpha', '>= 1.26.0-stable', false ],
+ [ '1.26.0', '>= 1.26.0-stable', true ],
+ [ '1.26.1', '>= 1.26.0-stable', true ],
+ [ '1.27.1', '>= 1.26.0-stable', true ],
+ [ '1.26alpha', '>= 1.26.1', false ],
+ [ '1.26alpha', '>= 1.26alpha', true ],
+ [ '1.26alpha', '>= 1.25', true ],
+ [ '1.26.0-alpha.14', '>= 1.26.0-alpha.15', false ],
+ [ '1.26.0-alpha.14', '>= 1.26.0-alpha.10', true ],
+ [ '1.26.1', '>= 1.26.2, <=1.26.0', false ],
+ [ '1.26.1', '^1.26.2', false ],
+ // Accept anything for un-parsable version strings
+ [ '1.26mwf14', '== 1.25alpha', true ],
+ [ 'totallyinvalid', '== 1.0', true ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideType
+ */
+ public function testType( $given, $expected ) {
+ $checker = new VersionChecker( '1.0.0' );
+ $checker
+ ->setLoadedExtensionsAndSkins( [
+ 'FakeDependency' => [
+ 'version' => '1.0.0',
+ ],
+ ] );
+ $this->assertEquals( $expected, $checker->checkArray( [
+ 'FakeExtension' => $given,
+ ] )
+ );
+ }
+
+ public static function provideType() {
+ return [
+ // valid type
+ [
+ [
+ 'extensions' => [
+ 'FakeDependency' => '1.0.0'
+ ]
+ ],
+ []
+ ],
+ [
+ [
+ 'MediaWiki' => '1.0.0'
+ ],
+ []
+ ],
+ ];
+ }
+
+ /**
+ * Check, if a non-parsable version constraint does not throw an exception or
+ * returns any error message.
+ */
+ public function testInvalidConstraint() {
+ $checker = new VersionChecker( '1.0.0' );
+ $checker
+ ->setLoadedExtensionsAndSkins( [
+ 'FakeDependency' => [
+ 'version' => 'not really valid',
+ ],
+ ] );
+ $this->assertEquals( [ "FakeDependency does not have a valid version string." ],
+ $checker->checkArray( [
+ 'FakeExtension' => [
+ 'extensions' => [
+ 'FakeDependency' => '1.24.3',
+ ],
+ ],
+ ] )
+ );
+
+ $checker = new VersionChecker( '1.0.0' );
+ $checker
+ ->setLoadedExtensionsAndSkins( [
+ 'FakeDependency' => [
+ 'version' => '1.24.3',
+ ],
+ ] );
+
+ $this->setExpectedException( 'UnexpectedValueException' );
+ $checker->checkArray( [
+ 'FakeExtension' => [
+ 'FakeDependency' => 'not really valid',
+ ]
+ ] );
+ }
+}
'ResourceLoaderDebug' => false,
'DefaultSkin' => 'fallback',
'LanguageCode' => 'nl',
+ 'LoadScript' => '/w/load.php',
] ) );
}
'Foo' => '#eeeeee',
'bar' => 5,
],
+ // Clear ResourceLoaderGetConfigVars hooks (called by StartupModule)
+ // to avoid notices during testMakeModuleResponse for missing
+ // wgResourceLoaderLESSVars keys in extension hooks.
+ 'wgHooks' => [],
] );
}
$this->assertTrue( true );
}
}
+
+ protected function getFailFerryMock() {
+ $mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
+ ->setMethods( [ 'getScript' ] )
+ ->getMock();
+ $mock->method( 'getScript' )->will( $this->throwException(
+ new Exception( 'Ferry not found' )
+ ) );
+ return $mock;
+ }
+
+ protected function getSimpleModuleMock( $script = '' ) {
+ $mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
+ ->setMethods( [ 'getScript' ] )
+ ->getMock();
+ $mock->method( 'getScript' )->willReturn( $script );
+ return $mock;
+ }
+
+ /**
+ * @covers ResourceLoader::getCombinedVersion
+ */
+ public function testGetCombinedVersion() {
+ $rl = new EmptyResourceLoader();
+ $rl->register( [
+ 'foo' => self::getSimpleModuleMock(),
+ 'ferry' => self::getFailFerryMock(),
+ 'bar' => self::getSimpleModuleMock(),
+ ] );
+ $context = $this->getResourceLoaderContext( [], $rl );
+
+ $this->assertEquals(
+ ResourceLoader::makeHash( self::BLANK_VERSION ),
+ $rl->getCombinedVersion( $context, [ 'foo' ] ),
+ 'compute foo'
+ );
+
+ // Verify that getCombinedVersion() does not throw when ferry fails.
+ // Instead it gracefully continues to combine the remaining modules.
+ $this->assertEquals(
+ ResourceLoader::makeHash( self::BLANK_VERSION . self::BLANK_VERSION ),
+ $rl->getCombinedVersion( $context, [ 'foo', 'ferry', 'bar' ] ),
+ 'compute foo+ferry+bar (T152266)'
+ );
+ }
+
+ /**
+ * Verify that when building module content in a load.php response,
+ * an exception from one module will not break script output from
+ * other modules.
+ */
+ public function testMakeModuleResponseError() {
+ $modules = [
+ 'foo' => self::getSimpleModuleMock( 'foo();' ),
+ 'ferry' => self::getFailFerryMock(),
+ 'bar' => self::getSimpleModuleMock( 'bar();' ),
+ ];
+ $rl = new EmptyResourceLoader();
+ $rl->register( $modules );
+ $context = $this->getResourceLoaderContext(
+ [
+ 'modules' => 'foo|ferry|bar',
+ 'only' => 'scripts',
+ ],
+ $rl
+ );
+
+ $response = $rl->makeModuleResponse( $context, $modules );
+ $errors = $rl->getErrors();
+
+ $this->assertCount( 1, $errors );
+ $this->assertRegExp( '/Ferry not found/', $errors[0] );
+ $this->assertEquals(
+ 'foo();bar();mw.loader.state( {
+ "ferry": "error",
+ "foo": "ready",
+ "bar": "ready"
+} );',
+ $response
+ );
+ }
+
+ /**
+ * Verify that when building the startup module response,
+ * an exception from one module class will not break the entire
+ * startup module response. See T152266.
+ */
+ public function testMakeModuleResponseStartupError() {
+ $rl = new EmptyResourceLoader();
+ $rl->register( [
+ 'foo' => self::getSimpleModuleMock( 'foo();' ),
+ 'ferry' => self::getFailFerryMock(),
+ 'bar' => self::getSimpleModuleMock( 'bar();' ),
+ 'startup' => [ 'class' => 'ResourceLoaderStartUpModule' ],
+ ] );
+ $context = $this->getResourceLoaderContext(
+ [
+ 'modules' => 'startup',
+ 'only' => 'scripts',
+ ],
+ $rl
+ );
+
+ $this->assertEquals(
+ [ 'foo', 'ferry', 'bar', 'startup' ],
+ $rl->getModuleNames(),
+ 'getModuleNames'
+ );
+
+ $modules = [ 'startup' => $rl->getModule( 'startup' ) ];
+ $response = $rl->makeModuleResponse( $context, $modules );
+ $errors = $rl->getErrors();
+
+ $this->assertRegExp( '/Ferry not found/', $errors[0] );
+ $this->assertCount( 1, $errors );
+ $this->assertRegExp(
+ '/isCompatible.*function startUp/s',
+ $response,
+ 'startup response undisrupted (T152266)'
+ );
+ $this->assertRegExp(
+ '/register\([^)]+"ferry",\s*""/s',
+ $response,
+ 'startup response registers broken module'
+ );
+ $this->assertRegExp(
+ '/state\([^)]+"ferry":\s*"error"/s',
+ $response,
+ 'startup response sets state to error'
+ );
+ }
}