Merge "Tests: Set dbname for DatabaseSqliteTest"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 20 Aug 2019 16:51:53 +0000 (16:51 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 20 Aug 2019 16:51:53 +0000 (16:51 +0000)
95 files changed:
RELEASE-NOTES-1.34
autoload.php
docs/hooks.txt
docs/pageupdater.txt
includes/DefaultSettings.php
includes/MediaWikiServices.php
includes/MovePage.php
includes/ServiceWiring.php
includes/Title.php
includes/api/ApiMove.php
includes/cache/MessageCache.php
includes/changes/ChangesList.php
includes/jobqueue/jobs/ActivityUpdateJob.php
includes/libs/objectcache/APCBagOStuff.php
includes/libs/objectcache/APCUBagOStuff.php
includes/libs/objectcache/CachedBagOStuff.php
includes/libs/objectcache/EmptyBagOStuff.php
includes/libs/objectcache/HashBagOStuff.php
includes/libs/objectcache/MediumSpecificBagOStuff.php
includes/libs/objectcache/MemcachedPeclBagOStuff.php
includes/libs/objectcache/MemcachedPhpBagOStuff.php
includes/libs/objectcache/RESTBagOStuff.php
includes/libs/objectcache/RedisBagOStuff.php
includes/libs/objectcache/WinCacheBagOStuff.php
includes/libs/rdbms/database/DatabaseMysqli.php
includes/libs/services/ServiceContainer.php
includes/logging/LogEntry.php
includes/objectcache/ObjectCache.php
includes/objectcache/SqlBagOStuff.php
includes/page/MovePageFactory.php [new file with mode: 0644]
includes/parser/Parser.php
includes/registration/ExtensionRegistry.php
includes/resourceloader/ResourceLoaderFileModule.php
includes/skins/BaseTemplate.php
includes/specialpage/ChangesListSpecialPage.php
includes/specials/SpecialApiSandbox.php
includes/specials/SpecialJavaScriptTest.php
includes/specials/SpecialMovepage.php
includes/title/MediaWikiTitleCodec.php
languages/i18n/ar.json
languages/i18n/da.json
languages/i18n/en.json
languages/i18n/fr.json
languages/i18n/ja.json
languages/i18n/jv.json
languages/i18n/lv.json
languages/i18n/min.json
languages/i18n/nap.json
languages/i18n/nds.json
languages/i18n/nqo.json
languages/i18n/pt-br.json
languages/i18n/qqq.json
languages/i18n/ro.json
languages/i18n/roa-tara.json
languages/i18n/sd.json
languages/i18n/sh.json
languages/i18n/sl.json
languages/i18n/sv.json
languages/i18n/th.json
languages/i18n/vec.json
languages/i18n/zh-hant.json
maintenance/cleanupCaps.php
maintenance/createAndPromote.php
maintenance/mctest.php
maintenance/moveBatch.php
resources/Resources.php
resources/src/mediawiki.rcfilters/Controller.js
resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js
tests/common/TestsAutoLoader.php
tests/parser/ParserTestRunner.php
tests/phpunit/MediaWikiIntegrationTestCase.php
tests/phpunit/includes/MessageTest.php
tests/phpunit/includes/MovePageTest.php
tests/phpunit/includes/TitleTest.php
tests/phpunit/includes/block/BlockManagerTest.php
tests/phpunit/includes/cache/MessageCacheTest.php
tests/phpunit/includes/config/LoggedServiceOptions.php [new file with mode: 0644]
tests/phpunit/includes/config/TestAllServiceOptionsUsed.php [new file with mode: 0644]
tests/phpunit/includes/libs/filebackend/fsfile/TempFSFileIntegrationTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/objectcache/BagOStuffTest.php
tests/phpunit/includes/libs/services/ServiceContainerTest.php [deleted file]
tests/phpunit/includes/libs/services/TestWiring1.php [deleted file]
tests/phpunit/includes/libs/services/TestWiring2.php [deleted file]
tests/phpunit/includes/parser/ParserFactoryIntegrationTest.php [new file with mode: 0644]
tests/phpunit/includes/parser/ParserFactoryTest.php [deleted file]
tests/phpunit/includes/preferences/DefaultPreferencesFactoryTest.php
tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php
tests/phpunit/includes/title/NamespaceInfoTest.php
tests/phpunit/unit/includes/FactoryArgTestTrait.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/filebackend/fsfile/TempFSFileTestTrait.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/services/ServiceContainerTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/services/TestWiring1.php [new file with mode: 0644]
tests/phpunit/unit/includes/libs/services/TestWiring2.php [new file with mode: 0644]
tests/phpunit/unit/includes/page/MovePageFactoryTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/parser/ParserFactoryTest.php [new file with mode: 0644]

index 0c6fec2..9a4aeb3 100644 (file)
@@ -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 ===
 * …
index 52a5eda..acdf8dd 100644 (file)
@@ -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',
index d832012..6207b12 100644 (file)
@@ -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
index fd084c0..3d113f6 100644 (file)
@@ -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
index 208cfe6..f2446da 100644 (file)
@@ -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;
index 7fda452..bb9c05f 100644 (file)
@@ -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
index 832e24a..a63eeae 100644 (file)
  * @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();
index c192b5a..0b4ce4a 100644 (file)
@@ -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() ) );
index 281f75b..f681852 100644 (file)
@@ -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() ) {
index 540860b..0a788d4 100644 (file)
@@ -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;
index 5745451..93fdb16 100644 (file)
@@ -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();
        }
 
        /**
index e2b35a8..7807877 100644 (file)
@@ -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 ) {
index 4de72a9..d27056d 100644 (file)
@@ -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;
index 0954ac8..aa83b1f 100644 (file)
@@ -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 ),
index 021cdf7..80383d1 100644 (file)
@@ -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 ),
index 0ab26c9..9fa9a89 100644 (file)
@@ -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;
index dab8ba1..b2613b2 100644 (file)
@@ -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;
        }
 
index 1cfa0c7..b4087be 100644 (file)
@@ -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
                }
index 62a8aec..329e600 100644 (file)
@@ -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
index cc7ee2a..3df483d 100644 (file)
@@ -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(
index b1d5d29..8144231 100644 (file)
@@ -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 ),
index aa4a9b3..b8ce38b 100644 (file)
@@ -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 );
index f75d3a1..252b1fa 100644 (file)
@@ -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;
index 0e4e3fb..3c4efbb 100644 (file)
  * @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
                }
index 2f9abcf..8931ae2 100644 (file)
@@ -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 <https://cloud.google.com/sql/docs/mysql/connect-app-engine>
+               //
+               // 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 );
                }
 
index d1f1052..84755ed 100644 (file)
@@ -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 );
index 17f72bd..ce68a91 100644 (file)
@@ -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
         */
