From: jenkins-bot Date: Tue, 20 Aug 2019 16:51:53 +0000 (+0000) Subject: Merge "Tests: Set dbname for DatabaseSqliteTest" X-Git-Tag: 1.34.0-rc.0~661 X-Git-Url: http://git.cyclocoop.org//%22javascript:ModifierStyle%28%27%22.%24id.%22%27%29/%22?a=commitdiff_plain;h=43ca34e912579e3fdba9874ff4255daa2a6a4180;hp=c860482d29c0dc629ef248f7b717d349e23da78d;p=lhc%2Fweb%2Fwiklou.git Merge "Tests: Set dbname for DatabaseSqliteTest" --- diff --git a/RELEASE-NOTES-1.34 b/RELEASE-NOTES-1.34 index 0c6fec26fe..9a4aeb38f9 100644 --- a/RELEASE-NOTES-1.34 +++ b/RELEASE-NOTES-1.34 @@ -343,7 +343,8 @@ because of Phabricator reports. Use IDatabase::getDomainID() instead. * (T191231) Support for using Oracle or MSSQL as database backends has been dropped. -* … +* MessageCache::destroyInstance() has been removed. Instead, call + MediaWikiTestCase::resetServices(). === Deprecations in 1.34 === * The MWNamespace class is deprecated. Use NamespaceInfo. @@ -446,6 +447,9 @@ because of Phabricator reports. deprecation above this method is no longer needed/called and should not be implemented by SearchEngine implementation. * IDatabase::bufferResults() has been deprecated. Use query batching instead. +* MessageCache::singleton() is deprecated. Use + MediaWikiServices::getMessageCache(). +* Constructing MovePage directly is deprecated. Use MovePageFactory. === Other changes in 1.34 === * … diff --git a/autoload.php b/autoload.php index 52a5edaef1..acdf8dd3d2 100644 --- a/autoload.php +++ b/autoload.php @@ -913,6 +913,7 @@ $wgAutoloadLocalClasses = [ 'MediaWiki\\MediaWikiServices' => __DIR__ . '/includes/MediaWikiServices.php', 'MediaWiki\\Navigation\\PrevNextNavigationRenderer' => __DIR__ . '/includes/Navigation/PrevNextNavigationRenderer.php', 'MediaWiki\\OutputHandler' => __DIR__ . '/includes/OutputHandler.php', + 'MediaWiki\\Page\\MovePageFactory' => __DIR__ . '/includes/page/MovePageFactory.php', 'MediaWiki\\ProcOpenError' => __DIR__ . '/includes/exception/ProcOpenError.php', 'MediaWiki\\Search\\ParserOutputSearchDataExtractor' => __DIR__ . '/includes/search/ParserOutputSearchDataExtractor.php', 'MediaWiki\\Services\\CannotReplaceActiveServiceException' => __DIR__ . '/includes/libs/services/CannotReplaceActiveServiceException.php', diff --git a/docs/hooks.txt b/docs/hooks.txt index d832012df5..6207b12e2b 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -91,23 +91,29 @@ title-reversing if-blocks spread all over the codebase in showAnArticle, deleteAnArticle, exportArticle, etc., we can concentrate it all in an extension file: - function reverseArticleTitle( $article ) { + function onArticleShow( &$article ) { # ... } - function reverseForExport( $article ) { + function onArticleDelete( &$article ) { # ... } -The setup function for the extension just has to add its hook functions to the -appropriate events: - - setupTitleReversingExtension() { - global $wgHooks; + function onArticleExport( &$article ) { + # ... + } - $wgHooks['ArticleShow'][] = 'reverseArticleTitle'; - $wgHooks['ArticleDelete'][] = 'reverseArticleTitle'; - $wgHooks['ArticleExport'][] = 'reverseForExport'; +General practice is to have a dedicated file for functions activated by hooks, +which functions named 'onHookName'. In the example above, the file +'ReverseHooks.php' includes the functions that should be activated by the +'ArticleShow', 'ArticleDelete', and 'ArticleExport' hooks. The 'extension.json' +file with the extension's registration just has to add its hook functions +to the appropriate events: + + "Hooks": { + "ArticleShow": "ReverseHooks:onArticleShow", + "ArticleDelete": "ReverseHooks::onArticleDelete", + "ArticleExport": "ReverseHooks::onArticleExport" } Having all this code related to the title-reversion option in one place means diff --git a/docs/pageupdater.txt b/docs/pageupdater.txt index fd084c0587..3d113f6569 100644 --- a/docs/pageupdater.txt +++ b/docs/pageupdater.txt @@ -148,7 +148,7 @@ parent of $revision parameter passed to prepareUpdate(). transformation (PST) and allow subsequent access to the canonical ParserOutput of the revision. getSlots() and getCanonicalParserOutput() as well as getSecondaryDataUpdates() may be used after prepareContent() was called. Calling prepareContent() with the same -parameters again has no effect. Calling it again with mismatching paramters, or calling +parameters again has no effect. Calling it again with mismatching parameters, or calling it after prepareUpdate() was called, triggers a LogicException. - prepareUpdate() is called after the new revision has been created. This may happen diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 208cfe6ea0..f2446da40b 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -5860,6 +5860,7 @@ $wgGrantPermissions['createeditmovepage']['move'] = true; $wgGrantPermissions['createeditmovepage']['move-rootuserpages'] = true; $wgGrantPermissions['createeditmovepage']['move-subpages'] = true; $wgGrantPermissions['createeditmovepage']['move-categorypages'] = true; +$wgGrantPermissions['createeditmovepage']['suppressredirect'] = true; $wgGrantPermissions['uploadfile']['upload'] = true; $wgGrantPermissions['uploadfile']['reupload-own'] = true; diff --git a/includes/MediaWikiServices.php b/includes/MediaWikiServices.php index 7fda45280a..bb9c05f424 100644 --- a/includes/MediaWikiServices.php +++ b/includes/MediaWikiServices.php @@ -17,11 +17,12 @@ use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface; use MediaWiki\Block\BlockManager; use MediaWiki\Block\BlockRestrictionStore; use MediaWiki\Http\HttpRequestFactory; +use MediaWiki\Page\MovePageFactory; use MediaWiki\Permissions\PermissionManager; use MediaWiki\Preferences\PreferencesFactory; -use MediaWiki\Shell\CommandFactory; use MediaWiki\Revision\RevisionRenderer; use MediaWiki\Revision\SlotRoleRegistry; +use MediaWiki\Shell\CommandFactory; use MediaWiki\Special\SpecialPageFactory; use MediaWiki\Storage\BlobStore; use MediaWiki\Storage\BlobStoreFactory; @@ -40,6 +41,7 @@ use MediaWiki\Config\ConfigRepository; use MediaWiki\Linker\LinkRenderer; use MediaWiki\Linker\LinkRendererFactory; use MWException; +use MessageCache; use MimeAnalyzer; use NamespaceInfo; use ObjectCache; @@ -689,6 +691,14 @@ class MediaWikiServices extends ServiceContainer { return $this->getService( 'MediaHandlerFactory' ); } + /** + * @since 1.34 + * @return MessageCache + */ + public function getMessageCache() : MessageCache { + return $this->getService( 'MessageCache' ); + } + /** * @since 1.28 * @return MimeAnalyzer @@ -697,6 +707,14 @@ class MediaWikiServices extends ServiceContainer { return $this->getService( 'MimeAnalyzer' ); } + /** + * @since 1.34 + * @return MovePageFactory + */ + public function getMovePageFactory() : MovePageFactory { + return $this->getService( 'MovePageFactory' ); + } + /** * @since 1.34 * @return NamespaceInfo diff --git a/includes/MovePage.php b/includes/MovePage.php index 832e24af81..a63eeaebda 100644 --- a/includes/MovePage.php +++ b/includes/MovePage.php @@ -19,9 +19,13 @@ * @file */ +use MediaWiki\Config\ServiceOptions; use MediaWiki\MediaWikiServices; +use MediaWiki\Page\MovePageFactory; +use MediaWiki\Permissions\PermissionManager; use MediaWiki\Revision\SlotRecord; use Wikimedia\Rdbms\IDatabase; +use Wikimedia\Rdbms\LoadBalancer; /** * Handles the backend logic of moving a page from one title @@ -41,9 +45,69 @@ class MovePage { */ protected $newTitle; - public function __construct( Title $oldTitle, Title $newTitle ) { + /** + * @var ServiceOptions + */ + protected $options; + + /** + * @var LoadBalancer + */ + protected $loadBalancer; + + /** + * @var NamespaceInfo + */ + protected $nsInfo; + + /** + * @var WatchedItemStore + */ + protected $watchedItems; + + /** + * @var PermissionManager + */ + protected $permMgr; + + /** + * @var RepoGroup + */ + protected $repoGroup; + + /** + * Calling this directly is deprecated in 1.34. Use MovePageFactory instead. + * + * @param Title $oldTitle + * @param Title $newTitle + * @param ServiceOptions|null $options + * @param LoadBalancer|null $loadBalancer + * @param NamespaceInfo|null $nsInfo + * @param WatchedItemStore|null $watchedItems + * @param PermissionManager|null $permMgr + */ + public function __construct( + Title $oldTitle, + Title $newTitle, + ServiceOptions $options = null, + LoadBalancer $loadBalancer = null, + NamespaceInfo $nsInfo = null, + WatchedItemStore $watchedItems = null, + PermissionManager $permMgr = null, + RepoGroup $repoGroup = null + ) { $this->oldTitle = $oldTitle; $this->newTitle = $newTitle; + $this->options = $options ?? + new ServiceOptions( MovePageFactory::$constructorOptions, + MediaWikiServices::getInstance()->getMainConfig() ); + $this->loadBalancer = + $loadBalancer ?? MediaWikiServices::getInstance()->getDBLoadBalancer(); + $this->nsInfo = $nsInfo ?? MediaWikiServices::getInstance()->getNamespaceInfo(); + $this->watchedItems = + $watchedItems ?? MediaWikiServices::getInstance()->getWatchedItemStore(); + $this->permMgr = $permMgr ?? MediaWikiServices::getInstance()->getPermissionManager(); + $this->repoGroup = $repoGroup ?? MediaWikiServices::getInstance()->getRepoGroup(); } /** @@ -58,10 +122,10 @@ class MovePage { $status = new Status(); $errors = wfMergeErrorArrays( - $this->oldTitle->getUserPermissionsErrors( 'move', $user ), - $this->oldTitle->getUserPermissionsErrors( 'edit', $user ), - $this->newTitle->getUserPermissionsErrors( 'move-target', $user ), - $this->newTitle->getUserPermissionsErrors( 'edit', $user ) + $this->permMgr->getPermissionErrors( 'move', $user, $this->oldTitle ), + $this->permMgr->getPermissionErrors( 'edit', $user, $this->oldTitle ), + $this->permMgr->getPermissionErrors( 'move-target', $user, $this->newTitle ), + $this->permMgr->getPermissionErrors( 'edit', $user, $this->newTitle ) ); // Convert into a Status object @@ -96,44 +160,41 @@ class MovePage { * @return Status */ public function isValidMove() { - global $wgContentHandlerUseDB; $status = new Status(); if ( $this->oldTitle->equals( $this->newTitle ) ) { $status->fatal( 'selfmove' ); + } elseif ( $this->newTitle->getArticleID() && !$this->isValidMoveTarget() ) { + // The move is allowed only if (1) the target doesn't exist, or (2) the target is a + // redirect to the source, and has no history (so we can undo bad moves right after + // they're done). + $status->fatal( 'articleexists' ); } - if ( !$this->oldTitle->isMovable() ) { + + // @todo If the old title is invalid, maybe we should check if it somehow exists in the + // database and allow moving it to a valid name? Why prohibit the move from an empty name + // without checking in the database? + if ( $this->oldTitle->getDBkey() == '' ) { + $status->fatal( 'badarticleerror' ); + } elseif ( $this->oldTitle->isExternal() ) { + $status->fatal( 'immobile-source-namespace-iw' ); + } elseif ( !$this->oldTitle->isMovable() ) { $status->fatal( 'immobile-source-namespace', $this->oldTitle->getNsText() ); + } elseif ( !$this->oldTitle->exists() ) { + $status->fatal( 'movepage-source-doesnt-exist' ); } + if ( $this->newTitle->isExternal() ) { $status->fatal( 'immobile-target-namespace-iw' ); - } - if ( !$this->newTitle->isMovable() ) { + } elseif ( !$this->newTitle->isMovable() ) { $status->fatal( 'immobile-target-namespace', $this->newTitle->getNsText() ); } - - $oldid = $this->oldTitle->getArticleID(); - - if ( $this->newTitle->getDBkey() === '' ) { - $status->fatal( 'articleexists' ); - } - if ( - ( $this->oldTitle->getDBkey() == '' ) || - ( !$oldid ) || - ( $this->newTitle->getDBkey() == '' ) - ) { - $status->fatal( 'badarticleerror' ); - } - - # The move is allowed only if (1) the target doesn't exist, or - # (2) the target is a redirect to the source, and has no history - # (so we can undo bad moves right after they're done). - if ( $this->newTitle->getArticleID() && !$this->isValidMoveTarget() ) { - $status->fatal( 'articleexists' ); + if ( !$this->newTitle->isValid() ) { + $status->fatal( 'movepage-invalid-target-title' ); } // Content model checks - if ( !$wgContentHandlerUseDB && + if ( !$this->options->get( 'ContentHandlerUseDB' ) && $this->oldTitle->getContentModel() !== $this->newTitle->getContentModel() ) { // can't move a page if that would change the page's content model $status->fatal( @@ -174,7 +235,14 @@ class MovePage { */ protected function isValidFileMove() { $status = new Status(); - $file = wfLocalFile( $this->oldTitle ); + + if ( !$this->newTitle->inNamespace( NS_FILE ) ) { + $status->fatal( 'imagenocrossnamespace' ); + // No need for further errors about the target filename being wrong + return $status; + } + + $file = $this->repoGroup->getLocalRepo()->newFile( $this->oldTitle ); $file->load( File::READ_LATEST ); if ( $file->exists() ) { if ( $this->newTitle->getText() != wfStripIllegalFilenameChars( $this->newTitle->getText() ) ) { @@ -185,10 +253,6 @@ class MovePage { } } - if ( !$this->newTitle->inNamespace( NS_FILE ) ) { - $status->fatal( 'imagenocrossnamespace' ); - } - return $status; } @@ -202,7 +266,7 @@ class MovePage { protected function isValidMoveTarget() { # Is it an existing file? if ( $this->newTitle->inNamespace( NS_FILE ) ) { - $file = wfLocalFile( $this->newTitle ); + $file = $this->repoGroup->getLocalRepo()->newFile( $this->newTitle ); $file->load( File::READ_LATEST ); if ( $file->exists() ) { wfDebug( __METHOD__ . ": file exists\n" ); @@ -430,8 +494,6 @@ class MovePage { * @return Status */ private function moveUnsafe( User $user, $reason, $createRedirect, array $changeTags ) { - global $wgCategoryCollation; - $status = Status::newGood(); Hooks::run( 'TitleMove', [ $this->oldTitle, $this->newTitle, $user, $reason, &$status ] ); if ( !$status->isOK() ) { @@ -439,7 +501,7 @@ class MovePage { return $status; } - $dbw = wfGetDB( DB_MASTER ); + $dbw = $this->loadBalancer->getConnection( DB_MASTER ); $dbw->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); Hooks::run( 'TitleMoveStarting', [ $this->oldTitle, $this->newTitle, $user ] ); @@ -461,9 +523,7 @@ class MovePage { [ 'cl_from' => $pageid ], __METHOD__ ); - $services = MediaWikiServices::getInstance(); - $type = $services->getNamespaceInfo()-> - getCategoryLinkType( $this->newTitle->getNamespace() ); + $type = $this->nsInfo->getCategoryLinkType( $this->newTitle->getNamespace() ); foreach ( $prefixes as $prefixRow ) { $prefix = $prefixRow->cl_sortkey_prefix; $catTo = $prefixRow->cl_to; @@ -471,7 +531,7 @@ class MovePage { [ 'cl_sortkey' => Collation::singleton()->getSortKey( $this->newTitle->getCategorySortkey( $prefix ) ), - 'cl_collation' => $wgCategoryCollation, + 'cl_collation' => $this->options->get( 'CategoryCollation' ), 'cl_type' => $type, 'cl_timestamp=cl_timestamp' ], [ @@ -563,13 +623,10 @@ class MovePage { # Update watchlists $oldtitle = $this->oldTitle->getDBkey(); $newtitle = $this->newTitle->getDBkey(); - $oldsnamespace = $services->getNamespaceInfo()-> - getSubject( $this->oldTitle->getNamespace() ); - $newsnamespace = $services->getNamespaceInfo()-> - getSubject( $this->newTitle->getNamespace() ); + $oldsnamespace = $this->nsInfo->getSubject( $this->oldTitle->getNamespace() ); + $newsnamespace = $this->nsInfo->getSubject( $this->newTitle->getNamespace() ); if ( $oldsnamespace != $newsnamespace || $oldtitle != $newtitle ) { - $services->getWatchedItemStore()->duplicateAllAssociatedEntries( - $this->oldTitle, $this->newTitle ); + $this->watchedItems->duplicateAllAssociatedEntries( $this->oldTitle, $this->newTitle ); } // If it is a file then move it last. @@ -630,15 +687,15 @@ class MovePage { $oldTitle->getPrefixedText() ); - $file = wfLocalFile( $oldTitle ); + $file = $this->repoGroup->getLocalRepo()->newFile( $oldTitle ); $file->load( File::READ_LATEST ); if ( $file->exists() ) { $status = $file->move( $newTitle ); } // Clear RepoGroup process cache - RepoGroup::singleton()->clearCache( $oldTitle ); - RepoGroup::singleton()->clearCache( $newTitle ); # clear false negative cache + $this->repoGroup->clearCache( $oldTitle ); + $this->repoGroup->clearCache( $newTitle ); # clear false negative cache return $status; } @@ -739,7 +796,7 @@ class MovePage { $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason; } - $dbw = wfGetDB( DB_MASTER ); + $dbw = $this->loadBalancer->getConnection( DB_MASTER ); $oldpage = WikiPage::factory( $this->oldTitle ); $oldcountable = $oldpage->isCountable(); diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php index c192b5a266..0b4ce4aa3a 100644 --- a/includes/ServiceWiring.php +++ b/includes/ServiceWiring.php @@ -50,6 +50,7 @@ use MediaWiki\Linker\LinkRenderer; use MediaWiki\Linker\LinkRendererFactory; use MediaWiki\Logger\LoggerFactory; use MediaWiki\MediaWikiServices; +use MediaWiki\Page\MovePageFactory; use MediaWiki\Permissions\PermissionManager; use MediaWiki\Preferences\PreferencesFactory; use MediaWiki\Preferences\DefaultPreferencesFactory; @@ -267,9 +268,10 @@ return [ }, 'LocalServerObjectCache' => function ( MediaWikiServices $services ) : BagOStuff { + $config = $services->getMainConfig(); $cacheId = \ObjectCache::detectLocalServerCache(); - return \ObjectCache::newFromId( $cacheId ); + return \ObjectCache::newFromParams( $config->get( 'ObjectCaches' )[$cacheId] ); }, 'MagicWordFactory' => function ( MediaWikiServices $services ) : MagicWordFactory { @@ -319,6 +321,20 @@ return [ ); }, + 'MessageCache' => function ( MediaWikiServices $services ) : MessageCache { + $mainConfig = $services->getMainConfig(); + return new MessageCache( + $services->getMainWANObjectCache(), + ObjectCache::getInstance( $mainConfig->get( 'MessageCacheType' ) ), + $mainConfig->get( 'UseLocalMessageCache' ) + ? $services->getLocalServerObjectCache() + : new EmptyBagOStuff(), + $mainConfig->get( 'UseDatabaseMessages' ), + $mainConfig->get( 'MsgCacheExpiry' ), + $services->getContentLanguage() + ); + }, + 'MimeAnalyzer' => function ( MediaWikiServices $services ) : MimeAnalyzer { $logger = LoggerFactory::getInstance( 'Mime' ); $mainConfig = $services->getMainConfig(); @@ -377,6 +393,17 @@ return [ return new MimeAnalyzer( $params ); }, + 'MovePageFactory' => function ( MediaWikiServices $services ) : MovePageFactory { + return new MovePageFactory( + new ServiceOptions( MovePageFactory::$constructorOptions, $services->getMainConfig() ), + $services->getDBLoadBalancer(), + $services->getNamespaceInfo(), + $services->getWatchedItemStore(), + $services->getPermissionManager(), + $services->getRepoGroup() + ); + }, + 'NamespaceInfo' => function ( MediaWikiServices $services ) : NamespaceInfo { return new NamespaceInfo( new ServiceOptions( NamespaceInfo::$constructorOptions, $services->getMainConfig() ) ); diff --git a/includes/Title.php b/includes/Title.php index 281f75bac1..f6818525d8 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -3461,7 +3461,7 @@ class Title implements LinkTarget, IDBAccessObject { return [ [ 'badtitletext' ] ]; } - $mp = new MovePage( $this, $nt ); + $mp = MediaWikiServices::getInstance()->getMovePageFactory()->newMovePage( $this, $nt ); $errors = $mp->isValidMove()->getErrorsArray(); if ( $auth ) { $errors = wfMergeErrorArrays( @@ -3493,7 +3493,7 @@ class Title implements LinkTarget, IDBAccessObject { global $wgUser; - $mp = new MovePage( $this, $nt ); + $mp = MediaWikiServices::getInstance()->getMovePageFactory()->newMovePage( $this, $nt ); $method = $auth ? 'moveIfAllowed' : 'move'; $status = $mp->$method( $wgUser, $reason, $createRedirect, $changeTags ); if ( $status->isOK() ) { diff --git a/includes/api/ApiMove.php b/includes/api/ApiMove.php index 540860b3a9..0a788d4e26 100644 --- a/includes/api/ApiMove.php +++ b/includes/api/ApiMove.php @@ -172,7 +172,7 @@ class ApiMove extends ApiBase { * @return Status */ protected function movePage( Title $from, Title $to, $reason, $createRedirect, $changeTags ) { - $mp = new MovePage( $from, $to ); + $mp = MediaWikiServices::getInstance()->getMovePageFactory()->newMovePage( $from, $to ); $valid = $mp->isValidMove(); if ( !$valid->isOK() ) { return $valid; diff --git a/includes/cache/MessageCache.php b/includes/cache/MessageCache.php index 57454516f3..93fdb162e2 100644 --- a/includes/cache/MessageCache.php +++ b/includes/cache/MessageCache.php @@ -105,44 +105,14 @@ class MessageCache { private $loadedLanguages = []; /** - * Singleton instance - * - * @var MessageCache $instance - */ - private static $instance; - - /** - * Get the signleton instance of this class + * Get the singleton instance of this class * + * @deprecated in 1.34 inject an instance of this class instead of using global state * @since 1.18 * @return MessageCache */ public static function singleton() { - if ( self::$instance === null ) { - global $wgUseDatabaseMessages, $wgMsgCacheExpiry, $wgUseLocalMessageCache; - $services = MediaWikiServices::getInstance(); - self::$instance = new self( - $services->getMainWANObjectCache(), - wfGetMessageCacheStorage(), - $wgUseLocalMessageCache - ? $services->getLocalServerObjectCache() - : new EmptyBagOStuff(), - $wgUseDatabaseMessages, - $wgMsgCacheExpiry, - $services->getContentLanguage() - ); - } - - return self::$instance; - } - - /** - * Destroy the singleton instance - * - * @since 1.18 - */ - public static function destroyInstance() { - self::$instance = null; + return MediaWikiServices::getInstance()->getMessageCache(); } /** diff --git a/includes/changes/ChangesList.php b/includes/changes/ChangesList.php index e2b35a8632..78078770b2 100644 --- a/includes/changes/ChangesList.php +++ b/includes/changes/ChangesList.php @@ -232,6 +232,13 @@ class ChangesList extends ContextSource { $classes[] = Sanitizer::escapeClass( self::CSS_CLASS_PREFIX . 'ns-' . $rc->mAttribs['rc_namespace'] ); + $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo(); + $classes[] = Sanitizer::escapeClass( + self::CSS_CLASS_PREFIX . + 'ns-' . + ( $nsInfo->isTalk( $rc->mAttribs['rc_namespace'] ) ? 'talk' : 'subject' ) + ); + if ( $this->filterGroups !== null ) { foreach ( $this->filterGroups as $filterGroup ) { foreach ( $filterGroup->getFilters() as $filter ) { diff --git a/includes/jobqueue/jobs/ActivityUpdateJob.php b/includes/jobqueue/jobs/ActivityUpdateJob.php index 4de72a9b12..d27056dc02 100644 --- a/includes/jobqueue/jobs/ActivityUpdateJob.php +++ b/includes/jobqueue/jobs/ActivityUpdateJob.php @@ -42,7 +42,7 @@ class ActivityUpdateJob extends Job { static $required = [ 'type', 'userid', 'notifTime', 'curTime' ]; $missing = implode( ', ', array_diff( $required, array_keys( $this->params ) ) ); if ( $missing != '' ) { - throw new InvalidArgumentException( "Missing paramter(s) $missing" ); + throw new InvalidArgumentException( "Missing parameter(s) $missing" ); } $this->removeDuplicates = true; diff --git a/includes/libs/objectcache/APCBagOStuff.php b/includes/libs/objectcache/APCBagOStuff.php index 0954ac8061..aa83b1ff1e 100644 --- a/includes/libs/objectcache/APCBagOStuff.php +++ b/includes/libs/objectcache/APCBagOStuff.php @@ -73,7 +73,7 @@ class APCBagOStuff extends MediumSpecificBagOStuff { return true; } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { return apc_add( $key . self::KEY_SUFFIX, $this->nativeSerialize ? $value : $this->serialize( $value ), diff --git a/includes/libs/objectcache/APCUBagOStuff.php b/includes/libs/objectcache/APCUBagOStuff.php index 021cdf7b76..80383d1fb1 100644 --- a/includes/libs/objectcache/APCUBagOStuff.php +++ b/includes/libs/objectcache/APCUBagOStuff.php @@ -71,7 +71,7 @@ class APCUBagOStuff extends MediumSpecificBagOStuff { ); } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { return apcu_add( $key . self::KEY_SUFFIX, $this->nativeSerialize ? $value : $this->serialize( $value ), diff --git a/includes/libs/objectcache/CachedBagOStuff.php b/includes/libs/objectcache/CachedBagOStuff.php index 0ab26c9520..9fa9a89f9b 100644 --- a/includes/libs/objectcache/CachedBagOStuff.php +++ b/includes/libs/objectcache/CachedBagOStuff.php @@ -79,7 +79,7 @@ class CachedBagOStuff extends BagOStuff { } } - $valuesByKeyFetched = $this->backend->getMulti( $keys, $flags ); + $valuesByKeyFetched = $this->backend->getMulti( $keysMissing, $flags ); $this->setMulti( $valuesByKeyFetched, self::TTL_INDEFINITE, self::WRITE_CACHE_ONLY ); return $valuesByKeyCached + $valuesByKeyFetched; diff --git a/includes/libs/objectcache/EmptyBagOStuff.php b/includes/libs/objectcache/EmptyBagOStuff.php index dab8ba1d35..b2613b2e45 100644 --- a/includes/libs/objectcache/EmptyBagOStuff.php +++ b/includes/libs/objectcache/EmptyBagOStuff.php @@ -41,7 +41,7 @@ class EmptyBagOStuff extends MediumSpecificBagOStuff { return true; } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { return true; } diff --git a/includes/libs/objectcache/HashBagOStuff.php b/includes/libs/objectcache/HashBagOStuff.php index 1cfa0c7921..b4087bed56 100644 --- a/includes/libs/objectcache/HashBagOStuff.php +++ b/includes/libs/objectcache/HashBagOStuff.php @@ -94,7 +94,7 @@ class HashBagOStuff extends MediumSpecificBagOStuff { return true; } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { if ( $this->hasKey( $key ) && !$this->expire( $key ) ) { return false; // key already set } diff --git a/includes/libs/objectcache/MediumSpecificBagOStuff.php b/includes/libs/objectcache/MediumSpecificBagOStuff.php index 62a8aec967..329e600bb5 100644 --- a/includes/libs/objectcache/MediumSpecificBagOStuff.php +++ b/includes/libs/objectcache/MediumSpecificBagOStuff.php @@ -160,57 +160,9 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { * @return bool Success */ public function set( $key, $value, $exptime = 0, $flags = 0 ) { - if ( - is_int( $value ) || // avoid breaking incr()/decr() - ( $flags & self::WRITE_ALLOW_SEGMENTS ) != self::WRITE_ALLOW_SEGMENTS || - is_infinite( $this->segmentationSize ) - ) { - return $this->doSet( $key, $value, $exptime, $flags ); - } - - $serialized = $this->serialize( $value ); - $segmentSize = $this->getSegmentationSize(); - $maxTotalSize = $this->getSegmentedValueMaxSize(); - - $size = strlen( $serialized ); - if ( $size <= $segmentSize ) { - // Since the work of serializing it was already done, just use it inline - return $this->doSet( - $key, - SerializedValueContainer::newUnified( $serialized ), - $exptime, - $flags - ); - } elseif ( $size > $maxTotalSize ) { - $this->setLastError( "Key $key exceeded $maxTotalSize bytes." ); - - return false; - } - - $chunksByKey = []; - $segmentHashes = []; - $count = intdiv( $size, $segmentSize ) + ( ( $size % $segmentSize ) ? 1 : 0 ); - for ( $i = 0; $i < $count; ++$i ) { - $segment = substr( $serialized, $i * $segmentSize, $segmentSize ); - $hash = sha1( $segment ); - $chunkKey = $this->makeGlobalKey( self::SEGMENT_COMPONENT, $key, $hash ); - $chunksByKey[$chunkKey] = $segment; - $segmentHashes[] = $hash; - } - - $flags &= ~self::WRITE_ALLOW_SEGMENTS; // sanity - $ok = $this->setMulti( $chunksByKey, $exptime, $flags ); - if ( $ok ) { - // Only when all segments are stored should the main key be changed - $ok = $this->doSet( - $key, - SerializedValueContainer::newSegmented( $segmentHashes ), - $exptime, - $flags - ); - } - - return $ok; + list( $entry, $usable ) = $this->makeValueOrSegmentList( $key, $value, $exptime, $flags ); + // Only when all segments (if any) are stored should the main key be changed + return $usable ? $this->doSet( $key, $entry, $exptime, $flags ) : false; } /** @@ -268,6 +220,23 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { */ abstract protected function doDelete( $key, $flags = 0 ); + public function add( $key, $value, $exptime = 0, $flags = 0 ) { + list( $entry, $usable ) = $this->makeValueOrSegmentList( $key, $value, $exptime, $flags ); + // Only when all segments (if any) are stored should the main key be changed + return $usable ? $this->doAdd( $key, $entry, $exptime, $flags ) : false; + } + + /** + * Insert an item if it does not already exist + * + * @param string $key + * @param mixed $value + * @param int $exptime + * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33) + * @return bool Success + */ + abstract protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ); + /** * Merge changes into the existing cache value (possibly creating a new one) * @@ -283,7 +252,6 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { * @param int $attempts The amount of times to attempt a merge in case of failure * @param int $flags Bitfield of BagOStuff::WRITE_* constants * @return bool Success - * @throws InvalidArgumentException */ public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) { return $this->mergeViaCas( $key, $callback, $exptime, $attempts, $flags ); @@ -297,9 +265,9 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { * @param int $flags Bitfield of BagOStuff::WRITE_* constants * @return bool Success * @see BagOStuff::merge() - * */ final protected function mergeViaCas( $key, callable $callback, $exptime, $attempts, $flags ) { + $attemptsLeft = $attempts; do { $casToken = null; // passed by reference // Get the old value and CAS token from cache @@ -309,23 +277,27 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { $this->doGet( $key, self::READ_LATEST, $casToken ) ); if ( $this->getLastError() ) { + // Don't spam slow retries due to network problems (retry only on races) $this->logger->warning( - __METHOD__ . ' failed due to I/O error on get() for {key}.', + __METHOD__ . ' failed due to read I/O error on get() for {key}.', [ 'key' => $key ] ); - - return false; // don't spam retries (retry only on races) + $success = false; + break; } // Derive the new value from the old value $value = call_user_func( $callback, $this, $key, $currentValue, $exptime ); - $hadNoCurrentValue = ( $currentValue === false ); + $keyWasNonexistant = ( $currentValue === false ); + $valueMatchesOldValue = ( $value === $currentValue ); unset( $currentValue ); // free RAM in case the value is large $this->clearLastError(); if ( $value === false ) { $success = true; // do nothing - } elseif ( $hadNoCurrentValue ) { + } elseif ( $valueMatchesOldValue && $attemptsLeft !== $attempts ) { + $success = true; // recently set by another thread to the same value + } elseif ( $keyWasNonexistant ) { // Try to create the key, failing if it gets created in the meantime $success = $this->add( $key, $value, $exptime, $flags ); } else { @@ -333,15 +305,16 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { $success = $this->cas( $casToken, $key, $value, $exptime, $flags ); } if ( $this->getLastError() ) { + // Don't spam slow retries due to network problems (retry only on races) $this->logger->warning( - __METHOD__ . ' failed due to I/O error for {key}.', + __METHOD__ . ' failed due to write I/O error for {key}.', [ 'key' => $key ] ); - - return false; // IO error; don't spam retries + $success = false; + break; } - } while ( !$success && --$attempts ); + } while ( !$success && --$attemptsLeft ); return $success; } @@ -357,21 +330,58 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { * @return bool Success */ protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { + if ( $casToken === null ) { + $this->logger->warning( + __METHOD__ . ' got empty CAS token for {key}.', + [ 'key' => $key ] + ); + + return false; // caller may have meant to use add()? + } + + list( $entry, $usable ) = $this->makeValueOrSegmentList( $key, $value, $exptime, $flags ); + // Only when all segments (if any) are stored should the main key be changed + return $usable ? $this->doCas( $casToken, $key, $entry, $exptime, $flags ) : false; + } + + /** + * Check and set an item + * + * @param mixed $casToken + * @param string $key + * @param mixed $value + * @param int $exptime Either an interval in seconds or a unix timestamp for expiry + * @param int $flags Bitfield of BagOStuff::WRITE_* constants + * @return bool Success + */ + protected function doCas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { + // @TODO: the lock() call assumes that all other relavent sets() use one if ( !$this->lock( $key, 0 ) ) { return false; // non-blocking } $curCasToken = null; // passed by reference + $this->clearLastError(); $this->doGet( $key, self::READ_LATEST, $curCasToken ); - if ( $casToken === $curCasToken ) { - $success = $this->set( $key, $value, $exptime, $flags ); + if ( is_object( $curCasToken ) ) { + // Using === does not work with objects since it checks for instance identity + throw new UnexpectedValueException( "CAS token cannot be an object" ); + } + if ( $this->getLastError() ) { + // Fail if the old CAS token could not be read + $success = false; + $this->logger->warning( + __METHOD__ . ' failed due to write I/O error for {key}.', + [ 'key' => $key ] + ); + } elseif ( $casToken === $curCasToken ) { + $success = $this->doSet( $key, $value, $exptime, $flags ); } else { + $success = false; // mismatched or failed $this->logger->info( __METHOD__ . ' failed due to race condition for {key}.', [ 'key' => $key ] ); - - $success = false; // mismatched or failed } $this->unlock( $key ); @@ -782,6 +792,59 @@ abstract class MediumSpecificBagOStuff extends BagOStuff { $this->busyCallbacks[] = $workCallback; } + /** + * Determine the entry (inline or segment list) to store under a key to save the value + * + * @param string $key + * @param mixed $value + * @param int $exptime + * @param int $flags + * @return array (inline value or segment list, whether the entry is usable) + * @since 1.34 + */ + final protected function makeValueOrSegmentList( $key, $value, $exptime, $flags ) { + $entry = $value; + $usable = true; + + if ( + ( $flags & self::WRITE_ALLOW_SEGMENTS ) === self::WRITE_ALLOW_SEGMENTS && + !is_int( $value ) && // avoid breaking incr()/decr() + is_finite( $this->segmentationSize ) + ) { + $segmentSize = $this->segmentationSize; + $maxTotalSize = $this->segmentedValueMaxSize; + + $serialized = $this->serialize( $value ); + $size = strlen( $serialized ); + if ( $size > $maxTotalSize ) { + $this->logger->warning( + "Value for {key} exceeds $maxTotalSize bytes; cannot segment.", + [ 'key' => $key ] + ); + } elseif ( $size <= $segmentSize ) { + // The serialized value was already computed, so just use it inline + $entry = SerializedValueContainer::newUnified( $serialized ); + } else { + // Split the serialized value into chunks and store them at different keys + $chunksByKey = []; + $segmentHashes = []; + $count = intdiv( $size, $segmentSize ) + ( ( $size % $segmentSize ) ? 1 : 0 ); + for ( $i = 0; $i < $count; ++$i ) { + $segment = substr( $serialized, $i * $segmentSize, $segmentSize ); + $hash = sha1( $segment ); + $chunkKey = $this->makeGlobalKey( self::SEGMENT_COMPONENT, $key, $hash ); + $chunksByKey[$chunkKey] = $segment; + $segmentHashes[] = $hash; + } + $flags &= ~self::WRITE_ALLOW_SEGMENTS; // sanity + $usable = $this->setMulti( $chunksByKey, $exptime, $flags ); + $entry = SerializedValueContainer::newSegmented( $segmentHashes ); + } + } + + return [ $entry, $usable ]; + } + /** * @param int|float $exptime * @return bool Whether the expiry is non-infinite, and, negative or not a UNIX timestamp diff --git a/includes/libs/objectcache/MemcachedPeclBagOStuff.php b/includes/libs/objectcache/MemcachedPeclBagOStuff.php index cc7ee2a5f5..3df483def1 100644 --- a/includes/libs/objectcache/MemcachedPeclBagOStuff.php +++ b/includes/libs/objectcache/MemcachedPeclBagOStuff.php @@ -214,7 +214,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff { : $this->checkResult( $key, $result ); } - protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { + protected function doCas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { $this->debug( "cas($key)" ); $result = $this->acquireSyncClient()->cas( @@ -238,7 +238,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff { : $this->checkResult( $key, $result ); } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { $this->debug( "add($key)" ); $result = $this->acquireSyncClient()->add( diff --git a/includes/libs/objectcache/MemcachedPhpBagOStuff.php b/includes/libs/objectcache/MemcachedPhpBagOStuff.php index b1d5d29f16..81442314c9 100644 --- a/includes/libs/objectcache/MemcachedPhpBagOStuff.php +++ b/includes/libs/objectcache/MemcachedPhpBagOStuff.php @@ -76,7 +76,7 @@ class MemcachedPhpBagOStuff extends MemcachedBagOStuff { return $this->client->delete( $this->validateKeyEncoding( $key ) ); } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { return $this->client->add( $this->validateKeyEncoding( $key ), $value, @@ -84,7 +84,7 @@ class MemcachedPhpBagOStuff extends MemcachedBagOStuff { ); } - protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { + protected function doCas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { return $this->client->cas( $casToken, $this->validateKeyEncoding( $key ), diff --git a/includes/libs/objectcache/RESTBagOStuff.php b/includes/libs/objectcache/RESTBagOStuff.php index aa4a9b31fc..b8ce38b1cc 100644 --- a/includes/libs/objectcache/RESTBagOStuff.php +++ b/includes/libs/objectcache/RESTBagOStuff.php @@ -164,7 +164,7 @@ class RESTBagOStuff extends MediumSpecificBagOStuff { return $this->handleError( "Failed to store $key", $rcode, $rerr, $rhdrs, $rbody ); } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { // @TODO: make this atomic if ( $this->get( $key ) === false ) { return $this->set( $key, $value, $exptime, $flags ); diff --git a/includes/libs/objectcache/RedisBagOStuff.php b/includes/libs/objectcache/RedisBagOStuff.php index f75d3a1015..252b1faf8c 100644 --- a/includes/libs/objectcache/RedisBagOStuff.php +++ b/includes/libs/objectcache/RedisBagOStuff.php @@ -341,7 +341,7 @@ class RedisBagOStuff extends MediumSpecificBagOStuff { return $result; } - public function add( $key, $value, $expiry = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $expiry = 0, $flags = 0 ) { $conn = $this->getConnection( $key ); if ( !$conn ) { return false; diff --git a/includes/libs/objectcache/WinCacheBagOStuff.php b/includes/libs/objectcache/WinCacheBagOStuff.php index 0e4e3fb63d..3c4efbbedd 100644 --- a/includes/libs/objectcache/WinCacheBagOStuff.php +++ b/includes/libs/objectcache/WinCacheBagOStuff.php @@ -28,6 +28,11 @@ * @ingroup Cache */ class WinCacheBagOStuff extends MediumSpecificBagOStuff { + public function __construct( array $params = [] ) { + $params['segmentationSize'] = $params['segmentationSize'] ?? INF; + parent::__construct( $params ); + } + protected function doGet( $key, $flags = 0, &$casToken = null ) { $casToken = null; @@ -44,7 +49,7 @@ class WinCacheBagOStuff extends MediumSpecificBagOStuff { return $value; } - protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { + protected function doCas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { if ( !wincache_lock( $key ) ) { // optimize with FIFO lock return false; } @@ -76,7 +81,7 @@ class WinCacheBagOStuff extends MediumSpecificBagOStuff { return ( $result === [] || $result === true ); } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { if ( wincache_ucache_exists( $key ) ) { return false; // avoid warnings } diff --git a/includes/libs/rdbms/database/DatabaseMysqli.php b/includes/libs/rdbms/database/DatabaseMysqli.php index 2f9abcf7d8..8931ae266e 100644 --- a/includes/libs/rdbms/database/DatabaseMysqli.php +++ b/includes/libs/rdbms/database/DatabaseMysqli.php @@ -56,9 +56,14 @@ class DatabaseMysqli extends DatabaseMysqlBase { ); } - // Other than mysql_connect, mysqli_real_connect expects an explicit port - // and socket parameters. So we need to parse the port and socket out of - // $realServer + // Other than mysql_connect, mysqli_real_connect expects an explicit port number + // e.g. "localhost:1234" or "127.0.0.1:1234" + // or Unix domain socket path + // e.g. "localhost:/socket_path" or "localhost:/foo/bar:bar:bar" + // colons are known to be used by Google AppEngine, + // see + // + // We need to parse the port or socket path out of $realServer $port = null; $socket = null; $hostAndPort = IP::splitHostAndPort( $realServer ); @@ -67,9 +72,9 @@ class DatabaseMysqli extends DatabaseMysqlBase { if ( $hostAndPort[1] ) { $port = $hostAndPort[1]; } - } elseif ( substr_count( $realServer, ':' ) == 1 ) { - // If we have a colon and something that's not a port number - // inside the hostname, assume it's the socket location + } elseif ( substr_count( $realServer, ':/' ) == 1 ) { + // If we have a colon slash instead of a colon and a port number + // after the ip or hostname, assume it's the Unix domain socket path list( $realServer, $socket ) = explode( ':', $realServer, 2 ); } diff --git a/includes/libs/services/ServiceContainer.php b/includes/libs/services/ServiceContainer.php index d1f10524d5..84755edb90 100644 --- a/includes/libs/services/ServiceContainer.php +++ b/includes/libs/services/ServiceContainer.php @@ -6,6 +6,7 @@ use InvalidArgumentException; use Psr\Container\ContainerInterface; use RuntimeException; use Wikimedia\Assert\Assert; +use Wikimedia\ScopedCallback; /** * Generic service container. @@ -77,6 +78,11 @@ class ServiceContainer implements ContainerInterface, DestructibleService { */ private $destroyed = false; + /** + * @var array Set of services currently being created, to detect loops + */ + private $servicesBeingCreated = []; + /** * @param array $extraInstantiationParams Any additional parameters to be passed to the * instantiator function when creating a service. This is typically used to provide @@ -433,10 +439,20 @@ class ServiceContainer implements ContainerInterface, DestructibleService { * @param string $name * * @throws InvalidArgumentException if $name is not a known service. + * @throws RuntimeException if a circular dependency is detected. * @return object */ private function createService( $name ) { if ( isset( $this->serviceInstantiators[$name] ) ) { + if ( isset( $this->servicesBeingCreated[$name] ) ) { + throw new RuntimeException( "Circular dependency when creating service! " . + implode( ' -> ', array_keys( $this->servicesBeingCreated ) ) . " -> $name" ); + } + $this->servicesBeingCreated[$name] = true; + $removeFromStack = new ScopedCallback( function () use ( $name ) { + unset( $this->servicesBeingCreated[$name] ); + } ); + $service = ( $this->serviceInstantiators[$name] )( $this, ...$this->extraInstantiationParams @@ -458,6 +474,8 @@ class ServiceContainer implements ContainerInterface, DestructibleService { } } + $removeFromStack->consume(); + // NOTE: when adding more wiring logic here, make sure importWiring() is kept in sync! } else { throw new NoSuchServiceException( $name ); diff --git a/includes/logging/LogEntry.php b/includes/logging/LogEntry.php index 17f72bd574..ce68a91897 100644 --- a/includes/logging/LogEntry.php +++ b/includes/logging/LogEntry.php @@ -59,7 +59,7 @@ interface LogEntry { public function getParameters(); /** - * Get the user for performed this action. + * Get the user who performed this action. * * @return User */ diff --git a/includes/objectcache/ObjectCache.php b/includes/objectcache/ObjectCache.php index 3bb077173f..ad0f67e590 100644 --- a/includes/objectcache/ObjectCache.php +++ b/includes/objectcache/ObjectCache.php @@ -119,7 +119,7 @@ class ObjectCache { * @return BagOStuff * @throws InvalidArgumentException */ - public static function newFromId( $id ) { + private static function newFromId( $id ) { global $wgObjectCaches; if ( !isset( $wgObjectCaches[$id] ) ) { @@ -146,7 +146,7 @@ class ObjectCache { * * @return string */ - public static function getDefaultKeyspace() { + private static function getDefaultKeyspace() { global $wgCachePrefix; $keyspace = $wgCachePrefix; @@ -297,7 +297,7 @@ class ObjectCache { * @return WANObjectCache * @throws UnexpectedValueException */ - public static function newWANCacheFromId( $id ) { + private static function newWANCacheFromId( $id ) { global $wgWANObjectCaches, $wgObjectCaches; if ( !isset( $wgWANObjectCaches[$id] ) ) { diff --git a/includes/objectcache/SqlBagOStuff.php b/includes/objectcache/SqlBagOStuff.php index e97dc41af7..d9fe319277 100644 --- a/includes/objectcache/SqlBagOStuff.php +++ b/includes/objectcache/SqlBagOStuff.php @@ -467,11 +467,11 @@ class SqlBagOStuff extends MediumSpecificBagOStuff { return $this->modifyMulti( [ $key => $value ], $exptime, $flags, self::$OP_SET ); } - public function add( $key, $value, $exptime = 0, $flags = 0 ) { + protected function doAdd( $key, $value, $exptime = 0, $flags = 0 ) { return $this->modifyMulti( [ $key => $value ], $exptime, $flags, self::$OP_ADD ); } - protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { + protected function doCas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) { list( $serverIndex, $tableName ) = $this->getTableByKey( $key ); $exptime = $this->getExpirationAsTimestamp( $exptime ); diff --git a/includes/page/MovePageFactory.php b/includes/page/MovePageFactory.php new file mode 100644 index 0000000000..26da151844 --- /dev/null +++ b/includes/page/MovePageFactory.php @@ -0,0 +1,91 @@ +assertRequiredOptions( self::$constructorOptions ); + + $this->options = $options; + $this->loadBalancer = $loadBalancer; + $this->nsInfo = $nsInfo; + $this->watchedItems = $watchedItems; + $this->permMgr = $permMgr; + $this->repoGroup = $repoGroup; + } + + /** + * @param Title $from + * @param Title $to + * @return MovePage + */ + public function newMovePage( Title $from, Title $to ) : MovePage { + return new MovePage( $from, $to, $this->options, $this->loadBalancer, $this->nsInfo, + $this->watchedItems, $this->permMgr, $this->repoGroup ); + } +} diff --git a/includes/parser/Parser.php b/includes/parser/Parser.php index d14db039a1..b643c3f09c 100644 --- a/includes/parser/Parser.php +++ b/includes/parser/Parser.php @@ -351,7 +351,6 @@ class Parser { $nsInfo = null, $logger = null ) { - $services = MediaWikiServices::getInstance(); if ( !$svcOptions || is_array( $svcOptions ) ) { // Pre-1.34 calling convention is the first parameter is just ParserConf, the seventh is // Config, and the eighth is LinkRendererFactory. @@ -363,8 +362,8 @@ class Parser { $this->mConf['preprocessorClass'] = self::getDefaultPreprocessorClass(); } $this->svcOptions = new ServiceOptions( self::$constructorOptions, - $this->mConf, - func_num_args() > 6 ? func_get_arg( 6 ) : $services->getMainConfig() + $this->mConf, func_num_args() > 6 + ? func_get_arg( 6 ) : MediaWikiServices::getInstance()->getMainConfig() ); $linkRendererFactory = func_num_args() > 7 ? func_get_arg( 7 ) : null; $nsInfo = func_num_args() > 8 ? func_get_arg( 8 ) : null; @@ -386,14 +385,16 @@ class Parser { self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F\\x{FFFD}]*?)\]/Su'; $this->magicWordFactory = $magicWordFactory ?? - $services->getMagicWordFactory(); + MediaWikiServices::getInstance()->getMagicWordFactory(); - $this->contLang = $contLang ?? $services->getContentLanguage(); + $this->contLang = $contLang ?? MediaWikiServices::getInstance()->getContentLanguage(); - $this->factory = $factory ?? $services->getParserFactory(); - $this->specialPageFactory = $spFactory ?? $services->getSpecialPageFactory(); - $this->linkRendererFactory = $linkRendererFactory ?? $services->getLinkRendererFactory(); - $this->nsInfo = $nsInfo ?? $services->getNamespaceInfo(); + $this->factory = $factory ?? MediaWikiServices::getInstance()->getParserFactory(); + $this->specialPageFactory = $spFactory ?? + MediaWikiServices::getInstance()->getSpecialPageFactory(); + $this->linkRendererFactory = $linkRendererFactory ?? + MediaWikiServices::getInstance()->getLinkRendererFactory(); + $this->nsInfo = $nsInfo ?? MediaWikiServices::getInstance()->getNamespaceInfo(); $this->logger = $logger ?: new NullLogger(); } diff --git a/includes/registration/ExtensionRegistry.php b/includes/registration/ExtensionRegistry.php index 9cae73c907..3e65f6c1ba 100644 --- a/includes/registration/ExtensionRegistry.php +++ b/includes/registration/ExtensionRegistry.php @@ -146,7 +146,7 @@ class ExtensionRegistry { * be loaded then). */ public function loadFromQueue() { - global $wgVersion, $wgDevelopmentWarnings; + global $wgVersion, $wgDevelopmentWarnings, $wgObjectCaches; if ( !$this->queued ) { return; } @@ -169,10 +169,9 @@ class ExtensionRegistry { // We use a try/catch because we don't want to fail here // if $wgObjectCaches is not configured properly for APC setup try { - // Don't use MediaWikiServices here to prevent instantiating it before extensions have - // been loaded + // Avoid MediaWikiServices to prevent instantiating it before extensions have loaded $cacheId = ObjectCache::detectLocalServerCache(); - $cache = ObjectCache::newFromId( $cacheId ); + $cache = ObjectCache::newFromParams( $wgObjectCaches[$cacheId] ); } catch ( InvalidArgumentException $e ) { $cache = new EmptyBagOStuff(); } diff --git a/includes/resourceloader/ResourceLoaderFileModule.php b/includes/resourceloader/ResourceLoaderFileModule.php index eed2aeda80..d308d5038a 100644 --- a/includes/resourceloader/ResourceLoaderFileModule.php +++ b/includes/resourceloader/ResourceLoaderFileModule.php @@ -1015,7 +1015,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { * Keeps track of all used files and adds them to localFileRefs. * * @since 1.22 - * @since 1.27 Added $context paramter. + * @since 1.27 Added $context parameter. * @throws Exception If less.php encounters a parse error * @param string $fileName File path of LESS source * @param ResourceLoaderContext $context Context in which to generate script diff --git a/includes/skins/BaseTemplate.php b/includes/skins/BaseTemplate.php index cd79259e5e..cad69a594e 100644 --- a/includes/skins/BaseTemplate.php +++ b/includes/skins/BaseTemplate.php @@ -466,6 +466,10 @@ abstract class BaseTemplate extends QuickTemplate { * @return string */ function makeListItem( $key, $item, $options = [] ) { + // In case this is still set from SkinTemplate, we don't want it to appear in + // the HTML output (normally removed in SkinTemplate::buildContentActionUrls()) + unset( $item['redundant'] ); + if ( isset( $item['links'] ) ) { $links = []; foreach ( $item['links'] as $linkKey => $link ) { diff --git a/includes/specialpage/ChangesListSpecialPage.php b/includes/specialpage/ChangesListSpecialPage.php index bbbd6a8585..2fa8fab647 100644 --- a/includes/specialpage/ChangesListSpecialPage.php +++ b/includes/specialpage/ChangesListSpecialPage.php @@ -1503,6 +1503,8 @@ abstract class ChangesListSpecialPage extends SpecialPage { if ( $opts[ 'namespace' ] !== '' ) { $namespaces = explode( ';', $opts[ 'namespace' ] ); + $namespaces = $this->expandSymbolicNamespaceFilters( $namespaces ); + if ( $opts[ 'associated' ] ) { $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo(); $associatedNamespaces = array_map( @@ -1948,4 +1950,21 @@ abstract class ChangesListSpecialPage extends SpecialPage { public function getDefaultDays() { return floatval( $this->getUser()->getOption( static::$daysPreferenceName ) ); } + + private function expandSymbolicNamespaceFilters( array $namespaces ) { + $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo(); + $symbolicFilters = [ + 'all-contents' => $nsInfo->getSubjectNamespaces(), + 'all-discussions' => $nsInfo->getTalkNamespaces(), + ]; + $additionalNamespaces = []; + foreach ( $symbolicFilters as $name => $values ) { + if ( in_array( $name, $namespaces ) ) { + $additionalNamespaces = array_merge( $additionalNamespaces, $values ); + } + } + $namespaces = array_diff( $namespaces, array_keys( $symbolicFilters ) ); + $namespaces = array_merge( $namespaces, $additionalNamespaces ); + return array_unique( $namespaces ); + } } diff --git a/includes/specials/SpecialApiSandbox.php b/includes/specials/SpecialApiSandbox.php index 034e569e6c..9e496845f8 100644 --- a/includes/specials/SpecialApiSandbox.php +++ b/includes/specials/SpecialApiSandbox.php @@ -38,6 +38,7 @@ class SpecialApiSandbox extends SpecialPage { $out->addJsConfigVars( 'apihighlimits', $this->getUser()->isAllowed( 'apihighlimits' ) ); $out->addModuleStyles( [ 'mediawiki.special', + 'mediawiki.hlist', ] ); $out->addModules( [ 'mediawiki.special.apisandbox', diff --git a/includes/specials/SpecialJavaScriptTest.php b/includes/specials/SpecialJavaScriptTest.php index 8c137aa175..50a909f5e5 100644 --- a/includes/specials/SpecialJavaScriptTest.php +++ b/includes/specials/SpecialJavaScriptTest.php @@ -111,7 +111,7 @@ class SpecialJavaScriptTest extends SpecialPage { $qunitConfig = 'QUnit.config.autostart = false;' . 'if (window.__karma__) {' // karma-qunit's use of autostart=false and QUnit.start conflicts with ours. - // Hack around this by replacing 'karma.loaded' with a no-op and perfom its duty of calling + // Hack around this by replacing 'karma.loaded' with a no-op and perform its duty of calling // `__karma__.start()` ourselves. See . . 'window.__karma__.loaded = function () {};' . '}'; diff --git a/includes/specials/SpecialMovepage.php b/includes/specials/SpecialMovepage.php index 161b41ad0a..da34d81af9 100644 --- a/includes/specials/SpecialMovepage.php +++ b/includes/specials/SpecialMovepage.php @@ -419,9 +419,7 @@ class MovePageForm extends UnlistedSpecialPage { 'name' => 'wpMovesubpages', 'id' => 'wpMovesubpages', 'value' => '1', - # Don't check the box if we only have talk subpages to - # move and we aren't moving the talk page. - 'selected' => $this->moveSubpages && ( $this->oldTitle->hasSubpages() || $this->moveTalk ), + 'selected' => true, // T222953 Always check the box ] ), [ 'label' => new OOUI\HtmlSnippet( diff --git a/includes/title/MediaWikiTitleCodec.php b/includes/title/MediaWikiTitleCodec.php index 7e7d85a38e..3bd66d40c6 100644 --- a/includes/title/MediaWikiTitleCodec.php +++ b/includes/title/MediaWikiTitleCodec.php @@ -62,7 +62,8 @@ class MediaWikiTitleCodec implements TitleFormatter, TitleParser { protected $nsInfo; /** - * @param Language $language The language object to use for localizing namespace names. + * @param Language $language The language object to use for localizing namespace names, + * capitalization, etc. * @param GenderCache $genderCache The gender cache for generating gendered namespace names * @param string[]|string $localInterwikis * @param InterwikiLookup|null $interwikiLookup @@ -467,8 +468,8 @@ class MediaWikiTitleCodec implements TitleFormatter, TitleParser { # and [[Foo]] point to the same place. Don't force it for interwikis, since the # other site might be case-sensitive. $parts['user_case_dbkey'] = $dbkey; - if ( $parts['interwiki'] === '' ) { - $dbkey = Title::capitalize( $dbkey, $parts['namespace'] ); + if ( $parts['interwiki'] === '' && $this->nsInfo->isCapitalized( $parts['namespace'] ) ) { + $dbkey = $this->language->ucfirst( $dbkey ); } # Can't make a link to a namespace alone... "empty" local links can only be diff --git a/languages/i18n/ar.json b/languages/i18n/ar.json index 8e3fd792ea..60f54e23bb 100644 --- a/languages/i18n/ar.json +++ b/languages/i18n/ar.json @@ -2737,6 +2737,7 @@ "move-subpages": "انقل الصفحات الفرعية (حتى $1)", "move-talk-subpages": "انقل الصفحات الفرعية لصفحة النقاش (حتى $1)", "movepage-page-exists": "الصفحة $1 موجودة بالفعل ولا يمكن الكتابة عليها تلقائياً.", + "movepage-source-doesnt-exist": "الصفحة $1 غير موجودة ولا يمكن نقلها.", "movepage-page-moved": "نقلت صفحة $1 إلى $2 بنجاح.", "movepage-page-unmoved": "لم يمكن نقل صفحة $1 إلى $2.", "movepage-max-pages": "تم نقل الحد الأقصى وهو {{PLURAL:$1|صفحة واحدة|صفحتان|$1 صفحات|$1 صفحة}} ولن يتم نقل المزيد تلقائيا.", @@ -2753,10 +2754,12 @@ "delete_and_move_reason": "حُذِفت لإفساح مجال لنقل \"[[$1]]\"", "selfmove": "العنوان هو نفسه؛\nلا يمكن نقل صفحة على نفسها.", "immobile-source-namespace": "غير قادر على نقل الصفحات في النطاق \"$1\"", + "immobile-source-namespace-iw": "لا يمكن نقل الصفحات على الويكيات الأخرى من هذه الويكي.", "immobile-target-namespace": "غير قادر على نقل الصفحات إلى النطاق \"$1\"", "immobile-target-namespace-iw": "وصلة الإنترويكي ليست هدفاً صالحاً لنقل صفحة.", "immobile-source-page": "هذه الصفحة غير قابلة للنقل.", "immobile-target-page": "غير قادر على النقل إلى العنوان الوجهة هذا.", + "movepage-invalid-target-title": "الاسم المطلوب غير صحيح.", "bad-target-model": "الوجهة المطلوبة تستخدم نموذج محتوى مختلف. لا يمكن تحويل من $1 إلى $2.", "imagenocrossnamespace": "لا يمكن نقل الملف إلى نطاق غير نطاق الملفات", "nonfile-cannot-move-to-file": "لا يمكن نقل غير الملفات إلى نطاق الملفات", diff --git a/languages/i18n/da.json b/languages/i18n/da.json index 0d88510385..10d170905a 100644 --- a/languages/i18n/da.json +++ b/languages/i18n/da.json @@ -720,6 +720,7 @@ "autoblockedtext": "Din IP-adresse er blevet blokeret automatisk fordi den blev brugt af en anden bruger som er blevet blokeret af $1.\nDen givne begrundelse er:\n\n:$2\n\n* Blokeringen starter: $8\n* Blokeringen udløber: $6\n* Blokeringen er rettet mod: $7\n\nDu kan kontakte $1 eller en af de andre [[{{MediaWiki:Grouppage-sysop}}|administratorer]] for at diskutere blokeringen.\n\nBemærk at du ikke kan bruge funktionen \"{{int:emailuser}}\" medmindre du har en gyldig e-mailadresse registreret i dine [[Special:Preferences|brugerindstillinger]] og du ikke er blevet blokeret fra at bruge den.\n\nDin nuværende IP-adresse er $3, og blokerings-id'et er #$5.\nAngiv venligst alle de ovenstående detaljer ved eventuelle henvendelser.", "systemblockedtext": "Dit brugernavn eller din IP-adresse er automatisk blokeret af MediaWiki.\nBegrundelsen for det er:\n\n:$2\n\n* Blokeringsperiodens start: $8\n* Blokeringen udløber: $6\n* Blokeringen er ment for: $7\n\nDin nuværende IP-adresse er $3.\nAngiv venligst alle de ovenstående detaljer ved eventuelle henvendelser.", "blockednoreason": "ingen begrundelse givet", + "blockedtext-composite-no-ids": "Din IP-adresse findes i flere sortlister", "whitelistedittext": "Du skal $1 for at kunne redigere sider.", "confirmedittext": "Du skal bekræfte din e-mailadresse, før du kan redigere sider. Udfyld og bekræft din e-mailadresse i dine [[Special:Preferences|bruger indstillinger]].", "nosuchsectiontitle": "Kan ikke finde afsnittet", @@ -2514,6 +2515,7 @@ "blocklink": "blokér", "unblocklink": "ophæv blokering", "change-blocklink": "ændring af blokering", + "empty-username": "(intet tilgængeligt brugernavn)", "contribslink": "bidrag", "emaillink": "send e-mail", "autoblocker": "Du er automatisk blokeret, fordi din IP-adresse for nylig er blevet brugt af \"[[User:$1|$1]]\".\nBegrundelsen for blokeringen af $1 er \"$2\".", @@ -2617,6 +2619,7 @@ "immobile-target-namespace-iw": "En side kan ikke flyttes til en interwiki-henvisning.", "immobile-source-page": "Denne side kan ikke flyttes.", "immobile-target-page": "Kan ikke flytte til det navn.", + "movepage-invalid-target-title": "Det ønskede navn er ugyldigt.", "bad-target-model": "Den ønskede destination bruger en anden indholdsmodel. Kan ikke konvertere fra $1 til $2.", "imagenocrossnamespace": "Filer kan ikke flyttes til et navnerum der ikke indeholder filer", "nonfile-cannot-move-to-file": "Kan ikke flytte ikke-filer til fil-navnerummet", @@ -3225,6 +3228,8 @@ "permanentlink": "Permanent link", "permanentlink-revid": "Versions-ID", "permanentlink-submit": "Gå til version", + "newsection": "Nyt afsnit", + "newsection-submit": "Gå til side", "dberr-problems": "Undskyld! Siden har tekniske problemer.", "dberr-again": "Prøv at vente et par minutter og opdater så siden igen.", "dberr-info": "(Kan ikke tilgå databasen: $1)", @@ -3428,6 +3433,7 @@ "mw-widgets-abandonedit-discard": "Kasser redigeringer", "mw-widgets-abandonedit-keep": "Fortsæt med at redigere", "mw-widgets-abandonedit-title": "Er du sikker?", + "mw-widgets-copytextlayout-copy": "Kopiér", "mw-widgets-dateinput-no-date": "Ingen dato valgt", "mw-widgets-dateinput-placeholder-day": "ÅÅÅÅ-MM-DD", "mw-widgets-dateinput-placeholder-month": "ÅÅÅÅ-MM", diff --git a/languages/i18n/en.json b/languages/i18n/en.json index 7bcda5bd41..8988419bcb 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -1584,6 +1584,8 @@ "rcfilters-filter-showlinkedto-label": "Show changes on pages linking to", "rcfilters-filter-showlinkedto-option-label": "Pages linking to the selected page", "rcfilters-target-page-placeholder": "Enter a page name (or category)", + "rcfilters-allcontents-label": "All contents", + "rcfilters-alldiscussions-label": "All discussions", "rcnotefrom": "Below {{PLURAL:$5|is the change|are the changes}} since $3, $4 (up to $1 shown).", "rclistfromreset": "Reset date selection", "rclistfrom": "Show new changes starting from $2, $3", @@ -2817,6 +2819,7 @@ "move-subpages": "Move subpages (up to $1)", "move-talk-subpages": "Move subpages of talk page (up to $1)", "movepage-page-exists": "The page $1 already exists and cannot be automatically overwritten.", + "movepage-source-doesnt-exist": "The page $1 doesn't exist and cannot be moved.", "movepage-page-moved": "The page $1 has been moved to $2.", "movepage-page-unmoved": "The page $1 could not be moved to $2.", "movepage-max-pages": "The maximum of $1 {{PLURAL:$1|page|pages}} has been moved and no more will be moved automatically.", @@ -2835,10 +2838,12 @@ "delete_and_move_reason": "Deleted to make way for move from \"[[$1]]\"", "selfmove": "The title is the same;\ncannot move a page over itself.", "immobile-source-namespace": "Cannot move pages in namespace \"$1\".", + "immobile-source-namespace-iw": "Pages on other wikis cannot be moved from this wiki.", "immobile-target-namespace": "Cannot move pages into namespace \"$1\".", "immobile-target-namespace-iw": "Interwiki link is not a valid target for page move.", "immobile-source-page": "This page is not movable.", "immobile-target-page": "Cannot move to that destination title.", + "movepage-invalid-target-title": "The requested name is invalid.", "bad-target-model": "The desired destination uses a different content model. Cannot convert from $1 to $2.", "imagenocrossnamespace": "Cannot move file to non-file namespace.", "nonfile-cannot-move-to-file": "Cannot move non-file to file namespace.", diff --git a/languages/i18n/fr.json b/languages/i18n/fr.json index 52ed8af213..27ac340998 100644 --- a/languages/i18n/fr.json +++ b/languages/i18n/fr.json @@ -2852,6 +2852,7 @@ "move-subpages": "Renommer les sous-pages (maximum $1)", "move-talk-subpages": "Renommer les sous-pages de la page de discussion (maximum $1)", "movepage-page-exists": "La page $1 existe déjà et ne peut pas être écrasée automatiquement.", + "movepage-source-doesnt-exist": "La page $1 n’existe pas et n’a pas pu être supprimée.", "movepage-page-moved": "La page $1 a été renommée en $2.", "movepage-page-unmoved": "La page $1 n'a pas pu être renommée en $2.", "movepage-max-pages": "Le maximum de $1 {{PLURAL:$1|page renommée|pages renommées}} a été atteint et aucune autre page ne sera renommée automatiquement.", @@ -2868,10 +2869,12 @@ "delete_and_move_reason": "Page supprimée pour permettre le renommage depuis « [[$1]] »", "selfmove": "Le titre est le même ;\nimpossible de renommer une page sur elle-même.", "immobile-source-namespace": "Vous ne pouvez pas renommer les pages dans l'espace de noms « $1 »", + "immobile-source-namespace-iw": "Les pages sur d’autres wikis ne peuvent être déplacées depuis ce wiki.", "immobile-target-namespace": "Vous ne pouvez pas renommer des pages vers l’espace de noms « $1 ».", "immobile-target-namespace-iw": "Un lien interwiki n’est pas une cible valide pour un renommage de page.", "immobile-source-page": "Cette page n'est pas renommable.", "immobile-target-page": "Il n'est pas possible de renommer la page vers ce titre.", + "movepage-invalid-target-title": "Le nom demandé n’est pas valide.", "bad-target-model": "La destination souhaitée utilise un autre modèle de contenu. Impossible de convertir de $1 vers $2.", "imagenocrossnamespace": "Impossible de renommer un fichier vers un espace de noms autre que fichier.", "nonfile-cannot-move-to-file": "Impossible de renommer quelque chose d'autre qu’un fichier vers l’espace de noms fichier.", diff --git a/languages/i18n/ja.json b/languages/i18n/ja.json index c2e544f3a3..65f2f8ea61 100644 --- a/languages/i18n/ja.json +++ b/languages/i18n/ja.json @@ -4050,8 +4050,10 @@ "specialmute-label-mute-email": "この利用者からのウィキメールをミュートする", "specialmute-header": "{{BIDI:[[User:$1|$1]]}}さんに対するミュートを個人設定で選択してください。", "specialmute-error-invalid-user": "あなたが要求した利用者名は見つかりませんでした。", + "specialmute-error-no-options": "ミュート機能はご使用になれません。いくつかの理由が考えられます: メールアドレスをまだ確認されていないか、このウィキの管理者がメールの機能および/もしくはこのウィキのメールのブラックリストを無効にしているか、です。", "specialmute-email-footer": "{{BIDI:$2}}のメール発信者の個人設定を変更するには<$1>を開いてください。", "specialmute-login-required": "ミュートの個人設定を変更するにはログインしてください。", + "mute-preferences": "ミュート設定", "revid": "版 $1", "pageid": "ページID $1", "interfaceadmin-info": "$1\n\nサイト全体のCSS/JavaScriptの編集権限は、最近editinterface 権限から分離されました。なぜこのエラーが表示されたのかわからない場合は、[[mw:MediaWiki_1.32/interface-admin]]をご覧ください。", diff --git a/languages/i18n/jv.json b/languages/i18n/jv.json index 8bf1a308eb..4b23cf273a 100644 --- a/languages/i18n/jv.json +++ b/languages/i18n/jv.json @@ -1288,7 +1288,7 @@ "rcfilters-savedqueries-already-saved": "Saringan iki wis kasimpen. Ganti setèlané panjenengan saperlu nggawé Saringan Kasimpen kang anyar.", "rcfilters-restore-default-filters": "Pulihaké saringan gawan", "rcfilters-clear-all-filters": "Resiki kabèh saringan", - "rcfilters-show-new-changes": "Deleng owah-owahan anyar dhéwé", + "rcfilters-show-new-changes": "Deleng owah-owahan anyar kawit $1", "rcfilters-search-placeholder": "Owah-owahan saringan (anggo menu utawa golèk jeneng saringan)", "rcfilters-invalid-filter": "Saringan ora sah", "rcfilters-empty-filter": "Ora ana saringan kang aktif. Kabèh sumbangan katuduhaké.", diff --git a/languages/i18n/lv.json b/languages/i18n/lv.json index db50961883..a427820cbc 100644 --- a/languages/i18n/lv.json +++ b/languages/i18n/lv.json @@ -35,7 +35,7 @@ "tog-hideminor": "Paslēpt maznozīmīgus labojumus pēdējo izmaiņu lapā", "tog-hidepatrolled": "Slēpt apstiprinātās izmaņas pēdējo izmaiņu sarakstā", "tog-newpageshidepatrolled": "Paslēpt pārbaudītās lapas jauno lapu sarakstā", - "tog-hidecategorization": "Paslēpt lapu kategorizēšanu", + "tog-hidecategorization": "Slēpt lapu kategorizēšanu", "tog-extendwatchlist": "Izvērst uzraugāmo lapu sarakstu, lai parādītu visas veiktās izmaiņas (ne tikai pašas svaigākās)", "tog-usenewrc": "Grupēt izmaiņas pēc lapas pēdējās izmaiņās un uzraugāmo lapu sarakstā", "tog-numberheadings": "Automātiski numurēt virsrakstus", @@ -1397,7 +1397,7 @@ "uploadinvalidxml": "Nevarēja apstrādāt augšupielādētā faila XML saturu.", "uploadvirus": "Šis fails satur vīrusu! Sīkāk: $1", "uploadjava": "Fails ir ZIP fails, kas satur Java .class failu.\nJava failu augšupielāde nav atļauta, jo tas var radīt iespējas apiet drošības ierobežojumus.", - "upload-source": "Augšuplādējamais fails", + "upload-source": "Augšupielādējamais fails", "sourcefilename": "Faila adrese:", "sourceurl": "Avota URL:", "destfilename": "Mērķa faila nosaukums:", @@ -2221,7 +2221,7 @@ "export-download": "Saglabāt kā failu", "export-templates": "Iekļaut veidnes", "export-manual": "Pievienot lapas manuāli:", - "allmessages": "Visi sistēmas paziņojumi", + "allmessages": "Sistēmas ziņojumi", "allmessagesname": "Nosaukums", "allmessagesdefault": "Noklusētais ziņojuma teksts", "allmessagescurrent": "Pašreizējais teksts", diff --git a/languages/i18n/min.json b/languages/i18n/min.json index 1272347c11..110841f477 100644 --- a/languages/i18n/min.json +++ b/languages/i18n/min.json @@ -383,7 +383,7 @@ "virus-badscanner": "Kasalahan konfigurasi: pamindai virus indak dikenal: ''$1''", "virus-scanfailed": "Pamindaian gagal (kode $1)", "virus-unknownscanner": "Antivirus indak dikenal:", - "logouttext": "Sanak alah kalua logSanak alah kalua log\n\nMohon diingek kalau babarapo laman mungkin masih tampil cando Sanak alun kalua log. Silakan untuak mambarasiahan singgahan panjalajah web Sanak.", "cannotlogoutnow-title": "Indak bisa kalua kini", "cannotlogoutnow-text": "Indak bisa kalua katiko manggunoan $1.", "welcomeuser": "Salamaik datang, $1!", diff --git a/languages/i18n/nap.json b/languages/i18n/nap.json index 96e06faa2c..c75710fefd 100644 --- a/languages/i18n/nap.json +++ b/languages/i18n/nap.json @@ -2590,7 +2590,7 @@ "importuploaderrortemp": "'A carreca d' 'o file mpurtato nun se facette.\nNa cartella temporanea nun se truova.", "import-parse-failure": "mpurtaziune XML scassata pe' n'errore d'analiso", "import-noarticle": "Nisciuna paggena 'a mpurtà!", - "import-nonewrevisions": "Nisciuna verziona mpurtata (Tutt' 'e verziune so' state già mpurtate o pure zumpajeno pe' bbia 'e cocch'errore).", + "import-nonewrevisions": "Nisciuna verziona mpurtata (Tutt' 'e verziune so' state mpurtate già o zumpajeno pe bbia 'e cocch'errore).", "xml-error-string": "$1 a 'a linea $2, culonne $3 (byte $4): $5", "import-upload": "Carreca 'e date 'e XML", "import-token-mismatch": "Se so' perdut' 'e date d' 'a sessione.\n\nPuò darse ca site asciuto/a. Pe' piacere cuntrullate si site ancora dinto e tentate n'ata vota.\n\nSi chesto nun funziunasse ancora, tentate 'e ve n'[[Special:UserLogout|ascì]] e trasì n'ata vota dinto, cuntrullate si 'o navigatore vuosto premmettesse 'e cookies 'e stu sito.", @@ -2635,7 +2635,7 @@ "tooltip-ca-move": "Mòve sta paggena", "tooltip-ca-watch": "Azzecca sta paggena int' 'a lista 'e paggene cuntrullate vuosta", "tooltip-ca-unwatch": "Lèva sta paggena d' 'a lista 'e paggene cuntrullate vuosta", - "tooltip-search": "Truova dint'ô {{SITENAME}}", + "tooltip-search": "Truova dint'a {{SITENAME}}", "tooltip-search-go": "Vaje â paggena cu stu nomme si esiste", "tooltip-search-fulltext": "Ascià 'o testo indicato dint'e paggene", "tooltip-p-logo": "Visita a paggena prencepale", @@ -3231,7 +3231,7 @@ "feedback-thanks": "Grazie! 'O feedback vuosto s'è mpizzato dint' 'a paggena \"[$2 $1]\".", "feedback-thanks-title": "Ve ringraziammo!", "feedback-useragent": "Aggente utente:", - "searchsuggest-search": "Truova dint'ô {{SITENAME}}", + "searchsuggest-search": "Truova dint'a {{SITENAME}}", "searchsuggest-containing": "tène...", "api-error-badtoken": "Errore interno: 'O token nun è buono.", "api-error-emptypage": "'A criazione 'e paggene nuove abbacante nun è permessa.", diff --git a/languages/i18n/nds.json b/languages/i18n/nds.json index 192a8526d9..d3983c8ae2 100644 --- a/languages/i18n/nds.json +++ b/languages/i18n/nds.json @@ -892,6 +892,7 @@ "recentchanges-label-unpatrolled": "Düsse Ännern is noch nich kontrolleert worrn", "recentchanges-label-plusminus": "Disse Siedengrött is mit dit Antall Bytes ännert", "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}}
(süh ok de [[Special:NewPages|List mit ne'e Sieden]])", + "rcfilters-search-placeholder-mobile": "Filters", "rcnotefrom": "Dit sünd de Ännern siet $2 (bet to $1 wiest).", "rclistfrom": "Wies ne’e Ännern siet $3 $2", "rcshowhideminor": "lütte Ännern $1", diff --git a/languages/i18n/nqo.json b/languages/i18n/nqo.json index 6904c4c221..92fb9bb0a2 100644 --- a/languages/i18n/nqo.json +++ b/languages/i18n/nqo.json @@ -2295,6 +2295,10 @@ "metadata-fields": "ߟߐ߲ߕߊߞߐ߫ ߖߌ߬ߦߊ߬ߓߍ ߞߣߍ ߡߍ߲ ߦߋ߫ ߗߋߛߓߍ ߣߌ߲߬ ߘߐ߫߸ ߏ߬ ߘߌ߫ ߣߊ߬ ߥߟߏ߫ ߖߌ߬ߦߊ߬ߓߍ ߞߐߜߍ ߘߐ߫ ߣߌ߫ ߟߐ߲ߕߊߞߐ߫ ߥߟߊ߬ߟߋ߲ ߠߊߘߐ߯ߦߊ߫ ߘߊ߫. ߊ߬ ߕߐ߭ ߟߎ߬ ߢߡߊߘߏ߲߰ߣߍ߲ ߘߌ߫ ߕߏ߫ ߝߍ߭ ߞߏߛߐ߲߬.\n•ߊ߬ ߞߍ߫ \n•ߛߎ߯ߦߊ \n•ߕߎ߬ߡߊ߬ߘߊ ߣߌ߫ ߕߎ߬ߡߊ߬ߙߋ߲߫ ߓߐߛߎ߲ߡߊ \n•ߟߊ߬ߝߏߦߌ ߕߎ߬ߡߊ߬ߘߊ߬ ߖߐ߲ߖߐ߲ \n•ߞ ߝߙߍߕߍ \n•ߡ.ߛ.ߛ ߞߊߟߌߦߊ ߡߐ߬ߟߐ߲߬ߦߊ߬ߟߌ \n•ߕߊߞߎ߲ߡߊ ߥߊ߲߬ߥߊ߲ \n•ߞߎ߬ߛߊ߲ \n•ߓߊߦߟߍߡߊ߲ ߤߊߞߍ ߘߞߖ \n•ߖߌ߬ߦߊ߬ߓߍ ߞߊ߲߬ߛߓߍ\n•ߘߟߊߕߍ߮ ߘߞߖ (ߘߊ߲߬ߠߊ߬ߕߍ߰ ߞߊ߲ߞߋ߫ ߖߊ߯ߓߡߊ)\n•ߘߎ߰ߕߍߟߍ߲ ߘߞߖ (ߘߊ߲߬ߠߊ߬ߕߍ߰ ߞߊ߲ߞߋ߫ ߖߊ߯ߓߡߊ)\n•ߞߐߓߋ ߘߞߖ (ߘߊ߲߬ߠߊ߬ߕߍ߰ ߞߊ߲ߞߋ߫ ߖߊ߯ߓߡߊ)", "namespacesall": "ߊ߬ ߓߍ߯", "monthsall": "ߡߎ߰ߡߍ", + "scarytranscludetoolong": "[URL ߖߊ߰ߡߊ߲߬ ߞߏߖߎ߰]", + "deletedwhileediting": "ߖߊ߲߬ߓߌ߬ߟߊ߬ߟߌ ߞߐߜߍ ߣߌ߲߬ ߕߎ߲߬ ߓߘߊ߫ ߖߏ߰ߛߌ߫ ߊ߬ ߡߊߦߟߍ߬ߡߊ߲ ߘߊߡߌ߬ߣߊ ߞߐ߫ ߌ ߓߟߏ߫.", + "recreate": "ߊ߬ ߟߊߘߊ߲߫ ߕߎ߲߯", + "confirm_purge_button": "ߏ߬ߞߍ߫", "parentheses-start": "⸜", "parentheses-end": "⸝", "imgmultipagenext": "ߞߐߜߍ ߣߊ߬ߕߐ ←", diff --git a/languages/i18n/pt-br.json b/languages/i18n/pt-br.json index d7167ba215..f6491eb2c8 100644 --- a/languages/i18n/pt-br.json +++ b/languages/i18n/pt-br.json @@ -2803,6 +2803,7 @@ "move-subpages": "Mover subpáginas (até $1)", "move-talk-subpages": "Mover subpáginas da página de discussão (até $1)", "movepage-page-exists": "A página $1 já existe e não pode ser substituída.", + "movepage-source-doesnt-exist": "A página $1 não existe e não pode ser movida.", "movepage-page-moved": "A página $1 foi movida para $2", "movepage-page-unmoved": "A página $1 não pôde ser movida para $2.", "movepage-max-pages": "O limite de $1 {{PLURAL:$1|página movida|páginas movidas}} foi atingido; não será possível mover mais páginas de forma automática.", @@ -2819,10 +2820,12 @@ "delete_and_move_reason": "Eliminada para mover \"[[$1]]\"", "selfmove": "O título fonte e o título destinatário são os mesmos; não é possível mover uma página para ela mesma.", "immobile-source-namespace": "Não é possível mover páginas no espaço nominal \"$1\"", + "immobile-source-namespace-iw": "Páginas em outras wikis não podem ser movidas dessa wiki.", "immobile-target-namespace": "Não é possível mover páginas para o espaço nominal \"$1\"", "immobile-target-namespace-iw": "Um link interwiki não é um destino válido para movimentação de página.", "immobile-source-page": "Esta página não pode ser movida.", "immobile-target-page": "Não é possível mover para esse título de destino.", + "movepage-invalid-target-title": "O nome solicitado é inválido.", "bad-target-model": "O destino especificado usa um modelo de conteúdo diferente. Não é possível converter $1 para $2.", "imagenocrossnamespace": "Não é possível mover imagem para espaço nominal que não de imagens", "nonfile-cannot-move-to-file": "Não é possível mover não arquivos para espaço nominal de arquivos", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index a2ee29a66e..86933e3a39 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -1793,6 +1793,8 @@ "rcfilters-filter-showlinkedto-label": "Label that indicates that the page is showing changes that link TO the target page. Used on [[Special:Recentchangeslinked]] when structured filters are enabled.", "rcfilters-filter-showlinkedto-option-label": "Menu option to show changes TO the target page. Used on [[Special:Recentchangeslinked]] when structured filters are enabled.", "rcfilters-target-page-placeholder": "Placeholder text for the title lookup [[Special:Recentchangeslinked]] when structured filters are enabled.", + "rcfilters-allcontents-label": "Label of the filter for all content namespaces on [[Special:Recentchanges]] or [[Special:Watchlist]] when structured filters are enabled.", + "rcfilters-alldiscussions-label": "Label of the filter for all discussion namespaces on [[Special:Recentchanges]] or [[Special:Watchlist]] when structured filters are enabled.", "rcnotefrom": "This message is displayed at [[Special:RecentChanges]] when viewing recentchanges from some specific time.\n\nThe corresponding message is {{msg-mw|Rclistfrom}}.\n\nParameters:\n* $1 - the maximum number of changes that are displayed\n* $2 - (Optional) a date and time\n* $3 - a date\n* $4 - a time\n* $5 - Number of changes are displayed, for use with PLURAL", "rclistfromreset": "Used on [[Special:RecentChanges]] to reset a selection of a certain date range.", "rclistfrom": "Used on [[Special:RecentChanges]]. Parameters:\n* $1 - (Currently not use) date and time. The date and the time adds to the rclistfrom description.\n* $2 - time. The time adds to the rclistfrom link description (with split of date and time).\n* $3 - date. The date adds to the rclistfrom link description (with split of date and time).\n\nThe corresponding message is {{msg-mw|Rcnotefrom}}.", @@ -3026,6 +3028,7 @@ "move-subpages": "The text of an option on the special page [[Special:MovePage|MovePage]]. If this option is ticked, any subpages will be moved with the main page to a new title.\n\nParameters:\n* $1 - ...\nSee also:\n* {{msg-mw|Move-page-legend|legend for the form}}\n* {{msg-mw|newtitle|label for new title}}\n* {{msg-mw|Movereason|label for textarea}}\n* {{msg-mw|Movetalk|label for checkbox}}\n* {{msg-mw|Move-leave-redirect|label for checkbox}}\n* {{msg-mw|Fix-double-redirects|label for checkbox}}\n* {{msg-mw|Move-talk-subpages|label for checkbox}}\n* {{msg-mw|Move-watch|label for checkbox}}", "move-talk-subpages": "The text of an option on the special page [[Special:MovePage|MovePage]]. If this option is ticked, any talk subpages will be moved with the talk page to a new title.\n\nParameters:\n* $1 - ...\nSee also:\n* {{msg-mw|Move-page-legend|legend for the form}}\n* {{msg-mw|newtitle|label for new title}}\n* {{msg-mw|Movereason|label for textarea}}\n* {{msg-mw|Movetalk|label for checkbox}}\n* {{msg-mw|Move-leave-redirect|label for checkbox}}\n* {{msg-mw|Fix-double-redirects|label for checkbox}}\n* {{msg-mw|Move-subpages|label for checkbox}}\n* {{msg-mw|Move-watch|label for checkbox}}", "movepage-page-exists": "Used as error message when moving page.\n* $1 - page title", + "movepage-source-doesnt-exist": "Used as error message when trying to move a page that doesn't exist.\n* $1 - page title", "movepage-page-moved": "Used as success message when moving page.\n\nCan be followed by {{msg-mw|Movepage-max-pages}}.\n\nParameters:\n* $1 - old page title (with link)\n* $2 - new page title (with link)\nSee also:\n* {{msg-mw|Movepage-page-unmoved}}", "movepage-page-unmoved": "Used as error message when moving page. Parameters:\n* $1 - old page title (with link)\n* $2 - new page title (with link)\nSee also:\n* {{msg-mw|Movepage-page-moved}}", "movepage-max-pages": "PROBABLY (A GUESS): when moving a page, you can select an option of moving its subpages, but there is a maximum that can be moved automatically.\n\nParameters:\n* $1 - maximum moved pages, defined in the variable [[mw:Special:MyLanguage/Manual:$wgMaximumMovedPages|$wgMaximumMovedPages]]", @@ -3044,10 +3047,12 @@ "delete_and_move_reason": "Shown as reason in content language in the deletion log. Parameter:\n* $1 - The page name for which this page was deleted.", "selfmove": "Used as error message when moving page.\n\nSee also:\n* {{msg-mw|badtitletext}}\n* {{msg-mw|immobile-source-namespace}}\n* {{msg-mw|immobile-target-namespace-iw}}\n* {{msg-mw|immobile-target-namespace}}", "immobile-source-namespace": "Used as error message. Parameters:\n* $1 - source namespace name\nSee also:\n* {{msg-mw|Immobile-source-page}}\n* {{msg-mw|Immobile-target-namespace}}\n* {{msg-mw|Immobile-target-page}}", + "immobile-source-namespace-iw": "Used as error message if somehow something tries to move a page that's on a different wiki.\nSee also:\n* {{msg-mw|Immobile-source-namespace}}\n* {{msg-mw|Immobile-target-namespace-iw}}", "immobile-target-namespace": "Used as error message. Parameters:\n* $1 - destination namespace name\nSee also:\n* {{msg-mw|Immobile-source-namespace}}\n* {{msg-mw|Immobile-source-page}}\n* {{msg-mw|Immobile-target-page}}", "immobile-target-namespace-iw": "This message appears when attempting to move a page, if a person has typed an interwiki link as a namespace prefix in the input box labelled 'To new title'. The special page 'Movepage' cannot be used to move a page to another wiki.\n\n'Destination' can be used instead of 'target' in this message.", "immobile-source-page": "See also:\n* {{msg-mw|Immobile-source-namespace}}\n* {{msg-mw|Immobile-source-page}}\n* {{msg-mw|Immobile-target-namespace}}\n* {{msg-mw|Immobile-target-page}}", "immobile-target-page": "See also:\n* {{msg-mw|Immobile-source-namespace}}\n* {{msg-mw|Immobile-source-page}}\n* {{msg-mw|Immobile-target-namespace}}\n* {{msg-mw|Immobile-target-page}}", + "movepage-invalid-target-title": "Error displayed when trying to move a page to an invalid title, e.g., empty or contains prohibited characters.", "bad-target-model": "This message is shown when attempting to move a page, but the move would change the page's content model.\nThis may be the case when [[mw:Manual:$wgContentHandlerUseDB|$wgContentHandlerUseDB]] is set to false, because then a page's content model is derived from the page's title.\n\nParameters:\n* $1 - The localized name of the original page's content model:\n**{{msg-mw|Content-model-wikitext}}, {{msg-mw|Content-model-javascript}}, {{msg-mw|Content-model-css}} or {{msg-mw|Content-model-text}}\n* $2 - The localized name of the content model used by the destination title:\n**{{msg-mw|Content-model-wikitext}}, {{msg-mw|Content-model-javascript}}, {{msg-mw|Content-model-css}} or {{msg-mw|Content-model-text}}", "imagenocrossnamespace": "Used as error message.\n\nSee also:\n* {{msg-mw|Imagenocrossnamespace}}\n* {{msg-mw|Nonfile-cannot-move-to-file}}", "nonfile-cannot-move-to-file": "Used as error message.\n\nSee also:\n* {{msg-mw|Imagenocrossnamespace}}\n* {{msg-mw|Nonfile-cannot-move-to-file}}", diff --git a/languages/i18n/ro.json b/languages/i18n/ro.json index edb642aac5..add380308b 100644 --- a/languages/i18n/ro.json +++ b/languages/i18n/ro.json @@ -203,7 +203,7 @@ "history": "Istoricul paginii", "history_short": "Istoric", "history_small": "istoric", - "updatedmarker": "actualizat de la ultima mea vizită", + "updatedmarker": "actualizat de la ultima dumneavoastră vizită", "printableversion": "Versiune de tipărit", "permalink": "Legătură permanentă", "print": "Tipărire", @@ -676,6 +676,9 @@ "autoblockedtext": "Această adresă IP a fost blocată automat deoarece a fost folosită de către un alt utilizator, care a fost blocat de $1.\nMotivul blocării este:\n\n:$2\n\n* Începutul blocării: $8\n* Sfârșitul blocării: $6\n* Intervalul blocării: $7\n\nPuteți contacta pe $1 sau pe unul dintre ceilalți [[{{MediaWiki:Grouppage-sysop}}|administratori]] pentru a discuta blocarea.\n\nNu veți putea folosi opțiunea de \"{{int:emailuser}}\" decât dacă aveți înregistrată o adresă de e-mail validă la [[Special:Preferences|preferințe]] și nu sunteți blocat la folosirea ei.\n\nAveți adresa IP $3, iar identificatorul dumneavoastră de blocare este #$5.\nVă rugăm să includeți detaliile de mai sus în orice mesaje pe care le trimiteți.", "systemblockedtext": "Numele de utilizator sau adresa IP a fost blocat automat de MediaWiki.\nMotivul indicat este:\n\n:$2\n\n\n* Începutul blocării: $8\n* Expirarea blocării: $6\n* Utilizatorul vizat: $7\n\nAdresa IP curentă a dumneavoastră este $3.\nVă rugăm să includeți toate detaliile de mai sus în orice interogare pe care o veți faceți.", "blockednoreason": "nici un motiv oferit", + "blockedtext-composite": "Numele dumneavoastră de utilizator sau adresa IP au fost blocate.\nMotivul indicat este:\n\n:$2\n\n\n* Începutul blocării: $8\n* Expirarea celei mai lungi blocări: $6\n\n* $5\n\nAdresa dumneavoastră IP este $3.\nVă rugăm să includeți toate detaliile de mai sus în orice demers pe care îl veți face.", + "blockedtext-composite-no-ids": "Adresa dumneavoastră ip apare în mai multe liste negre", + "blockedtext-composite-reason": "Există mai multe blocări asupra contului sau adresei dumneavoastră IP", "whitelistedittext": "Trebuie să vă $1 pentru a putea modifica pagini.", "confirmedittext": "Trebuie să vă confirmați adresa de e-mail înainte de a edita pagini. Vă rugăm să vă setați și să vă validați adresa de e-mail cu ajutorul [[Special:Preferences|preferințelor utilizatorului]].", "nosuchsectiontitle": "Secțiunea nu poate fi găsită", @@ -1183,7 +1186,7 @@ "group-bureaucrat-member": "{{GENDER:$1|birocrat}}", "group-suppress-member": "{{GENDER:$1|suprimător|suprimătoare}}", "grouppage-user": "{{ns:project}}:Utilizatori", - "grouppage-autoconfirmed": "{{ns:project}}:Utilizator autoconfirmați", + "grouppage-autoconfirmed": "{{ns:project}}:Utilizatori confirmați automat", "grouppage-bot": "{{ns:project}}:Boți", "grouppage-sysop": "{{ns:project}}:Administratori", "grouppage-interface-admin": "{{ns:project}}:Administratori de interfață", @@ -1437,6 +1440,7 @@ "rcfilters-clear-all-filters": "Ștergeți toate filtrele", "rcfilters-show-new-changes": "Arată schimbările mai noi de la $1", "rcfilters-search-placeholder": "Filtrați modificările recente (folosiți meniul sau căutați numele filtrului)", + "rcfilters-search-placeholder-mobile": "Filtre", "rcfilters-invalid-filter": "Filtru invalid", "rcfilters-empty-filter": "Nu există filtre active. Toate contribuțiile sunt afișate.", "rcfilters-filterlist-title": "Filtre", @@ -2336,6 +2340,7 @@ "changecontentmodel": "Modificare model de conținut al unei pagini", "changecontentmodel-legend": "Modifică modelul de conținut", "changecontentmodel-title-label": "Titlul paginii", + "changecontentmodel-current-label": "Modelul de conținut curent:", "changecontentmodel-model-label": "Model de conținut nou", "changecontentmodel-reason-label": "Motiv:", "changecontentmodel-submit": "Schimbă", @@ -2461,6 +2466,7 @@ "contribsub2": "Pentru {{GENDER:$3|$1}} ($2)", "contributions-subtitle": "Pentru {{GENDER:$3|$1}}", "contributions-userdoesnotexist": "Contul de utilizator „$1” nu este înregistrat.", + "negative-namespace-not-supported": "Spațiile de nume cu valori negative nu sunt permise.", "nocontribs": "Nu a fost găsită nici o modificare care să satisfacă acest criteriu.", "uctop": "actuală", "month": "Din luna (și dinainte):", @@ -2896,7 +2902,9 @@ "interlanguage-link-title-nonlang": "$1 – $2", "common.css": "/** CSS plasate aici vor fi aplicate tuturor aparițiilor */", "print.css": "/* CSS plasate aici vor afecta modul în care paginile vor fi imprimate */", + "group-autoconfirmed.css": "/* Orice stil CSS din această pagină va afecta doar utilizatorii autoconfirmați */", "common.json": "/* Orice JSON din această pagină va fi încărcat pentru toți utilizatorii la fiecare pagină încărcată. */", + "group-autoconfirmed.js": "/* Orice cod JavaScript din această pagină va fi încărcat doar pentru utilizatorii autoconfirmați */", "anonymous": "{{PLURAL:$1|Utilizator anonim|Utilizatori anonimi}} ai {{SITENAME}}", "siteuser": "Utilizator {{SITENAME}} $1", "anonuser": "utlizator anonim $1 al {{SITENAME}}", @@ -3381,6 +3389,9 @@ "permanentlink": "Legătură permanentă", "permanentlink-revid": "ID versiune", "permanentlink-submit": "Mergi la versiunea", + "newsection": "Secțiune nouă", + "newsection-page": "Pagină țintă", + "newsection-submit": "Mergi la pagină", "dberr-problems": "Ne cerem scuze! Acest site întâmpină dificultăți tehnice.", "dberr-again": "Așteptați câteva minute și încercați din nou.", "dberr-info": "(Nu se poate accesa baza de date: $1)", @@ -3647,6 +3658,9 @@ "mw-widgets-abandonedit-discard": "Renunță la modificări", "mw-widgets-abandonedit-keep": "Continuă editarea", "mw-widgets-abandonedit-title": "Sunteți sigur(ă)?", + "mw-widgets-copytextlayout-copy": "Copiază", + "mw-widgets-copytextlayout-copy-fail": "Nu am putut copia în clipboard.", + "mw-widgets-copytextlayout-copy-success": "Copiat în clipboard.", "mw-widgets-dateinput-no-date": "Nicio dată selectată", "mw-widgets-dateinput-placeholder-day": "AAAA-LL-ZZ", "mw-widgets-dateinput-placeholder-month": "AAAA-LL", @@ -3758,12 +3772,17 @@ "changecredentials": "Schimbă credențialele", "changecredentials-submit": "Schimbă credențialele", "changecredentials-invalidsubpage": "„$1” nu este un tip de credențiale valid.", + "removecredentials-invalidsubpage": "$1 nu este un tip de credențiale valid.", + "removecredentials-success": "Credențialele dumneavoastră au fost șterse.", + "credentialsform-provider": "Tipuri de credențiale:", "credentialsform-account": "Numele contului:", "cannotlink-no-provider-title": "Nu există conturi conectate", "cannotlink-no-provider": "Nu există conturi conectate.", "linkaccounts": "Conectează conturile", "linkaccounts-success-text": "Contul a fost conectat.", "linkaccounts-submit": "Leagă conturile", + "cannotunlink-no-provider-title": "Nu există conturi ce pot fi deconectate", + "cannotunlink-no-provider": "Nu există conturi ce pot fi deconectate", "unlinkaccounts": "Dezleagă conturile", "unlinkaccounts-success": "Contul a fost dezlegat", "userjsispublic": "Atenție: subpaginile JavaScript nu trebuie să conțină date confidențiale, întrucât ele sunt vizibile altor utilizatori.", @@ -3772,6 +3791,9 @@ "restrictionsfield-help": "O adresă IP sau gamă CIDR pe linie. Pentru a activa tot, folosiți:
0.0.0.0/0\n::/0
", "edit-error-short": "Eroare: $1", "edit-error-long": "Erori:\n\n$1", + "specialmute-submit": "Confirmare", + "specialmute-label-mute-email": "Ascunde e-mailuri de la acest utilizator", + "specialmute-error-invalid-user": "Numele de utilizator solicitat nu a putut fi găsit.", "revid": "versiunea $1", "pageid": "ID pagină $1", "interfaceadmin-info": "$1\n\nPermisiunile pentru editarea de CSS/JS/JSON global au fost recent separate de dreptul editinterface. Dacă nu înțelegeți de ce primiți această eroare, vedeți [[mw:MediaWiki_1.32/interface-admin]].", @@ -3793,5 +3815,9 @@ "passwordpolicies-policy-passwordcannotmatchblacklist": "Parolele nu pot fi cele de pe lista neagră", "passwordpolicies-policy-maximalpasswordlength": "Parola trebuie să aibă cel puțin $1 {{PLURAL:$1|caracter|caractere|de caractere}}.", "passwordpolicies-policy-passwordcannotbepopular": "Parola nu poate fi {{PLURAL:$1|o parolă populară|în lista celor $1 parole populare|în lista celor $1 de parole populare}}.", - "easydeflate-invaliddeflate": "Conținutul oferit nu este comprimat corect" + "passwordpolicies-policy-passwordnotinlargeblacklist": "Parola nu poate fi în lista celor mai comune 100.000 de parole.", + "passwordpolicies-policyflag-forcechange": "trebuie schimbată la conectare", + "passwordpolicies-policyflag-suggestchangeonlogin": "sugerează schimbarea la conectare", + "easydeflate-invaliddeflate": "Conținutul oferit nu este comprimat corect", + "userlogout-continue": "Doriți să vă deconectați?" } diff --git a/languages/i18n/roa-tara.json b/languages/i18n/roa-tara.json index 585a532cdf..c4fa1538f3 100644 --- a/languages/i18n/roa-tara.json +++ b/languages/i18n/roa-tara.json @@ -651,6 +651,7 @@ "blockedtext": "'U nome de l'utende o l'indirizze IP ha state bloccate.\n\n'U blocche ha state fatte da $1.\n'U mutive date jè $2.\n\n* 'U Blocche accumenze: $8\n* 'U Blocche spicce: $6\n* Tipe de blocche: $7\n\nTu puè condatta $1 o n'otre [[{{MediaWiki:Grouppage-sysop}}|amministratore]] pe 'ngazzarte sus a 'u blocche.\nTu non ge puè ausà 'u strumende \"{{int:emailuser}}\" senza ca mitte n'indirizze email valide jndr'à le\n[[Special:Preferences|preferenze tune]] e ce è state bloccate sus a l'use sue.\nL'IP ca tine mò jè $3 e 'u codece d'u blocche jè #$5.\nPe piacere mitte ste doje 'mbormaziune ce manne 'na richieste de sblocche.", "autoblockedtext": "L'indirizze IP tue ha state automaticamende blocchete purcè ha state ausete da n'otre utende, ca avère state blocchete da $1.\n'U mutive date jè 'u seguende:\n\n:''$2''\n\n* Inizie d'u blocche: $8\n* Scadenze d'u blocche: $6\n* Blocche 'ndise: $7\n\nTu puè cundattà $1 o une de l'otre [[{{MediaWiki:Grouppage-sysop}}|amministrature]] pe parà de stu probbleme.\n\nVide Bbuene ca tu non ge puè ausà 'a funziona \"manne n'e-mail a stu utende\" senze ca tu tìne 'n'indirizze e-mail valide e reggistrete jndr'à seziona [[Special:Preferences|me piace accussì]] e tu non ge sinde blocchete da ausarle.\n\nL'indirizze IP corrende jè $3, e 'u codece d'u blocche jè #$5.\nPe piacere mitte tutte le dettaglie ca ponne essere utile pe le richieste tune.", "blockednoreason": "nisciune mutive", + "blockedtext-composite-ids": "ID d'u blocche relevande: $1 ('u 'ndirizze IP tune pò sta pure jndr'à lista gnore)", "blockedtext-composite-no-ids": "'U 'ndirizze IP tune jesse jndr'à 'nu sacche de liste gnore", "blockedtext-composite-reason": "Stonne attive cchiù blocche sus a 'u cunde tune e/o indirizze IP", "whitelistedittext": "Tu ha $1 pàggene da cangià.", @@ -700,6 +701,7 @@ "yourtext": "'U teste tue", "storedversion": "Versione archivijete", "editingold": "'''FA ATTENZIO': Tu ste cange 'na revisione de sta pàgena scadute.'''\nCe tu a reggistre, ogne cangiamende fatte apprisse a sta revisione avène perdute.", + "unicode-support-fail": "Pare ca 'u browser tune non ge supporte l'Unicode. Essenne richieste pe cangià le pàggene, 'u cangiamende tune non g'avène reggistrate.", "yourdiff": "Differenze", "copyrightwarning": "Pe piacere vide ca tutte le condrebbute de {{SITENAME}} sonde considerete de essere rilasciete sotte 'a $2 (vide $1 pe le dettaglie).\nCe tu non ge vuè ca le condrebbute tue avènene ausete da otre o avènene cangete, non le scè mettènne proprie.
\nTu na promettere pure ca le cose ca scrive tu, sonde 'mbormaziune libbere o copiete da 'nu pubbleche dominie.
\n'''NON METTE' NISCIUNA FATJE CA JE' PROTETTE DA DERITTE SENZA PERMESSE!'''", "copyrightwarning2": "Pe piacere vide ca tutte le condrebbute de {{SITENAME}} ponne essere cangete, alterate o luvete da otre condrebbutore.\nCe tu non ge vuè ca quidde ca scrive avène cangete da tre, allore non scè scrivenne proprie aqquà.
\nTu ne stè promitte ca quidde ca scrive tu, o lè scritte cu 'u penziere tue o lè cupiate da risorse de pubbliche dominie o sembre robba libbere (vide $1 pe cchiù dettaglie).\n'''NO REGGISTRA' FATIJE CUPERTE DA 'U COPYRIGHT SENZA PERMESSE! NO FA STUDECARIE!'''", diff --git a/languages/i18n/sd.json b/languages/i18n/sd.json index 4d575f462d..0719083e93 100644 --- a/languages/i18n/sd.json +++ b/languages/i18n/sd.json @@ -1030,7 +1030,7 @@ "rcfilters-filter-watchlistactivity-seen-label": "ڏٺل سنوارون", "rcfilters-filtergroup-changetype": "تبديليءَ جو قِسم", "rcfilters-filter-pageedits-label": "صفحي سنوارون", - "rcfilters-filter-newpages-label": "صفحي تخليقون", + "rcfilters-filter-newpages-label": "صفحي سرجايون", "rcfilters-filter-newpages-description": "نوان صفحا ٺاھيندڙ سنوارون.", "rcfilters-filter-categorization-label": "زمري ۾ تبديليون", "rcfilters-filter-logactions-label": "لاگڊ عمل", @@ -1520,7 +1520,7 @@ "sp-contributions-search": "ڀاڱيدارين لاءِ ڳولا ڪريو", "sp-contributions-username": "آءِپي پتو يا واپرائيندڙ-نانءُ:", "sp-contributions-toponly": "صرف اھي سنوارون ڏيکاريو جيڪي تازا ترين مسودا آھن", - "sp-contributions-newonly": "صرف اھي سنوارون ڏيکاريو جيڪي صرف صفحي تخليقون آھن", + "sp-contributions-newonly": "صرف اھي سنوارون ڏيکاريو جيڪي صفحي سرجايون آھن", "sp-contributions-hideminor": "معمولي سنوارون لڪايو", "sp-contributions-submit": "ڳوليو", "whatlinkshere": "هتان ڇا ڳنڍيل آهي", diff --git a/languages/i18n/sh.json b/languages/i18n/sh.json index b231f8911c..e0f4f5ca01 100644 --- a/languages/i18n/sh.json +++ b/languages/i18n/sh.json @@ -3799,5 +3799,5 @@ "mycustomjsredirectprotected": "Nemate dopuštenje za uređivanje ove JavaScript stranice jer predstavlja preusmjeravanje i ne vodi do vašeg imenskog prostora.", "easydeflate-invaliddeflate": "Sadržaj nije ispravno pročišćen", "unprotected-js": "JavaScript ne može da se učita sa nezaštićenih stranica iz bezbednosnih razloga. Samo napravite JavaScript u imenskom prostoru MediaWiki: ili kao korisničku podstranicu", - "userlogout-continue": "Ako se želite odjaviti, [$1 nastavite na odjavnoj strnaici]." + "userlogout-continue": "Želite se odjaviti?" } diff --git a/languages/i18n/sl.json b/languages/i18n/sl.json index 7cfbd84646..a65feb194f 100644 --- a/languages/i18n/sl.json +++ b/languages/i18n/sl.json @@ -2672,6 +2672,7 @@ "move-subpages": "Premakni podstrani (do $1)", "move-talk-subpages": "Premakni podstrani pogovorne strani (do $1)", "movepage-page-exists": "Stran $1 že obstaja in je ni mogoče samodejno prepisati.", + "movepage-source-doesnt-exist": "Stran $1 ne obstaja in je ni mogoče premakniti.", "movepage-page-moved": "Stran $1 je bila prestavljena na $2.", "movepage-page-unmoved": "Strani $1 ni bilo mogoče prestaviti na $2.", "movepage-max-pages": "{{PLURAL:$1|Premaknjena je bila največ $1 stran|Premaknjeni sta bili največ $1 strani|Premaknjene so bile največ $1 strani|Premaknjenih je bilo največ $1 strani}} in nobena več ne bo samodejno premaknjena.", @@ -2688,10 +2689,12 @@ "delete_and_move_reason": "Izbrisano z namenom pripraviti prostor za »[[$1]]«", "selfmove": "Naslov je enak;\nstrani ni mogoče prestaviti čez njo.", "immobile-source-namespace": "Ne morem premikati strani v imenskem prostoru »$1«", + "immobile-source-namespace-iw": "Strani na drugih wikijih ne morete premikati s tega wikija.", "immobile-target-namespace": "Ne morem premakniti strani v imenski prostor »$1«", "immobile-target-namespace-iw": "Povezava interwiki ni veljaven cilj za premik strani.", "immobile-source-page": "Te strani ni mogoče prestaviti.", "immobile-target-page": "Ne morem premakniti na ta ciljni naslov.", + "movepage-invalid-target-title": "Zahtevano ime ni veljavno.", "bad-target-model": "Želen cilj uporablja drugačno obliko vsebine. Ne morem pretvoriti iz $1 v $2.", "imagenocrossnamespace": "Ne morem premakniti datoteke izven imenskega prostora datotek", "nonfile-cannot-move-to-file": "Ne morem premakniti nedatoteko v imenski prostor datotek", diff --git a/languages/i18n/sv.json b/languages/i18n/sv.json index 2f330b3b14..51297167c0 100644 --- a/languages/i18n/sv.json +++ b/languages/i18n/sv.json @@ -1323,7 +1323,7 @@ "grant-group-customization": "Anpassning och inställningar", "grant-group-administration": "Utför administrativa åtgärder", "grant-group-private-information": "Få tillgång till privat data om dig", - "grant-group-other": "Diverse aktivitet", + "grant-group-other": "Övrig aktivitet", "grant-blockusers": "Blockera och avblockera användare", "grant-createaccount": "Skapa konton", "grant-createeditmovepage": "Skapa, redigera och flytta sidor", diff --git a/languages/i18n/th.json b/languages/i18n/th.json index 560eeb6509..873fae34dd 100644 --- a/languages/i18n/th.json +++ b/languages/i18n/th.json @@ -367,7 +367,7 @@ "title-invalid-talk-namespace": "ชื่อเรื่องหน้าที่ขออ้างถึงหน้าพูดคุยซึ่งมีไม่ได้", "title-invalid-characters": "ชื่อเรื่องหน้าที่ขอมีอักขระไม่สมเหตุสมผล: \"$1\"", "title-invalid-relative": "ชื่อเรื่องมีเส้นทางสัมพัทธ์ ชื่อเรื่องหน้าสัมพัทธ์ (./, ../) ไม่สมเหตุสมผล เพราะมักจะเข้าถึงไม่ได้เมื่อจัดการด้วยเบราว์เซอร์ของผู้ใช้", - "title-invalid-magic-tilde": "ชื่อเรื่องหน้าที่ขอมีลำดับทิลดาเมจิกไม่สมเหตุสมผล (~~~)", + "title-invalid-magic-tilde": "ชื่อหน้าที่ร้องขอมีลำดับทิลเดอพิเศษที่ใช้ไม่ได้ (~~~)", "title-invalid-too-long": "ชื่อเรื่องหน้าที่ขอยาวเกินไป ไม่สามารถยาวกว่า $1 ไบต์ในการเข้ารหัส UTF-8", "title-invalid-leading-colon": "ชื่อเรื่องหน้าที่ขอขึ้นต้นด้วยโคลอนไม่สมเหตุสมผล", "perfcached": "ข้อมูลต่อไปนี้ถูกเก็บในแคชและอาจล้าสมัย มีผลการค้นหาสูงสุด $1 รายการในแคช", @@ -2017,6 +2017,12 @@ "booksources-search": "ค้นหา", "booksources-text": "ด้านล่างเป็นรายการการเชื่อมโยงไปยังเว็บไซต์อื่นที่ขายหนังสือใหม่และหนังสือใช้แล้ว และอาจมีข้อมูลเพิ่มเติมเกี่ยวกับหนังสือที่คุณกำลังมองหา:", "booksources-invalid-isbn": "รหัส ISBN ที่ให้ไว้ไม่ถูกต้อง กรุณาตรวจสอบจากต้นฉบับอีกครั้ง", + "magiclink-tracking-rfc": "หน้าที่ใช้ลิงก์พิเศษ RFC", + "magiclink-tracking-rfc-desc": "หน้านี้ใช้ลิงก์พิเศษ RFC วิธีโยกย้ายให้ดูที่ [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Magic_links mediawiki.org]", + "magiclink-tracking-pmid": "หน้าที่ใช้ลิงก์พิเศษ PMID", + "magiclink-tracking-pmid-desc": "หน้านี้ใช้ลิงก์พิเศษ PMID วิธีโยกย้ายให้ดูที่ [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Magic_links mediawiki.org]", + "magiclink-tracking-isbn": "หน้าที่ใช้ลิงก์พิเศษ ISBN", + "magiclink-tracking-isbn-desc": "หน้านี้ใช้ลิงก์พิเศษ ISBN วิธีโยกย้ายให้ดูที่ [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Magic_links mediawiki.org]", "specialloguserlabel": "ผู้ดำเนินการ:", "speciallogtitlelabel": "เป้าหมาย (ชื่อเรื่องหรือ {{ns:user}}:ชื่อผู้ใช้ สำหรับผู้ใช้):", "log": "ปูม", @@ -2103,7 +2109,7 @@ "trackingcategories-desc": "เกณฑ์การรวมหมวดหมู่", "restricted-displaytitle-ignored": "หน้าที่มีชื่อเรื่องแสดงที่ถูกละเลย", "restricted-displaytitle-ignored-desc": "หน้ามี {{DISPLAYTITLE}} ที่ถูกละเลย เพราะไม่สมนัยกับชื่อเรื่องแท้จริงของหน้า", - "noindex-category-desc": "โรบอตไม่สามารถทำดัชนีหน้านี้เพราะมีเมจิกเวิร์ด __NOINDEX__ อยู่และอยู่ในเนมสเปซซึ่งอนุญาตตัวบ่งชี้นี้", + "noindex-category-desc": "บอตไม่สามารถทำดัชนีหน้านี้เพราะมีคำสั่งพิเศษ __NOINDEX__ อยู่และอยู่ในเนมสเปซซึ่งอนุญาตตัวบ่งชี้นี้", "index-category-desc": "หน้านี้มี __INDEX__ อยู่ (และอยู่ในเนมสเปซซึ่งอนุญาตตัวบ่งชี้นี้) ฉะนั้น โรบอตจึงทำดัชนี้ได้ ซึ่งปกติไม่สามารถทำได้", "post-expand-template-inclusion-category-desc": "การแทนที่แม่แบบทั้งหมดทำให้ขนาดของหน้าใหญ่กว่า $wgMaxArticleSize จึงไม่มีการแทนที่แม่แบบบางตัว", "post-expand-template-argument-category-desc": "หน้านี้มีขนาดใหญ่กว่า $wgMaxArticleSize หลังจากขยายอาร์กิวเมนต์ของแม่แบบ (สิ่งที่อยู่ภายในวงเล็บปีกกาสามวง เช่น {{{Foo}}})", diff --git a/languages/i18n/vec.json b/languages/i18n/vec.json index ff50d0b99f..49b249b1ef 100644 --- a/languages/i18n/vec.json +++ b/languages/i18n/vec.json @@ -300,7 +300,7 @@ "nstab-template": "Modèl", "nstab-help": "Ajuto", "nstab-category": "Categoria", - "mainpage-nstab": "Pàgina prinsipale", + "mainpage-nstab": "Pajina prinsipałe", "nosuchaction": "Operasion no riconossua", "nosuchactiontext": "L'asion spesifegà ne l'URL no a xè vałida.\nXè posibiłe che l'URL sia sta dizità en modo erato o che sia sta seguio on cołegamento no vałido.\nCiò podaria anca indicare on bug en {{SITENAME}}.", "nosuchspecialpage": "Pajina prinsipałe no disponibiłe", diff --git a/languages/i18n/zh-hant.json b/languages/i18n/zh-hant.json index 164568783f..c5641c986b 100644 --- a/languages/i18n/zh-hant.json +++ b/languages/i18n/zh-hant.json @@ -516,7 +516,7 @@ "userlogin-resetpassword-link": "忘記密碼?", "userlogin-helplink2": "登入說明", "userlogin-loggedin": "您目前已登入 {{GENDER:$1|$1}} 使用者,\n請使用下列表單改登入另一位使用者。", - "userlogin-reauth": "您必須再登入一次來驗証您為 {{GENDER:$1|$1}}。", + "userlogin-reauth": "您必須再登入一次來驗證您為{{GENDER:$1|$1}}。", "userlogin-createanother": "建立另一個帳號", "createacct-emailrequired": "電子郵件地址", "createacct-emailoptional": "電子郵件地址(選填)", @@ -2761,6 +2761,7 @@ "move-subpages": "移動子頁面(至多 $1 頁)", "move-talk-subpages": "移動討論頁面的子頁面 (共 $1 頁)", "movepage-page-exists": "頁面 $1 已存在,無法自動覆蓋。", + "movepage-source-doesnt-exist": "頁面$1不存在因此無法移動。", "movepage-page-moved": "已移動頁面 $1 到 $2。", "movepage-page-unmoved": "無法移動頁面 $1 到 $2。", "movepage-max-pages": "移動頁面的上限為 $1 頁,超出限制的頁面將不會自動移動。", @@ -2777,10 +2778,12 @@ "delete_and_move_reason": "已刪除讓來自 [[$1]] 頁面可移動", "selfmove": "標題相同;無法移動頁面到自己本身。", "immobile-source-namespace": "無法移動在命名空間 \"$1\" 中的頁面", + "immobile-source-namespace-iw": "在其它 wiki 的頁面無法從此 wiki 移動。", "immobile-target-namespace": "無法移動頁面至命名空間 \"$1\"", "immobile-target-namespace-iw": "移動頁面不可使用 Interwiki 連結做為目標。", "immobile-source-page": "此頁面無法移動。", "immobile-target-page": "無法移動至目標標題。", + "movepage-invalid-target-title": "請求的名稱無效。", "bad-target-model": "指定的目標地使用不同的內容模型。無法轉換 $1 為 $2。", "imagenocrossnamespace": "不可以移動檔案到非檔案命名空間", "nonfile-cannot-move-to-file": "不可以移動非檔案到檔案命名空間", diff --git a/maintenance/cleanupCaps.php b/maintenance/cleanupCaps.php index 20be9fd1d7..da241e5337 100644 --- a/maintenance/cleanupCaps.php +++ b/maintenance/cleanupCaps.php @@ -160,7 +160,8 @@ class CleanupCaps extends TableCleanup { $this->output( "\"$display\" -> \"$targetDisplay\": DRY RUN, NOT MOVED\n" ); $ok = 'OK'; } else { - $mp = new MovePage( $current, $target ); + $mp = MediaWikiServices::getInstance()->getMovePageFactory() + ->newMovePage( $current, $target ); $status = $mp->move( $this->user, $reason, $createRedirect ); $ok = $status->isOK() ? 'OK' : $status->getWikiText( false, false, 'en' ); $this->output( "\"$display\" -> \"$targetDisplay\": $ok\n" ); diff --git a/maintenance/createAndPromote.php b/maintenance/createAndPromote.php index da9b4d63a0..505168e84b 100644 --- a/maintenance/createAndPromote.php +++ b/maintenance/createAndPromote.php @@ -114,7 +114,7 @@ class CreateAndPromote extends Maintenance { if ( !$exists ) { // Create the user via AuthManager as there may be various side - // effects that are perfomed by the configured AuthManager chain. + // effects that are performed by the configured AuthManager chain. $status = MediaWiki\Auth\AuthManager::singleton()->autoCreateUser( $user, MediaWiki\Auth\AuthManager::AUTOCREATE_SOURCE_MAINT, diff --git a/maintenance/mctest.php b/maintenance/mctest.php index 9548d6b688..6b1cdc39a7 100644 --- a/maintenance/mctest.php +++ b/maintenance/mctest.php @@ -33,8 +33,11 @@ require_once __DIR__ . '/Maintenance.php'; class McTest extends Maintenance { public function __construct() { parent::__construct(); - $this->addDescription( "Makes several 'set', 'incr' and 'get' requests on every" - . " memcached server and shows a report" ); + $this->addDescription( + "Makes several operation requests on every cache server and shows a report.\n" . + "This tests both per-key and batched *Multi() methods as well as WRITE_BACKGROUND.\n" . + "\"IB\" means \"immediate blocking\" and \"DB\" means \"deferred blocking.\"" + ); $this->addOption( 'i', 'Number of iterations', false, true ); $this->addOption( 'cache', 'Use servers from this $wgObjectCaches store', false, true ); $this->addOption( 'driver', 'Either "php" or "pecl"', false, true ); @@ -76,37 +79,47 @@ class McTest extends Maintenance { $this->fatalError( "Invalid driver type '$type'" ); } + $this->output( "Warming up connections to cache servers..." ); + $mccByServer = []; foreach ( $servers as $server ) { - $this->output( str_pad( $server, $maxSrvLen ) . "\n" ); - /** @var BagOStuff $mcc */ - $mcc = new $class( [ + $mccByServer[$server] = new $class( [ 'servers' => [ $server ], 'persistent' => true, + 'allow_tcp_nagle_delay' => false, 'timeout' => $wgMemCachedTimeout ] ); + $mccByServer[$server]->get( 'key' ); + } + $this->output( "done\n" ); + $this->output( "Single and batched operation profiling/test results:\n" ); + + $valueByKey = []; + for ( $i = 1; $i <= $iterations; $i++ ) { + $valueByKey["test$i"] = 'S' . str_pad( $i, 2048 ); + } - $this->benchmarkSingleKeyOps( $mcc, $iterations ); - $this->benchmarkMultiKeyOpsImmediateBlocking( $mcc, $iterations ); - $this->benchmarkMultiKeyOpsDeferredBlocking( $mcc, $iterations ); + foreach ( $mccByServer as $server => $mcc ) { + $this->output( str_pad( $server, $maxSrvLen ) . "\n" ); + $this->benchmarkSingleKeyOps( $mcc, $valueByKey ); + $this->benchmarkMultiKeyOpsImmediateBlocking( $mcc, $valueByKey ); + $this->benchmarkMultiKeyOpsDeferredBlocking( $mcc, $valueByKey ); } } /** * @param BagOStuff $mcc - * @param int $iterations + * @param array $valueByKey */ - private function benchmarkSingleKeyOps( $mcc, $iterations ) { + private function benchmarkSingleKeyOps( BagOStuff $mcc, array $valueByKey ) { $add = 0; $set = 0; $incr = 0; $get = 0; $delete = 0; - $keys = []; - for ( $i = 1; $i <= $iterations; $i++ ) { - $keys[] = "test$i"; - } + $i = count( $valueByKey ); + $keys = array_keys( $valueByKey ); // Clear out any old values $mcc->deleteMulti( $keys ); @@ -153,43 +166,40 @@ class McTest extends Maintenance { $delMs = intval( 1e3 * ( microtime( true ) - $time_start ) ); $this->output( - " add: $add/$iterations {$addMs}ms " . - "set: $set/$iterations {$setMs}ms " . - "incr: $incr/$iterations {$incrMs}ms " . - "get: $get/$iterations ({$getMs}ms) " . - "delete: $delete/$iterations ({$delMs}ms)\n" + " add: $add/$i {$addMs}ms " . + "set: $set/$i {$setMs}ms " . + "incr: $incr/$i {$incrMs}ms " . + "get: $get/$i ({$getMs}ms) " . + "delete: $delete/$i ({$delMs}ms)\n" ); } /** * @param BagOStuff $mcc - * @param int $iterations + * @param array $valueByKey */ - private function benchmarkMultiKeyOpsImmediateBlocking( $mcc, $iterations ) { - $keysByValue = []; - for ( $i = 1; $i <= $iterations; $i++ ) { - $keysByValue["test$i"] = 'S' . str_pad( $i, 2048 ); - } - $keyList = array_keys( $keysByValue ); + private function benchmarkMultiKeyOpsImmediateBlocking( BagOStuff $mcc, array $valueByKey ) { + $keys = array_keys( $valueByKey ); + $iterations = count( $valueByKey ); $time_start = microtime( true ); - $mSetOk = $mcc->setMulti( $keysByValue ) ? 'S' : 'F'; + $mSetOk = $mcc->setMulti( $valueByKey ) ? '✓' : '✗'; $mSetMs = intval( 1e3 * ( microtime( true ) - $time_start ) ); $time_start = microtime( true ); - $found = $mcc->getMulti( $keyList ); + $found = $mcc->getMulti( $keys ); $mGetMs = intval( 1e3 * ( microtime( true ) - $time_start ) ); $mGetOk = 0; foreach ( $found as $key => $value ) { - $mGetOk += ( $value === $keysByValue[$key] ); + $mGetOk += ( $value === $valueByKey[$key] ); } $time_start = microtime( true ); - $mChangeTTLOk = $mcc->changeTTLMulti( $keyList, 3600 ) ? 'S' : 'F'; + $mChangeTTLOk = $mcc->changeTTLMulti( $keys, 3600 ) ? '✓' : '✗'; $mChangeTTTMs = intval( 1e3 * ( microtime( true ) - $time_start ) ); $time_start = microtime( true ); - $mDelOk = $mcc->deleteMulti( $keyList ) ? 'S' : 'F'; + $mDelOk = $mcc->deleteMulti( $keys ) ? '✓' : '✗'; $mDelMs = intval( 1e3 * ( microtime( true ) - $time_start ) ); $this->output( @@ -202,34 +212,31 @@ class McTest extends Maintenance { /** * @param BagOStuff $mcc - * @param int $iterations + * @param array $valueByKey */ - private function benchmarkMultiKeyOpsDeferredBlocking( $mcc, $iterations ) { + private function benchmarkMultiKeyOpsDeferredBlocking( BagOStuff $mcc, array $valueByKey ) { + $keys = array_keys( $valueByKey ); + $iterations = count( $valueByKey ); $flags = $mcc::WRITE_BACKGROUND; - $keysByValue = []; - for ( $i = 1; $i <= $iterations; $i++ ) { - $keysByValue["test$i"] = 'A' . str_pad( $i, 2048 ); - } - $keyList = array_keys( $keysByValue ); $time_start = microtime( true ); - $mSetOk = $mcc->setMulti( $keysByValue, 0, $flags ) ? 'S' : 'F'; + $mSetOk = $mcc->setMulti( $valueByKey, 0, $flags ) ? '✓' : '✗'; $mSetMs = intval( 1e3 * ( microtime( true ) - $time_start ) ); $time_start = microtime( true ); - $found = $mcc->getMulti( $keyList ); + $found = $mcc->getMulti( $keys ); $mGetMs = intval( 1e3 * ( microtime( true ) - $time_start ) ); $mGetOk = 0; foreach ( $found as $key => $value ) { - $mGetOk += ( $value === $keysByValue[$key] ); + $mGetOk += ( $value === $valueByKey[$key] ); } $time_start = microtime( true ); - $mChangeTTLOk = $mcc->changeTTLMulti( $keyList, 3600, $flags ) ? 'S' : 'F'; + $mChangeTTLOk = $mcc->changeTTLMulti( $keys, 3600, $flags ) ? '✓' : '✗'; $mChangeTTTMs = intval( 1e3 * ( microtime( true ) - $time_start ) ); $time_start = microtime( true ); - $mDelOk = $mcc->deleteMulti( $keyList, $flags ) ? 'S' : 'F'; + $mDelOk = $mcc->deleteMulti( $keys, $flags ) ? '✓' : '✗'; $mDelMs = intval( 1e3 * ( microtime( true ) - $time_start ) ); $this->output( diff --git a/maintenance/moveBatch.php b/maintenance/moveBatch.php index 47828e690f..09f3120a97 100644 --- a/maintenance/moveBatch.php +++ b/maintenance/moveBatch.php @@ -35,6 +35,8 @@ * e.g. immobile_namespace for namespaces which can't be moved */ +use MediaWiki\MediaWikiServices; + require_once __DIR__ . '/Maintenance.php'; /** @@ -105,7 +107,8 @@ class MoveBatch extends Maintenance { $this->output( $source->getPrefixedText() . ' --> ' . $dest->getPrefixedText() ); $this->beginTransaction( $dbw, __METHOD__ ); - $mp = new MovePage( $source, $dest ); + $mp = MediaWikiServices::getInstance()->getMovePageFactory() + ->newMovePage( $source, $dest ); $status = $mp->move( $wgUser, $reason, !$noredirects ); if ( !$status->isOK() ) { $this->output( "\nFAILED: " . $status->getWikiText( false, false, 'en' ) ); diff --git a/resources/Resources.php b/resources/Resources.php index eaf720c0da..d33e3dee2c 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -1934,6 +1934,8 @@ return [ 'rcfilters-filter-showlinkedto-label', 'rcfilters-filter-showlinkedto-option-label', 'rcfilters-target-page-placeholder', + 'rcfilters-allcontents-label', + 'rcfilters-alldiscussions-label', 'blanknamespace', 'namespaces', 'tags-title', diff --git a/resources/src/mediawiki.rcfilters/Controller.js b/resources/src/mediawiki.rcfilters/Controller.js index 97b73ae2bd..85a4efe5f7 100644 --- a/resources/src/mediawiki.rcfilters/Controller.js +++ b/resources/src/mediawiki.rcfilters/Controller.js @@ -58,6 +58,7 @@ OO.initClass( Controller ); */ Controller.prototype.initialize = function ( filterStructure, namespaceStructure, tagList, conditionalViews ) { var parsedSavedQueries, pieces, + nsAllContents, nsAllDiscussions, displayConfig = mw.config.get( 'StructuredChangeFiltersDisplayConfig' ), defaultSavedQueryExists = mw.config.get( 'wgStructuredChangeFiltersDefaultSavedQueryExists' ), controller = this, @@ -67,20 +68,38 @@ Controller.prototype.initialize = function ( filterStructure, namespaceStructure // Prepare views if ( namespaceStructure ) { - items = []; + nsAllContents = { + name: 'all-contents', + label: mw.msg( 'rcfilters-allcontents-label' ), + description: '', + identifiers: [ 'subject' ], + cssClass: 'mw-changeslist-ns-subject', + subset: [] + }; + nsAllDiscussions = { + name: 'all-discussions', + label: mw.msg( 'rcfilters-alldiscussions-label' ), + description: '', + identifiers: [ 'talk' ], + cssClass: 'mw-changeslist-ns-talk', + subset: [] + }; + items = [ nsAllContents, nsAllDiscussions ]; // eslint-disable-next-line no-jquery/no-each-util $.each( namespaceStructure, function ( namespaceID, label ) { // Build and clean up the individual namespace items definition - items.push( { - name: namespaceID, - label: label || mw.msg( 'blanknamespace' ), - description: '', - identifiers: [ - mw.Title.isTalkNamespace( namespaceID ) ? - 'talk' : 'subject' - ], - cssClass: 'mw-changeslist-ns-' + namespaceID - } ); + var isTalk = mw.Title.isTalkNamespace( namespaceID ), + nsFilter = { + name: namespaceID, + label: label || mw.msg( 'blanknamespace' ), + description: '', + identifiers: [ + isTalk ? 'talk' : 'subject' + ], + cssClass: 'mw-changeslist-ns-' + namespaceID + }; + items.push( nsFilter ); + ( isTalk ? nsAllDiscussions : nsAllContents ).subset.push( { filter: namespaceID } ); } ); views.namespaces = { diff --git a/resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js b/resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js index a5b71b989b..0eb1134f11 100644 --- a/resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js +++ b/resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js @@ -23,7 +23,7 @@ * @cfg {boolean} [redirect] Page is a redirect * @cfg {boolean} [disambiguation] Page is a disambiguation page * @cfg {string} [query] Matching query string to highlight - * @cfg {string} [compare] String comparison function for query highlighting + * @cfg {Function} [compare] String comparison function for query highlighting */ mw.widgets.TitleOptionWidget = function MwWidgetsTitleOptionWidget( config ) { var icon; diff --git a/tests/common/TestsAutoLoader.php b/tests/common/TestsAutoLoader.php index c35e80fada..e71cc88b3e 100644 --- a/tests/common/TestsAutoLoader.php +++ b/tests/common/TestsAutoLoader.php @@ -74,6 +74,7 @@ $wgAutoloadClasses += [ 'TestUserRegistry' => "$testDir/phpunit/includes/TestUserRegistry.php", # tests/phpunit/includes + 'FactoryArgTestTrait' => "$testDir/phpunit/unit/includes/FactoryArgTestTrait.php", 'PageArchiveTestBase' => "$testDir/phpunit/includes/page/PageArchiveTestBase.php", 'RevisionDbTestBase' => "$testDir/phpunit/includes/RevisionDbTestBase.php", 'RevisionTestModifyableContent' => "$testDir/phpunit/includes/RevisionTestModifyableContent.php", @@ -103,6 +104,10 @@ $wgAutoloadClasses += [ # tests/phpunit/includes/changes 'TestRecentChangesHelper' => "$testDir/phpunit/includes/changes/TestRecentChangesHelper.php", + # tests/phpunit/includes/config + 'TestAllServiceOptionsUsed' => "$testDir/phpunit/includes/config/TestAllServiceOptionsUsed.php", + 'LoggedServiceOptions' => "$testDir/phpunit/includes/config/LoggedServiceOptions.php", + # tests/phpunit/includes/content 'DummyContentHandlerForTesting' => "$testDir/phpunit/mocks/content/DummyContentHandlerForTesting.php", @@ -212,6 +217,9 @@ $wgAutoloadClasses += [ 'MockSearchResultSet' => "$testDir/phpunit/mocks/search/MockSearchResultSet.php", 'MockSearchResult' => "$testDir/phpunit/mocks/search/MockSearchResult.php", + # tests/phpunit/unit/includes/libs/filebackend/fsfile + 'TempFSFileTestTrait' => "$testDir/phpunit/unit/includes/libs/filebackend/fsfile/TempFSFileTestTrait.php", + # tests/suites 'ParserTestFileSuite' => "$testDir/phpunit/suites/ParserTestFileSuite.php", 'ParserTestTopLevelSuite' => "$testDir/phpunit/suites/ParserTestTopLevelSuite.php", diff --git a/tests/parser/ParserTestRunner.php b/tests/parser/ParserTestRunner.php index 29453082ef..36c3fe20b0 100644 --- a/tests/parser/ParserTestRunner.php +++ b/tests/parser/ParserTestRunner.php @@ -327,7 +327,7 @@ class ParserTestRunner { // This is essential and overrides disabling of database messages in TestSetup $setup['wgUseDatabaseMessages'] = true; $reset = function () { - MessageCache::destroyInstance(); + MediaWikiServices::getInstance()->resetServiceForTesting( 'MessageCache' ); }; $setup[] = $reset; $teardown[] = $reset; diff --git a/tests/phpunit/MediaWikiIntegrationTestCase.php b/tests/phpunit/MediaWikiIntegrationTestCase.php index aa43e5db25..496f265b37 100644 --- a/tests/phpunit/MediaWikiIntegrationTestCase.php +++ b/tests/phpunit/MediaWikiIntegrationTestCase.php @@ -253,7 +253,7 @@ abstract class MediaWikiIntegrationTestCase extends PHPUnit\Framework\TestCase { if ( !$page->exists() ) { $user = self::getTestSysop()->getUser(); $page->doEditContent( - new WikitextContent( 'UTContent' ), + ContentHandler::makeContent( 'UTContent', $title ), 'UTPageSummary', EDIT_NEW | EDIT_SUPPRESS_RC, false, diff --git a/tests/phpunit/includes/MessageTest.php b/tests/phpunit/includes/MessageTest.php index e745960a45..fb32ef7fda 100644 --- a/tests/phpunit/includes/MessageTest.php +++ b/tests/phpunit/includes/MessageTest.php @@ -406,7 +406,6 @@ class MessageTest extends MediaWikiLangTestCase { $this->setMwGlobals( 'wgRawHtml', true ); // We have to reset the core hook registration. // to register the html hook - MessageCache::destroyInstance(); $this->overrideMwServices(); $msg = new RawMessage( '' ); diff --git a/tests/phpunit/includes/MovePageTest.php b/tests/phpunit/includes/MovePageTest.php index 31a0e79aa7..2895fa286c 100644 --- a/tests/phpunit/includes/MovePageTest.php +++ b/tests/phpunit/includes/MovePageTest.php @@ -1,63 +1,407 @@ createMock( $class ); + $mock->expects( $this->never() )->method( $this->anythingBut( '__destruct' ) ); + return $mock; + } + + /** + * The only files that exist are 'File:Existent.jpg', 'File:Existent2.jpg', and + * 'File:Existent-file-no-page.jpg'. Calling unexpected methods causes a test failure. + * + * @return RepoGroup + */ + private function getMockRepoGroup() : RepoGroup { + $mockExistentFile = $this->createMock( LocalFile::class ); + $mockExistentFile->method( 'exists' )->willReturn( true ); + $mockExistentFile->method( 'getMimeType' )->willReturn( 'image/jpeg' ); + $mockExistentFile->expects( $this->never() ) + ->method( $this->anythingBut( 'exists', 'load', 'getMimeType', '__destruct' ) ); + + $mockNonexistentFile = $this->createMock( LocalFile::class ); + $mockNonexistentFile->method( 'exists' )->willReturn( false ); + $mockNonexistentFile->expects( $this->never() ) + ->method( $this->anythingBut( 'exists', 'load', '__destruct' ) ); + + $mockLocalRepo = $this->createMock( LocalRepo::class ); + $mockLocalRepo->method( 'newFile' )->will( $this->returnCallback( + function ( Title $title ) use ( $mockExistentFile, $mockNonexistentFile ) { + if ( in_array( $title->getPrefixedText(), + [ 'File:Existent.jpg', 'File:Existent2.jpg', 'File:Existent-file-no-page.jpg' ] + ) ) { + return $mockExistentFile; + } + return $mockNonexistentFile; + } + ) ); + $mockLocalRepo->expects( $this->never() ) + ->method( $this->anythingBut( 'newFile', '__destruct' ) ); + + $mockRepoGroup = $this->createMock( RepoGroup::class ); + $mockRepoGroup->method( 'getLocalRepo' )->willReturn( $mockLocalRepo ); + $mockRepoGroup->expects( $this->never() ) + ->method( $this->anythingBut( 'getLocalRepo', '__destruct' ) ); + + return $mockRepoGroup; + } + + /** + * @param LinkTarget $old + * @param LinkTarget $new + * @param array $params Valid keys are: db, options, nsInfo, wiStore, repoGroup. + * options is an indexed array that will overwrite our defaults, not a ServiceOptions, so it + * need not contain all keys. + * @return MovePage + */ + private function newMovePage( $old, $new, array $params = [] ) : MovePage { + $mockLB = $this->createMock( LoadBalancer::class ); + $mockLB->method( 'getConnection' ) + ->willReturn( $params['db'] ?? $this->getNoOpMock( IDatabase::class ) ); + $mockLB->expects( $this->never() ) + ->method( $this->anythingBut( 'getConnection', '__destruct' ) ); + + $mockNsInfo = $this->createMock( NamespaceInfo::class ); + $mockNsInfo->method( 'isMovable' )->will( $this->returnCallback( + function ( $ns ) { + return $ns >= 0; + } + ) ); + $mockNsInfo->expects( $this->never() ) + ->method( $this->anythingBut( 'isMovable', '__destruct' ) ); + + return new MovePage( + $old, + $new, + new ServiceOptions( + MovePageFactory::$constructorOptions, + $params['options'] ?? [], + [ + 'CategoryCollation' => 'uppercase', + 'ContentHandlerUseDB' => true, + ] + ), + $mockLB, + $params['nsInfo'] ?? $mockNsInfo, + $params['wiStore'] ?? $this->getNoOpMock( WatchedItemStore::class ), + $params['permMgr'] ?? $this->getNoOpMock( PermissionManager::class ), + $params['repoGroup'] ?? $this->getMockRepoGroup() + ); + } public function setUp() { parent::setUp(); + + // Ensure we have some pages that are guaranteed to exist or not + $this->getExistingTestPage( 'Existent' ); + $this->getExistingTestPage( 'Existent2' ); + $this->getExistingTestPage( 'File:Existent.jpg' ); + $this->getExistingTestPage( 'File:Existent2.jpg' ); + $this->getExistingTestPage( 'MediaWiki:Existent.js' ); + $this->getExistingTestPage( 'Hooked in place' ); + $this->getNonExistingTestPage( 'Nonexistent' ); + $this->getNonExistingTestPage( 'Nonexistent2' ); + $this->getNonExistingTestPage( 'File:Nonexistent.jpg' ); + $this->getNonExistingTestPage( 'File:Nonexistent.png' ); + $this->getNonExistingTestPage( 'File:Existent-file-no-page.jpg' ); + $this->getNonExistingTestPage( 'MediaWiki:Nonexistent' ); + $this->getNonExistingTestPage( 'No content allowed' ); + + // Set a couple of hooks for specific pages + $this->setTemporaryHook( 'ContentModelCanBeUsedOn', + function ( $modelId, Title $title, &$ok ) { + if ( $title->getPrefixedText() === 'No content allowed' ) { + $ok = false; + } + } + ); + + $this->setTemporaryHook( 'TitleIsMovable', + function ( Title $title, &$result ) { + if ( strtolower( $title->getPrefixedText() ) === 'hooked in place' ) { + $result = false; + } + } + ); + $this->tablesUsed[] = 'page'; $this->tablesUsed[] = 'revision'; $this->tablesUsed[] = 'comment'; } + /** + * @covers MovePage::__construct + */ + public function testConstructorDefaults() { + $services = MediaWikiServices::getInstance(); + + $obj1 = new MovePage( Title::newFromText( 'A' ), Title::newFromText( 'B' ) ); + $obj2 = new MovePage( + Title::newFromText( 'A' ), + Title::newFromText( 'B' ), + new ServiceOptions( MovePageFactory::$constructorOptions, $services->getMainConfig() ), + $services->getDBLoadBalancer(), + $services->getNamespaceInfo(), + $services->getWatchedItemStore(), + $services->getPermissionManager(), + $services->getRepoGroup(), + $services->getTitleFormatter() + ); + + $this->assertEquals( $obj2, $obj1 ); + } + /** * @dataProvider provideIsValidMove * @covers MovePage::isValidMove + * @covers MovePage::isValidMoveTarget * @covers MovePage::isValidFileMove + * @covers MovePage::__construct + * @covers Title::isValidMoveOperation + * + * @param string|Title $old + * @param string|Title $new + * @param array $expectedErrors + * @param array $extraOptions */ - public function testIsValidMove( $old, $new, $error ) { - global $wgMultiContentRevisionSchemaMigrationStage; - if ( $wgMultiContentRevisionSchemaMigrationStage === SCHEMA_COMPAT_OLD ) { - // We can only set this to false with the old schema - $this->setMwGlobals( 'wgContentHandlerUseDB', false ); + public function testIsValidMove( + $old, $new, array $expectedErrors, array $extraOptions = [] + ) { + if ( is_string( $old ) ) { + $old = Title::newFromText( $old ); } - $mp = new MovePage( - Title::newFromText( $old ), - Title::newFromText( $new ) - ); - $status = $mp->isValidMove(); - if ( $error === true ) { - $this->assertTrue( $status->isGood() ); - } else { - $this->assertTrue( $status->hasMessage( $error ) ); + if ( is_string( $new ) ) { + $new = Title::newFromText( $new ); + } + // Can't test MovePage with a null target, only isValidMoveOperation + if ( $new ) { + $mp = $this->newMovePage( $old, $new, [ 'options' => $extraOptions ] ); + $this->assertSame( $expectedErrors, $mp->isValidMove()->getErrorsArray() ); } + + foreach ( $extraOptions as $key => $val ) { + $this->setMwGlobals( "wg$key", $val ); + } + $this->overrideMwServices(); + $this->setService( 'RepoGroup', $this->getMockRepoGroup() ); + // This returns true instead of an array if there are no errors + $this->hideDeprecated( 'Title::isValidMoveOperation' ); + $this->assertSame( $expectedErrors ?: true, $old->isValidMoveOperation( $new, false ) ); } - /** - * This should be kept in sync with TitleTest::provideTestIsValidMoveOperation - */ public static function provideIsValidMove() { global $wgMultiContentRevisionSchemaMigrationStage; $ret = [ - // for MovePage::isValidMove - [ 'Test', 'Test', 'selfmove' ], - [ 'Special:FooBar', 'Test', 'immobile-source-namespace' ], - [ 'Test', 'Special:FooBar', 'immobile-target-namespace' ], - [ 'Page', 'File:Test.jpg', 'nonfile-cannot-move-to-file' ], - // for MovePage::isValidFileMove - [ 'File:Test.jpg', 'Page', 'imagenocrossnamespace' ], + 'Self move' => [ + 'Existent', + 'Existent', + [ [ 'selfmove' ] ], + ], + 'Move to null' => [ + 'Existent', + null, + [ [ 'badtitletext' ] ], + ], + 'Move from empty name' => [ + Title::makeTitle( NS_MAIN, '' ), + 'Nonexistent', + // @todo More specific error message, or make the move valid if the page actually + // exists somehow in the database + [ [ 'badarticleerror' ] ], + ], + 'Move to empty name' => [ + 'Existent', + Title::makeTitle( NS_MAIN, '' ), + [ [ 'movepage-invalid-target-title' ] ], + ], + 'Move to invalid name' => [ + 'Existent', + Title::makeTitle( NS_MAIN, '<' ), + [ [ 'movepage-invalid-target-title' ] ], + ], + 'Move between invalid names' => [ + Title::makeTitle( NS_MAIN, '<' ), + Title::makeTitle( NS_MAIN, '>' ), + // @todo First error message should be more specific, or maybe we should make moving + // such pages valid if they actually exist somehow in the database + [ [ 'movepage-source-doesnt-exist' ], [ 'movepage-invalid-target-title' ] ], + ], + 'Move nonexistent' => [ + 'Nonexistent', + 'Nonexistent2', + [ [ 'movepage-source-doesnt-exist' ] ], + ], + 'Move over existing' => [ + 'Existent', + 'Existent2', + [ [ 'articleexists' ] ], + ], + 'Move from another wiki' => [ + Title::makeTitle( NS_MAIN, 'Test', '', 'otherwiki' ), + 'Nonexistent', + [ [ 'immobile-source-namespace-iw' ] ], + ], + 'Move special page' => [ + 'Special:FooBar', + 'Nonexistent', + [ [ 'immobile-source-namespace', 'Special' ] ], + ], + 'Move to another wiki' => [ + 'Existent', + Title::makeTitle( NS_MAIN, 'Test', '', 'otherwiki' ), + [ [ 'immobile-target-namespace-iw' ] ], + ], + 'Move to special page' => + [ 'Existent', 'Special:FooBar', [ [ 'immobile-target-namespace', 'Special' ] ] ], + 'Move to allowed content model' => [ + 'MediaWiki:Existent.js', + 'MediaWiki:Nonexistent', + [], + ], + 'Move to prohibited content model' => [ + 'Existent', + 'No content allowed', + [ [ 'content-not-allowed-here', 'wikitext', 'No content allowed', 'main' ] ], + ], + 'Aborted by hook' => [ + 'Hooked in place', + 'Nonexistent', + // @todo Error is wrong + [ [ 'immobile-source-namespace', '' ] ], + ], + 'Doubly aborted by hook' => [ + 'Hooked in place', + 'Hooked In Place', + // @todo Both errors are wrong + [ [ 'immobile-source-namespace', '' ], [ 'immobile-target-namespace', '' ] ], + ], + 'Non-file to file' => + [ 'Existent', 'File:Nonexistent.jpg', [ [ 'nonfile-cannot-move-to-file' ] ] ], + 'File to non-file' => [ + 'File:Existent.jpg', + 'Nonexistent', + [ [ 'imagenocrossnamespace' ] ], + ], + 'Existing file to non-existing file' => [ + 'File:Existent.jpg', + 'File:Nonexistent.jpg', + [], + ], + 'Existing file to existing file' => [ + 'File:Existent.jpg', + 'File:Existent2.jpg', + [ [ 'articleexists' ] ], + ], + 'Existing file to existing file with no page' => [ + 'File:Existent.jpg', + 'File:Existent-file-no-page.jpg', + // @todo Is this correct? Moving over an existing file with no page should succeed? + [], + ], + 'Existing file to name with slash' => [ + 'File:Existent.jpg', + 'File:Existent/slashed.jpg', + [ [ 'imageinvalidfilename' ] ], + ], + 'Mismatched file extension' => [ + 'File:Existent.jpg', + 'File:Nonexistent.png', + [ [ 'imagetypemismatch' ] ], + ], ]; if ( $wgMultiContentRevisionSchemaMigrationStage === SCHEMA_COMPAT_OLD ) { - // The error can only occur if $wgContentHandlerUseDB is false, which doesn't work with - // the new schema, so omit the test in that case - array_push( $ret, - [ 'MediaWiki:Common.js', 'Help:Some wikitext page', 'bad-target-model' ] ); + // ContentHandlerUseDB = false only works with the old schema + $ret['Move to different content model (ContentHandlerUseDB false)'] = [ + 'MediaWiki:Existent.js', + 'MediaWiki:Nonexistent', + [ [ 'bad-target-model', 'JavaScript', 'wikitext' ] ], + [ 'ContentHandlerUseDB' => false ], + ]; + } + return $ret; + } + + /** + * @dataProvider provideMove + * @covers MovePage::move + * + * @param string $old Old name + * @param string $new New name + * @param array $expectedErrors + * @param array $extraOptions + */ + public function testMove( $old, $new, array $expectedErrors, array $extraOptions = [] ) { + if ( is_string( $old ) ) { + $old = Title::newFromText( $old ); + } + if ( is_string( $new ) ) { + $new = Title::newFromText( $new ); + } + + $params = [ 'options' => $extraOptions ]; + if ( $expectedErrors === [] ) { + $this->markTestIncomplete( 'Checking actual moves has not yet been implemented' ); + } + + $obj = $this->newMovePage( $old, $new, $params ); + $status = $obj->move( $this->getTestUser()->getUser() ); + $this->assertSame( $expectedErrors, $status->getErrorsArray() ); + } + + public static function provideMove() { + $ret = []; + foreach ( self::provideIsValidMove() as $name => $arr ) { + list( $old, $new, $expectedErrors, $extraOptions ) = array_pad( $arr, 4, [] ); + if ( !$new ) { + // Not supported by testMove + continue; + } + $ret[$name] = $arr; } return $ret; } + /** + * Integration test to catch regressions like T74870. Taken and modified + * from SemanticMediaWiki + * + * @covers Title::moveTo + * @covers MovePage::move + */ + public function testTitleMoveCompleteIntegrationTest() { + $this->hideDeprecated( 'Title::moveTo' ); + + $oldTitle = Title::newFromText( 'Help:Some title' ); + WikiPage::factory( $oldTitle )->doEditContent( new WikitextContent( 'foo' ), 'bar' ); + $newTitle = Title::newFromText( 'Help:Some other title' ); + $this->assertNull( + WikiPage::factory( $newTitle )->getRevision() + ); + + $this->assertTrue( $oldTitle->moveTo( $newTitle, false, 'test1', true ) ); + $this->assertNotNull( + WikiPage::factory( $oldTitle )->getRevision() + ); + $this->assertNotNull( + WikiPage::factory( $newTitle )->getRevision() + ); + } + /** * Test for the move operation being aborted via the TitleMove hook * @covers MovePage::move @@ -73,7 +417,7 @@ class MovePageTest extends MediaWikiTestCase { $oldTitle = Title::newFromText( 'Some old title' ); WikiPage::factory( $oldTitle )->doEditContent( new WikitextContent( 'foo' ), 'bar' ); $newTitle = Title::newFromText( 'A brand new title' ); - $mp = new MovePage( $oldTitle, $newTitle ); + $mp = $this->newMovePage( $oldTitle, $newTitle ); $user = User::newFromName( 'TitleMove tester' ); $status = $mp->move( $user, 'Reason', true ); $this->assertTrue( $status->hasMessage( $error ) ); diff --git a/tests/phpunit/includes/TitleTest.php b/tests/phpunit/includes/TitleTest.php index e8f08732e8..6cfc3779fb 100644 --- a/tests/phpunit/includes/TitleTest.php +++ b/tests/phpunit/includes/TitleTest.php @@ -3,7 +3,6 @@ use MediaWiki\Interwiki\InterwikiLookup; use MediaWiki\Linker\LinkTarget; use MediaWiki\MediaWikiServices; -use Wikimedia\TestingAccessWrapper; /** * @group Database @@ -22,12 +21,6 @@ class TitleTest extends MediaWikiTestCase { $this->setContentLang( 'en' ); } - protected function tearDown() { - // For testNewMainPage - MessageCache::destroyInstance(); - parent::tearDown(); - } - /** * @covers Title::legalChars */ @@ -286,59 +279,6 @@ class TitleTest extends MediaWikiTestCase { ]; } - /** - * Auth-less test of Title::isValidMoveOperation - * - * @param string $source - * @param string $target - * @param array|string|bool $expected Required error - * @dataProvider provideTestIsValidMoveOperation - * @covers Title::isValidMoveOperation - */ - public function testIsValidMoveOperation( $source, $target, $expected ) { - global $wgMultiContentRevisionSchemaMigrationStage; - - $this->hideDeprecated( 'Title::isValidMoveOperation' ); - - if ( $wgMultiContentRevisionSchemaMigrationStage === SCHEMA_COMPAT_OLD ) { - // We can only set this to false with the old schema - $this->setMwGlobals( 'wgContentHandlerUseDB', false ); - } - - $title = Title::newFromText( $source ); - $nt = Title::newFromText( $target ); - $errors = $title->isValidMoveOperation( $nt, false ); - if ( $expected === true ) { - $this->assertTrue( $errors ); - } else { - $errors = $this->flattenErrorsArray( $errors ); - foreach ( (array)$expected as $error ) { - $this->assertContains( $error, $errors ); - } - } - } - - public static function provideTestIsValidMoveOperation() { - global $wgMultiContentRevisionSchemaMigrationStage; - $ret = [ - // for Title::isValidMoveOperation - [ 'Some page', '', 'badtitletext' ], - [ 'Test', 'Test', 'selfmove' ], - [ 'Special:FooBar', 'Test', 'immobile-source-namespace' ], - [ 'Test', 'Special:FooBar', 'immobile-target-namespace' ], - [ 'Page', 'File:Test.jpg', 'nonfile-cannot-move-to-file' ], - [ 'File:Test.jpg', 'Page', 'imagenocrossnamespace' ], - ]; - if ( $wgMultiContentRevisionSchemaMigrationStage === SCHEMA_COMPAT_OLD ) { - // The error can only occur if $wgContentHandlerUseDB is false, which doesn't work with - // the new schema, so omit the test in that case - array_push( $ret, - [ 'MediaWiki:Common.js', 'Help:Some wikitext page', 'bad-target-model' ] - ); - } - return $ret; - } - /** * Auth-less test of Title::userCan * @@ -1007,6 +947,41 @@ class TitleTest extends MediaWikiTestCase { $title->getOtherPage(); } + /** + * @dataProvider provideIsMovable + * @covers Title::isMovable + * + * @param string|Title $title + * @param bool $expected + * @param callable|null $hookCallback For TitleIsMovable + */ + public function testIsMovable( $title, $expected, $hookCallback = null ) { + if ( $hookCallback ) { + $this->setTemporaryHook( 'TitleIsMovable', $hookCallback ); + } + if ( is_string( $title ) ) { + $title = Title::newFromText( $title ); + } + + $this->assertSame( $expected, $title->isMovable() ); + } + + public static function provideIsMovable() { + return [ + 'Simple title' => [ 'Foo', true ], + // @todo Should these next two really be true? + 'Empty name' => [ Title::makeTitle( NS_MAIN, '' ), true ], + 'Invalid name' => [ Title::makeTitle( NS_MAIN, '<' ), true ], + 'Interwiki' => [ Title::makeTitle( NS_MAIN, 'Test', '', 'otherwiki' ), false ], + 'Special page' => [ 'Special:FooBar', false ], + 'Aborted by hook' => [ 'Hooked in place', false, + function ( Title $title, &$result ) { + $result = false; + } + ], + ]; + } + public function provideCreateFragmentTitle() { return [ [ Title::makeTitle( NS_MAIN, 'Test' ), 'foo' ], @@ -1302,10 +1277,11 @@ class TitleTest extends MediaWikiTestCase { * @covers Title::newMainPage */ public function testNewMainPage() { - $msgCache = TestingAccessWrapper::newFromClass( MessageCache::class ); - $msgCache->instance = $this->createMock( MessageCache::class ); - $msgCache->instance->method( 'get' )->willReturn( 'Foresheet' ); - $msgCache->instance->method( 'transform' )->willReturn( 'Foresheet' ); + $mock = $this->createMock( MessageCache::class ); + $mock->method( 'get' )->willReturn( 'Foresheet' ); + $mock->method( 'transform' )->willReturn( 'Foresheet' ); + + $this->setService( 'MessageCache', $mock ); $this->assertSame( 'Foresheet', diff --git a/tests/phpunit/includes/block/BlockManagerTest.php b/tests/phpunit/includes/block/BlockManagerTest.php index f42777c503..71f76e760f 100644 --- a/tests/phpunit/includes/block/BlockManagerTest.php +++ b/tests/phpunit/includes/block/BlockManagerTest.php @@ -4,7 +4,6 @@ use MediaWiki\Block\BlockManager; use MediaWiki\Block\DatabaseBlock; use MediaWiki\Block\CompositeBlock; use MediaWiki\Block\SystemBlock; -use MediaWiki\Config\ServiceOptions; use MediaWiki\MediaWikiServices; use Wikimedia\TestingAccessWrapper; @@ -14,6 +13,7 @@ use Wikimedia\TestingAccessWrapper; * @coversDefaultClass \MediaWiki\Block\BlockManager */ class BlockManagerTest extends MediaWikiTestCase { + use TestAllServiceOptionsUsed; /** @var User */ protected $user; @@ -50,7 +50,8 @@ class BlockManagerTest extends MediaWikiTestCase { $this->setMwGlobals( $blockManagerConfig ); $this->overrideMwServices(); return [ - new ServiceOptions( + new LoggedServiceOptions( + self::$serviceOptionsAccessLog, BlockManager::$constructorOptions, MediaWikiServices::getInstance()->getMainConfig() ), @@ -680,4 +681,10 @@ class BlockManagerTest extends MediaWikiTestCase { ]; } + /** + * @coversNothing + */ + public function testAllServiceOptionsUsed() { + $this->assertAllServiceOptionsUsed( [ 'ApplyIpBlocksToXff', 'SoftBlockRanges' ] ); + } } diff --git a/tests/phpunit/includes/cache/MessageCacheTest.php b/tests/phpunit/includes/cache/MessageCacheTest.php index 35dacac598..74ad84ade6 100644 --- a/tests/phpunit/includes/cache/MessageCacheTest.php +++ b/tests/phpunit/includes/cache/MessageCacheTest.php @@ -13,7 +13,6 @@ class MessageCacheTest extends MediaWikiLangTestCase { protected function setUp() { parent::setUp(); $this->configureLanguages(); - MessageCache::destroyInstance(); MessageCache::singleton()->enable(); } @@ -25,6 +24,7 @@ class MessageCacheTest extends MediaWikiLangTestCase { // let's choose e.g. German (de) $this->setUserLang( 'de' ); $this->setContentLang( 'de' ); + $this->resetServices(); } function addDBDataOnce() { @@ -152,7 +152,6 @@ class MessageCacheTest extends MediaWikiLangTestCase { ] ); $this->overrideMwServices(); - MessageCache::destroyInstance(); $messageCache = MessageCache::singleton(); $messageCache->enable(); @@ -260,7 +259,6 @@ class MessageCacheTest extends MediaWikiLangTestCase { $importer->import( $importRevision ); // Now, load the message from the wiki page - MessageCache::destroyInstance(); $messageCache = MessageCache::singleton(); $messageCache->enable(); $messageCache = TestingAccessWrapper::newFromObject( $messageCache ); diff --git a/tests/phpunit/includes/config/LoggedServiceOptions.php b/tests/phpunit/includes/config/LoggedServiceOptions.php new file mode 100644 index 0000000000..41fdf24b20 --- /dev/null +++ b/tests/phpunit/includes/config/LoggedServiceOptions.php @@ -0,0 +1,36 @@ +accessLog = &$accessLog; + if ( !$accessLog ) { + $accessLog = [ $keys, [] ]; + } + + parent::__construct( $keys, ...$args ); + } + + /** + * @param string $key + * @return mixed + */ + public function get( $key ) { + $this->accessLog[1][$key] = true; + + return parent::get( $key ); + } +} diff --git a/tests/phpunit/includes/config/TestAllServiceOptionsUsed.php b/tests/phpunit/includes/config/TestAllServiceOptionsUsed.php new file mode 100644 index 0000000000..618472b526 --- /dev/null +++ b/tests/phpunit/includes/config/TestAllServiceOptionsUsed.php @@ -0,0 +1,48 @@ +assertNotEmpty( self::$serviceOptionsAccessLog, + 'You need to pass LoggedServiceOptions to your class instead of ServiceOptions ' . + 'for TestAllServiceOptionsUsed to work.' + ); + + list( $expected, $actual ) = self::$serviceOptionsAccessLog; + + $expected = array_diff( $expected, $expectedUnused ); + + $this->assertSame( + [], + array_diff( $expected, array_keys( $actual ) ), + "Some ServiceOptions keys were not accessed in tests. If they really aren't used, " . + "remove them from the class' option list. If they are used, add tests to cover them, " . + "or ignore the problem for now by passing them to assertAllServiceOptionsUsed() in " . + "its \$expectedUnused argument." + ); + + if ( $expectedUnused ) { + $this->markTestIncomplete( 'Some ServiceOptions keys are not yet accessed by tests: ' . + implode( ', ', $expectedUnused ) ); + } + } +} diff --git a/tests/phpunit/includes/libs/filebackend/fsfile/TempFSFileIntegrationTest.php b/tests/phpunit/includes/libs/filebackend/fsfile/TempFSFileIntegrationTest.php new file mode 100644 index 0000000000..42805b2db2 --- /dev/null +++ b/tests/phpunit/includes/libs/filebackend/fsfile/TempFSFileIntegrationTest.php @@ -0,0 +1,9 @@ +getCliArg( 'use-bagostuff' ) !== null ) { - $name = $this->getCliArg( 'use-bagostuff' ); + global $wgObjectCaches; - $this->cache = ObjectCache::newFromId( $name ); + $id = $this->getCliArg( 'use-bagostuff' ); + $this->cache = ObjectCache::newFromParams( $wgObjectCaches[$id] ); } else { // no type defined - use simple hash $this->cache = new HashBagOStuff; @@ -36,7 +37,7 @@ class BagOStuffTest extends MediaWikiTestCase { * @covers MediumSpecificBagOStuff::makeKeyInternal */ public function testMakeKey() { - $cache = ObjectCache::newFromId( 'hash' ); + $cache = new HashBagOStuff(); $localKey = $cache->makeKey( 'first', 'second', 'third' ); $globalKey = $cache->makeGlobalKey( 'first', 'second', 'third' ); @@ -380,24 +381,39 @@ class BagOStuffTest extends MediaWikiTestCase { return $oldValue . '!'; }; - foreach ( [ $tiny, $small, $big ] as $value ) { + $cases = [ 'tiny' => $tiny, 'small' => $small, 'big' => $big ]; + foreach ( $cases as $case => $value ) { $this->cache->set( $key, $value, 10, BagOStuff::WRITE_ALLOW_SEGMENTS ); - $this->assertEquals( $value, $this->cache->get( $key ) ); - $this->assertEquals( $value, $this->cache->getMulti( [ $key ] )[$key] ); - - $this->assertTrue( $this->cache->merge( $key, $callback, 5 ) ); - $this->assertEquals( "$value!", $this->cache->get( $key ) ); - $this->assertEquals( "$value!", $this->cache->getMulti( [ $key ] )[$key] ); - - $this->assertTrue( $this->cache->deleteMulti( [ $key ] ) ); - $this->assertFalse( $this->cache->get( $key ) ); - $this->assertEquals( [], $this->cache->getMulti( [ $key ] ) ); + $this->assertEquals( $value, $this->cache->get( $key ), "get $case" ); + $this->assertEquals( $value, $this->cache->getMulti( [ $key ] )[$key], "get $case" ); + + $this->assertTrue( + $this->cache->merge( $key, $callback, 5, 1, BagOStuff::WRITE_ALLOW_SEGMENTS ), + "merge $case" + ); + $this->assertEquals( + "$value!", + $this->cache->get( $key ), + "merged $case" + ); + $this->assertEquals( + "$value!", + $this->cache->getMulti( [ $key ] )[$key], + "merged $case" + ); + + $this->assertTrue( $this->cache->deleteMulti( [ $key ] ), "delete $case" ); + $this->assertFalse( $this->cache->get( $key ), "deleted $case" ); + $this->assertEquals( [], $this->cache->getMulti( [ $key ] ), "deletd $case" ); $this->cache->set( $key, "@$value", 10, BagOStuff::WRITE_ALLOW_SEGMENTS ); - $this->assertEquals( "@$value", $this->cache->get( $key ) ); - $this->assertTrue( $this->cache->delete( $key, BagOStuff::WRITE_PRUNE_SEGMENTS ) ); - $this->assertFalse( $this->cache->get( $key ) ); - $this->assertEquals( [], $this->cache->getMulti( [ $key ] ) ); + $this->assertEquals( "@$value", $this->cache->get( $key ), "get $case" ); + $this->assertTrue( + $this->cache->delete( $key, BagOStuff::WRITE_PRUNE_SEGMENTS ), + "prune $case" + ); + $this->assertFalse( $this->cache->get( $key ), "pruned $case" ); + $this->assertEquals( [], $this->cache->getMulti( [ $key ] ), "pruned $case" ); } $this->cache->set( $key, 666, 10, BagOStuff::WRITE_ALLOW_SEGMENTS ); diff --git a/tests/phpunit/includes/libs/services/ServiceContainerTest.php b/tests/phpunit/includes/libs/services/ServiceContainerTest.php deleted file mode 100644 index 6e51883cfb..0000000000 --- a/tests/phpunit/includes/libs/services/ServiceContainerTest.php +++ /dev/null @@ -1,497 +0,0 @@ -newServiceContainer(); - $names = $services->getServiceNames(); - - $this->assertInternalType( 'array', $names ); - $this->assertEmpty( $names ); - - $name = 'TestService92834576'; - $services->defineService( $name, function () { - return null; - } ); - - $names = $services->getServiceNames(); - $this->assertContains( $name, $names ); - } - - public function testHasService() { - $services = $this->newServiceContainer(); - - $name = 'TestService92834576'; - $this->assertFalse( $services->hasService( $name ) ); - - $services->defineService( $name, function () { - return null; - } ); - - $this->assertTrue( $services->hasService( $name ) ); - } - - public function testGetService() { - $services = $this->newServiceContainer( [ 'Foo' ] ); - - $theService = new stdClass(); - $name = 'TestService92834576'; - $count = 0; - - $services->defineService( - $name, - function ( $actualLocator, $extra ) use ( $services, $theService, &$count ) { - $count++; - PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); - PHPUnit_Framework_Assert::assertSame( $extra, 'Foo' ); - return $theService; - } - ); - - $this->assertSame( $theService, $services->getService( $name ) ); - - $services->getService( $name ); - $this->assertSame( 1, $count, 'instantiator should be called exactly once!' ); - } - - public function testGetService_fail_unknown() { - $services = $this->newServiceContainer(); - - $name = 'TestService92834576'; - - $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class ); - - $services->getService( $name ); - } - - public function testPeekService() { - $services = $this->newServiceContainer(); - - $services->defineService( - 'Foo', - function () { - return new stdClass(); - } - ); - - $services->defineService( - 'Bar', - function () { - return new stdClass(); - } - ); - - // trigger instantiation of Foo - $services->getService( 'Foo' ); - - $this->assertInternalType( - 'object', - $services->peekService( 'Foo' ), - 'Peek should return the service object if it had been accessed before.' - ); - - $this->assertNull( - $services->peekService( 'Bar' ), - 'Peek should return null if the service was never accessed.' - ); - } - - public function testPeekService_fail_unknown() { - $services = $this->newServiceContainer(); - - $name = 'TestService92834576'; - - $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class ); - - $services->peekService( $name ); - } - - public function testDefineService() { - $services = $this->newServiceContainer(); - - $theService = new stdClass(); - $name = 'TestService92834576'; - - $services->defineService( $name, function ( $actualLocator ) use ( $services, $theService ) { - PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); - return $theService; - } ); - - $this->assertTrue( $services->hasService( $name ) ); - $this->assertSame( $theService, $services->getService( $name ) ); - } - - public function testDefineService_fail_duplicate() { - $services = $this->newServiceContainer(); - - $theService = new stdClass(); - $name = 'TestService92834576'; - - $services->defineService( $name, function () use ( $theService ) { - return $theService; - } ); - - $this->setExpectedException( Wikimedia\Services\ServiceAlreadyDefinedException::class ); - - $services->defineService( $name, function () use ( $theService ) { - return $theService; - } ); - } - - public function testApplyWiring() { - $services = $this->newServiceContainer(); - - $wiring = [ - 'Foo' => function () { - return 'Foo!'; - }, - 'Bar' => function () { - return 'Bar!'; - }, - ]; - - $services->applyWiring( $wiring ); - - $this->assertSame( 'Foo!', $services->getService( 'Foo' ) ); - $this->assertSame( 'Bar!', $services->getService( 'Bar' ) ); - } - - public function testImportWiring() { - $services = $this->newServiceContainer(); - - $wiring = [ - 'Foo' => function () { - return 'Foo!'; - }, - 'Bar' => function () { - return 'Bar!'; - }, - 'Car' => function () { - return 'FUBAR!'; - }, - ]; - - $services->applyWiring( $wiring ); - - $services->addServiceManipulator( 'Foo', function ( $service ) { - return $service . '+X'; - } ); - - $services->addServiceManipulator( 'Car', function ( $service ) { - return $service . '+X'; - } ); - - $newServices = $this->newServiceContainer(); - - // create a service with manipulator - $newServices->defineService( 'Foo', function () { - return 'Foo!'; - } ); - - $newServices->addServiceManipulator( 'Foo', function ( $service ) { - return $service . '+Y'; - } ); - - // create a service before importing, so we can later check that - // existing service instances survive importWiring() - $newServices->defineService( 'Car', function () { - return 'Car!'; - } ); - - // force instantiation - $newServices->getService( 'Car' ); - - // Define another service, so we can later check that extra wiring - // is not lost. - $newServices->defineService( 'Xar', function () { - return 'Xar!'; - } ); - - // import wiring, but skip `Bar` - $newServices->importWiring( $services, [ 'Bar' ] ); - - $this->assertNotContains( 'Bar', $newServices->getServiceNames(), 'Skip `Bar` service' ); - $this->assertSame( 'Foo!+Y+X', $newServices->getService( 'Foo' ) ); - - // import all wiring, but preserve existing service instance - $newServices->importWiring( $services ); - - $this->assertContains( 'Bar', $newServices->getServiceNames(), 'Import all services' ); - $this->assertSame( 'Bar!', $newServices->getService( 'Bar' ) ); - $this->assertSame( 'Car!', $newServices->getService( 'Car' ), 'Use existing service instance' ); - $this->assertSame( 'Xar!', $newServices->getService( 'Xar' ), 'Predefined services are kept' ); - } - - public function testLoadWiringFiles() { - $services = $this->newServiceContainer(); - - $wiringFiles = [ - __DIR__ . '/TestWiring1.php', - __DIR__ . '/TestWiring2.php', - ]; - - $services->loadWiringFiles( $wiringFiles ); - - $this->assertSame( 'Foo!', $services->getService( 'Foo' ) ); - $this->assertSame( 'Bar!', $services->getService( 'Bar' ) ); - } - - public function testLoadWiringFiles_fail_duplicate() { - $services = $this->newServiceContainer(); - - $wiringFiles = [ - __DIR__ . '/TestWiring1.php', - __DIR__ . '/./TestWiring1.php', - ]; - - // loading the same file twice should fail, because - $this->setExpectedException( Wikimedia\Services\ServiceAlreadyDefinedException::class ); - - $services->loadWiringFiles( $wiringFiles ); - } - - public function testRedefineService() { - $services = $this->newServiceContainer( [ 'Foo' ] ); - - $theService1 = new stdClass(); - $name = 'TestService92834576'; - - $services->defineService( $name, function () { - PHPUnit_Framework_Assert::fail( - 'The original instantiator function should not get called' - ); - } ); - - // redefine before instantiation - $services->redefineService( - $name, - function ( $actualLocator, $extra ) use ( $services, $theService1 ) { - PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); - PHPUnit_Framework_Assert::assertSame( 'Foo', $extra ); - return $theService1; - } - ); - - // force instantiation, check result - $this->assertSame( $theService1, $services->getService( $name ) ); - } - - public function testRedefineService_disabled() { - $services = $this->newServiceContainer( [ 'Foo' ] ); - - $theService1 = new stdClass(); - $name = 'TestService92834576'; - - $services->defineService( $name, function () { - return 'Foo'; - } ); - - // disable the service. we should be able to redefine it anyway. - $services->disableService( $name ); - - $services->redefineService( $name, function () use ( $theService1 ) { - return $theService1; - } ); - - // force instantiation, check result - $this->assertSame( $theService1, $services->getService( $name ) ); - } - - public function testRedefineService_fail_undefined() { - $services = $this->newServiceContainer(); - - $theService = new stdClass(); - $name = 'TestService92834576'; - - $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class ); - - $services->redefineService( $name, function () use ( $theService ) { - return $theService; - } ); - } - - public function testRedefineService_fail_in_use() { - $services = $this->newServiceContainer( [ 'Foo' ] ); - - $theService = new stdClass(); - $name = 'TestService92834576'; - - $services->defineService( $name, function () { - return 'Foo'; - } ); - - // create the service, so it can no longer be redefined - $services->getService( $name ); - - $this->setExpectedException( Wikimedia\Services\CannotReplaceActiveServiceException::class ); - - $services->redefineService( $name, function () use ( $theService ) { - return $theService; - } ); - } - - public function testAddServiceManipulator() { - $services = $this->newServiceContainer( [ 'Foo' ] ); - - $theService1 = new stdClass(); - $theService2 = new stdClass(); - $name = 'TestService92834576'; - - $services->defineService( - $name, - function ( $actualLocator, $extra ) use ( $services, $theService1 ) { - PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); - PHPUnit_Framework_Assert::assertSame( 'Foo', $extra ); - return $theService1; - } - ); - - $services->addServiceManipulator( - $name, - function ( - $theService, $actualLocator, $extra - ) use ( - $services, $theService1, $theService2 - ) { - PHPUnit_Framework_Assert::assertSame( $theService1, $theService ); - PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); - PHPUnit_Framework_Assert::assertSame( 'Foo', $extra ); - return $theService2; - } - ); - - // force instantiation, check result - $this->assertSame( $theService2, $services->getService( $name ) ); - } - - public function testAddServiceManipulator_fail_undefined() { - $services = $this->newServiceContainer(); - - $theService = new stdClass(); - $name = 'TestService92834576'; - - $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class ); - - $services->addServiceManipulator( $name, function () use ( $theService ) { - return $theService; - } ); - } - - public function testAddServiceManipulator_fail_in_use() { - $services = $this->newServiceContainer( [ 'Foo' ] ); - - $theService = new stdClass(); - $name = 'TestService92834576'; - - $services->defineService( $name, function () use ( $theService ) { - return $theService; - } ); - - // create the service, so it can no longer be redefined - $services->getService( $name ); - - $this->setExpectedException( Wikimedia\Services\CannotReplaceActiveServiceException::class ); - - $services->addServiceManipulator( $name, function () { - return 'Foo'; - } ); - } - - public function testDisableService() { - $services = $this->newServiceContainer( [ 'Foo' ] ); - - $destructible = $this->getMockBuilder( Wikimedia\Services\DestructibleService::class ) - ->getMock(); - $destructible->expects( $this->once() ) - ->method( 'destroy' ); - - $services->defineService( 'Foo', function () use ( $destructible ) { - return $destructible; - } ); - $services->defineService( 'Bar', function () { - return new stdClass(); - } ); - $services->defineService( 'Qux', function () { - return new stdClass(); - } ); - - // instantiate Foo and Bar services - $services->getService( 'Foo' ); - $services->getService( 'Bar' ); - - // disable service, should call destroy() once. - $services->disableService( 'Foo' ); - - // disabled service should still be listed - $this->assertContains( 'Foo', $services->getServiceNames() ); - - // getting other services should still work - $services->getService( 'Bar' ); - - // disable non-destructible service, and not-yet-instantiated service - $services->disableService( 'Bar' ); - $services->disableService( 'Qux' ); - - $this->assertNull( $services->peekService( 'Bar' ) ); - $this->assertNull( $services->peekService( 'Qux' ) ); - - // disabled service should still be listed - $this->assertContains( 'Bar', $services->getServiceNames() ); - $this->assertContains( 'Qux', $services->getServiceNames() ); - - $this->setExpectedException( Wikimedia\Services\ServiceDisabledException::class ); - $services->getService( 'Qux' ); - } - - public function testDisableService_fail_undefined() { - $services = $this->newServiceContainer(); - - $theService = new stdClass(); - $name = 'TestService92834576'; - - $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class ); - - $services->redefineService( $name, function () use ( $theService ) { - return $theService; - } ); - } - - public function testDestroy() { - $services = $this->newServiceContainer(); - - $destructible = $this->getMockBuilder( Wikimedia\Services\DestructibleService::class ) - ->getMock(); - $destructible->expects( $this->once() ) - ->method( 'destroy' ); - - $services->defineService( 'Foo', function () use ( $destructible ) { - return $destructible; - } ); - - $services->defineService( 'Bar', function () { - return new stdClass(); - } ); - - // create the service - $services->getService( 'Foo' ); - - // destroy the container - $services->destroy(); - - $this->setExpectedException( Wikimedia\Services\ContainerDisabledException::class ); - $services->getService( 'Bar' ); - } - -} diff --git a/tests/phpunit/includes/libs/services/TestWiring1.php b/tests/phpunit/includes/libs/services/TestWiring1.php deleted file mode 100644 index b6ff4eb3b4..0000000000 --- a/tests/phpunit/includes/libs/services/TestWiring1.php +++ /dev/null @@ -1,10 +0,0 @@ - function () { - return 'Foo!'; - }, -]; diff --git a/tests/phpunit/includes/libs/services/TestWiring2.php b/tests/phpunit/includes/libs/services/TestWiring2.php deleted file mode 100644 index dfff64f048..0000000000 --- a/tests/phpunit/includes/libs/services/TestWiring2.php +++ /dev/null @@ -1,10 +0,0 @@ - function () { - return 'Bar!'; - }, -]; diff --git a/tests/phpunit/includes/parser/ParserFactoryIntegrationTest.php b/tests/phpunit/includes/parser/ParserFactoryIntegrationTest.php new file mode 100644 index 0000000000..5bf4f3f5fb --- /dev/null +++ b/tests/phpunit/includes/parser/ParserFactoryIntegrationTest.php @@ -0,0 +1,56 @@ +createMock( 'Config' ); + $mockConfig->method( 'has' )->willReturn( true ); + $mockConfig->method( 'get' )->willReturn( 'I like otters.' ); + + $mocks = [ + [ 'the plural of platypus...' ], + $this->createMock( 'MagicWordFactory' ), + $this->createMock( 'Language' ), + '...is platypodes', + $this->createMock( 'MediaWiki\Special\SpecialPageFactory' ), + $mockConfig, + $this->createMock( 'MediaWiki\Linker\LinkRendererFactory' ), + ]; + + yield 'args_without_namespace_info' => [ + $mocks, + ]; + yield 'args_with_namespace_info' => [ + array_merge( $mocks, [ $this->createMock( 'NamespaceInfo' ) ] ), + ]; + } + + /** + * @dataProvider provideConstructorArguments + * @covers ParserFactory::__construct + */ + public function testBackwardsCompatibleConstructorArguments( $args ) { + $this->hideDeprecated( 'ParserFactory::__construct with Config parameter' ); + $factory = new ParserFactory( ...$args ); + $parser = $factory->create(); + + // It is expected that these are not present on the parser. + unset( $args[5] ); + unset( $args[0] ); + + foreach ( ( new ReflectionObject( $parser ) )->getProperties() as $prop ) { + $prop->setAccessible( true ); + foreach ( $args as $idx => $mockTest ) { + if ( $prop->getValue( $parser ) === $mockTest ) { + unset( $args[$idx] ); + } + } + } + + $this->assertCount( 0, $args, 'Not all arguments to the ParserFactory constructor were ' . + 'found in Parser member variables' ); + } +} diff --git a/tests/phpunit/includes/parser/ParserFactoryTest.php b/tests/phpunit/includes/parser/ParserFactoryTest.php deleted file mode 100644 index 048256d255..0000000000 --- a/tests/phpunit/includes/parser/ParserFactoryTest.php +++ /dev/null @@ -1,107 +0,0 @@ -assertSame( $instanceConstructor->getNumberOfParameters() - 1, - $factoryConstructor->getNumberOfParameters(), - 'Parser and ParserFactory constructors have an inconsistent number of parameters. ' . - 'Did you add a parameter to one and not the other?' ); - } - - public function testAllArgumentsWerePassed() { - $factoryConstructor = new ReflectionMethod( 'ParserFactory', '__construct' ); - $mocks = []; - foreach ( $factoryConstructor->getParameters() as $index => $param ) { - $type = (string)$param->getType(); - if ( $index === 0 ) { - $val = $this->createMock( 'MediaWiki\Config\ServiceOptions' ); - } elseif ( $type === 'array' ) { - $val = [ 'porcupines will tell me your secrets' . count( $mocks ) ]; - } elseif ( class_exists( $type ) || interface_exists( $type ) ) { - $val = $this->createMock( $type ); - } elseif ( $type === '' ) { - // Optimistically assume a string is okay - $val = 'I will de-quill them first' . count( $mocks ); - } else { - $this->fail( "Unrecognized parameter type $type in ParserFactory constructor" ); - } - $mocks[] = $val; - } - - $factory = new ParserFactory( ...$mocks ); - $parser = $factory->create(); - - foreach ( ( new ReflectionObject( $parser ) )->getProperties() as $prop ) { - $prop->setAccessible( true ); - foreach ( $mocks as $idx => $mock ) { - if ( $prop->getValue( $parser ) === $mock ) { - unset( $mocks[$idx] ); - } - } - } - - $this->assertCount( 0, $mocks, 'Not all arguments to the ParserFactory constructor were ' . - 'found in Parser member variables' ); - } - - public function provideConstructorArguments() { - // Create a mock Config object that will satisfy ServiceOptions::__construct - $mockConfig = $this->createMock( 'Config' ); - $mockConfig->method( 'has' )->willReturn( true ); - $mockConfig->method( 'get' )->willReturn( 'I like otters.' ); - - $mocks = [ - [ 'the plural of platypus...' ], - $this->createMock( 'MagicWordFactory' ), - $this->createMock( 'Language' ), - '...is platypodes', - $this->createMock( 'MediaWiki\Special\SpecialPageFactory' ), - $mockConfig, - $this->createMock( 'MediaWiki\Linker\LinkRendererFactory' ), - ]; - - yield 'args_without_namespace_info' => [ - $mocks, - ]; - yield 'args_with_namespace_info' => [ - array_merge( $mocks, [ $this->createMock( 'NamespaceInfo' ) ] ), - ]; - } - - /** - * @dataProvider provideConstructorArguments - * @covers ParserFactory::__construct - */ - public function testBackwardsCompatibleConstructorArguments( $args ) { - $this->hideDeprecated( 'ParserFactory::__construct with Config parameter' ); - $factory = new ParserFactory( ...$args ); - $parser = $factory->create(); - - // It is expected that these are not present on the parser. - unset( $args[5] ); - unset( $args[0] ); - - foreach ( ( new ReflectionObject( $parser ) )->getProperties() as $prop ) { - $prop->setAccessible( true ); - foreach ( $args as $idx => $mockTest ) { - if ( $prop->getValue( $parser ) === $mockTest ) { - unset( $args[$idx] ); - } - } - } - - $this->assertCount( 0, $args, 'Not all arguments to the ParserFactory constructor were ' . - 'found in Parser member variables' ); - } -} diff --git a/tests/phpunit/includes/preferences/DefaultPreferencesFactoryTest.php b/tests/phpunit/includes/preferences/DefaultPreferencesFactoryTest.php index a00eb3fcd0..e7f7067adb 100644 --- a/tests/phpunit/includes/preferences/DefaultPreferencesFactoryTest.php +++ b/tests/phpunit/includes/preferences/DefaultPreferencesFactoryTest.php @@ -1,7 +1,6 @@ method( $this->anythingBut( 'getValidNamespaces', '__destruct' ) ); return new DefaultPreferencesFactory( - new ServiceOptions( DefaultPreferencesFactory::$constructorOptions, $this->config ), + new LoggedServiceOptions( self::$serviceOptionsAccessLog, + DefaultPreferencesFactory::$constructorOptions, $this->config ), new Language(), AuthManager::singleton(), MediaWikiServices::getInstance()->getLinkRenderer(), @@ -237,4 +238,11 @@ class DefaultPreferencesFactoryTest extends \MediaWikiTestCase { $form->trySubmit(); $this->assertEquals( 12, $user->getOption( 'rclimit' ) ); } + + /** + * @coversNothing + */ + public function testAllServiceOptionsUsed() { + $this->assertAllServiceOptionsUsed( [ 'EnotifMinorEdits', 'EnotifRevealEditorAddress' ] ); + } } diff --git a/tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php b/tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php index dff18ca286..9d58cef71e 100644 --- a/tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php +++ b/tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php @@ -1,5 +1,6 @@ getNamespaceInfo()->getSubjectNamespaces(); + $this->assertConditions( + [ # expected + 'rc_namespace IN (' . $this->db->makeList( $namespaces ) . ')', + ], + [ + 'namespace' => 'all-contents', + ], + "rc conditions with all-contents" + ); + } + public function testRcHidemyselfFilter() { $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_NEW ); $this->overrideMwServices(); diff --git a/tests/phpunit/includes/title/NamespaceInfoTest.php b/tests/phpunit/includes/title/NamespaceInfoTest.php index c1e258dac0..028c43823d 100644 --- a/tests/phpunit/includes/title/NamespaceInfoTest.php +++ b/tests/phpunit/includes/title/NamespaceInfoTest.php @@ -9,6 +9,8 @@ use MediaWiki\Config\ServiceOptions; use MediaWiki\Linker\LinkTarget; class NamespaceInfoTest extends MediaWikiTestCase { + use TestAllServiceOptionsUsed; + /********************************************************************************************** * Shared code * %{ @@ -63,8 +65,11 @@ class NamespaceInfoTest extends MediaWikiTestCase { ]; private function newObj( array $options = [] ) : NamespaceInfo { - return new NamespaceInfo( new ServiceOptions( NamespaceInfo::$constructorOptions, - $options, self::$defaultOptions ) ); + return new NamespaceInfo( new LoggedServiceOptions( + self::$serviceOptionsAccessLog, + NamespaceInfo::$constructorOptions, + $options, self::$defaultOptions + ) ); } // %} End shared code @@ -1342,6 +1347,13 @@ class NamespaceInfoTest extends MediaWikiTestCase { } // %} End restriction levels + + /** + * @coversNothing + */ + public function testAllServiceOptionsUsed() { + $this->assertAllServiceOptionsUsed(); + } } /** diff --git a/tests/phpunit/unit/includes/FactoryArgTestTrait.php b/tests/phpunit/unit/includes/FactoryArgTestTrait.php new file mode 100644 index 0000000000..f7035b4d23 --- /dev/null +++ b/tests/phpunit/unit/includes/FactoryArgTestTrait.php @@ -0,0 +1,148 @@ +getInstanceClass(); + } + + /** + * Override if $factory->$method( ...$args ) isn't the right way to create an instance, where + * $method is returned from getFactoryMethodName(), and $args is constructed by applying + * getMockValueForParam() to the factory method's parameters. + * + * @param object $factory Factory object + * @return object Object created by factory + */ + protected function createInstanceFromFactory( $factory ) { + $methodName = $this->getFactoryMethodName(); + $methodObj = new ReflectionMethod( $factory, $methodName ); + $mocks = []; + foreach ( $methodObj->getParameters() as $param ) { + $mocks[] = $this->getMockValueForParam( $param ); + } + + return $factory->$methodName( ...$mocks ); + } + + public function testConstructorArgNum() { + $factoryClass = static::getFactoryClass(); + $instanceClass = static::getInstanceClass(); + $factoryConstructor = new ReflectionMethod( $factoryClass, '__construct' ); + $instanceConstructor = new ReflectionMethod( $instanceClass, '__construct' ); + $this->assertSame( + $instanceConstructor->getNumberOfParameters() - static::getExtraClassArgCount(), + $factoryConstructor->getNumberOfParameters(), + "$instanceClass and $factoryClass constructors have an inconsistent number of " . + ' parameters. Did you add a parameter to one and not the other?' ); + } + + /** + * Override if getMockValueForParam doesn't produce suitable values for one or more of the + * parameters to your factory constructor or create method. + * + * @param ReflectionParameter $param One of the factory constructor's arguments + * @return array Empty to not override, or an array of one element which is the value to pass + * that will allow the object to be constructed successfully + */ + protected function getOverriddenMockValueForParam( ReflectionParameter $param ) { + return []; + } + + /** + * Override if this doesn't produce suitable values for one or more of the parameters to your + * factory constructor or create method. + * + * @param ReflectionParameter $param One of the factory constructor's arguments + * @return mixed A value to pass that will allow the object to be constructed successfully + */ + protected function getMockValueForParam( ReflectionParameter $param ) { + $overridden = $this->getOverriddenMockValueForParam( $param ); + if ( $overridden ) { + return $overridden[0]; + } + + $pos = $param->getPosition(); + + $type = (string)$param->getType(); + + if ( $type === 'array' ) { + return [ "some unlikely string $pos" ]; + } + + if ( class_exists( $type ) || interface_exists( $type ) ) { + return $this->createMock( $type ); + } + + if ( $type === '' ) { + // Optimistically assume a string is okay + return "some unlikely string $pos"; + } + + $this->fail( "Unrecognized parameter type $type" ); + } + + /** + * Assert that the given $instance correctly received $val as the value for parameter $name. By + * default, checks that the instance has some member whose value is the same as $val. + * + * @param object $instance + * @param string $name Name of parameter to the factory object's constructor + * @param mixed $val + */ + protected function assertInstanceReceivedParam( $instance, $name, $val ) { + foreach ( ( new ReflectionObject( $instance ) )->getProperties() as $prop ) { + $prop->setAccessible( true ); + if ( $prop->getValue( $instance ) === $val ) { + $this->assertTrue( true ); + return; + } + } + + $this->assertFalse( true, "Param $name not received by " . static::getInstanceClass() ); + } + + public function testAllArgumentsWerePassed() { + $factoryClass = static::getFactoryClass(); + + $factoryConstructor = new ReflectionMethod( $factoryClass, '__construct' ); + $mocks = []; + foreach ( $factoryConstructor->getParameters() as $param ) { + $mocks[$param->getName()] = $this->getMockValueForParam( $param ); + } + + $instance = + $this->createInstanceFromFactory( new $factoryClass( ...array_values( $mocks ) ) ); + + foreach ( $mocks as $name => $mock ) { + $this->assertInstanceReceivedParam( $instance, $name, $mock ); + } + } +} diff --git a/tests/phpunit/unit/includes/libs/filebackend/fsfile/TempFSFileTestTrait.php b/tests/phpunit/unit/includes/libs/filebackend/fsfile/TempFSFileTestTrait.php new file mode 100644 index 0000000000..d32e01efcd --- /dev/null +++ b/tests/phpunit/unit/includes/libs/filebackend/fsfile/TempFSFileTestTrait.php @@ -0,0 +1,54 @@ +newFile(); + $this->assertTrue( file_exists( $file->getPath() ) ); + $file->purge(); + $this->assertFalse( file_exists( $file->getPath() ) ); + } + + /** + * @covers TempFSFile::__construct + * @covers TempFSFile::bind + * @covers TempFSFile::autocollect + * @covers TempFSFile::__destruct + */ + public function testBind() { + $file = $this->newFile(); + $path = $file->getPath(); + $this->assertTrue( file_exists( $path ) ); + $obj = new stdclass; + $file->bind( $obj ); + unset( $file ); + $this->assertTrue( file_exists( $path ) ); + unset( $obj ); + $this->assertFalse( file_exists( $path ) ); + } + + /** + * @covers TempFSFile::__construct + * @covers TempFSFile::preserve + * @covers TempFSFile::__destruct + */ + public function testPreserve() { + $file = $this->newFile(); + $path = $file->getPath(); + $this->assertTrue( file_exists( $path ) ); + $file->preserve(); + unset( $file ); + $this->assertTrue( file_exists( $path ) ); + Wikimedia\suppressWarnings(); + unlink( $path ); + Wikimedia\restoreWarnings(); + } +} diff --git a/tests/phpunit/unit/includes/libs/services/ServiceContainerTest.php b/tests/phpunit/unit/includes/libs/services/ServiceContainerTest.php new file mode 100644 index 0000000000..f9e820ac6b --- /dev/null +++ b/tests/phpunit/unit/includes/libs/services/ServiceContainerTest.php @@ -0,0 +1,523 @@ +newServiceContainer(); + $names = $services->getServiceNames(); + + $this->assertInternalType( 'array', $names ); + $this->assertEmpty( $names ); + + $name = 'TestService92834576'; + $services->defineService( $name, function () { + return null; + } ); + + $names = $services->getServiceNames(); + $this->assertContains( $name, $names ); + } + + public function testHasService() { + $services = $this->newServiceContainer(); + + $name = 'TestService92834576'; + $this->assertFalse( $services->hasService( $name ) ); + + $services->defineService( $name, function () { + return null; + } ); + + $this->assertTrue( $services->hasService( $name ) ); + } + + public function testGetService() { + $services = $this->newServiceContainer( [ 'Foo' ] ); + + $theService = new stdClass(); + $name = 'TestService92834576'; + $count = 0; + + $services->defineService( + $name, + function ( $actualLocator, $extra ) use ( $services, $theService, &$count ) { + $count++; + PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); + PHPUnit_Framework_Assert::assertSame( $extra, 'Foo' ); + return $theService; + } + ); + + $this->assertSame( $theService, $services->getService( $name ) ); + + $services->getService( $name ); + $this->assertSame( 1, $count, 'instantiator should be called exactly once!' ); + } + + public function testGetServiceRecursionCheck() { + $services = $this->newServiceContainer(); + + $services->defineService( 'service1', function ( ServiceContainer $services ) { + $services->getService( 'service2' ); + } ); + + $services->defineService( 'service2', function ( ServiceContainer $services ) { + $services->getService( 'service3' ); + } ); + + $services->defineService( 'service3', function ( ServiceContainer $services ) { + $services->getService( 'service1' ); + } ); + + $exceptionThrown = false; + try { + $services->getService( 'service1' ); + } catch ( RuntimeException $e ) { + $exceptionThrown = true; + $this->assertSame( 'Circular dependency when creating service! ' . + 'service1 -> service2 -> service3 -> service1', $e->getMessage() ); + } + $this->assertTrue( $exceptionThrown, 'RuntimeException must be thrown' ); + } + + public function testGetService_fail_unknown() { + $services = $this->newServiceContainer(); + + $name = 'TestService92834576'; + + $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class ); + + $services->getService( $name ); + } + + public function testPeekService() { + $services = $this->newServiceContainer(); + + $services->defineService( + 'Foo', + function () { + return new stdClass(); + } + ); + + $services->defineService( + 'Bar', + function () { + return new stdClass(); + } + ); + + // trigger instantiation of Foo + $services->getService( 'Foo' ); + + $this->assertInternalType( + 'object', + $services->peekService( 'Foo' ), + 'Peek should return the service object if it had been accessed before.' + ); + + $this->assertNull( + $services->peekService( 'Bar' ), + 'Peek should return null if the service was never accessed.' + ); + } + + public function testPeekService_fail_unknown() { + $services = $this->newServiceContainer(); + + $name = 'TestService92834576'; + + $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class ); + + $services->peekService( $name ); + } + + public function testDefineService() { + $services = $this->newServiceContainer(); + + $theService = new stdClass(); + $name = 'TestService92834576'; + + $services->defineService( $name, function ( $actualLocator ) use ( $services, $theService ) { + PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); + return $theService; + } ); + + $this->assertTrue( $services->hasService( $name ) ); + $this->assertSame( $theService, $services->getService( $name ) ); + } + + public function testDefineService_fail_duplicate() { + $services = $this->newServiceContainer(); + + $theService = new stdClass(); + $name = 'TestService92834576'; + + $services->defineService( $name, function () use ( $theService ) { + return $theService; + } ); + + $this->setExpectedException( Wikimedia\Services\ServiceAlreadyDefinedException::class ); + + $services->defineService( $name, function () use ( $theService ) { + return $theService; + } ); + } + + public function testApplyWiring() { + $services = $this->newServiceContainer(); + + $wiring = [ + 'Foo' => function () { + return 'Foo!'; + }, + 'Bar' => function () { + return 'Bar!'; + }, + ]; + + $services->applyWiring( $wiring ); + + $this->assertSame( 'Foo!', $services->getService( 'Foo' ) ); + $this->assertSame( 'Bar!', $services->getService( 'Bar' ) ); + } + + public function testImportWiring() { + $services = $this->newServiceContainer(); + + $wiring = [ + 'Foo' => function () { + return 'Foo!'; + }, + 'Bar' => function () { + return 'Bar!'; + }, + 'Car' => function () { + return 'FUBAR!'; + }, + ]; + + $services->applyWiring( $wiring ); + + $services->addServiceManipulator( 'Foo', function ( $service ) { + return $service . '+X'; + } ); + + $services->addServiceManipulator( 'Car', function ( $service ) { + return $service . '+X'; + } ); + + $newServices = $this->newServiceContainer(); + + // create a service with manipulator + $newServices->defineService( 'Foo', function () { + return 'Foo!'; + } ); + + $newServices->addServiceManipulator( 'Foo', function ( $service ) { + return $service . '+Y'; + } ); + + // create a service before importing, so we can later check that + // existing service instances survive importWiring() + $newServices->defineService( 'Car', function () { + return 'Car!'; + } ); + + // force instantiation + $newServices->getService( 'Car' ); + + // Define another service, so we can later check that extra wiring + // is not lost. + $newServices->defineService( 'Xar', function () { + return 'Xar!'; + } ); + + // import wiring, but skip `Bar` + $newServices->importWiring( $services, [ 'Bar' ] ); + + $this->assertNotContains( 'Bar', $newServices->getServiceNames(), 'Skip `Bar` service' ); + $this->assertSame( 'Foo!+Y+X', $newServices->getService( 'Foo' ) ); + + // import all wiring, but preserve existing service instance + $newServices->importWiring( $services ); + + $this->assertContains( 'Bar', $newServices->getServiceNames(), 'Import all services' ); + $this->assertSame( 'Bar!', $newServices->getService( 'Bar' ) ); + $this->assertSame( 'Car!', $newServices->getService( 'Car' ), 'Use existing service instance' ); + $this->assertSame( 'Xar!', $newServices->getService( 'Xar' ), 'Predefined services are kept' ); + } + + public function testLoadWiringFiles() { + $services = $this->newServiceContainer(); + + $wiringFiles = [ + __DIR__ . '/TestWiring1.php', + __DIR__ . '/TestWiring2.php', + ]; + + $services->loadWiringFiles( $wiringFiles ); + + $this->assertSame( 'Foo!', $services->getService( 'Foo' ) ); + $this->assertSame( 'Bar!', $services->getService( 'Bar' ) ); + } + + public function testLoadWiringFiles_fail_duplicate() { + $services = $this->newServiceContainer(); + + $wiringFiles = [ + __DIR__ . '/TestWiring1.php', + __DIR__ . '/./TestWiring1.php', + ]; + + // loading the same file twice should fail, because + $this->setExpectedException( Wikimedia\Services\ServiceAlreadyDefinedException::class ); + + $services->loadWiringFiles( $wiringFiles ); + } + + public function testRedefineService() { + $services = $this->newServiceContainer( [ 'Foo' ] ); + + $theService1 = new stdClass(); + $name = 'TestService92834576'; + + $services->defineService( $name, function () { + PHPUnit_Framework_Assert::fail( + 'The original instantiator function should not get called' + ); + } ); + + // redefine before instantiation + $services->redefineService( + $name, + function ( $actualLocator, $extra ) use ( $services, $theService1 ) { + PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); + PHPUnit_Framework_Assert::assertSame( 'Foo', $extra ); + return $theService1; + } + ); + + // force instantiation, check result + $this->assertSame( $theService1, $services->getService( $name ) ); + } + + public function testRedefineService_disabled() { + $services = $this->newServiceContainer( [ 'Foo' ] ); + + $theService1 = new stdClass(); + $name = 'TestService92834576'; + + $services->defineService( $name, function () { + return 'Foo'; + } ); + + // disable the service. we should be able to redefine it anyway. + $services->disableService( $name ); + + $services->redefineService( $name, function () use ( $theService1 ) { + return $theService1; + } ); + + // force instantiation, check result + $this->assertSame( $theService1, $services->getService( $name ) ); + } + + public function testRedefineService_fail_undefined() { + $services = $this->newServiceContainer(); + + $theService = new stdClass(); + $name = 'TestService92834576'; + + $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class ); + + $services->redefineService( $name, function () use ( $theService ) { + return $theService; + } ); + } + + public function testRedefineService_fail_in_use() { + $services = $this->newServiceContainer( [ 'Foo' ] ); + + $theService = new stdClass(); + $name = 'TestService92834576'; + + $services->defineService( $name, function () { + return 'Foo'; + } ); + + // create the service, so it can no longer be redefined + $services->getService( $name ); + + $this->setExpectedException( Wikimedia\Services\CannotReplaceActiveServiceException::class ); + + $services->redefineService( $name, function () use ( $theService ) { + return $theService; + } ); + } + + public function testAddServiceManipulator() { + $services = $this->newServiceContainer( [ 'Foo' ] ); + + $theService1 = new stdClass(); + $theService2 = new stdClass(); + $name = 'TestService92834576'; + + $services->defineService( + $name, + function ( $actualLocator, $extra ) use ( $services, $theService1 ) { + PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); + PHPUnit_Framework_Assert::assertSame( 'Foo', $extra ); + return $theService1; + } + ); + + $services->addServiceManipulator( + $name, + function ( + $theService, $actualLocator, $extra + ) use ( + $services, $theService1, $theService2 + ) { + PHPUnit_Framework_Assert::assertSame( $theService1, $theService ); + PHPUnit_Framework_Assert::assertSame( $services, $actualLocator ); + PHPUnit_Framework_Assert::assertSame( 'Foo', $extra ); + return $theService2; + } + ); + + // force instantiation, check result + $this->assertSame( $theService2, $services->getService( $name ) ); + } + + public function testAddServiceManipulator_fail_undefined() { + $services = $this->newServiceContainer(); + + $theService = new stdClass(); + $name = 'TestService92834576'; + + $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class ); + + $services->addServiceManipulator( $name, function () use ( $theService ) { + return $theService; + } ); + } + + public function testAddServiceManipulator_fail_in_use() { + $services = $this->newServiceContainer( [ 'Foo' ] ); + + $theService = new stdClass(); + $name = 'TestService92834576'; + + $services->defineService( $name, function () use ( $theService ) { + return $theService; + } ); + + // create the service, so it can no longer be redefined + $services->getService( $name ); + + $this->setExpectedException( Wikimedia\Services\CannotReplaceActiveServiceException::class ); + + $services->addServiceManipulator( $name, function () { + return 'Foo'; + } ); + } + + public function testDisableService() { + $services = $this->newServiceContainer( [ 'Foo' ] ); + + $destructible = $this->getMockBuilder( Wikimedia\Services\DestructibleService::class ) + ->getMock(); + $destructible->expects( $this->once() ) + ->method( 'destroy' ); + + $services->defineService( 'Foo', function () use ( $destructible ) { + return $destructible; + } ); + $services->defineService( 'Bar', function () { + return new stdClass(); + } ); + $services->defineService( 'Qux', function () { + return new stdClass(); + } ); + + // instantiate Foo and Bar services + $services->getService( 'Foo' ); + $services->getService( 'Bar' ); + + // disable service, should call destroy() once. + $services->disableService( 'Foo' ); + + // disabled service should still be listed + $this->assertContains( 'Foo', $services->getServiceNames() ); + + // getting other services should still work + $services->getService( 'Bar' ); + + // disable non-destructible service, and not-yet-instantiated service + $services->disableService( 'Bar' ); + $services->disableService( 'Qux' ); + + $this->assertNull( $services->peekService( 'Bar' ) ); + $this->assertNull( $services->peekService( 'Qux' ) ); + + // disabled service should still be listed + $this->assertContains( 'Bar', $services->getServiceNames() ); + $this->assertContains( 'Qux', $services->getServiceNames() ); + + $this->setExpectedException( Wikimedia\Services\ServiceDisabledException::class ); + $services->getService( 'Qux' ); + } + + public function testDisableService_fail_undefined() { + $services = $this->newServiceContainer(); + + $theService = new stdClass(); + $name = 'TestService92834576'; + + $this->setExpectedException( Wikimedia\Services\NoSuchServiceException::class ); + + $services->redefineService( $name, function () use ( $theService ) { + return $theService; + } ); + } + + public function testDestroy() { + $services = $this->newServiceContainer(); + + $destructible = $this->getMockBuilder( Wikimedia\Services\DestructibleService::class ) + ->getMock(); + $destructible->expects( $this->once() ) + ->method( 'destroy' ); + + $services->defineService( 'Foo', function () use ( $destructible ) { + return $destructible; + } ); + + $services->defineService( 'Bar', function () { + return new stdClass(); + } ); + + // create the service + $services->getService( 'Foo' ); + + // destroy the container + $services->destroy(); + + $this->setExpectedException( Wikimedia\Services\ContainerDisabledException::class ); + $services->getService( 'Bar' ); + } + +} diff --git a/tests/phpunit/unit/includes/libs/services/TestWiring1.php b/tests/phpunit/unit/includes/libs/services/TestWiring1.php new file mode 100644 index 0000000000..b6ff4eb3b4 --- /dev/null +++ b/tests/phpunit/unit/includes/libs/services/TestWiring1.php @@ -0,0 +1,10 @@ + function () { + return 'Foo!'; + }, +]; diff --git a/tests/phpunit/unit/includes/libs/services/TestWiring2.php b/tests/phpunit/unit/includes/libs/services/TestWiring2.php new file mode 100644 index 0000000000..dfff64f048 --- /dev/null +++ b/tests/phpunit/unit/includes/libs/services/TestWiring2.php @@ -0,0 +1,10 @@ + function () { + return 'Bar!'; + }, +]; diff --git a/tests/phpunit/unit/includes/page/MovePageFactoryTest.php b/tests/phpunit/unit/includes/page/MovePageFactoryTest.php new file mode 100644 index 0000000000..99fc631c78 --- /dev/null +++ b/tests/phpunit/unit/includes/page/MovePageFactoryTest.php @@ -0,0 +1,23 @@ +getPosition() === 0 ) { + return [ $this->createMock( MediaWiki\Config\ServiceOptions::class ) ]; + } + return []; + } +}