diff options
42 files changed, 401 insertions, 208 deletions
diff --git a/RELEASE-NOTES-1.38 b/RELEASE-NOTES-1.38 index 714da17f46c6..86d287eb6c23 100644 --- a/RELEASE-NOTES-1.38 +++ b/RELEASE-NOTES-1.38 @@ -342,6 +342,8 @@ because of Phabricator reports. - ::pendingMasterChangeCallers() since 1.37 - ::forEachOpenMasterConnection() since 1.37 - ::waitForMasterPos() since 1.37 +* LoadBalancer::safeGetLag() which has been soft-deprecated since 1.34 has been + removed. * Special:ListFiles and Special:NewFiles no longer allows to search for parts of file names (option was not available for $wgMiserMode = true) * … diff --git a/docs/extension.schema.v1.json b/docs/extension.schema.v1.json index b654843870be..f2bfc571e611 100644 --- a/docs/extension.schema.v1.json +++ b/docs/extension.schema.v1.json @@ -1118,27 +1118,38 @@ }, { "type": "object", - "description": "An object factory specification for a class implementing Wikimedia\\Parsoid\\Ext\\ExtensionModule", + "description": "Object factory specification for a class implementing Wikimedia\\Parsoid\\Ext\\ExtensionModule", "properties": { "factory": { "type": ["string", "array"], "description": "A factory function to be called to create an ExtensionModule. This should be omitted if a class is specified." - }, + } + }, + "required": [ "factory" ] + }, + { + "type": "object", + "description": "Object factory specification for a class implementing Wikimedia\\Parsoid\\Ext\\ExtensionModule", + "properties": { "class": { "type": "string", "description": "The fully-qualified class name of a class implemeting Wikimedia\\Parsoid\\Ext\\ExtensionModule. This should be omitted if a factory is specified." - }, - "args": { - "type": "array", - "description": "The arguments passed to the ExtensionModule constructor or factory" } }, - "additionalProperties": false + "required": [ "class" ] }, { "type": "object", "description": "A Parsoid extension module configuration array", "properties": { + "class": { + "type": "null", + "description": "This property must not be present in a configuration array" + }, + "factory": { + "type": "null", + "description": "This property must not be present in a configuration array" + }, "domProcessors": { "type": "array", "items": { @@ -1152,6 +1163,13 @@ "description": "Additional ResourceLoader styles to include" } }, + "annotations": { + "type": "array", + "items": { + "type": "string", + "description": "The name of an annotation tag" + } + }, "tags": { "type": "array", "items": { @@ -1173,8 +1191,7 @@ "required": ["name"] } } - }, - "additionalProperties": false + } } ] } diff --git a/docs/extension.schema.v2.json b/docs/extension.schema.v2.json index 95ca01ded6d5..e2438d3d954d 100644 --- a/docs/extension.schema.v2.json +++ b/docs/extension.schema.v2.json @@ -1182,27 +1182,38 @@ }, { "type": "object", - "description": "An object factory specification for a class implementing Wikimedia\\Parsoid\\Ext\\ExtensionModule", + "description": "Object factory specification for a class implementing Wikimedia\\Parsoid\\Ext\\ExtensionModule", "properties": { "factory": { "type": ["string", "array"], "description": "A factory function to be called to create an ExtensionModule. This should be omitted if a class is specified." - }, + } + }, + "required": [ "factory" ] + }, + { + "type": "object", + "description": "Object factory specification for a class implementing Wikimedia\\Parsoid\\Ext\\ExtensionModule", + "properties": { "class": { "type": "string", "description": "The fully-qualified class name of a class implemeting Wikimedia\\Parsoid\\Ext\\ExtensionModule. This should be omitted if a factory is specified." - }, - "args": { - "type": "array", - "description": "The arguments passed to the ExtensionModule constructor or factory" } }, - "additionalProperties": false + "required": [ "class" ] }, { "type": "object", "description": "A Parsoid extension module configuration array", "properties": { + "class": { + "type": "null", + "description": "This property must not be present in a configuration array" + }, + "factory": { + "type": "null", + "description": "This property must not be present in a configuration array" + }, "domProcessors": { "type": "array", "items": { @@ -1216,6 +1227,13 @@ "description": "Additional ResourceLoader styles to include" } }, + "annotations": { + "type": "array", + "items": { + "type": "string", + "description": "The name of an annotation tag" + } + }, "tags": { "type": "array", "items": { @@ -1237,8 +1255,7 @@ "required": ["name"] } } - }, - "additionalProperties": false + } } ] } diff --git a/includes/Linker.php b/includes/Linker.php index 44ad1a66a79a..cc4019e2a1fe 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -1983,14 +1983,17 @@ class Linker { $editCount = self::getRollbackEditCount( $revRecord, false ); } - if ( $editCount > $showRollbackEditCount ) { - $html = $context->msg( 'rollbacklinkcount-morethan' ) - ->numParams( $showRollbackEditCount )->parse(); - } else { - $html = $context->msg( 'rollbacklinkcount' )->numParams( $editCount )->parse(); - } + // The edit count can be 0 on replica lag, fall back to the generic rollbacklink message + if ( $editCount > 0 ) { + if ( $editCount > $showRollbackEditCount ) { + $html = $context->msg( 'rollbacklinkcount-morethan' ) + ->numParams( $showRollbackEditCount )->parse(); + } else { + $html = $context->msg( 'rollbacklinkcount' )->numParams( $editCount )->parse(); + } - return self::link( $title, $html, $attrs, $query, $options ); + return self::link( $title, $html, $attrs, $query, $options ); + } } $html = $context->msg( 'rollbacklink' )->escaped(); diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 63653aaff770..321c70e7b1cd 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -20,6 +20,7 @@ * @file */ +use MediaWiki\Config\ServiceOptions; use MediaWiki\HookContainer\ProtectedHookAccessorTrait; use MediaWiki\Linker\LinkTarget; use MediaWiki\MediaWikiServices; @@ -27,6 +28,7 @@ use MediaWiki\Page\PageRecord; use MediaWiki\Page\PageReference; use MediaWiki\Permissions\PermissionStatus; use MediaWiki\Session\SessionManager; +use MediaWiki\Tidy\RemexDriver; use Wikimedia\Rdbms\IResultWrapper; use Wikimedia\RelPath; use Wikimedia\WrappedString; @@ -989,7 +991,15 @@ class OutputPage extends ContextSource { # change "<script>foo&bar</script>" to "<script>foo&bar</script>" # but leave "<i>foobar</i>" alone - $nameWithTags = Sanitizer::normalizeCharReferences( Sanitizer::removeHTMLtags( $name ) ); + # == start explicit tidy == + # T299722/T298401: The explicit tidy here can be removed once + # Sanitizer::removeHTMLtags() is made more robust. + $tidy = new RemexDriver( new ServiceOptions( [ 'TidyConfig' ], [ + 'TidyConfig' => [ 'pwrap' => false ], + ] ) ); + $nameWithTags = $tidy->tidy( $name, [ Sanitizer::class, 'armorFrenchSpaces' ] ); + # == end explicit tidy == + $nameWithTags = Sanitizer::normalizeCharReferences( Sanitizer::removeHTMLtags( $nameWithTags ) ); $this->mPageTitle = $nameWithTags; # change "<i>foo&bar</i>" to "foo&bar" diff --git a/includes/Settings/Cache/CacheableSource.php b/includes/Settings/Cache/CacheableSource.php index c6d6e293f019..408667d23b1e 100644 --- a/includes/Settings/Cache/CacheableSource.php +++ b/includes/Settings/Cache/CacheableSource.php @@ -13,6 +13,20 @@ use MediaWiki\Settings\Source\SettingsSource; */ interface CacheableSource extends SettingsSource { /** + * Allow the caching layer to attempt to return stale results in the event + * that loading from the original source fails. + * + * Note that allowing stale results will result in cache items being + * stored indefinitely regardless of the {@link getExpiryTtl()} value, and + * since there is currently no pruning of cache items, it is advised that + * sources allowing stale results also implement an immutable + * `getHashKey()` based only on constructor arguments. + * + * @return bool + */ + public function allowsStaleLoad(): bool; + + /** * Returns the cache TTL (in seconds) for this source. * * @return int diff --git a/includes/Settings/Cache/CachedSource.php b/includes/Settings/Cache/CachedSource.php index 7e1cf55f29d1..1430f9e94f86 100644 --- a/includes/Settings/Cache/CachedSource.php +++ b/includes/Settings/Cache/CachedSource.php @@ -3,6 +3,7 @@ namespace MediaWiki\Settings\Cache; use BagOStuff; +use MediaWiki\Settings\SettingsBuilderException; use MediaWiki\Settings\Source\SettingsIncludeLocator; use MediaWiki\Settings\Source\SettingsSource; @@ -38,6 +39,10 @@ class CachedSource implements SettingsSource, SettingsIncludeLocator { * Queries cache for source contents and performs loading/caching of the * source contents on miss. * + * If the load fails but the source implements {@link + * CacheableSource::allowsStaleLoad()} as <code>true</code>, stale results + * may be returned if still present in the cache store. + * * @return array */ public function load(): array { @@ -46,19 +51,25 @@ class CachedSource implements SettingsSource, SettingsIncludeLocator { $this->source->getHashKey() ); $item = $this->cache->get( $key ); - - $miss = - $item === false || - $this->expiresEarly( $item, $this->source->getExpiryWeight() ); - - if ( $miss ) { - $item = $this->loadWithMetadata(); - $this->cache->set( $key, $item, $this->source->getExpiryTtl() ); - } - - // This shouldn't be possible but let's make phan happy - if ( !is_array( $item ) || !array_key_exists( 'value', $item ) ) { - return []; + $miss = $this->isMiss( $item ); + + if ( $miss || $this->isExpired( $item ) ) { + $allowsStale = $this->source->allowsStaleLoad(); + + try { + $item = $this->loadWithMetadata(); + $this->cache->set( + $key, + $item, + $allowsStale ? BagOStuff::TTL_INDEFINITE : $this->source->getExpiryTtl() + ); + } catch ( SettingsBuilderException $e ) { + if ( $miss || !$allowsStale ) { + throw $e; + } + // Return the stale results since the source allows it and + // this was only a soft miss + } } return $item['value']; @@ -74,10 +85,50 @@ class CachedSource implements SettingsSource, SettingsIncludeLocator { } /** + * Whether the given cache item is considered a real miss, in other words: + * - it is a falsey value + * - it has the wrong type or structure for this cache implementation + * + * @param mixed $item Cache item. + * + * @return bool + */ + private function isMiss( $item ): bool { + return !$item || + !is_array( $item ) || + !isset( $item['expiry'] ) || + !isset( $item['generation'] ) || + !isset( $item['value'] ); + } + + /** + * Whether the given cache item is considered expired, in other words: + * - its expiry timestamp has passed + * - it is deemed to expire early so as to mitigate cache stampedes + * + * @param array $item Cache item. + * + * @return bool + */ + private function isExpired( $item ): bool { + return $item['expiry'] < microtime( true ) || + $this->expiresEarly( $item, $this->source->getExpiryWeight() ); + } + + /** * Decide whether the cached source should be expired early according to a * probabilistic calculation that becomes more likely as the normal expiry * approaches. * + * In other words, we're going to pretend we're a bit further into the + * future than we are so that we might expire and regenerate the cached + * settings before other threads attempt to the do the same. The number of + * threads that will pretend to be far into the future (and thus will + * concurrently reload/cache the settings) will most probably be so + * exponentially fewer than the number of threads pretending to be near + * into the future that it will approach optimal stampede protection + * without the use of an exclusive lock. + * * @param array $item Cached source with expiry metadata. * @param float $weight Coefficient used to increase/decrease the * likelihood of early expiration. @@ -87,10 +138,13 @@ class CachedSource implements SettingsSource, SettingsIncludeLocator { * @return bool */ private function expiresEarly( array $item, float $weight ): bool { - if ( !isset( $item['expiry'] ) || !isset( $item['generation'] ) ) { + if ( $weight == 0 || !isset( $item['expiry'] ) || !isset( $item['generation'] ) ) { return false; } + // Calculate a negative expiry offset using generation time, expiry + // weight, and a random number within the exponentially distributed + // range of log n where n: (0, 1] (which is always negative) $expiryOffset = $item['generation'] * $weight * diff --git a/includes/Settings/Source/FileSource.php b/includes/Settings/Source/FileSource.php index 7e05dbc59037..22af9c202b13 100644 --- a/includes/Settings/Source/FileSource.php +++ b/includes/Settings/Source/FileSource.php @@ -90,6 +90,17 @@ class FileSource implements CacheableSource, SettingsIncludeLocator { } /** + * Disallow stale results from file sources in the case of load failure as + * failing to read from disk would be quite catastrophic and worthy of + * propagation. + * + * @return bool + */ + public function allowsStaleLoad(): bool { + return false; + } + + /** * Loads contents from the file and decodes them using the first format * to claim support for the file's extension. * diff --git a/includes/actions/HistoryAction.php b/includes/actions/HistoryAction.php index 1078f13a518b..3e85516c6acb 100644 --- a/includes/actions/HistoryAction.php +++ b/includes/actions/HistoryAction.php @@ -89,7 +89,7 @@ class HistoryAction extends FormlessAction { // Precache various messages if ( !isset( $this->message ) ) { $this->message = []; - $msgs = [ 'cur', 'last', 'pipe-separator' ]; + $msgs = [ 'cur', 'tooltip-cur', 'last', 'tooltip-last', 'pipe-separator' ]; foreach ( $msgs as $msg ) { $this->message[$msg] = $this->msg( $msg )->escaped(); } @@ -203,7 +203,7 @@ class HistoryAction extends FormlessAction { // Fail nicely if article doesn't exist. if ( !$this->getWikiPage()->exists() ) { - $send404Code = MediaWikiServices::getInstance()->getMainConfig()->get( 'Send404Code' ); + $send404Code = $config->get( 'Send404Code' ); if ( $send404Code ) { $out->setStatusCode( 404 ); } diff --git a/includes/actions/pagers/HistoryPager.php b/includes/actions/pagers/HistoryPager.php index f3cbb096493d..06bb2982f294 100644 --- a/includes/actions/pagers/HistoryPager.php +++ b/includes/actions/pagers/HistoryPager.php @@ -594,7 +594,9 @@ class HistoryPager extends ReverseChronologicalPager { return $this->getLinkRenderer()->makeKnownLink( $this->getTitle(), new HtmlArmor( $cur ), - [], + [ + 'title' => $this->historyPage->message['tooltip-cur'] + ], [ 'diff' => $latest, 'oldid' => $rev->getId() @@ -626,7 +628,9 @@ class HistoryPager extends ReverseChronologicalPager { return $linkRenderer->makeKnownLink( $this->getTitle(), new HtmlArmor( $last ), - [], + [ + 'title' => $this->historyPage->message['tooltip-last'] + ], [ 'diff' => $prevRev->getId(), 'oldid' => 'prev' @@ -649,7 +653,9 @@ class HistoryPager extends ReverseChronologicalPager { return $linkRenderer->makeKnownLink( $this->getTitle(), new HtmlArmor( $last ), - [], + [ + 'title' => $this->historyPage->message['tooltip-last'] + ], [ 'diff' => $prevRev->getId(), 'oldid' => $next->rev_id diff --git a/includes/export/XmlDumpWriter.php b/includes/export/XmlDumpWriter.php index 8f70f045da31..01a649401309 100644 --- a/includes/export/XmlDumpWriter.php +++ b/includes/export/XmlDumpWriter.php @@ -92,13 +92,13 @@ class XmlDumpWriter { $schemaVersion = XML_DUMP_SCHEMA_VERSION_11 ) { Assert::parameter( - in_array( $contentMode, [ self::WRITE_CONTENT, self::WRITE_STUB ] ), + in_array( $contentMode, [ self::WRITE_CONTENT, self::WRITE_STUB ], true ), '$contentMode', 'must be one of the following constants: WRITE_CONTENT or WRITE_STUB.' ); Assert::parameter( - in_array( $schemaVersion, self::$supportedSchemas ), + in_array( $schemaVersion, self::$supportedSchemas, true ), '$schemaVersion', 'must be one of the following schema versions: ' . implode( ',', self::$supportedSchemas ) diff --git a/includes/libs/MapCacheLRU.php b/includes/libs/MapCacheLRU.php index f65e9c2b899c..4325412766a4 100644 --- a/includes/libs/MapCacheLRU.php +++ b/includes/libs/MapCacheLRU.php @@ -140,7 +140,10 @@ class MapCacheLRU implements ExpirationAwareness, Serializable { * @since 1.32 Added $maxAge */ public function has( $key, $maxAge = INF ) { - // Optimization: Forego type check because array_key_exists does it already (T275673) + if ( !is_int( $key ) && !is_string( $key ) ) { + throw new UnexpectedValueException( + __METHOD__ . ': invalid key; must be string or integer.' ); + } return array_key_exists( $key, $this->cache ) && ( // Optimization: Avoid expensive getAge/getCurrentTime for common case (T275673) @@ -195,8 +198,8 @@ class MapCacheLRU implements ExpirationAwareness, Serializable { } if ( !is_string( $field ) && !is_int( $field ) ) { - trigger_error( "Field keys must be string or integer (key '$key')", E_USER_WARNING ); - return; + throw new UnexpectedValueException( + __METHOD__ . ": invalid field for '$key'; must be string or integer." ); } $this->cache[$key][$field] = $value; @@ -213,8 +216,11 @@ class MapCacheLRU implements ExpirationAwareness, Serializable { public function hasField( $key, $field, $maxAge = INF ) { $value = $this->get( $key ); + if ( !is_int( $field ) && !is_string( $field ) ) { + throw new UnexpectedValueException( + __METHOD__ . ": invalid field for '$key'; must be string or integer." ); + } return is_array( $value ) - // Optimization: Forego $field type check because array_key_exists does it already (T275673) && array_key_exists( $field, $value ) && ( $maxAge === INF diff --git a/includes/libs/rdbms/database/DatabasePostgres.php b/includes/libs/rdbms/database/DatabasePostgres.php index a107ef08159d..4e18a288f223 100644 --- a/includes/libs/rdbms/database/DatabasePostgres.php +++ b/includes/libs/rdbms/database/DatabasePostgres.php @@ -1213,9 +1213,9 @@ SQL; } public function buildGroupConcatField( - $delimiter, $table, $field, $conds = '', $options = [], $join_conds = [] + $delim, $table, $field, $conds = '', $join_conds = [] ) { - $fld = "array_to_string(array_agg($field)," . $this->addQuotes( $delimiter ) . ')'; + $fld = "array_to_string(array_agg($field)," . $this->addQuotes( $delim ) . ')'; return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')'; } diff --git a/includes/libs/rdbms/loadbalancer/LoadBalancer.php b/includes/libs/rdbms/loadbalancer/LoadBalancer.php index 3493dde4be1d..99366de5289a 100644 --- a/includes/libs/rdbms/loadbalancer/LoadBalancer.php +++ b/includes/libs/rdbms/loadbalancer/LoadBalancer.php @@ -83,8 +83,6 @@ class LoadBalancer implements ILoadBalancer { private $servers; /** @var array[] Map of (group => server index => weight) */ private $groupLoads; - /** @var int[] Map of (server index => seconds of lag considered "high") */ - private $maxLagByIndex; /** @var int Seconds to spend waiting on replica DB lag to resolve */ private $waitTimeout; /** @var array The LoadMonitor configuration */ @@ -210,7 +208,6 @@ class LoadBalancer implements ILoadBalancer { $this->groupLoads[$group][$i] = $ratio; } $this->groupLoads[self::GROUP_GENERIC][$i] = $server['load']; - $this->maxLagByIndex[$i] = $server['max lag'] ?? $this->maxLag; } $this->waitTimeout = $params['waitTimeout'] ?? self::MAX_WAIT_DEFAULT; @@ -2315,25 +2312,6 @@ class LoadBalancer implements ILoadBalancer { return $this->getLoadMonitor()->getLagTimes( $indexesWithLag, $domain ) + $knownLagTimes; } - /** - * Get the lag in seconds for a given connection, or zero if this load - * balancer does not have replication enabled. - * - * This should be used in preference to Database::getLag() in cases where - * replication may not be in use, since there is no way to determine if - * replication is in use at the connection level without running - * potentially restricted queries such as SHOW SLAVE STATUS. Using this - * function instead of Database::getLag() avoids a fatal error in this - * case on many installations. - * - * @param IDatabase $conn - * @return int|bool Returns false on error - * @deprecated Since 1.34 Use IDatabase::getLag() instead - */ - public function safeGetLag( IDatabase $conn ) { - return $conn->getLag(); - } - public function waitForPrimaryPos( IDatabase $conn, $pos = false, $timeout = null ) { $timeout = max( 1, $timeout ?: $this->waitTimeout ); diff --git a/includes/session/SessionManager.php b/includes/session/SessionManager.php index 324d0f97e1e8..a2506e6a97b2 100644 --- a/includes/session/SessionManager.php +++ b/includes/session/SessionManager.php @@ -105,6 +105,9 @@ class SessionManager implements SessionManagerInterface { /** @var UserNameUtils */ private $userNameUtils; + /** @var ObjectFactory */ + private $objectFactory; + /** @var CachedBagOStuff|null */ private $store; @@ -226,7 +229,9 @@ class SessionManager implements SessionManagerInterface { $this->logger->debug( 'SessionManager using store ' . get_class( $store ) ); $this->store = $store instanceof CachedBagOStuff ? $store : new CachedBagOStuff( $store ); - $this->userNameUtils = MediawikiServices::getInstance()->getUserNameUtils(); + $services = MediawikiServices::getInstance(); + $this->userNameUtils = $services->getUserNameUtils(); + $this->objectFactory = $services->getObjectFactory(); register_shutdown_function( [ $this, 'shutdown' ] ); } @@ -462,7 +467,7 @@ class SessionManager implements SessionManagerInterface { $this->sessionProviders = []; foreach ( $this->config->get( 'SessionProviders' ) as $spec ) { /** @var SessionProvider */ - $provider = ObjectFactory::getObjectFromSpec( $spec ); + $provider = $this->objectFactory->createObject( $spec ); $provider->init( $this->logger, $this->config, diff --git a/includes/skins/Skin.php b/includes/skins/Skin.php index 6c40766c34f0..b70c2cf83a4f 100644 --- a/includes/skins/Skin.php +++ b/includes/skins/Skin.php @@ -2637,6 +2637,7 @@ abstract class Skin extends ContextSource { * preference 'search-special-page' instead. */ public function setSearchPageTitle( Title $title ) { + wfDeprecated( __METHOD__, '1.38 Use SpecialPage::newSearchPage' ); $userOptionsManager = MediaWikiServices::getInstance()->getUserOptionsManager(); $user = $this->getUser(); $currentTitle = $userOptionsManager->getOption( $user, 'search-special-page' ); diff --git a/languages/i18n/en.json b/languages/i18n/en.json index 506e9a3cc7fc..9e28d3127d81 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -848,8 +848,10 @@ "nextrevision": "Newer revision →", "currentrevisionlink": "Latest revision", "cur": "cur", + "tooltip-cur": "Difference with latest revision", "next": "next", "last": "prev", + "tooltip-last": "Difference with preceding revision", "page_first": "first", "page_last": "last", "histlegend": "Diff selection: Mark the radio buttons of the revisions to compare and hit enter or the button at the bottom.<br />\nLegend: <strong>({{int:cur}})</strong> = difference with latest revision, <strong>({{int:last}})</strong> = difference with preceding revision, <strong>{{int:minoreditletter}}</strong> = minor edit.", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index 7802e85b5abc..c26cd07fc766 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -1080,8 +1080,10 @@ "nextrevision": "See also:\n* {{msg-mw|Previousrevision}}", "currentrevisionlink": "{{Identical|Current revision}}", "cur": "Link in page history ([https://translatewiki.net/w/i.php?title=MediaWiki:Cur/qqq&action=history example]).\n\nThis is short for \"current\".", + "tooltip-cur": "Tooltip for {{msg-mw|cur}} link in page history. Should match entry in {{msg-mw|histlegend}}", "next": "Link in page history\n\n{{Identical|Next}}", "last": "Link in page history", + "tooltip-last": "Tooltip for {{msg-mw|last}} link in page history. Should match entry in {{msg-mw|histlegend}}", "page_first": "This is part of the navigation message on the top and bottom of Special pages which are lists of things in alphabetical order, e.g. the '[[Special:Categories|Categories]]' special page. It is followed by the message {{msg-mw|Viewprevnext}}.\n{{Identical|First}}", "page_last": "This is part of the navigation message on the top and bottom of Special pages which are lists of things in alphabetical order, e.g. the '[[Special:Categories|Categories]]' special page. It is followed by the message {{msg-mw|Viewprevnext}}.\n\n{{Identical|Last}}", "histlegend": "Text at the top of the history page, explaining how to use it.\n\nOne or two [[:w:en:Radio button|radio buttons]] appear in each row that represents a revision of a wiki page.\n\n\"Legend\" means \"explanation of symbols\".\n\nSee also:\n* {{msg-mw|Cur}}\n* {{msg-mw|Last}}\n* {{msg-mw|Minoreditletter}}\n\nScreenshot:\n[[File:Screenshot of Revision history of Douglas Adams (Q42) - Wikidata.png|thumb|Screenshot of the revision history feature]]", diff --git a/languages/messages/MessagesSq.php b/languages/messages/MessagesSq.php index 2562b1485c63..7384433b72b5 100644 --- a/languages/messages/MessagesSq.php +++ b/languages/messages/MessagesSq.php @@ -29,6 +29,7 @@ * @author Urhixidur * @author Vinie007 * @author לערי ריינהארט + * @author Klein Muçi */ $namespaceNames = [ @@ -55,6 +56,8 @@ $namespaceAliases = [ 'Perdoruesi_diskutim' => NS_USER_TALK, 'Figura' => NS_FILE, 'Figura_diskutim' => NS_FILE_TALK, + 'Ndihme' => NS_HELP, + 'Ndihme_diskutim' => NS_HELP_TALK, 'Kategori' => NS_CATEGORY, 'Kategori_Diskutim' => NS_CATEGORY_TALK, ]; @@ -67,64 +70,64 @@ $namespaceGenderAliases = [ /** @phpcs-require-sorted-array */ $specialPageAliases = [ 'Activeusers' => [ 'PërdoruesitAktivë' ], - 'Allmessages' => [ 'TëgjithaMesazhet' ], - 'Allpages' => [ 'TëgjithaFaqet' ], - 'Ancientpages' => [ 'FaqetAntike' ], + 'Allmessages' => [ 'Mesazhet', 'TëgjithaMesazhet' ], + 'Allpages' => [ 'Faqet', 'TëgjithaFaqet' ], + 'Ancientpages' => [ 'FaqeTëVjetra', 'FaqetAntike' ], 'Blankpage' => [ 'FaqeBosh' ], - 'Block' => [ 'BllokoIP' ], - 'Booksources' => [ 'BurimeteLibrave' ], - 'Categories' => [ 'Kategori' ], + 'Block' => [ 'Blloko', 'BllokoIP' ], + 'Booksources' => [ 'BurimetELibrave', 'BurimeteLibrave' ], + 'Categories' => [ 'Kategoritë', 'Kategori' ], 'ChangeEmail' => [ 'NdryshoEmail' ], 'ChangePassword' => [ 'NdryshoFjalëkalimin' ], 'ComparePages' => [ 'KrahasoFaqet' ], 'Confirmemail' => [ 'KonfirmoEmail' ], 'Contributions' => [ 'Kontributet' ], 'CreateAccount' => [ 'HapLlogari' ], - 'DeletedContributions' => [ 'GrisKontributet' ], - 'Emailuser' => [ 'EmailPërdoruesit' ], + 'DeletedContributions' => [ 'KontributeTëGrisura', 'GrisKontributet' ], + 'Emailuser' => [ 'DërgoEmail', 'EmailPërdoruesit' ], 'Export' => [ 'Eksporto' ], 'Import' => [ 'Importo' ], - 'Listadmins' => [ 'RreshtoAdmin' ], - 'Listbots' => [ 'RreshtoBotët' ], - 'Listfiles' => [ 'ListaSkedave' ], - 'Listusers' => [ 'RreshtoPërdoruesit' ], + 'Listadmins' => [ 'RadhitAdminët', 'RreshtoAdmin' ], + 'Listbots' => [ 'RadhitRobotët', 'RreshtoBotët' ], + 'Listfiles' => [ 'RadhitSkedat', 'ListaSkedave' ], + 'Listusers' => [ 'RadhitPërdoruesit', 'RreshtoPërdoruesit' ], 'Lockdb' => [ 'MbyllDB' ], - 'Longpages' => [ 'FaqeteGjata' ], - 'Movepage' => [ 'LëvizFaqe' ], - 'Mycontributions' => [ 'KontributetëMiat' ], - 'Mypage' => [ 'FaqjaIme' ], - 'Mytalk' => [ 'DiskutimiImë' ], - 'Myuploads' => [ 'NgarkimeteMia' ], + 'Longpages' => [ 'FaqeTëGjata', 'FaqeteGjata' ], + 'Movepage' => [ 'LëvizFaqen', 'LëvizFaqe' ], + 'Mycontributions' => [ 'KontributetVetjake', 'KontributetëMiat' ], + 'Mypage' => [ 'FaqjaVetjake', 'FaqjaIme' ], + 'Mytalk' => [ 'DiskutimetVetjake', 'DiskutimiImë' ], + 'Myuploads' => [ 'NgarkimetVetjake', 'NgarkimeteMia' ], 'Newimages' => [ 'SkedaTëReja' ], - 'Newpages' => [ 'FaqeteReja' ], - 'Preferences' => [ 'Preferencat' ], - 'Protectedpages' => [ 'FaqeteMbrojtura' ], - 'Protectedtitles' => [ 'TitujteMbrojtur' ], - 'Randompage' => [ 'Rastësishme', 'FaqeRastësishme' ], + 'Newpages' => [ 'FaqeTëReja', 'FaqeteReja' ], + 'Preferences' => [ 'Parapëlqimet', 'Preferencat' ], + 'Protectedpages' => [ 'FaqeTëMbrojtura', 'FaqeteMbrojtura' ], + 'Protectedtitles' => [ 'TitujTëMbrojtur', 'TitujteMbrojtur' ], + 'Randompage' => [ 'ArtikullIRastësishëm', 'Rastësishme', 'FaqeRastësishme' ], 'Recentchanges' => [ 'NdryshimeSëFundmi' ], - 'Search' => [ 'Kërkim' ], - 'Shortpages' => [ 'FasheteShkurta' ], + 'Search' => [ 'Kërko', 'Kërkim' ], + 'Shortpages' => [ 'FaqeTëShkurtra', 'FasheteShkurta' ], 'Specialpages' => [ 'FaqetSpeciale' ], - 'Statistics' => [ 'Statistika' ], + 'Statistics' => [ 'Statistikat', 'Statistika' ], 'Unblock' => [ 'Zhblloko' ], - 'Uncategorizedcategories' => [ 'KategoriTëpakategorizuara' ], - 'Uncategorizedimages' => [ 'SkedaTëpakategorizuara' ], - 'Uncategorizedpages' => [ 'FaqeTëpakategorizuara' ], - 'Uncategorizedtemplates' => [ 'StampaTëpakategorizuara' ], + 'Uncategorizedcategories' => [ 'KategoriTëPakategorizuara', 'KategoriTëpakategorizuara' ], + 'Uncategorizedimages' => [ 'SkedaTëPakategorizuara', 'SkedaTëpakategorizuara' ], + 'Uncategorizedpages' => [ 'FaqeTëPakategorizuara', 'FaqeTëpakategorizuara' ], + 'Uncategorizedtemplates' => [ 'StampaTëPakategorizuara', 'StampaTëpakategorizuara' ], 'Undelete' => [ 'Rikthe' ], 'Unlockdb' => [ 'HapDB' ], - 'Unusedcategories' => [ 'KategoriTëpapërdorura' ], - 'Unusedimages' => [ 'SkedaTëpapërdorura' ], + 'Unusedcategories' => [ 'KategoriTëPapërdorura', 'KategoriTëpapërdorura' ], + 'Unusedimages' => [ 'SkedaTëPapërdorura', 'SkedaTëpapërdorura' ], 'Upload' => [ 'Ngarko' ], - 'Userlogin' => [ 'HyrjePërdoruesi' ], - 'Userlogout' => [ 'DaljePërdoruesi' ], - 'Version' => [ 'Verzioni' ], - 'Wantedcategories' => [ 'KaetgoritëeDëshiruara' ], - 'Wantedfiles' => [ 'SkedateDëshiruara' ], - 'Wantedpages' => [ 'FaqeteDëshiruara' ], - 'Wantedtemplates' => [ 'StampateDëshiruara' ], + 'Userlogin' => [ 'Hyrja', 'HyrjePërdoruesi' ], + 'Userlogout' => [ 'Dalja', 'DaljePërdoruesi' ], + 'Version' => [ 'Versioni', 'Verzioni' ], + 'Wantedcategories' => [ 'KategoriTëDëshiruara', 'KaetgoritëeDëshiruara' ], + 'Wantedfiles' => [ 'SkedaTëDëshiruara', 'SkedateDëshiruara' ], + 'Wantedpages' => [ 'FaqeTëDëshiruara', 'FaqeteDëshiruara' ], + 'Wantedtemplates' => [ 'StampaTëDëshiruara', 'StampateDëshiruara' ], 'Whatlinkshere' => [ 'LidhjetKëtu' ], - 'Withoutinterwiki' => [ 'PaInterwiki' ], + 'Withoutinterwiki' => [ 'PaLidhje', 'PaInterwiki' ], ]; /** @phpcs-require-sorted-array */ @@ -135,11 +138,11 @@ $magicWords = [ 'currentday2' => [ '1', 'DITASOT2', 'CURRENTDAY2' ], 'currentdayname' => [ '1', 'EMRIIDITËSOT', 'CURRENTDAYNAME' ], 'currenthour' => [ '1', 'ORATANI', 'CURRENTHOUR' ], - 'currentmonth' => [ '1', 'MUAJIMOMENTAL', 'MUAJIMOMENTAL2', 'CURRENTMONTH', 'CURRENTMONTH2' ], - 'currentmonth1' => [ '1', 'MUAJIMOMENTAL1', 'CURRENTMONTH1' ], - 'currentmonthname' => [ '1', 'EMRIIMUAJITMOMENTAL', 'CURRENTMONTHNAME' ], + 'currentmonth' => [ '1', 'MUAJIAKTUAL', 'MUAJIAKTUAL2', 'MUAJIMOMENTAL', 'MUAJIMOMENTAL2', 'CURRENTMONTH', 'CURRENTMONTH2' ], + 'currentmonth1' => [ '1', 'MUAJIAKTUAL1', 'MUAJIMOMENTAL1', 'CURRENTMONTH1' ], + 'currentmonthname' => [ '1', 'EMRIIMUAJITAKTUAL', 'EMRIIMUAJITMOMENTAL', 'CURRENTMONTHNAME' ], 'currenttime' => [ '1', 'KOHATANI', 'CURRENTTIME' ], - 'currentweek' => [ '1', 'JAVAMOMENTALE', 'CURRENTWEEK' ], + 'currentweek' => [ '1', 'JAVAAKTUALE', 'JAVAMOMENTALE', 'CURRENTWEEK' ], 'currentyear' => [ '1', 'SIVJET', 'CURRENTYEAR' ], 'fullpagename' => [ '1', 'EMRIIPLOTËIFAQES', 'FULLPAGENAME' ], 'fullpagenamee' => [ '1', 'EMRIIPLOTËIFAQESE', 'FULLPAGENAMEE' ], @@ -153,17 +156,17 @@ $magicWords = [ 'img_center' => [ '1', 'qendër', 'qendrore', 'center', 'centre' ], 'img_framed' => [ '1', 'i_kornizuar', 'pa_kornizë', 'kornizë', 'frame', 'framed', 'enframed' ], 'img_left' => [ '1', 'majtas', 'left' ], - 'img_link' => [ '1', 'lidhje=$1', 'link=$1' ], + 'img_link' => [ '1', 'lidhja=$1', 'lidhje=$1', 'link=$1' ], 'img_manualthumb' => [ '1', 'parapamje=$1', 'pamje=$1', 'thumbnail=$1', 'thumb=$1' ], 'img_middle' => [ '1', 'mes', 'middle' ], - 'img_none' => [ '1', 's\'ka', 'none' ], + 'img_none' => [ '1', 'asnjë', 's\'ka', 'none' ], 'img_page' => [ '1', 'faqja=$1', 'faqja $1', 'page=$1', 'page $1' ], 'img_right' => [ '1', 'djathtas', 'right' ], 'img_sub' => [ '1', 'nën', 'sub' ], 'img_text_bottom' => [ '1', 'tekst-fund', 'text-bottom' ], - 'img_text_top' => [ '1', 'tekst-top', 'text-top' ], + 'img_text_top' => [ '1', 'tekst-majë', 'tekst-top', 'text-top' ], 'img_thumbnail' => [ '1', 'parapamje', 'pamje', 'thumb', 'thumbnail' ], - 'img_upright' => [ '1', 'lartdjathtas', 'lartdjathtas=$1', 'lartdjathtas $1', 'upright', 'upright=$1', 'upright $1' ], + 'img_upright' => [ '1', 'vertikale', 'vertikale=$1', 'vertikale $1', 'lartdjathtas', 'lartdjathtas=$1', 'lartdjathtas $1', 'upright', 'upright=$1', 'upright $1' ], 'language' => [ '0', '#GJUHA:', '#LANGUAGE:' ], 'localday' => [ '1', 'DITALOKALE', 'LOCALDAY' ], 'localday2' => [ '1', 'DITALOKALE2', 'LOCALDAY2' ], @@ -175,11 +178,11 @@ $magicWords = [ 'localurl' => [ '0', 'URLLOKALE', 'LOCALURL:' ], 'localyear' => [ '1', 'VITILOKAL', 'LOCALYEAR' ], 'namespace' => [ '1', 'HAPËSIRA', 'NAMESPACE' ], - 'noeditsection' => [ '0', '__JOREDAKTIMSEKSIONI__', '__NOEDITSECTION__' ], - 'nogallery' => [ '0', '__JOGALERI__', '__NOGALLERY__' ], - 'notoc' => [ '0', '__JOTP__', '__NOTOC__' ], + 'noeditsection' => [ '0', '__PAREDAKTIMPJESE__', '__JOREDAKTIMSEKSIONI__', '__NOEDITSECTION__' ], + 'nogallery' => [ '0', '__PAGALERI__', '__JOGALERI__', '__NOGALLERY__' ], + 'notoc' => [ '0', '__PATP__', '__JOTP__', '__NOTOC__' ], 'numberofactiveusers' => [ '1', 'NUMRIIPËRDORUESVEAKTIVË', 'NUMBEROFACTIVEUSERS' ], - 'numberofadmins' => [ '1', 'NUMRIIADMINISTRUESVE', 'NUMBEROFADMINS' ], + 'numberofadmins' => [ '1', 'NUMRIIADMINISTRATORËVE', 'NUMRIIADMINISTRUESVE', 'NUMBEROFADMINS' ], 'numberofarticles' => [ '1', 'NUMRIIARTIKUJVE', 'NUMBEROFARTICLES' ], 'numberofedits' => [ '1', 'NUMRIREDAKTIMEVE', 'NUMBEROFEDITS' ], 'numberoffiles' => [ '1', 'NUMRIISKEDAVE', 'NUMBEROFFILES' ], @@ -191,7 +194,7 @@ $magicWords = [ 'redirect' => [ '0', '#RIDREJTO', '#REDIRECT' ], 'server' => [ '0', 'SERVERI', 'SERVER' ], 'servername' => [ '0', 'EMRIISERVERIT', 'SERVERNAME' ], - 'sitename' => [ '1', 'EMRIISAJTIT', 'SITENAME' ], + 'sitename' => [ '1', 'EMRIISITIT', 'EMRIISAJTIT', 'SITENAME' ], 'subpagename' => [ '1', 'EMRIINËNFAQES', 'SUBPAGENAME' ], 'subpagenamee' => [ '1', 'EMRIINËNFAQESE', 'SUBPAGENAMEE' ], 'subst' => [ '0', 'ZËVN', 'SUBST:' ], diff --git a/maintenance/deleteAutoPatrolLogs.php b/maintenance/deleteAutoPatrolLogs.php index 6f122239c092..60636c605967 100644 --- a/maintenance/deleteAutoPatrolLogs.php +++ b/maintenance/deleteAutoPatrolLogs.php @@ -119,7 +119,7 @@ class DeleteAutoPatrolLogs extends Maintenance { 'log_id', $conds, __METHOD__, - [ 'LIMIT' => $this->getBatchSize() ] + [ 'LIMIT' => $this->getBatchSize(), 'ORDER BY' => 'log_id' ] ); } @@ -148,7 +148,7 @@ class DeleteAutoPatrolLogs extends Maintenance { [ 'log_id', 'log_params' ], $conds, __METHOD__, - [ 'LIMIT' => $batchSize ] + [ 'LIMIT' => $batchSize, 'ORDER BY' => 'log_id' ] ); $last = null; diff --git a/maintenance/generateSchemaChangeSql.php b/maintenance/generateSchemaChangeSql.php index 870dbe04e87f..31587029c95f 100644 --- a/maintenance/generateSchemaChangeSql.php +++ b/maintenance/generateSchemaChangeSql.php @@ -61,7 +61,13 @@ class GenerateSchemaChangeSql extends Maintenance { public function execute() { global $IP; $jsonPath = $this->getOption( 'json' ); - $relativeJsonPath = str_replace( "$IP/", '', $jsonPath ); + $installPath = $IP; + // For windows + if ( DIRECTORY_SEPARATOR === '\\' ) { + $installPath = strtr( $installPath, '\\', '/' ); + $jsonPath = strtr( $jsonPath, '\\', '/' ); + } + $relativeJsonPath = str_replace( "$installPath/", '', $jsonPath ); $sqlPath = $this->getOption( 'sql' ); $abstractSchemaChange = json_decode( file_get_contents( $jsonPath ), true ); diff --git a/maintenance/generateSchemaSql.php b/maintenance/generateSchemaSql.php index 0108be85e85c..14583828603e 100644 --- a/maintenance/generateSchemaSql.php +++ b/maintenance/generateSchemaSql.php @@ -62,8 +62,14 @@ class GenerateSchemaSql extends Maintenance { global $IP; $platform = $this->getOption( 'type', 'mysql' ); $jsonPath = $this->getOption( 'json', __DIR__ . '/tables.json' ); + $installPath = $IP; + // For windows + if ( DIRECTORY_SEPARATOR === '\\' ) { + $installPath = strtr( $installPath, '\\', '/' ); + $jsonPath = strtr( $jsonPath, '\\', '/' ); + } $relativeJsonPath = str_replace( - [ "$IP/extensions/", "$IP/" ], + [ "$installPath/extensions/", "$installPath/" ], '', $jsonPath ); diff --git a/maintenance/resetPageRandom.php b/maintenance/resetPageRandom.php index 5362149b37bc..65fa9136b987 100644 --- a/maintenance/resetPageRandom.php +++ b/maintenance/resetPageRandom.php @@ -33,9 +33,9 @@ class ResetPageRandom extends Maintenance { parent::__construct(); $this->addDescription( 'Reset the page_random for articles within given date range' ); $this->addOption( 'from', - 'From date range selector to select articles to update, ex: 20041011000000' ); + 'From date range selector to select articles to update, ex: 20041011000000', false, true ); $this->addOption( 'to', - 'To date range selector to select articles to update, ex: 20050708000000' ); + 'To date range selector to select articles to update, ex: 20050708000000', false, true ); $this->addOption( 'dry', 'Do not update column' ); $this->addOption( 'batch-start', 'Optional: Use when you need to restart the reset process from a given page ID offset' diff --git a/maintenance/update.php b/maintenance/update.php index 38698b981022..5580ecc75a0f 100755 --- a/maintenance/update.php +++ b/maintenance/update.php @@ -93,7 +93,7 @@ class UpdateMediaWiki extends Maintenance { } $this->fileHandle = null; - if ( substr( $this->getOption( 'schema' ), 0, 2 ) === "--" ) { + if ( substr( $this->getOption( 'schema', '' ), 0, 2 ) === "--" ) { $this->fatalError( "The --schema option requires a file as an argument.\n" ); } elseif ( $this->hasOption( 'schema' ) ) { $file = $this->getOption( 'schema' ); diff --git a/tests/phpunit/MediaWikiIntegrationTestCase.php b/tests/phpunit/MediaWikiIntegrationTestCase.php index 9e48075ebe92..fea9766c9639 100644 --- a/tests/phpunit/MediaWikiIntegrationTestCase.php +++ b/tests/phpunit/MediaWikiIntegrationTestCase.php @@ -2471,8 +2471,8 @@ abstract class MediaWikiIntegrationTestCase extends PHPUnit\Framework\TestCase { if ( $ignoreFormat !== false ) { $this->assertThat( $jobStatus['error'], $this->logicalOr( - $this->matches( $ignoreFormat ), - $this->isNull() + $this->isNull(), + $this->matches( $ignoreFormat ) ), "Error for job of type {$jobStatus['type']}" ); diff --git a/tests/phpunit/includes/OutputPageTest.php b/tests/phpunit/includes/OutputPageTest.php index 4d10d8931db0..b9734419e1ba 100644 --- a/tests/phpunit/includes/OutputPageTest.php +++ b/tests/phpunit/includes/OutputPageTest.php @@ -727,15 +727,35 @@ class OutputPageTest extends MediaWikiIntegrationTestCase { // HTML title should change as well $this->assertSame( $this->getMsgText( $op, 'pagetitle', 'foobar' ), $op->getHTMLTitle() ); - // Test set to text with good and bad HTML. We don't try to be comprehensive here, that - // belongs in Sanitizer tests. - $op->setPageTitle( '<script>a</script>&<i>b</i>' ); + // Test set to text with good and bad HTML. We don't try to be *too* + // comprehensive here, that belongs in Sanitizer tests, but we'll + // address the issues specifically noted in T298401/T67747 at least... + $sanitizerTests = [ + [ + 'input' => '<script>a</script>&<i>b</i>', + 'getPageTitle' => '<script>a</script>&<i>b</i>', + 'getHTMLTitle' => '<script>a</script>&b', + ], + [ + 'input' => '<code style="display:none">', # T298401 + 'getPageTitle' => '<code style="display:none"></code>', + 'getHTMLTitle' => '', + ], + [ + 'input' => '<b>Foo bar<b>', # T67747 + 'getPageTitle' => '<b>Foo bar<b></b></b>', + 'getHTMLTitle' => 'Foo bar', + ], + ]; + foreach ( $sanitizerTests as $case ) { + $op->setPageTitle( $case['input'] ); - $this->assertSame( '<script>a</script>&<i>b</i>', $op->getPageTitle() ); - $this->assertSame( - $this->getMsgText( $op, 'pagetitle', '<script>a</script>&b' ), - $op->getHTMLTitle() - ); + $this->assertSame( $case['getPageTitle'], $op->getPageTitle() ); + $this->assertSame( + $this->getMsgText( $op, 'pagetitle', $case['getHTMLTitle'] ), + $op->getHTMLTitle() + ); + } // Test set to message $text = $this->getMsgText( $op, 'mainpage' ); diff --git a/tests/phpunit/includes/Revision/ContributionsLookupTest.php b/tests/phpunit/includes/Revision/ContributionsLookupTest.php index cba3ce82cd95..26b6191750d8 100644 --- a/tests/phpunit/includes/Revision/ContributionsLookupTest.php +++ b/tests/phpunit/includes/Revision/ContributionsLookupTest.php @@ -399,18 +399,15 @@ class ContributionsLookupTest extends MediaWikiIntegrationTestCase { ContributionsSegment $segmentObject, RevisionRecord $actual ): void { - // FIXME: fails under postgres, see T195807 - if ( $this->db->getType() !== 'postgres' ) { - $actualTags = $segmentObject->getTagsForRevision( $actual->getId() ); - - // Tag 3 was disabled and should not be included in results - $this->assertArrayNotHasKey( self::TAG3, $actualTags ); - foreach ( $actualTags as $tagName => $actualTag ) { - $this->assertContains( $tagName, $expectedTags ); - $this->assertInstanceOf( Message::class, $actualTag ); - $this->assertEquals( "<i>" . self::TAG_DISPLAY . "</i>", $actualTag->parse() ); - $this->assertEquals( "''" . self::TAG_DISPLAY . "''", $actualTag->text() ); - } + $actualTags = $segmentObject->getTagsForRevision( $actual->getId() ); + + // Tag 3 was disabled and should not be included in results + $this->assertArrayNotHasKey( self::TAG3, $actualTags ); + foreach ( $actualTags as $tagName => $actualTag ) { + $this->assertContains( $tagName, $expectedTags ); + $this->assertInstanceOf( Message::class, $actualTag ); + $this->assertEquals( "<i>" . self::TAG_DISPLAY . "</i>", $actualTag->parse() ); + $this->assertEquals( "''" . self::TAG_DISPLAY . "''", $actualTag->text() ); } } } diff --git a/tests/phpunit/includes/Storage/NameTableStoreTest.php b/tests/phpunit/includes/Storage/NameTableStoreTest.php index 18dc2d26b126..b84cc05571cd 100644 --- a/tests/phpunit/includes/Storage/NameTableStoreTest.php +++ b/tests/phpunit/includes/Storage/NameTableStoreTest.php @@ -333,7 +333,7 @@ class NameTableStoreTest extends MediaWikiIntegrationTestCase { } public function testGetAndAcquireIdInsertCallback() { - // FIXME: fails under postgres + // Postgres does not allow to specify the SERIAL column on insert to fake an id $this->markTestSkippedIfDbType( 'postgres' ); $store = $this->getNameTableSqlStore( diff --git a/tests/phpunit/includes/Storage/SqlBlobStoreTest.php b/tests/phpunit/includes/Storage/SqlBlobStoreTest.php index 3b7730643ae3..4d435b311acd 100644 --- a/tests/phpunit/includes/Storage/SqlBlobStoreTest.php +++ b/tests/phpunit/includes/Storage/SqlBlobStoreTest.php @@ -425,7 +425,7 @@ class SqlBlobStoreTest extends MediaWikiIntegrationTestCase { * @covers \MediaWiki\Storage\SqlBlobStore::getBlob */ public function testSimpleStoreGetBlobSimpleRoundtripWindowsLegacyEncodingGzip( $blob ) { - // FIXME: fails under postgres + // FIXME: fails under postgres - T298692 $this->markTestSkippedIfDbType( 'postgres' ); $store = $this->getBlobStore(); $store->setLegacyEncoding( 'windows-1252' ); diff --git a/tests/phpunit/includes/api/ApiBaseTest.php b/tests/phpunit/includes/api/ApiBaseTest.php index 223bb42166a5..e7cf45c88f99 100644 --- a/tests/phpunit/includes/api/ApiBaseTest.php +++ b/tests/phpunit/includes/api/ApiBaseTest.php @@ -186,9 +186,6 @@ class ApiBaseTest extends ApiTestCase { } public function testGetTitleOrPageIdInvalidPageId() { - // FIXME: fails under postgres - $this->markTestSkippedIfDbType( 'postgres' ); - $this->expectException( ApiUsageException::class ); $this->expectExceptionMessage( 'There is no page with ID 2147483648.' ); $mock = new MockApi(); diff --git a/tests/phpunit/includes/api/query/ApiQueryUserContribsTest.php b/tests/phpunit/includes/api/query/ApiQueryUserContribsTest.php index 75537466d6e4..d5d6ecef159f 100644 --- a/tests/phpunit/includes/api/query/ApiQueryUserContribsTest.php +++ b/tests/phpunit/includes/api/query/ApiQueryUserContribsTest.php @@ -41,9 +41,6 @@ class ApiQueryUserContribsTest extends ApiTestCase { * @param int $revs Number of revisions to expect */ public function testSorting( $params, $reverse, $revs ) { - // FIXME: fails under sqlite - $this->markTestSkippedIfDbType( 'sqlite' ); - if ( isset( $params['ucuserids'] ) ) { $params['ucuserids'] = implode( '|', array_map( [ User::class, 'idFromName' ], $params['ucuserids'] ) ); } diff --git a/tests/phpunit/includes/parser/ParserCacheSerializationTestCases.php b/tests/phpunit/includes/parser/ParserCacheSerializationTestCases.php index b9cc5636ec9b..6e321b6de833 100644 --- a/tests/phpunit/includes/parser/ParserCacheSerializationTestCases.php +++ b/tests/phpunit/includes/parser/ParserCacheSerializationTestCases.php @@ -185,8 +185,8 @@ abstract class ParserCacheSerializationTestCases { ); $parserOutputWithMetadata->addExternalLink( 'https://test.org' ); $parserOutputWithMetadata->addHeadItem( 'head_item1', 'tag1' ); - $parserOutputWithMetadata->addModules( 'module1' ); - $parserOutputWithMetadata->addModuleStyles( 'module_style1' ); + $parserOutputWithMetadata->addModules( [ 'module1' ] ); + $parserOutputWithMetadata->addModuleStyles( [ 'module_style1' ] ); $parserOutputWithMetadata->addJsConfigVars( 'key1', 'value1' ); $parserOutputWithMetadata->addOutputHook( 'hook1', self::MOCK_EXT_DATA ); $parserOutputWithMetadata->addWarning( 'warning1' ); diff --git a/tests/phpunit/includes/parser/ParserOutputTest.php b/tests/phpunit/includes/parser/ParserOutputTest.php index 86029cf83131..11de4a4654bc 100644 --- a/tests/phpunit/includes/parser/ParserOutputTest.php +++ b/tests/phpunit/includes/parser/ParserOutputTest.php @@ -594,8 +594,8 @@ EOF $a = new ParserOutput(); $a->addHeadItem( '<foo1>' ); $a->addHeadItem( '<bar1>', 'bar' ); - $a->addModules( 'test-module-a' ); - $a->addModuleStyles( 'test-module-styles-a' ); + $a->addModules( [ 'test-module-a' ] ); + $a->addModuleStyles( [ 'test-module-styles-a' ] ); $a->addJsConfigVars( 'test-config-var-a', 'a' ); $a->addExtraCSPStyleSrc( 'css.com' ); $a->addExtraCSPStyleSrc( 'css2.com' ); @@ -606,8 +606,8 @@ EOF $b->setIndexPolicy( 'noindex' ); $b->addHeadItem( '<foo2>' ); $b->addHeadItem( '<bar2>', 'bar' ); - $b->addModules( 'test-module-b' ); - $b->addModuleStyles( 'test-module-styles-b' ); + $b->addModules( [ 'test-module-b' ] ); + $b->addModuleStyles( [ 'test-module-styles-b' ] ); $b->addJsConfigVars( 'test-config-var-b', 'b' ); $b->addJsConfigVars( 'test-config-var-a', 'X' ); $b->addExtraCSPStyleSrc( 'https://css.ca' ); diff --git a/tests/phpunit/includes/search/PrefixSearchTest.php b/tests/phpunit/includes/search/PrefixSearchTest.php index c6e8fdedec3f..9b8cb76921b4 100644 --- a/tests/phpunit/includes/search/PrefixSearchTest.php +++ b/tests/phpunit/includes/search/PrefixSearchTest.php @@ -190,17 +190,10 @@ class PrefixSearchTest extends MediaWikiLangTestCase { * @covers PrefixSearch::searchBackend */ public function testSearch( array $case ) { - // FIXME: fails under postgres - $this->markTestSkippedIfDbType( 'postgres' ); $this->searchProvision( null ); $namespaces = $case['namespaces'] ?? []; - if ( wfGetDB( DB_REPLICA )->getType() === 'postgres' ) { - // Postgres will sort lexicographically on utf8 code units (" " before "/") - sort( $case['results'], SORT_STRING ); - } - $searcher = new StringPrefixSearch; $results = $searcher->search( $case['query'], 3, $namespaces ); $this->assertEquals( @@ -216,8 +209,6 @@ class PrefixSearchTest extends MediaWikiLangTestCase { * @covers PrefixSearch::searchBackend */ public function testSearchWithOffset( array $case ) { - // FIXME: fails under postgres - $this->markTestSkippedIfDbType( 'postgres' ); $this->searchProvision( null ); $namespaces = $case['namespaces'] ?? []; @@ -225,11 +216,6 @@ class PrefixSearchTest extends MediaWikiLangTestCase { $searcher = new StringPrefixSearch; $results = $searcher->search( $case['query'], 3, $namespaces, 1 ); - if ( wfGetDB( DB_REPLICA )->getType() === 'postgres' ) { - // Postgres will sort lexicographically on utf8 code units (" " before "/") - sort( $case['results'], SORT_STRING ); - } - // We don't expect the first result when offsetting array_shift( $case['results'] ); // And sometimes we expect a different last result diff --git a/tests/phpunit/includes/user/UserTest.php b/tests/phpunit/includes/user/UserTest.php index 241d8ee9d6ee..fb28d5f5cd18 100644 --- a/tests/phpunit/includes/user/UserTest.php +++ b/tests/phpunit/includes/user/UserTest.php @@ -560,9 +560,6 @@ class UserTest extends MediaWikiIntegrationTestCase { * @covers User::findUsersByGroup */ public function testFindUsersByGroup() { - // FIXME: fails under postgres - $this->markTestSkippedIfDbType( 'postgres' ); - $users = User::findUsersByGroup( [] ); $this->assertSame( 0, iterator_count( $users ) ); diff --git a/tests/phpunit/integration/includes/export/WikiExporterFactoryTest.php b/tests/phpunit/integration/includes/export/WikiExporterFactoryTest.php index 8f9db6fcdd38..8cb1facf4df8 100644 --- a/tests/phpunit/integration/includes/export/WikiExporterFactoryTest.php +++ b/tests/phpunit/integration/includes/export/WikiExporterFactoryTest.php @@ -9,7 +9,7 @@ use WikiExporter; use XmlDumpWriter; /** - * @covers MediaWiki\Export\WikiExporterFactory + * @covers \MediaWiki\Export\WikiExporterFactory */ class WikiExporterFactoryTest extends MediaWikiIntegrationTestCase { use FactoryArgTestTrait; @@ -36,4 +36,11 @@ class WikiExporterFactoryTest extends MediaWikiIntegrationTestCase { protected function getFactoryMethodName() { return 'getWikiExporter'; } + + protected function getOverriddenMockValueForParam( $param ) { + if ( $param->getName() === 'text' ) { + return [ WikiExporter::TEXT ]; + } + return []; + } } diff --git a/tests/phpunit/maintenance/deleteAutoPatrolLogsTest.php b/tests/phpunit/maintenance/deleteAutoPatrolLogsTest.php index 88a8e8f2d63c..809e6eefea9f 100644 --- a/tests/phpunit/maintenance/deleteAutoPatrolLogsTest.php +++ b/tests/phpunit/maintenance/deleteAutoPatrolLogsTest.php @@ -257,9 +257,6 @@ class DeleteAutoPatrolLogsTest extends MaintenanceBaseTestCase { * @dataProvider runProvider */ public function testRun( $expected, $args ) { - // FIXME: fails under postgres - $this->markTestSkippedIfDbType( 'postgres' ); - $this->maintenance->loadWithArgv( $args ); $this->maintenance->execute(); diff --git a/tests/phpunit/tests/MediaWikiIntegrationTestCaseSchema1Test.php b/tests/phpunit/tests/MediaWikiIntegrationTestCaseSchema1Test.php index ad6e8d217270..69c2d0a74b04 100644 --- a/tests/phpunit/tests/MediaWikiIntegrationTestCaseSchema1Test.php +++ b/tests/phpunit/tests/MediaWikiIntegrationTestCaseSchema1Test.php @@ -12,7 +12,7 @@ class MediaWikiIntegrationTestCaseSchema1Test extends MediaWikiIntegrationTestCa protected function setUp(): void { parent::setUp(); - // FIXME: fails under postgres + // FIXME: fails under postgres - T198222 $this->markTestSkippedIfDbType( 'postgres' ); } diff --git a/tests/phpunit/tests/MediaWikiIntegrationTestCaseSchema2Test.php b/tests/phpunit/tests/MediaWikiIntegrationTestCaseSchema2Test.php index 8d73e188d759..39c948493fdb 100644 --- a/tests/phpunit/tests/MediaWikiIntegrationTestCaseSchema2Test.php +++ b/tests/phpunit/tests/MediaWikiIntegrationTestCaseSchema2Test.php @@ -14,7 +14,7 @@ class MediaWikiIntegrationTestCaseSchema2Test extends MediaWikiIntegrationTestCa protected function setUp(): void { parent::setUp(); - // FIXME: fails under postgres + // FIXME: fails under postgres - T198222 $this->markTestSkippedIfDbType( 'postgres' ); } diff --git a/tests/phpunit/unit/includes/Settings/Cache/CachedSourceTest.php b/tests/phpunit/unit/includes/Settings/Cache/CachedSourceTest.php index d6e9386632c3..81ef09f5a38f 100644 --- a/tests/phpunit/unit/includes/Settings/Cache/CachedSourceTest.php +++ b/tests/phpunit/unit/includes/Settings/Cache/CachedSourceTest.php @@ -5,6 +5,7 @@ namespace MediaWiki\Tests\Unit\Settings\Cache; use BagOStuff; use MediaWiki\Settings\Cache\CacheableSource; use MediaWiki\Settings\Cache\CachedSource; +use MediaWiki\Settings\SettingsBuilderException; use PHPUnit\Framework\TestCase; /** @@ -118,4 +119,50 @@ class CachedSourceTest extends TestCase { $this->assertSame( $settings, $cacheSource->load() ); } + + public function testLoadStale() { + $cache = $this->createMock( BagOStuff::class ); + $source = $this->createMock( CacheableSource::class ); + $cacheSource = new CachedSource( $cache, $source ); + + $settings = [ 'config' => [ 'Foo' => 'value' ] ]; + $hashKey = 'abc123'; + $key = 'global:MediaWiki\Tests\Unit\Settings\Cache\CachedSourceTest:' . $hashKey; + $expired = microtime( true ) - 1; + + $source + ->expects( $this->atLeastOnce() ) + ->method( 'allowsStaleLoad' ) + ->willReturn( true ); + + $source + ->expects( $this->once() ) + ->method( 'getHashKey' ) + ->willReturn( $hashKey ); + + $cache + ->expects( $this->once() ) + ->method( 'makeGlobalKey' ) + ->with( 'MediaWiki\Settings\Cache\CachedSource', $hashKey ) + ->willReturn( $key ); + + $cache + ->expects( $this->once() ) + ->method( 'get' ) + ->with( $key ) + ->willReturn( + [ + 'value' => $settings, + 'expiry' => $expired, + 'generation' => 1.0 + ] + ); + + $source + ->expects( $this->once() ) + ->method( 'load' ) + ->will( $this->throwException( new SettingsBuilderException( 'foo' ) ) ); + + $this->assertSame( $settings, $cacheSource->load() ); + } } diff --git a/tests/phpunit/unit/includes/Settings/Source/FileSourceTest.php b/tests/phpunit/unit/includes/Settings/Source/FileSourceTest.php index e1782af427c5..4b7295d825a9 100644 --- a/tests/phpunit/unit/includes/Settings/Source/FileSourceTest.php +++ b/tests/phpunit/unit/includes/Settings/Source/FileSourceTest.php @@ -11,6 +11,11 @@ use PHPUnit\Framework\TestCase; * @covers \MediaWiki\Settings\Source\FileSource */ class FileSourceTest extends TestCase { + public function testAllowsStaleLoad() { + $source = new FileSource( __DIR__ . 'foo.json' ); + $this->assertFalse( $source->allowsStaleLoad() ); + } + public function testLoad() { $source = new FileSource( __DIR__ . '/fixtures/settings.json' ); $settings = $source->load(); diff --git a/tests/phpunit/unit/includes/libs/MapCacheLRUTest.php b/tests/phpunit/unit/includes/libs/MapCacheLRUTest.php index 4a5bc3c66243..9020c67ce8ab 100644 --- a/tests/phpunit/unit/includes/libs/MapCacheLRUTest.php +++ b/tests/phpunit/unit/includes/libs/MapCacheLRUTest.php @@ -250,8 +250,8 @@ class MapCacheLRUTest extends PHPUnit\Framework\TestCase { */ public function testHasInvalidKey( $key ) { $cache = new MapCacheLRU( 3 ); - $this->expectWarning(); - $this->expectWarningMessageMatches( '/should be either a string or an integer/' ); + $this->expectException( UnexpectedValueException::class ); + $this->expectExceptionMessage( 'must be string or integer' ); $cache->has( $key ); } @@ -261,8 +261,8 @@ class MapCacheLRUTest extends PHPUnit\Framework\TestCase { */ public function testGetInvalidKey( $key ) { $cache = new MapCacheLRU( 3 ); - $this->expectWarning(); - $this->expectWarningMessageMatches( '/should be either a string or an integer/' ); + $this->expectException( UnexpectedValueException::class ); + $this->expectExceptionMessage( 'must be string or integer' ); $cache->get( $key ); } @@ -272,8 +272,8 @@ class MapCacheLRUTest extends PHPUnit\Framework\TestCase { */ public function testSetInvalidKey( $key ) { $cache = new MapCacheLRU( 3 ); - $this->expectWarning(); - $this->expectWarningMessageMatches( '/should be either a string or an integer/' ); + $this->expectException( UnexpectedValueException::class ); + $this->expectExceptionMessage( 'must be string or integer' ); $cache->set( $key, 'x' ); } @@ -283,8 +283,8 @@ class MapCacheLRUTest extends PHPUnit\Framework\TestCase { */ public function testHasFieldInvalidKey( $field ) { $cache = MapCacheLRU::newFromArray( [ 'key' => [] ], 3 ); - $this->expectWarning(); - $this->expectWarningMessageMatches( '/should be either a string or an integer/' ); + $this->expectException( UnexpectedValueException::class ); + $this->expectExceptionMessage( 'must be string or integer' ); $cache->hasField( 'key', $field ); } @@ -294,8 +294,8 @@ class MapCacheLRUTest extends PHPUnit\Framework\TestCase { */ public function testGetFieldInvalidKey( $field ) { $cache = MapCacheLRU::newFromArray( [ 'key' => [] ], 3 ); - $this->expectWarning(); - $this->expectWarningMessageMatches( '/should be either a string or an integer/' ); + $this->expectException( UnexpectedValueException::class ); + $this->expectExceptionMessage( 'must be string or integer' ); $cache->getField( 'key', $field ); } @@ -305,8 +305,8 @@ class MapCacheLRUTest extends PHPUnit\Framework\TestCase { */ public function testSetFieldInvalidKey( $field ) { $cache = new MapCacheLRU( 3 ); - $this->expectWarning(); - $this->expectWarningMessageMatches( '/Field keys must be string or integer/' ); + $this->expectException( UnexpectedValueException::class ); + $this->expectExceptionMessage( 'must be string or integer' ); $cache->setField( 'key', $field, 'x' ); } } |