index 3bb0771..ad0f67e 100644 (file)
@@ -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] ) ) {
index e97dc41..d9fe319 100644 (file)
@@ -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 (file)
index 0000000..26da151
--- /dev/null
@@ -0,0 +1,91 @@
+<?php
+
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Page;
+
+use MediaWiki\Config\ServiceOptions;
+use MediaWiki\Permissions\PermissionManager;
+use MovePage;
+use NamespaceInfo;
+use RepoGroup;
+use Title;
+use WatchedItemStore;
+use Wikimedia\Rdbms\LoadBalancer;
+
+/**
+ * @since 1.34
+ */
+class MovePageFactory {
+       /** @var ServiceOptions */
+       private $options;
+
+       /** @var LoadBalancer */
+       private $loadBalancer;
+
+       /** @var NamespaceInfo */
+       private $nsInfo;
+
+       /** @var WatchedItemStore */
+       private $watchedItems;
+
+       /** @var PermissionManager */
+       private $permMgr;
+
+       /** @var RepoGroup */
+       private $repoGroup;
+
+       /**
+        * @todo Make this a const when we drop HHVM support (T192166)
+        * @var array
+        */
+       public static $constructorOptions = [
+               'CategoryCollation',
+               'ContentHandlerUseDB',
+       ];
+
+       public function __construct(
+               ServiceOptions $options,
+               LoadBalancer $loadBalancer,
+               NamespaceInfo $nsInfo,
+               WatchedItemStore $watchedItems,
+               PermissionManager $permMgr,
+               RepoGroup $repoGroup
+       ) {
+               $options->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 );
+       }
+}
index d14db03..b643c3f 100644 (file)
@@ -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();
        }
 
index 9cae73c..3e65f6c 100644 (file)
@@ -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();
                }
index eed2aed..d308d50 100644 (file)
@@ -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
index cd79259..cad69a5 100644 (file)
@@ -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 ) {
index bbbd6a8..2fa8fab 100644 (file)
@@ -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 );
+       }
 }
index 034e569..9e49684 100644 (file)
@@ -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',
index 8c137aa..50a909f 100644 (file)
@@ -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 <https://github.com/karma-runner/karma-qunit/issues/27>.
                        . 'window.__karma__.loaded = function () {};'
                        . '}';
index 161b41a..da34d81 100644 (file)
@@ -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(
index 7e7d85a..3bd66d4 100644 (file)
@@ -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
index 8e3fd79..60f54e2 100644 (file)
        "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 صفحة}} ولن يتم نقل المزيد تلقائيا.",
        "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": "لا يمكن نقل غير الملفات إلى نطاق الملفات",
index 0d88510..10d1709 100644 (file)
        "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:<em>$2</em>\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:<em>$2</em>\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",
        "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\".",
        "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",
        "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)",
        "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",
index 7bcda5b..8988419 100644 (file)
        "rcfilters-filter-showlinkedto-label": "Show changes on pages linking to",
        "rcfilters-filter-showlinkedto-option-label": "<strong>Pages linking to</strong> 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 <strong>$3, $4</strong> (up to <strong>$1</strong> shown).",
        "rclistfromreset": "Reset date selection",
        "rclistfrom": "Show new changes starting from $2, $3",
        "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.",
        "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.",
index 52ed8af..27ac340 100644 (file)
        "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.",
        "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.",
index c2e544f..65f2f8e 100644 (file)
        "specialmute-label-mute-email": "この利用者からのウィキメールをミュートする",
        "specialmute-header": "<b>{{BIDI:[[User:$1|$1]]}}</b>さんに対するミュートを個人設定で選択してください。",
        "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の編集権限は、最近<code>editinterface</code> 権限から分離されました。なぜこのエラーが表示されたのかわからない場合は、[[mw:MediaWiki_1.32/interface-admin]]をご覧ください。",
index 8bf1a30..4b23cf2 100644 (file)
        "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é.",
index db50961..a427820 100644 (file)
@@ -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",
        "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:",
        "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",
index 1272347..110841f 100644 (file)
        "virus-badscanner": "Kasalahan konfigurasi: pamindai virus indak dikenal: ''$1''",
        "virus-scanfailed": "Pamindaian gagal (kode $1)",
        "virus-unknownscanner": "Antivirus indak dikenal:",
-       "logouttext": "<strong>Sanak alah kalua log</strong\n\nMohon diingek kalau babarapo laman mungkin masih tampil cando Sanak alun kalua log. Silakan untuak mambarasiahan singgahan panjalajah web Sanak.",
+       "logouttext": "<strong>Sanak alah kalua log</strong>\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!",
index 96e06fa..c75710f 100644 (file)
        "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. <strong>Pe' piacere cuntrullate si site ancora dinto e tentate n'ata vota</strong>.\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.",
        "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",
        "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.",
index 192a852..d3983c8 100644 (file)
        "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}}<br />(süh ok de [[Special:NewPages|List mit ne'e Sieden]])",
+       "rcfilters-search-placeholder-mobile": "Filters",
        "rcnotefrom": "Dit sünd de Ännern siet <b>$2</b> (bet to <b>$1</b> wiest).",
        "rclistfrom": "Wies ne’e Ännern siet $3 $2",
        "rcshowhideminor": "lütte Ännern $1",
index 6904c4c..92fb9bb 100644 (file)
        "metadata-fields": "ߟߐ߲ߕߊߞߐ߫ ߖߌ߬ߦߊ߬ߓߍ ߞߣߍ ߡߍ߲ ߦߋ߫ ߗߋߛߓߍ ߣߌ߲߬ ߘߐ߫߸ ߏ߬ ߘߌ߫ ߣߊ߬ ߥߟߏ߫ ߖߌ߬ߦߊ߬ߓߍ ߞߐߜߍ ߘߐ߫ ߣߌ߫ ߟߐ߲ߕߊߞߐ߫ ߥߟߊ߬ߟߋ߲ ߠߊߘߐ߯ߦߊ߫ ߘߊ߫. ߊ߬ ߕߐ߭ ߟߎ߬ ߢߡߊߘߏ߲߰ߣߍ߲ ߘߌ߫ ߕߏ߫ ߝߍ߭ ߞߏߛߐ߲߬.\n•ߊ߬ ߞߍ߫ \n•ߛߎ߯ߦߊ \n•ߕߎ߬ߡߊ߬ߘߊ ߣߌ߫ ߕߎ߬ߡߊ߬ߙߋ߲߫ ߓߐߛߎ߲ߡߊ \n•ߟߊ߬ߝߏߦߌ ߕߎ߬ߡߊ߬ߘߊ߬ ߖߐ߲ߖߐ߲ \n•ߞ ߝߙߍߕߍ \n•ߡ.ߛ.ߛ ߞߊߟߌߦߊ ߡߐ߬ߟߐ߲߬ߦߊ߬ߟߌ \n•ߕߊߞߎ߲ߡߊ ߥߊ߲߬ߥߊ߲ \n•ߞߎ߬ߛߊ߲ \n•ߓߊߦߟߍߡߊ߲ ߤߊߞߍ  ߘߞߖ \n•ߖߌ߬ߦߊ߬ߓߍ ߞߊ߲߬ߛߓߍ\n•ߘߟߊߕߍ߮ ߘߞߖ (ߘߊ߲߬ߠߊ߬ߕߍ߰ ߞߊ߲ߞߋ߫ ߖߊ߯ߓߡߊ)\n•ߘߎ߰ߕߍߟߍ߲ ߘߞߖ (ߘߊ߲߬ߠߊ߬ߕߍ߰ ߞߊ߲ߞߋ߫ ߖߊ߯ߓߡߊ)\n•ߞߐߓߋ ߘߞߖ (ߘߊ߲߬ߠߊ߬ߕߍ߰ ߞߊ߲ߞߋ߫ ߖߊ߯ߓߡߊ)",
        "namespacesall": "ߊ߬ ߓߍ߯",
        "monthsall": "ߡߎ߰ߡߍ",
+       "scarytranscludetoolong": "[URL ߖߊ߰ߡߊ߲߬ ߞߏߖߎ߰]",
+       "deletedwhileediting": "<strong>ߖߊ߲߬ߓߌ߬ߟߊ߬ߟߌ</strong> ߞߐߜߍ ߣߌ߲߬ ߕߎ߲߬ ߓߘߊ߫ ߖߏ߰ߛߌ߫ ߊ߬ ߡߊߦߟߍ߬ߡߊ߲ ߘߊߡߌ߬ߣߊ ߞߐ߫ ߌ ߓߟߏ߫.",
+       "recreate": "ߊ߬ ߟߊߘߊ߲߫ ߕߎ߲߯",
+       "confirm_purge_button": "ߏ߬ߞߍ߫",
        "parentheses-start": "⸜",
        "parentheses-end": "⸝",
        "imgmultipagenext": "ߞߐߜߍ ߣߊ߬ߕߐ ←",
index d7167ba..f6491eb 100644 (file)
        "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.",
        "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",
index a2ee29a..86933e3 100644 (file)
        "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}}.",
        "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]]",
        "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}}",
index edb642a..add3803 100644 (file)
        "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",
        "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:<em>$2</em>\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:<em>$2</em>\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": "<strong>Numele dumneavoastră de utilizator sau adresa IP au fost blocate.</strong>\nMotivul indicat este:\n\n:<em>$2</em>\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ă",
        "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ță",
        "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",
        "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ă",
        "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):",
        "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}}",
        "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)",
        "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",
        "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.",
        "restrictionsfield-help": "O adresă IP sau gamă CIDR pe linie. Pentru a activa tot, folosiți:<pre>0.0.0.0/0\n::/0</pre>",
        "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 <code>editinterface</code>. Dacă nu înțelegeți de ce primiți această eroare, vedeți [[mw:MediaWiki_1.32/interface-admin]].",
        "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?"
 }
index 585a532..c4fa153 100644 (file)
        "blockedtext": "<strong>'U nome de l'utende o l'indirizze IP ha state bloccate.</strong>\n\n'U blocche ha state fatte da $1.\n'U mutive date jè <em>$2</em>.\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à.",
        "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.<br />\nTu na promettere pure ca le cose ca scrive tu, sonde 'mbormaziune libbere o copiete da 'nu pubbleche dominie.<br />\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à.<br />\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!'''",
index 4d575f4..0719083 100644 (file)
        "rcfilters-filter-watchlistactivity-seen-label": "ڏٺل سنوارون",
        "rcfilters-filtergroup-changetype": "تبديليءَ جو قِسم",
        "rcfilters-filter-pageedits-label": "صفحي سنوارون",
-       "rcfilters-filter-newpages-label": "صÙ\81Ø­Ù\8a ØªØ®Ù\84Ù\8aÙ\82ون",
+       "rcfilters-filter-newpages-label": "صÙ\81Ø­Ù\8a Ø³Ø±Ø¬Ø§Ù\8aون",
        "rcfilters-filter-newpages-description": "نوان صفحا ٺاھيندڙ سنوارون.",
        "rcfilters-filter-categorization-label": "زمري ۾ تبديليون",
        "rcfilters-filter-logactions-label": "لاگڊ عمل",
        "sp-contributions-search": "ڀاڱيدارين لاءِ ڳولا ڪريو",
        "sp-contributions-username": "آءِپي پتو يا واپرائيندڙ-نانءُ:",
        "sp-contributions-toponly": "صرف اھي سنوارون ڏيکاريو جيڪي تازا ترين مسودا آھن",
-       "sp-contributions-newonly": "صرف اھي سنوارون ڏيکاريو جيڪي صرف صفحي تخليقون آھن",
+       "sp-contributions-newonly": "صرف اھي سنوارون ڏيکاريو جيڪي صفحي سرجايون آھن",
        "sp-contributions-hideminor": "معمولي سنوارون لڪايو",
        "sp-contributions-submit": "ڳوليو",
        "whatlinkshere": "هتان ڇا ڳنڍيل آهي",
index b231f89..e0f4f5c 100644 (file)
        "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?"
 }
index 7cfbd84..a65feb1 100644 (file)
        "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.",
        "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",
index 2f330b3..5129716 100644 (file)
        "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",
index 560eeb6..873fae3 100644 (file)
        "title-invalid-talk-namespace": "ชื่อเรื่องหน้าที่ขออ้างถึงหน้าพูดคุยซึ่งมีไม่ได้",
        "title-invalid-characters": "ชื่อเรื่องหน้าที่ขอมีอักขระไม่สมเหตุสมผล: \"$1\"",
        "title-invalid-relative": "ชื่อเรื่องมีเส้นทางสัมพัทธ์ ชื่อเรื่องหน้าสัมพัทธ์ (./, ../) ไม่สมเหตุสมผล เพราะมักจะเข้าถึงไม่ได้เมื่อจัดการด้วยเบราว์เซอร์ของผู้ใช้",
-       "title-invalid-magic-tilde": "à¸\8aืà¹\88อà¹\80รืà¹\88อà¸\87หà¸\99à¹\89าà¸\97ีà¹\88à¸\82อมีลำà¸\94ัà¸\9aà¸\97ิลà¸\94าà¹\80มà¸\88ิà¸\81à¹\84มà¹\88สมà¹\80หà¸\95ุสมà¸\9cล (<nowiki>~~~</nowiki>)",
+       "title-invalid-magic-tilde": "à¸\8aืà¹\88อหà¸\99à¹\89าà¸\97ีà¹\88รà¹\89อà¸\87à¸\82อมีลำà¸\94ัà¸\9aà¸\97ิลà¹\80à¸\94อà¸\9eิà¹\80ศษà¸\97ีà¹\88à¹\83à¸\8aà¹\89à¹\84มà¹\88à¹\84à¸\94à¹\89 (<nowiki>~~~</nowiki>)",
        "title-invalid-too-long": "ชื่อเรื่องหน้าที่ขอยาวเกินไป ไม่สามารถยาวกว่า $1 ไบต์ในการเข้ารหัส UTF-8",
        "title-invalid-leading-colon": "ชื่อเรื่องหน้าที่ขอขึ้นต้นด้วยโคลอนไม่สมเหตุสมผล",
        "perfcached": "ข้อมูลต่อไปนี้ถูกเก็บในแคชและอาจล้าสมัย มีผลการค้นหาสูงสุด $1 รายการในแคช",
        "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": "ปูม",
        "trackingcategories-desc": "เกณฑ์การรวมหมวดหมู่",
        "restricted-displaytitle-ignored": "หน้าที่มีชื่อเรื่องแสดงที่ถูกละเลย",
        "restricted-displaytitle-ignored-desc": "หน้ามี <code><nowiki>{{DISPLAYTITLE}}</nowiki></code> ที่ถูกละเลย เพราะไม่สมนัยกับชื่อเรื่องแท้จริงของหน้า",
-       "noindex-category-desc": "à¹\82รà¸\9aอà¸\95à¹\84มà¹\88สามารà¸\96à¸\97ำà¸\94ัà¸\8aà¸\99ีหà¸\99à¹\89าà¸\99ีà¹\89à¹\80à¸\9eราะมีà¹\80มà¸\88ิà¸\81à¹\80วิรà¹\8cà¸\94 <code><nowiki>__NOINDEX__</nowiki></code> อยู่และอยู่ในเนมสเปซซึ่งอนุญาตตัวบ่งชี้นี้",
+       "noindex-category-desc": "à¸\9aอà¸\95à¹\84มà¹\88สามารà¸\96à¸\97ำà¸\94ัà¸\8aà¸\99ีหà¸\99à¹\89าà¸\99ีà¹\89à¹\80à¸\9eราะมีà¸\84ำสัà¹\88à¸\87à¸\9eิà¹\80ศษ <code><nowiki>__NOINDEX__</nowiki></code> อยู่และอยู่ในเนมสเปซซึ่งอนุญาตตัวบ่งชี้นี้",
        "index-category-desc": "หน้านี้มี <code><nowiki>__INDEX__</nowiki></code> อยู่ (และอยู่ในเนมสเปซซึ่งอนุญาตตัวบ่งชี้นี้) ฉะนั้น โรบอตจึงทำดัชนี้ได้ ซึ่งปกติไม่สามารถทำได้",
        "post-expand-template-inclusion-category-desc": "การแทนที่แม่แบบทั้งหมดทำให้ขนาดของหน้าใหญ่กว่า <code>$wgMaxArticleSize</code> จึงไม่มีการแทนที่แม่แบบบางตัว",
        "post-expand-template-argument-category-desc": "หน้านี้มีขนาดใหญ่กว่า <code>$wgMaxArticleSize</code> หลังจากขยายอาร์กิวเมนต์ของแม่แบบ (สิ่งที่อยู่ภายในวงเล็บปีกกาสามวง เช่น <code>{{{Foo}}}</code>)",
index ff50d0b..49b249b 100644 (file)
        "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",
index 1645687..c5641c9 100644 (file)
        "userlogin-resetpassword-link": "忘記密碼?",
        "userlogin-helplink2": "登入說明",
        "userlogin-loggedin": "您目前已登入 {{GENDER:$1|$1}} 使用者,\n請使用下列表單改登入另一位使用者。",
-       "userlogin-reauth": "æ\82¨å¿\85é \88å\86\8dç\99»å\85¥ä¸\80次ä¾\86é©\97証æ\82¨ç\82º {{GENDER:$1|$1}}。",
+       "userlogin-reauth": "æ\82¨å¿\85é \88å\86\8dç\99»å\85¥ä¸\80次ä¾\86é©\97è­\89æ\82¨ç\82º{{GENDER:$1|$1}}。",
        "userlogin-createanother": "建立另一個帳號",
        "createacct-emailrequired": "電子郵件地址",
        "createacct-emailoptional": "電子郵件地址(選填)",
        "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 頁,超出限制的頁面將不會自動移動。",
        "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": "不可以移動非檔案到檔案命名空間",
index 20be9fd..da241e5 100644 (file)
@@ -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" );
index da9b4d6..505168e 100644 (file)
@@ -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,
index 9548d6b..6b1cdc3 100644 (file)
@@ -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(
index 47828e6..09f3120 100644 (file)
@@ -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' ) );
index eaf720c..d33e3de 100644 (file)
@@ -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',
index 97b73ae..85a4efe 100644 (file)
@@ -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 = {
index a5b71b9..0eb1134 100644 (file)
@@ -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;
index c35e80f..e71cc88 100644 (file)
@@ -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",
index 2945308..36c3fe2 100644 (file)
@@ -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;
index aa43e5d..496f265 100644 (file)
@@ -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,
index e745960..fb32ef7 100644 (file)
@@ -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( '<html><script>alert("xss")</script></html>' );
index 31a0e79..2895fa2 100644 (file)
 <?php
 
+use MediaWiki\Config\ServiceOptions;
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Page\MovePageFactory;
+use MediaWiki\Permissions\PermissionManager;
+use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Rdbms\LoadBalancer;
+
 /**
  * @group Database
  */
 class MovePageTest extends MediaWikiTestCase {
+       /**
+        * @param string $class
+        * @return object A mock that throws on any method call
+        */
+       private function getNoOpMock( $class ) {
+               $mock = $this->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 ) );
index e8f0873..6cfc377 100644 (file)
@@ -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',
index f42777c..71f76e7 100644 (file)
@@ -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' ] );
+       }
 }
index 35dacac..74ad84a 100644 (file)
@@ -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 (file)
index 0000000..41fdf24
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+
+use MediaWiki\Config\ServiceOptions;
+
+/**
+ * Helper for TestAllServiceOptionsUsed.
+ */
+class LoggedServiceOptions extends ServiceOptions {
+       /** @var array */
+       private $accessLog;
+
+       /**
+        * @param array &$accessLog Pass self::$serviceOptionsAccessLog from the class implementing
+        *   TestAllServiceOptionsUsed.
+        * @param string[] $keys
+        * @param mixed ...$args Forwarded to parent as-is.
+        */
+       public function __construct( array &$accessLog, array $keys, ...$args ) {
+               $this->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 (file)
index 0000000..618472b
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * Use this trait to check that code run by tests accesses every key declared for this class'
+ * ServiceOptions, e.g., in a $constructorOptions member variable. To use this trait, you need to do
+ * two things (other than use-ing it):
+ *
+ * 1) Don't use the regular ServiceOptions when constructing your objects, but rather
+ * LoggedServiceOptions. These are used the same as ServiceOptions, except in the constructor, pass
+ * self::$serviceOptionsAccessLog before the regular arguments.
+ *
+ * 2) Make a test that calls assertAllServiceOptionsUsed(). If some ServiceOptions keys are not yet
+ * accessed in tests but actually are used by the class, pass their names as an argument.
+ *
+ * Currently we support only one ServiceOptions per test class.
+ */
+trait TestAllServiceOptionsUsed {
+       /** @var array [ expected keys (as list), keys accessed so far (as dictionary) ] */
+       private static $serviceOptionsAccessLog = [];
+
+       /**
+        * @param string[] $expectedUnused Options that we know are not yet tested
+        */
+       public function assertAllServiceOptionsUsed( array $expectedUnused = [] ) {
+               $this->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 (file)
index 0000000..42805b2
--- /dev/null
@@ -0,0 +1,9 @@
+<?php
+
+class TempFSFileIntegrationTest extends MediaWikiIntegrationTestCase {
+       use TempFSFileTestTrait;
+
+       private function newFile() {
+               return TempFSFile::factory( 'tmp' );
+       }
+}
index 9ec86bc..f496fa3 100644 (file)
@@ -19,9 +19,10 @@ class BagOStuffTest extends MediaWikiTestCase {
 
                // type defined through parameter
                if ( $this->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 (file)
index 6e51883..0000000
+++ /dev/null
@@ -1,497 +0,0 @@
-<?php
-
-use Wikimedia\Services\ServiceContainer;
-
-/**
- * @covers Wikimedia\Services\ServiceContainer
- */
-class ServiceContainerTest extends PHPUnit\Framework\TestCase {
-
-       use MediaWikiCoversValidator; // TODO this library is supposed to be independent of MediaWiki
-       use PHPUnit4And6Compat;
-
-       private function newServiceContainer( $extraArgs = [] ) {
-               return new ServiceContainer( $extraArgs );
-       }
-
-       public function testGetServiceNames() {
-               $services = $this->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 (file)
index b6ff4eb..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php
-/**
- * Test file for testing ServiceContainer::loadWiringFiles
- */
-
-return [
-       'Foo' => 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 (file)
index dfff64f..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php
-/**
- * Test file for testing ServiceContainer::loadWiringFiles
- */
-
-return [
-       'Bar' => function () {
-               return 'Bar!';
-       },
-];
diff --git a/tests/phpunit/includes/parser/ParserFactoryIntegrationTest.php b/tests/phpunit/includes/parser/ParserFactoryIntegrationTest.php
new file mode 100644 (file)
index 0000000..5bf4f3f
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * @covers ParserFactory
+ */
+class ParserFactoryIntegrationTest extends MediaWikiIntegrationTestCase {
+       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/parser/ParserFactoryTest.php b/tests/phpunit/includes/parser/ParserFactoryTest.php
deleted file mode 100644 (file)
index 048256d..0000000
+++ /dev/null
@@ -1,107 +0,0 @@
-<?php
-
-/**
- * @covers ParserFactory
- */
-class ParserFactoryTest extends MediaWikiTestCase {
-       /**
-        * For backwards compatibility, all parameters to the parser constructor are optional and
-        * default to the appropriate global service, so it's easy to forget to update ParserFactory to
-        * actually pass the parameters it's supposed to.
-        */
-       public function testConstructorArgNum() {
-               $factoryConstructor = new ReflectionMethod( 'ParserFactory', '__construct' );
-               $instanceConstructor = new ReflectionMethod( 'Parser', '__construct' );
-               // Subtract one for the ParserFactory itself
-               $this->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' );
-       }
-}
index a00eb3f..e7f7067 100644 (file)
@@ -1,7 +1,6 @@
 <?php
 
 use MediaWiki\Auth\AuthManager;
-use MediaWiki\Config\ServiceOptions;
 use MediaWiki\MediaWikiServices;
 use MediaWiki\Preferences\DefaultPreferencesFactory;
 use Wikimedia\TestingAccessWrapper;
@@ -29,6 +28,7 @@ use Wikimedia\TestingAccessWrapper;
  * @group Preferences
  */
 class DefaultPreferencesFactoryTest extends \MediaWikiTestCase {
+       use TestAllServiceOptionsUsed;
 
        /** @var IContextSource */
        protected $context;
@@ -60,7 +60,8 @@ class DefaultPreferencesFactoryTest extends \MediaWikiTestCase {
                        ->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' ] );
+       }
 }
index dff18ca..9d58cef 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 
+use MediaWiki\MediaWikiServices;
 use Wikimedia\TestingAccessWrapper;
 
 /**
@@ -209,6 +210,19 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
                );
        }
 
+       public function testRcNsFilterAllContents() {
+               $namespaces = MediaWikiServices::getInstance()->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();
index c1e258d..028c438 100644 (file)
@@ -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 (file)
index 0000000..f7035b4
--- /dev/null
@@ -0,0 +1,148 @@
+<?php
+
+/**
+ * Test that a factory class correctly forwards all arguments to the class it constructs. This is
+ * useful because sometimes a class' constructor will have more arguments added, and it's easy to
+ * accidentally have the factory's constructor fall out of sync.
+ */
+trait FactoryArgTestTrait {
+       /**
+        * @return string Name of factory class
+        */
+       abstract protected static function getFactoryClass();
+
+       /**
+        * @return string Name of instance class
+        */
+       abstract protected static function getInstanceClass();
+
+       /**
+        * @return int The number of arguments that the instance constructor receives but the factory
+        * constructor doesn't. Used for a simple argument count check. Override if this isn't zero.
+        */
+       protected static function getExtraClassArgCount() {
+               return 0;
+       }
+
+       /**
+        * Override if your factory method name is different from newInstanceClassName.
+        *
+        * @return string
+        */
+       protected function getFactoryMethodName() {
+               return 'new' . $this->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 (file)
index 0000000..d32e01e
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * Code shared between the unit and integration tests
+ */
+trait TempFSFileTestTrait {
+       abstract protected function newFile();
+
+       /**
+        * @covers TempFSFile::__construct
+        * @covers TempFSFile::purge
+        */
+       public function testPurge() {
+               $file = $this->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 (file)
index 0000000..f9e820a
--- /dev/null
@@ -0,0 +1,523 @@
+<?php
+
+use Wikimedia\Services\ServiceContainer;
+
+/**
+ * @covers Wikimedia\Services\ServiceContainer
+ */
+class ServiceContainerTest extends PHPUnit\Framework\TestCase {
+
+       use MediaWikiCoversValidator; // TODO this library is supposed to be independent of MediaWiki
+       use PHPUnit4And6Compat;
+
+       private function newServiceContainer( $extraArgs = [] ) {
+               return new ServiceContainer( $extraArgs );
+       }
+
+       public function testGetServiceNames() {
+               $services = $this->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 (file)
index 0000000..b6ff4eb
--- /dev/null
@@ -0,0 +1,10 @@
+<?php
+/**
+ * Test file for testing ServiceContainer::loadWiringFiles
+ */
+
+return [
+       'Foo' => 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 (file)
index 0000000..dfff64f
--- /dev/null
@@ -0,0 +1,10 @@
+<?php
+/**
+ * Test file for testing ServiceContainer::loadWiringFiles
+ */
+
+return [
+       'Bar' => 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 (file)
index 0000000..99fc631
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+
+use MediaWiki\Page\MovePageFactory;
+
+/**
+ * @covers MediaWiki\Page\MovePageFactory
+ */
+class MovePageFactoryTest extends MediaWikiUnitTestCase {
+       use FactoryArgTestTrait;
+
+       protected function getFactoryClass() {
+               return MovePageFactory::class;
+       }
+
+       protected function getInstanceClass() {
+               return MovePage::class;
+       }
+
+       protected static function getExtraClassArgCount() {
+               // $to and $from
+               return 2;
+       }
+}
diff --git a/tests/phpunit/unit/includes/parser/ParserFactoryTest.php b/tests/phpunit/unit/includes/parser/ParserFactoryTest.php
new file mode 100644 (file)
index 0000000..f1e48c7
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @covers ParserFactory
+ */
+class ParserFactoryTest extends MediaWikiUnitTestCase {
+       use FactoryArgTestTrait;
+
+       protected static function getFactoryClass() {
+               return ParserFactory::class;
+       }
+
+       protected static function getInstanceClass() {
+               return Parser::class;
+       }
+
+       protected static function getFactoryMethodName() {
+               return 'create';
+       }
+
+       protected static function getExtraClassArgCount() {
+               // The parser factory itself is passed to the parser
+               return 1;
+       }
+
+       protected function getOverriddenMockValueForParam( ReflectionParameter $param ) {
+               if ( $param->getPosition() === 0 ) {
+                       return [ $this->createMock( MediaWiki\Config\ServiceOptions::class ) ];
+               }
+               return [];
+       }
+}