Merge "Optimize WikiMap::getWikiFromUrl() for the common local wiki case"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 7 May 2019 22:30:17 +0000 (22:30 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 7 May 2019 22:30:17 +0000 (22:30 +0000)
198 files changed:
Gruntfile.js
RELEASE-NOTES-1.34
autoload.php
includes/Block.php
includes/DefaultSettings.php
includes/EditPage.php
includes/GlobalFunctions.php
includes/Linker.php
includes/MediaWikiServices.php
includes/MovePage.php
includes/OutputPage.php
includes/Permissions/PermissionManager.php
includes/Pingback.php
includes/ProtectionForm.php
includes/Revision.php
includes/Revision/RevisionLookup.php
includes/Revision/RevisionStore.php
includes/ServiceWiring.php
includes/SiteStatsInit.php
includes/Title.php
includes/TrackingCategories.php
includes/actions/InfoAction.php
includes/api/ApiBase.php
includes/api/ApiBlock.php
includes/api/ApiBlockInfoTrait.php [new file with mode: 0644]
includes/api/ApiHelp.php
includes/api/ApiPageSet.php
includes/api/ApiQueryAllDeletedRevisions.php
includes/api/ApiQueryAllPages.php
includes/api/ApiQueryAllRevisions.php
includes/api/ApiQueryInfo.php
includes/api/ApiQuerySearch.php
includes/api/ApiQuerySiteinfo.php
includes/api/ApiQueryUserInfo.php
includes/api/ApiSetNotificationTimestamp.php
includes/api/ApiUnblock.php
includes/api/i18n/ko.json
includes/api/i18n/nl.json
includes/api/i18n/pl.json
includes/block/AbstractBlock.php [new file with mode: 0644]
includes/cache/GenderCache.php
includes/cache/LinkCache.php
includes/cache/MessageCache.php
includes/cache/localisation/LocalisationCache.php
includes/changes/ChangesFeed.php
includes/content/ContentHandler.php
includes/editpage/TextboxBuilder.php
includes/exception/MWExceptionHandler.php
includes/export/DumpNotalkFilter.php
includes/export/XmlDumpWriter.php
includes/externalstore/ExternalStoreHttp.php
includes/filerepo/FileRepo.php
includes/filerepo/ForeignAPIRepo.php
includes/filerepo/RepoGroup.php
includes/filerepo/file/File.php
includes/filerepo/file/ForeignDBFile.php
includes/gallery/TraditionalImageGallery.php
includes/http/CurlHttpRequest.php
includes/http/GuzzleHttpRequest.php
includes/http/Http.php
includes/http/HttpRequestFactory.php
includes/http/MWHttpRequest.php
includes/http/PhpHttpRequest.php
includes/import/ImportStreamSource.php
includes/import/ImportableUploadRevisionImporter.php
includes/import/WikiImporter.php
includes/installer/Installer.php
includes/jobqueue/jobs/ActivityUpdateJob.php
includes/jobqueue/jobs/ClearUserWatchlistJob.php
includes/jobqueue/jobs/UserOptionsUpdateJob.php [new file with mode: 0644]
includes/libs/filebackend/fsfile/TempFSFile.php
includes/linker/LinkRenderer.php
includes/linker/LinkRendererFactory.php
includes/page/Article.php
includes/preferences/DefaultPreferencesFactory.php
includes/rcfeed/UDPRCFeedEngine.php
includes/resourceloader/ResourceLoaderStartUpModule.php
includes/search/PrefixSearch.php
includes/search/SearchEngine.php
includes/skins/Skin.php
includes/skins/SkinTemplate.php
includes/specialpage/ChangesListSpecialPage.php
includes/specials/SpecialAncientpages.php
includes/specials/SpecialContributions.php
includes/specials/SpecialDeadendpages.php
includes/specials/SpecialDeletedContributions.php
includes/specials/SpecialEditWatchlist.php
includes/specials/SpecialFewestrevisions.php
includes/specials/SpecialListGroupRights.php
includes/specials/SpecialLonelypages.php
includes/specials/SpecialMostcategories.php
includes/specials/SpecialMostinterwikis.php
includes/specials/SpecialMovepage.php
includes/specials/SpecialPasswordPolicies.php
includes/specials/SpecialRandompage.php
includes/specials/SpecialSearch.php
includes/specials/SpecialShortpages.php
includes/specials/SpecialStatistics.php
includes/specials/SpecialUncategorizedpages.php
includes/specials/SpecialWatchlist.php
includes/specials/SpecialWithoutinterwiki.php
includes/specials/forms/UploadForm.php
includes/specials/pagers/ContribsPager.php
includes/title/NaiveImportTitleFactory.php
includes/title/NamespaceImportTitleFactory.php
includes/title/NamespaceInfo.php
includes/title/SubpageImportTitleFactory.php
includes/user/ExternalUserNames.php
includes/user/User.php
includes/user/UserIdentity.php
includes/user/UserIdentityValue.php
includes/watcheditem/NoWriteWatchedItemStore.php
includes/watcheditem/WatchedItem.php
includes/watcheditem/WatchedItemQueryService.php
includes/watcheditem/WatchedItemQueryServiceExtension.php
includes/watcheditem/WatchedItemStore.php
includes/watcheditem/WatchedItemStoreInterface.php
includes/widget/search/SearchFormWidget.php
languages/Language.php
languages/i18n/ang.json
languages/i18n/ar.json
languages/i18n/ban.json
languages/i18n/be-tarask.json
languages/i18n/en.json
languages/i18n/eo.json
languages/i18n/hr.json
languages/i18n/hu.json
languages/i18n/hyw.json
languages/i18n/jv.json
languages/i18n/nb.json
languages/i18n/nn.json
languages/i18n/nqo.json
languages/i18n/ps.json
languages/i18n/qqq.json
languages/i18n/sah.json
languages/i18n/sr-ec.json
languages/i18n/tr.json
languages/i18n/yue.json
languages/i18n/zh-hant.json
maintenance/benchmarks/bench_HTTP_HTTPS.php
maintenance/cleanupCaps.php
maintenance/cleanupTitles.php
maintenance/cleanupUsersWithNoId.php
maintenance/findHooks.php
maintenance/generateSitemap.php
maintenance/importSiteScripts.php
maintenance/namespaceDupes.php
maintenance/populateInterwiki.php
maintenance/rebuildFileCache.php
package.json
resources/Resources.php
resources/src/mediawiki.action/mediawiki.action.history.styles.less
resources/src/mediawiki.less/mediawiki.ui/mixins.buttons.less [new file with mode: 0644]
resources/src/mediawiki.ui/components/buttons.less
tests/integration/includes/http/MWHttpRequestTestCase.php
tests/parser/ParserTestPrinter.php
tests/parser/ParserTestRunner.php
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/includes/BlockTest.php
tests/phpunit/includes/ContentSecurityPolicyTest.php
tests/phpunit/includes/GlobalFunctions/GlobalTest.php
tests/phpunit/includes/LinkerTest.php
tests/phpunit/includes/PagePropsTest.php
tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php
tests/phpunit/includes/RevisionDbTestBase.php
tests/phpunit/includes/TestUserRegistry.php
tests/phpunit/includes/TitleTest.php
tests/phpunit/includes/api/ApiBaseTest.php
tests/phpunit/includes/api/ApiBlockInfoTraitTest.php [new file with mode: 0644]
tests/phpunit/includes/api/ApiQueryUserInfoTest.php [deleted file]
tests/phpunit/includes/config/GlobalVarConfigTest.php
tests/phpunit/includes/db/LBFactoryTest.php
tests/phpunit/includes/editpage/TextboxBuilderTest.php
tests/phpunit/includes/filebackend/FileBackendTest.php
tests/phpunit/includes/filerepo/FileBackendDBRepoWrapperTest.php
tests/phpunit/includes/filerepo/MigrateFileRepoLayoutTest.php
tests/phpunit/includes/filerepo/RepoGroupTest.php
tests/phpunit/includes/http/HttpTest.php
tests/phpunit/includes/jobqueue/JobQueueTest.php
tests/phpunit/includes/libs/CSSMinTest.php
tests/phpunit/includes/libs/objectcache/MultiWriteBagOStuffTest.php
tests/phpunit/includes/libs/objectcache/ReplicatedBagOStuffTest.php
tests/phpunit/includes/linker/LinkRendererFactoryTest.php
tests/phpunit/includes/linker/LinkRendererTest.php
tests/phpunit/includes/preferences/DefaultPreferencesFactoryTest.php
tests/phpunit/includes/title/NamespaceInfoTest.php
tests/phpunit/includes/user/UserTest.php
tests/phpunit/includes/watcheditem/NoWriteWatchedItemStoreUnitTest.php
tests/phpunit/includes/watcheditem/WatchedItemQueryServiceUnitTest.php
tests/phpunit/includes/watcheditem/WatchedItemStoreUnitTest.php
tests/phpunit/mocks/filebackend/MockFileBackend.php
tests/phpunit/mocks/filerepo/MockLocalRepo.php
tests/phpunit/structure/ApiStructureTest.php
tests/phpunit/suites/UploadFromUrlTestSuite.php
tests/selenium/pageobjects/history.page.js
tests/selenium/specs/rollback.js
tests/selenium/wdio-mediawiki/LoginPage.js
tests/selenium/wdio.conf.js

index 765fe55..f3950f6 100644 (file)
@@ -9,7 +9,6 @@ module.exports = function ( grunt ) {
        grunt.loadNpmTasks( 'grunt-banana-checker' );
        grunt.loadNpmTasks( 'grunt-contrib-copy' );
        grunt.loadNpmTasks( 'grunt-eslint' );
-       grunt.loadNpmTasks( 'grunt-jsonlint' );
        grunt.loadNpmTasks( 'grunt-karma' );
        grunt.loadNpmTasks( 'grunt-stylelint' );
        grunt.loadNpmTasks( 'grunt-svgmin' );
@@ -23,10 +22,11 @@ module.exports = function ( grunt ) {
                eslint: {
                        options: {
                                reportUnusedDisableDirectives: true,
+                               extensions: [ '.js', '.json' ],
                                cache: true
                        },
                        all: [
-                               '**/*.js',
+                               '**/*.js{,on}',
                                '!docs/**',
                                '!node_modules/**',
                                '!resources/lib/**',
@@ -36,14 +36,8 @@ module.exports = function ( grunt ) {
                                '!tests/coverage/**',
                                '!vendor/**',
                                // Explicitly say "**/*.js" here in case of symlinks
-                               '!extensions/**/*.js',
-                               '!skins/**/*.js'
-                       ]
-               },
-               jsonlint: {
-                       all: [
-                               '**/*.json',
-                               '!{docs/js,extensions,node_modules,skins,vendor}/**'
+                               '!extensions/**/*.js{,on}',
+                               '!skins/**/*.js{,on}'
                        ]
                },
                banana: {
index 5d46edd..9231380 100644 (file)
@@ -25,6 +25,7 @@ Some specific notes for MediaWiki 1.34 upgrades are below:
 For notes on 1.33.x and older releases, see HISTORY.
 
 === Configuration changes for system administrators in 1.34 ===
+
 ==== New configuration ====
 * …
 
@@ -41,6 +42,7 @@ For notes on 1.33.x and older releases, see HISTORY.
 * …
 
 === External library changes in 1.34 ===
+
 ==== New external libraries ====
 * …
 
@@ -53,7 +55,10 @@ For notes on 1.33.x and older releases, see HISTORY.
 * …
 
 === Bug fixes in 1.34 ===
-* …
+* (T222529) If a log entry or page revision is recorded in the database with an
+  empty username, attempting to display it will log an error and return a "no
+  username available" to the user instead of silently displaying nothing or
+  invalid links.
 
 === Action API changes in 1.34 ===
 * The 'recenteditcount' response property from action=query list=allusers,
@@ -107,6 +112,9 @@ because of Phabricator reports.
 * wfArrayFilter() and wfArrayFilterByKey(), deprecated in 1.32, have been
   removed.
 * wfMakeUrlIndexes() function, deprecated in 1.33, have been removed.
+* Method signatures in WatchedItemQueryServiceExtension have changed from taking
+  User objects to taking UserIdentity objects. Extensions implementing this
+  interface need to be changed accordingly.
 * User::getGroupPage() and ::makeGroupLinkHTML(), deprecated in 1.29, have been
   removed. Use UserGroupMembership::getGroupPage and ::getLink instead.
 * User::makeGroupLinkWiki(), deprecated in 1.29, has been removed. Use
@@ -114,7 +122,7 @@ because of Phabricator reports.
 * …
 
 === Deprecations in 1.34 ===
-* The MWNamespace class is deprecated. Use MediaWikiServices::getNamespaceInfo.
+* The MWNamespace class is deprecated. Use NamespaceInfo.
 * ExtensionRegistry->load() is deprecated, as it breaks dependency checking.
   Instead, use ->queue().
 * User::isBlocked() is deprecated since it does not tell you if the user is
@@ -126,6 +134,25 @@ because of Phabricator reports.
   instead.
 * The Config argument to ChangesListSpecialPage::checkStructuredFilterUiEnabled
   is deprecated. Pass only the User argument.
+* WatchedItem::getUser is deprecated. Use getUserIdentity.
+* Passing a Title as the first parameter to the getTimestampById method of
+  RevisionStore is deprecated. Omit it, passing only the remaining parameters.
+* Title::getPreviousRevisionId and Title::getNextRevisionId are deprecated. Use
+  RevisionLookup::getPreviousRevision and RevisionLookup::getNextRevision.
+* The Title parameter to RevisionLookup::getPreviousRevision and
+  RevisionLookup::getNextRevision is deprecated and should be omitted.
+* MWHttpRequest::factory is deprecated. Use HttpRequestFactory.
+* The Http class is deprecated. For the request, get, and post methods, use
+  HttpRequestFactory. For isValidURI, use MWHttpRequest::isValidURI.  For
+  getProxy, use (string)$wgHTTPProxy. For createMultiClient, construct a
+  MultiHttpClient directly.
+* Http::$httpEngine is deprecated and has no replacement. The default 'guzzle'
+  engine will eventually be made the only engine for HTTP requests.
+* RepoGroup::singleton(), RepoGroup::destroySingleton(),
+  RepoGroup::setSingleton(), wfFindFile(), and wfLocalFile() are all
+  deprecated. Use MediaWikiServices instead.
+* The getSubjectPage, getTalkPage, and getOtherPage of Title are deprecated.
+  Use NamespaceInfo's getSubjectPage, getTalkPage, and getAssociatedPage.
 
 === Other changes in 1.34 ===
 * …
index f8e90b7..5d3e578 100644 (file)
@@ -27,6 +27,7 @@ $wgAutoloadLocalClasses = [
        'ApiAuthManagerHelper' => __DIR__ . '/includes/api/ApiAuthManagerHelper.php',
        'ApiBase' => __DIR__ . '/includes/api/ApiBase.php',
        'ApiBlock' => __DIR__ . '/includes/api/ApiBlock.php',
+       'ApiBlockInfoTrait' => __DIR__ . '/includes/api/ApiBlockInfoTrait.php',
        'ApiCSPReport' => __DIR__ . '/includes/api/ApiCSPReport.php',
        'ApiChangeAuthenticationData' => __DIR__ . '/includes/api/ApiChangeAuthenticationData.php',
        'ApiCheckToken' => __DIR__ . '/includes/api/ApiCheckToken.php',
@@ -1564,6 +1565,7 @@ $wgAutoloadLocalClasses = [
        'UserNamePrefixSearch' => __DIR__ . '/includes/user/UserNamePrefixSearch.php',
        'UserNotLoggedIn' => __DIR__ . '/includes/exception/UserNotLoggedIn.php',
        'UserOptionsMaintenance' => __DIR__ . '/maintenance/userOptions.php',
+       'UserOptionsUpdateJob' => __DIR__ . '/includes/jobqueue/jobs/UserOptionsUpdateJob.php',
        'UserPasswordPolicy' => __DIR__ . '/includes/password/UserPasswordPolicy.php',
        'UserRightsProxy' => __DIR__ . '/includes/user/UserRightsProxy.php',
        'UserrightsPage' => __DIR__ . '/includes/specials/SpecialUserrights.php',
index 0d13f7d..6bfd7d3 100644 (file)
 
 use Wikimedia\Rdbms\Database;
 use Wikimedia\Rdbms\IDatabase;
+use MediaWiki\Block\AbstractBlock;
 use MediaWiki\Block\BlockRestrictionStore;
 use MediaWiki\Block\Restriction\Restriction;
 use MediaWiki\Block\Restriction\NamespaceRestriction;
 use MediaWiki\Block\Restriction\PageRestriction;
 use MediaWiki\MediaWikiServices;
 
-class Block {
+class Block extends AbstractBlock {
        /** @var string */
        public $mReason;
 
@@ -387,8 +388,9 @@ class Block {
                        if ( $block->getType() == self::TYPE_RANGE ) {
                                # This is the number of bits that are allowed to vary in the block, give
                                # or take some floating point errors
-                               $end = Wikimedia\base_convert( $block->getRangeEnd(), 16, 10 );
-                               $start = Wikimedia\base_convert( $block->getRangeStart(), 16, 10 );
+                               $prefix = 'v6-';
+                               $end = Wikimedia\base_convert( ltrim( $block->getRangeEnd(), $prefix ), 16, 10 );
+                               $start = Wikimedia\base_convert( ltrim( $block->getRangeStart(), $prefix ), 16, 10 );
                                $size = log( $end - $start + 1, 2 );
 
                                # Rank a range block covering a single IP equally with a single-IP block
index b40d33b..4ba1836 100644 (file)
@@ -6852,7 +6852,7 @@ $wgRCLinkDays = [ 1, 3, 7, 14, 30 ];
  * FormattedRCFeed-specific options:
  * - 'uri' -- [required] The address to which the messages are sent.
  *   The uri scheme of this string will be looked up in $wgRCEngines
- *   to determine which RCFeedEngine class to use.
+ *   to determine which FormattedRCFeed class to use.
  * - 'formatter' -- [required] The class (implementing RCFeedFormatter) which will
  *   produce the text to send. This can also be an object of the class.
  *   Formatters available by default: JSONRCFeedFormatter, XMLRCFeedFormatter,
@@ -7506,6 +7506,7 @@ $wgServiceWiringFiles = [
  * can add to this to provide custom jobs.
  * A job handler should either be a class name to be instantiated,
  * or (since 1.30) a callback to use for creating the job object.
+ * The callback takes (Title, array map of parameters) as arguments.
  */
 $wgJobClasses = [
        'deletePage' => DeletePageJob::class,
@@ -7530,6 +7531,7 @@ $wgJobClasses = [
        'cdnPurge' => CdnPurgeJob::class,
        'userGroupExpiry' => UserGroupExpiryJob::class,
        'clearWatchlistNotifications' => ClearWatchlistNotificationsJob::class,
+       'userOptionsUpdate' => UserOptionsUpdateJob::class,
        'enqueue' => EnqueueJob::class, // local queue for multi-DC setups
        'null' => NullJob::class,
 ];
@@ -8397,7 +8399,7 @@ $wgAsyncHTTPTimeout = 25;
 /**
  * Proxy to use for CURL requests.
  */
-$wgHTTPProxy = false;
+$wgHTTPProxy = '';
 
 /**
  * Local virtual hosts.
index 1d9ff05..2d5b9e2 100644 (file)
@@ -2609,7 +2609,8 @@ ERROR;
                                LogEventsList::showLogExtract(
                                        $out,
                                        'block',
-                                       MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
+                                       MediaWikiServices::getInstance()->getNamespaceInfo()->
+                                               getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
                                        '',
                                        [
                                                'lim' => 1,
@@ -4451,7 +4452,9 @@ ERROR;
        protected function addPageProtectionWarningHeaders() {
                $out = $this->context->getOutput();
                if ( $this->mTitle->isProtected( 'edit' ) &&
-                       MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
+                       MediaWikiServices::getInstance()->getNamespaceInfo()->getRestrictionLevels(
+                               $this->mTitle->getNamespace()
+                       ) !== [ '' ]
                ) {
                        # Is the title semi-protected?
                        if ( $this->mTitle->isSemiProtected() ) {
index c7a45c7..66a4d9a 100644 (file)
@@ -2633,25 +2633,25 @@ function wfGetLBFactory() {
 
 /**
  * Find a file.
- * Shortcut for RepoGroup::singleton()->findFile()
- *
+ * @deprecated since 1.34, use MediaWikiServices
  * @param string|LinkTarget $title String or LinkTarget object
  * @param array $options Associative array of options (see RepoGroup::findFile)
  * @return File|bool File, or false if the file does not exist
  */
 function wfFindFile( $title, $options = [] ) {
-       return RepoGroup::singleton()->findFile( $title, $options );
+       return MediaWikiServices::getInstance()->getRepoGroup()->findFile( $title, $options );
 }
 
 /**
  * Get an object referring to a locally registered file.
  * Returns a valid placeholder object if the file does not exist.
  *
+ * @deprecated since 1.34, use MediaWikiServices
  * @param Title|string $title
  * @return LocalFile|null A File, or null if passed an invalid Title
  */
 function wfLocalFile( $title ) {
-       return RepoGroup::singleton()->getLocalRepo()->newFile( $title );
+       return MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()->newFile( $title );
 }
 
 /**
index 9cca0be..ff4c786 100644 (file)
@@ -191,7 +191,7 @@ class Linker {
         */
        public static function getInvalidTitleDescription( IContextSource $context, $namespace, $title ) {
                // First we check whether the namespace exists or not.
-               if ( MWNamespace::exists( $namespace ) ) {
+               if ( MediaWikiServices::getInstance()->getNamespaceInfo()->exists( $namespace ) ) {
                        if ( $namespace == NS_MAIN ) {
                                $name = $context->msg( 'blanknamespace' )->text();
                        } else {
@@ -894,6 +894,12 @@ class Linker {
         * @since 1.16.3. $altUserName was added in 1.19.
         */
        public static function userLink( $userId, $userName, $altUserName = false ) {
+               if ( $userName === '' ) {
+                       wfLogWarning( __METHOD__ . ' received an empty username. Are there database errors ' .
+                               'that need to be fixed?' );
+                       return wfMessage( 'empty-username' )->parse();
+               }
+
                $classes = 'mw-userlink';
                $page = null;
                if ( $userId == 0 ) {
@@ -936,6 +942,12 @@ class Linker {
                $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits = null,
                $useParentheses = true
        ) {
+               if ( $userText === '' ) {
+                       wfLogWarning( __METHOD__ . ' received an empty username. Are there database errors ' .
+                               'that need to be fixed?' );
+                       return ' ' . wfMessage( 'empty-username' )->parse();
+               }
+
                global $wgUser, $wgDisableAnonTalk, $wgLang;
                $talkable = !( $wgDisableAnonTalk && $userId == 0 );
                $blockable = !( $flags & self::TOOL_LINKS_NOBLOCK );
@@ -1018,6 +1030,12 @@ class Linker {
         * @return string HTML fragment with user talk link
         */
        public static function userTalkLink( $userId, $userText ) {
+               if ( $userText === '' ) {
+                       wfLogWarning( __METHOD__ . ' received an empty username. Are there database errors ' .
+                               'that need to be fixed?' );
+                       return wfMessage( 'empty-username' )->parse();
+               }
+
                $userTalkPage = new TitleValue( NS_USER_TALK, strtr( $userText, ' ', '_' ) );
                $moreLinkAttribs['class'] = 'mw-usertoollinks-talk';
 
@@ -1034,6 +1052,12 @@ class Linker {
         * @return string HTML fragment with block link
         */
        public static function blockLink( $userId, $userText ) {
+               if ( $userText === '' ) {
+                       wfLogWarning( __METHOD__ . ' received an empty username. Are there database errors ' .
+                               'that need to be fixed?' );
+                       return wfMessage( 'empty-username' )->parse();
+               }
+
                $blockPage = SpecialPage::getTitleFor( 'Block', $userText );
                $moreLinkAttribs['class'] = 'mw-usertoollinks-block';
 
@@ -1049,6 +1073,12 @@ class Linker {
         * @return string HTML fragment with e-mail user link
         */
        public static function emailLink( $userId, $userText ) {
+               if ( $userText === '' ) {
+                       wfLogWarning( __METHOD__ . ' received an empty username. Are there database errors ' .
+                               'that need to be fixed?' );
+                       return wfMessage( 'empty-username' )->parse();
+               }
+
                $emailPage = SpecialPage::getTitleFor( 'Emailuser', $userText );
                $moreLinkAttribs['class'] = 'mw-usertoollinks-mail';
                return self::link( $emailPage,
@@ -1272,7 +1302,12 @@ class Linker {
                                ([^[]*) # 3. link trail (the text up until the next link)
                        /x',
                        function ( $match ) use ( $title, $local, $wikiId ) {
-                               $medians = '(?:' . preg_quote( MWNamespace::getCanonicalName( NS_MEDIA ), '/' ) . '|';
+                               $services = MediaWikiServices::getInstance();
+
+                               $medians = '(?:';
+                               $medians .= preg_quote(
+                                       $services->getNamespaceInfo()->getCanonicalName( NS_MEDIA ), '/' );
+                               $medians .= '|';
                                $medians .= preg_quote(
                                        MediaWikiServices::getInstance()->getContentLanguage()->getNsText( NS_MEDIA ),
                                        '/'
@@ -1380,8 +1415,9 @@ class Linker {
                                        $wikiId,
                                        $linkTarget->getNamespace() === 0
                                                ? $linkTarget->getDBkey()
-                                               : MWNamespace::getCanonicalName( $linkTarget->getNamespace() ) . ':'
-                                                       . $linkTarget->getDBkey(),
+                                               : MediaWikiServices::getInstance()->getNamespaceInfo()->
+                                                       getCanonicalName( $linkTarget->getNamespace() ) .
+                                                       ':' . $linkTarget->getDBkey(),
                                        $linkTarget->getFragment()
                                ),
                                $text,
@@ -1416,7 +1452,10 @@ class Linker {
 
                # Some namespaces don't allow subpages,
                # so only perform processing if subpages are allowed
-               if ( $contextTitle && MWNamespace::hasSubpages( $contextTitle->getNamespace() ) ) {
+               if (
+                       $contextTitle && MediaWikiServices::getInstance()->getNamespaceInfo()->
+                       hasSubpages( $contextTitle->getNamespace() )
+               ) {
                        $hash = strpos( $target, '#' );
                        if ( $hash !== false ) {
                                $suffix = substr( $target, $hash );
index c374a62..d6f50bf 100644 (file)
@@ -48,6 +48,7 @@ use ParserCache;
 use ParserFactory;
 use PasswordFactory;
 use ProxyLookup;
+use RepoGroup;
 use ResourceLoader;
 use SearchEngine;
 use SearchEngineConfig;
@@ -789,6 +790,14 @@ class MediaWikiServices extends ServiceContainer {
                return $this->getService( 'ReadOnlyMode' );
        }
 
+       /**
+        * @since 1.34
+        * @return RepoGroup
+        */
+       public function getRepoGroup() : RepoGroup {
+               return $this->getService( 'RepoGroup' );
+       }
+
        /**
         * @since 1.33
         * @return ResourceLoader
index 24178ac..004ca07 100644 (file)
@@ -233,14 +233,69 @@ class MovePage {
        }
 
        /**
+        * Move a page without taking user permissions into account. Only checks if the move is itself
+        * invalid, e.g., trying to move a special page or trying to move a page onto one that already
+        * exists.
+        *
+        * @param User $user
+        * @param string|null $reason
+        * @param bool|null $createRedirect
+        * @param string[] $changeTags Change tags to apply to the entry in the move log
+        * @return Status
+        */
+       public function move(
+               User $user, $reason = null, $createRedirect = true, array $changeTags = []
+       ) {
+               $status = $this->isValidMove();
+               if ( !$status->isOK() ) {
+                       return $status;
+               }
+
+               return $this->moveUnsafe( $user, $reason, $createRedirect, $changeTags );
+       }
+
+       /**
+        * Same as move(), but with permissions checks.
+        *
+        * @param User $user
+        * @param string|null $reason
+        * @param bool|null $createRedirect Ignored if user doesn't have suppressredirect permission
+        * @param string[] $changeTags Change tags to apply to the entry in the move log
+        * @return Status
+        */
+       public function moveIfAllowed(
+               User $user, $reason = null, $createRedirect = true, array $changeTags = []
+       ) {
+               $status = $this->isValidMove();
+               $status->merge( $this->checkPermissions( $user, $reason ) );
+               if ( $changeTags ) {
+                       $status->merge( ChangeTags::canAddTagsAccompanyingChange( $changeTags, $user ) );
+               }
+
+               if ( !$status->isOK() ) {
+                       // Auto-block user's IP if the account was "hard" blocked
+                       $user->spreadAnyEditBlock();
+                       return $status;
+               }
+
+               // Check suppressredirect permission
+               if ( !$user->isAllowed( 'suppressredirect' ) ) {
+                       $createRedirect = true;
+               }
+
+               return $this->moveUnsafe( $user, $reason, $createRedirect, $changeTags );
+       }
+
+       /**
+        * Moves *without* any sort of safety or sanity checks. Hooks can still fail the move, however.
+        *
         * @param User $user
         * @param string $reason
         * @param bool $createRedirect
-        * @param string[] $changeTags Change tags to apply to the entry in the move log. Caller
-        *  should perform permission checks with ChangeTags::canAddTagsAccompanyingChange
+        * @param string[] $changeTags Change tags to apply to the entry in the move log
         * @return Status
         */
-       public function move( User $user, $reason, $createRedirect, array $changeTags = [] ) {
+       private function moveUnsafe( User $user, $reason, $createRedirect, array $changeTags ) {
                global $wgCategoryCollation;
 
                $status = Status::newGood();
@@ -272,7 +327,8 @@ class MovePage {
                        [ 'cl_from' => $pageid ],
                        __METHOD__
                );
-               $type = MediaWikiServices::getInstance()->getNamespaceInfo()->
+               $services = MediaWikiServices::getInstance();
+               $type = $services->getNamespaceInfo()->
                        getCategoryLinkType( $this->newTitle->getNamespace() );
                foreach ( $prefixes as $prefixRow ) {
                        $prefix = $prefixRow->cl_sortkey_prefix;
@@ -373,11 +429,13 @@ class MovePage {
                # Update watchlists
                $oldtitle = $this->oldTitle->getDBkey();
                $newtitle = $this->newTitle->getDBkey();
-               $oldsnamespace = MWNamespace::getSubject( $this->oldTitle->getNamespace() );
-               $newsnamespace = MWNamespace::getSubject( $this->newTitle->getNamespace() );
+               $oldsnamespace = $services->getNamespaceInfo()->
+                       getSubject( $this->oldTitle->getNamespace() );
+               $newsnamespace = $services->getNamespaceInfo()->
+                       getSubject( $this->newTitle->getNamespace() );
                if ( $oldsnamespace != $newsnamespace || $oldtitle != $newtitle ) {
-                       $store = MediaWikiServices::getInstance()->getWatchedItemStore();
-                       $store->duplicateAllAssociatedEntries( $this->oldTitle, $this->newTitle );
+                       $services->getWatchedItemStore()->duplicateAllAssociatedEntries(
+                               $this->oldTitle, $this->newTitle );
                }
 
                // If it is a file then move it last.
index 3e91fb3..56e2370 100644 (file)
@@ -3430,8 +3430,9 @@ class OutputPage extends ContextSource {
 
                $title = $this->getTitle();
                $ns = $title->getNamespace();
-               $canonicalNamespace = MWNamespace::exists( $ns )
-                       ? MWNamespace::getCanonicalName( $ns )
+               $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
+               $canonicalNamespace = $nsInfo->exists( $ns )
+                       ? $nsInfo->getCanonicalName( $ns )
                        : $title->getNsText();
 
                $sk = $this->getSkin();
index 549b7ba..e443803 100644 (file)
@@ -66,12 +66,16 @@ class PermissionManager {
        /** @var bool If set to true, blocked users will no longer be allowed to log in */
        private $blockDisablesLogin;
 
+       /** @var NamespaceInfo */
+       private $nsInfo;
+
        /**
         * @param SpecialPageFactory $specialPageFactory
         * @param string[] $whitelistRead
         * @param string[] $whitelistReadRegexp
         * @param bool $emailConfirmToEdit
         * @param bool $blockDisablesLogin
+        * @param NamespaceInfo $nsInfo
         */
        public function __construct(
                SpecialPageFactory $specialPageFactory,
index 8d7c3b6..f4e85ad 100644 (file)
@@ -22,6 +22,7 @@
 
 use Psr\Log\LoggerInterface;
 use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
 
 /**
  * Send information about this MediaWiki instance to MediaWiki.org.
@@ -229,7 +230,7 @@ class Pingback {
                $json = FormatJson::encode( $data );
                $queryString = rawurlencode( str_replace( ' ', '\u0020', $json ) ) . ';';
                $url = 'https://www.mediawiki.org/beacon/event?' . $queryString;
-               return Http::post( $url ) !== false;
+               return MediaWikiServices::getInstance()->getHttpRequestFactory()->post( $url ) !== null;
        }
 
        /**
index 7972a1e..2f10598 100644 (file)
@@ -90,7 +90,7 @@ class ProtectionForm {
         * Loads the current state of protection into the object.
         */
        function loadData() {
-               $levels = MWNamespace::getRestrictionLevels(
+               $levels = MediaWikiServices::getInstance()->getNamespaceInfo()->getRestrictionLevels(
                        $this->mTitle->getNamespace(), $this->mContext->getUser()
                );
                $this->mCascade = $this->mTitle->areRestrictionsCascading();
@@ -179,7 +179,11 @@ class ProtectionForm {
         * Main entry point for action=protect and action=unprotect
         */
        function execute() {
-               if ( MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) === [ '' ] ) {
+               if (
+                       MediaWikiServices::getInstance()->getNamespaceInfo()->getRestrictionLevels(
+                               $this->mTitle->getNamespace()
+                       ) === [ '' ]
+               ) {
                        throw new ErrorPageError( 'protect-badnamespace-title', 'protect-badnamespace-text' );
                }
 
@@ -581,7 +585,8 @@ class ProtectionForm {
        function buildSelector( $action, $selected ) {
                // If the form is disabled, display all relevant levels. Otherwise,
                // just show the ones this user can use.
-               $levels = MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace(),
+               $levels = MediaWikiServices::getInstance()->getNamespaceInfo()->getRestrictionLevels(
+                       $this->mTitle->getNamespace(),
                        $this->disabled ? null : $this->mContext->getUser()
                );
 
index cbaff90..de3c299 100644 (file)
@@ -1008,9 +1008,8 @@ class Revision implements IDBAccessObject {
         * @return Revision|null
         */
        public function getPrevious() {
-               $title = $this->getTitle();
-               $rec = self::getRevisionLookup()->getPreviousRevision( $this->mRecord, $title );
-               return $rec ? new Revision( $rec, self::READ_NORMAL, $title ) : null;
+               $rec = self::getRevisionLookup()->getPreviousRevision( $this->mRecord );
+               return $rec ? new Revision( $rec, self::READ_NORMAL, $this->getTitle() ) : null;
        }
 
        /**
@@ -1019,9 +1018,8 @@ class Revision implements IDBAccessObject {
         * @return Revision|null
         */
        public function getNext() {
-               $title = $this->getTitle();
-               $rec = self::getRevisionLookup()->getNextRevision( $this->mRecord, $title );
-               return $rec ? new Revision( $rec, self::READ_NORMAL, $title ) : null;
+               $rec = self::getRevisionLookup()->getNextRevision( $this->mRecord );
+               return $rec ? new Revision( $rec, self::READ_NORMAL, $this->getTitle() ) : null;
        }
 
        /**
@@ -1256,13 +1254,13 @@ class Revision implements IDBAccessObject {
        /**
         * Get rev_timestamp from rev_id, without loading the rest of the row
         *
-        * @param Title $title
+        * @param Title $title (ignored since 1.34)
         * @param int $id
         * @param int $flags
         * @return string|bool False if not found
         */
        static function getTimestampFromId( $title, $id, $flags = 0 ) {
-               return self::getRevisionStore()->getTimestampFromId( $title, $id, $flags );
+               return self::getRevisionStore()->getTimestampFromId( $id, $flags );
        }
 
        /**
index db6c7c3..17cafc6 100644 (file)
@@ -85,11 +85,12 @@ interface RevisionLookup extends IDBAccessObject {
         * MCR migration note: this replaces Revision::getPrevious
         *
         * @param RevisionRecord $rev
-        * @param Title|null $title if known (optional)
+        * @param int $flags (optional) $flags include:
+        *      IDBAccessObject::READ_LATEST: Select the data from the master
         *
         * @return RevisionRecord|null
         */
-       public function getPreviousRevision( RevisionRecord $rev, Title $title = null );
+       public function getPreviousRevision( RevisionRecord $rev, $flags = 0 );
 
        /**
         * Get next revision for this title
@@ -97,11 +98,24 @@ interface RevisionLookup extends IDBAccessObject {
         * MCR migration note: this replaces Revision::getNext
         *
         * @param RevisionRecord $rev
-        * @param Title|null $title if known (optional)
+        * @param int $flags (optional) $flags include:
+        *      IDBAccessObject::READ_LATEST: Select the data from the master
         *
         * @return RevisionRecord|null
         */
-       public function getNextRevision( RevisionRecord $rev, Title $title = null );
+       public function getNextRevision( RevisionRecord $rev, $flags = 0 );
+
+       /**
+        * Get rev_timestamp from rev_id, without loading the rest of the row.
+        *
+        * MCR migration note: this replaces Revision::getTimestampFromId
+        *
+        * @param int $id
+        * @param int $flags
+        * @return string|bool False if not found
+        * @since 1.34 (present earlier in RevisionStore)
+        */
+       public function getTimestampFromId( $id, $flags = 0 );
 
        /**
         * Load a revision based on a known page ID and current revision ID from the DB
index 0329bd1..ea4cf88 100644 (file)
@@ -278,12 +278,13 @@ class RevisionStore
 
        /**
         * @param int $mode DB_MASTER or DB_REPLICA
+        * @param array $groups
         *
         * @return IDatabase
         */
-       private function getDBConnection( $mode ) {
+       private function getDBConnection( $mode, $groups = [] ) {
                $lb = $this->getDBLoadBalancer();
-               return $lb->getConnection( $mode, [], $this->wikiId );
+               return $lb->getConnection( $mode, $groups, $this->wikiId );
        }
 
        /**
@@ -1739,7 +1740,8 @@ class RevisionStore
                        $user = User::newFromAnyId(
                                $row->ar_user ?? null,
                                $row->ar_user_text ?? null,
-                               $row->ar_actor ?? null
+                               $row->ar_actor ?? null,
+                               $this->wikiId
                        );
                } catch ( InvalidArgumentException $ex ) {
                        wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
@@ -1793,7 +1795,8 @@ class RevisionStore
                        $user = User::newFromAnyId(
                                $row->rev_user ?? null,
                                $row->rev_user_text ?? null,
-                               $row->rev_actor ?? null
+                               $row->rev_actor ?? null,
+                               $this->wikiId
                        );
                } catch ( InvalidArgumentException $ex ) {
                        wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
@@ -1931,14 +1934,21 @@ class RevisionStore
                /** @var UserIdentity $user */
                $user = null;
 
-               if ( isset( $fields['user'] ) && ( $fields['user'] instanceof UserIdentity ) ) {
+               // If a user is passed in, use it if possible. We cannot use a user from a
+               // remote wiki with unsuppressed ids, due to issues described in T222212.
+               if ( isset( $fields['user'] ) &&
+                       ( $fields['user'] instanceof UserIdentity ) &&
+                       ( $this->wikiId === false ||
+                               ( !$fields['user']->getId() && !$fields['user']->getActorId() ) )
+               ) {
                        $user = $fields['user'];
                } else {
                        try {
                                $user = User::newFromAnyId(
                                        $fields['user'] ?? null,
                                        $fields['user_text'] ?? null,
-                                       $fields['actor'] ?? null
+                                       $fields['actor'] ?? null,
+                                       $this->wikiId
                                );
                        } catch ( InvalidArgumentException $ex ) {
                                $user = null;
@@ -2548,20 +2558,17 @@ class RevisionStore
        }
 
        /**
-        * Get the revision before $rev in the page's history, if any.
-        * Will return null for the first revision but also for deleted or unsaved revisions.
-        *
-        * MCR migration note: this replaces Revision::getPrevious
-        *
-        * @see Title::getPreviousRevisionID
-        * @see PageArchive::getPreviousRevision
+        * Implementation of getPreviousRevision and getNextRevision.
         *
         * @param RevisionRecord $rev
-        * @param Title|null $title if known (optional)
-        *
+        * @param int $flags
+        * @param string $dir 'next' or 'prev'
         * @return RevisionRecord|null
         */
-       public function getPreviousRevision( RevisionRecord $rev, Title $title = null ) {
+       private function getRelativeRevision( RevisionRecord $rev, $flags, $dir ) {
+               $op = $dir === 'next' ? '>' : '<';
+               $sort = $dir === 'next' ? 'ASC' : 'DESC';
+
                if ( !$rev->getId() || !$rev->getPageId() ) {
                        // revision is unsaved or otherwise incomplete
                        return null;
@@ -2572,54 +2579,86 @@ class RevisionStore
                        return null;
                }
 
-               if ( $title === null ) {
-                       // this would fail for deleted revisions
-                       $title = $this->getTitle( $rev->getPageId(), $rev->getId() );
+               list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
+               $db = $this->getDBConnection( $dbType, [ 'contributions' ] );
+
+               $ts = $this->getTimestampFromId( $rev->getId(), $flags );
+               if ( $ts === false ) {
+                       // XXX Should this be moved into getTimestampFromId?
+                       $ts = $db->selectField( 'archive', 'ar_timestamp',
+                               [ 'ar_rev_id' => $rev->getId() ], __METHOD__ );
+                       if ( $ts === false ) {
+                               // XXX Is this reachable? How can we have a page id but no timestamp?
+                               return null;
+                       }
                }
+               $ts = $db->addQuotes( $db->timestamp( $ts ) );
 
-               $prev = $title->getPreviousRevisionID( $rev->getId() );
-               if ( !$prev ) {
+               $revId = $db->selectField( 'revision', 'rev_id',
+                       [
+                               'rev_page' => $rev->getPageId(),
+                               "rev_timestamp $op $ts OR (rev_timestamp = $ts AND rev_id $op {$rev->getId()})"
+                       ],
+                       __METHOD__,
+                       [
+                               'ORDER BY' => "rev_timestamp $sort, rev_id $sort",
+                               'IGNORE INDEX' => 'rev_timestamp', // Probably needed for T159319
+                       ]
+               );
+
+               if ( $revId === false ) {
                        return null;
                }
 
-               return $this->getRevisionByTitle( $title, $prev );
+               return $this->getRevisionById( intval( $revId ) );
        }
 
        /**
-        * Get the revision after $rev in the page's history, if any.
-        * Will return null for the latest revision but also for deleted or unsaved revisions.
+        * Get the revision before $rev in the page's history, if any.
+        * Will return null for the first revision but also for deleted or unsaved revisions.
         *
-        * MCR migration note: this replaces Revision::getNext
+        * MCR migration note: this replaces Revision::getPrevious
         *
-        * @see Title::getNextRevisionID
+        * @see Title::getPreviousRevisionID
+        * @see PageArchive::getPreviousRevision
         *
         * @param RevisionRecord $rev
-        * @param Title|null $title if known (optional)
+        * @param int $flags (optional) $flags include:
+        *      IDBAccessObject::READ_LATEST: Select the data from the master
         *
         * @return RevisionRecord|null
         */
-       public function getNextRevision( RevisionRecord $rev, Title $title = null ) {
-               if ( !$rev->getId() || !$rev->getPageId() ) {
-                       // revision is unsaved or otherwise incomplete
-                       return null;
-               }
-
-               if ( $rev instanceof RevisionArchiveRecord ) {
-                       // revision is deleted, so it's not part of the page history
-                       return null;
+       public function getPreviousRevision( RevisionRecord $rev, $flags = 0 ) {
+               if ( $flags instanceof Title ) {
+                       // Old calling convention, we don't use Title here anymore
+                       wfDeprecated( __METHOD__ . ' with Title', '1.34' );
+                       $flags = 0;
                }
 
-               if ( $title === null ) {
-                       // this would fail for deleted revisions
-                       $title = $this->getTitle( $rev->getPageId(), $rev->getId() );
-               }
+               return $this->getRelativeRevision( $rev, $flags, 'prev' );
+       }
 
-               $next = $title->getNextRevisionID( $rev->getId() );
-               if ( !$next ) {
-                       return null;
+       /**
+        * Get the revision after $rev in the page's history, if any.
+        * Will return null for the latest revision but also for deleted or unsaved revisions.
+        *
+        * MCR migration note: this replaces Revision::getNext
+        *
+        * @see Title::getNextRevisionID
+        *
+        * @param RevisionRecord $rev
+        * @param int $flags (optional) $flags include:
+        *      IDBAccessObject::READ_LATEST: Select the data from the master
+        * @return RevisionRecord|null
+        */
+       public function getNextRevision( RevisionRecord $rev, $flags = 0 ) {
+               if ( $flags instanceof Title ) {
+                       // Old calling convention, we don't use Title here anymore
+                       wfDeprecated( __METHOD__ . ' with Title', '1.34' );
+                       $flags = 0;
                }
 
-               return $this->getRevisionByTitle( $title, $next );
+               return $this->getRelativeRevision( $rev, $flags, 'next' );
        }
 
        /**
@@ -2658,21 +2697,27 @@ class RevisionStore
        }
 
        /**
-        * Get rev_timestamp from rev_id, without loading the rest of the row
+        * Get rev_timestamp from rev_id, without loading the rest of the row.
+        *
+        * Historically, there was an extra Title parameter that was passed before $id. This is no
+        * longer needed and is deprecated in 1.34.
         *
         * MCR migration note: this replaces Revision::getTimestampFromId
         *
-        * @param Title $title
         * @param int $id
         * @param int $flags
         * @return string|bool False if not found
         */
-       public function getTimestampFromId( $title, $id, $flags = 0 ) {
+       public function getTimestampFromId( $id, $flags = 0 ) {
+               if ( $id instanceof Title ) {
+                       // Old deprecated calling convention supported for backwards compatibility
+                       $id = $flags;
+                       $flags = func_num_args() > 2 ? func_get_arg( 2 ) : 0;
+               }
                $db = $this->getDBConnectionRefForQueryFlags( $flags );
 
-               $conds = [ 'rev_id' => $id ];
-               $conds['rev_page'] = $title->getArticleID();
-               $timestamp = $db->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
+               $timestamp =
+                       $db->selectField( 'revision', 'rev_timestamp', [ 'rev_id' => $id ], __METHOD__ );
 
                return ( $timestamp !== false ) ? wfTimestamp( TS_MW, $timestamp ) : false;
        }
index bf722c3..a30534e 100644 (file)
@@ -208,7 +208,7 @@ return [
        },
 
        'GenderCache' => function ( MediaWikiServices $services ) : GenderCache {
-               return new GenderCache();
+               return new GenderCache( $services->getNamespaceInfo() );
        },
 
        'HttpRequestFactory' =>
@@ -231,7 +231,8 @@ return [
        'LinkCache' => function ( MediaWikiServices $services ) : LinkCache {
                return new LinkCache(
                        $services->getTitleFormatter(),
-                       $services->getMainWANObjectCache()
+                       $services->getMainWANObjectCache(),
+                       $services->getNamespaceInfo()
                );
        },
 
@@ -248,7 +249,8 @@ return [
        'LinkRendererFactory' => function ( MediaWikiServices $services ) : LinkRendererFactory {
                return new LinkRendererFactory(
                        $services->getTitleFormatter(),
-                       $services->getLinkCache()
+                       $services->getLinkCache(),
+                       $services->getNamespaceInfo()
                );
        },
 
@@ -363,7 +365,8 @@ return [
        },
 
        'NamespaceInfo' => function ( MediaWikiServices $services ) : NamespaceInfo {
-               return new NamespaceInfo( $services->getMainConfig() );
+               return new NamespaceInfo( new ServiceOptions( NamespaceInfo::$constructorOptions,
+                       $services->getMainConfig() ) );
        },
 
        'NameTableStoreFactory' => function ( MediaWikiServices $services ) : NameTableStoreFactory {
@@ -460,7 +463,8 @@ return [
                                DefaultPreferencesFactory::$constructorOptions, $services->getMainConfig() ),
                        $services->getContentLanguage(),
                        AuthManager::singleton(),
-                       $services->getLinkRendererFactory()->create()
+                       $services->getLinkRendererFactory()->create(),
+                       $services->getNamespaceInfo()
                );
                $factory->setLogger( LoggerFactory::getInstance( 'preferences' ) );
 
@@ -482,6 +486,15 @@ return [
                );
        },
 
+       'RepoGroup' => function ( MediaWikiServices $services ) : RepoGroup {
+               $config = $services->getMainConfig();
+               return new RepoGroup(
+                       $config->get( 'LocalFileRepo' ),
+                       $config->get( 'ForeignFileRepos' ),
+                       $services->getMainWANObjectCache()
+               );
+       },
+
        'ResourceLoader' => function ( MediaWikiServices $services ) : ResourceLoader {
                // @todo This should not take a Config object, but it's not so easy to remove because it
                // exposes it in a getter, which is actually used.
@@ -696,7 +709,9 @@ return [
                        $services->getMainObjectStash(),
                        new HashBagOStuff( [ 'maxKeys' => 100 ] ),
                        $services->getReadOnlyMode(),
-                       $services->getMainConfig()->get( 'UpdateRowsPerQuery' )
+                       $services->getMainConfig()->get( 'UpdateRowsPerQuery' ),
+                       $services->getNamespaceInfo(),
+                       $services->getRevisionLookup()
                );
                $store->setStatsdDataFactory( $services->getStatsdDataFactory() );
 
index 8adb218..e97db2d 100644 (file)
@@ -68,15 +68,15 @@ class SiteStatsInit {
         * @return int
         */
        public function articles() {
-               $config = MediaWikiServices::getInstance()->getMainConfig();
+               $services = MediaWikiServices::getInstance();
 
                $tables = [ 'page' ];
                $conds = [
-                       'page_namespace' => MWNamespace::getContentNamespaces(),
+                       'page_namespace' => $services->getNamespaceInfo()->getContentNamespaces(),
                        'page_is_redirect' => 0,
                ];
 
-               if ( $config->get( 'ArticleCountMethod' ) == 'link' ) {
+               if ( $services->getMainConfig()->get( 'ArticleCountMethod' ) == 'link' ) {
                        $tables[] = 'pagelinks';
                        $conds[] = 'pl_from=page_id';
                }
index 27baeb2..866f041 100644 (file)
@@ -618,7 +618,7 @@ class Title implements LinkTarget, IDBAccessObject {
                // NOTE: ideally, this would just call makeTitle() and then isValid(),
                // but presently, that means more overhead on a potential performance hotspot.
 
-               if ( !MWNamespace::exists( $ns ) ) {
+               if ( !MediaWikiServices::getInstance()->getNamespaceInfo()->exists( $ns ) ) {
                        return null;
                }
 
@@ -820,7 +820,8 @@ class Title implements LinkTarget, IDBAccessObject {
                $canonicalNamespace = false
        ) {
                if ( $canonicalNamespace ) {
-                       $namespace = MWNamespace::getCanonicalName( $ns );
+                       $namespace = MediaWikiServices::getInstance()->getNamespaceInfo()->
+                               getCanonicalName( $ns );
                } else {
                        $namespace = MediaWikiServices::getInstance()->getContentLanguage()->getNsText( $ns );
                }
@@ -862,13 +863,13 @@ class Title implements LinkTarget, IDBAccessObject {
         * @return bool
         */
        public function isValid() {
-               if ( !MWNamespace::exists( $this->mNamespace ) ) {
+               $services = MediaWikiServices::getInstance();
+               if ( !$services->getNamespaceInfo()->exists( $this->mNamespace ) ) {
                        return false;
                }
 
                try {
-                       $parser = MediaWikiServices::getInstance()->getTitleParser();
-                       $parser->parseTitle( $this->mDbkeyform, $this->mNamespace );
+                       $services->getTitleParser()->parseTitle( $this->mDbkeyform, $this->mNamespace );
                        return true;
                } catch ( MalformedTitleException $ex ) {
                        return false;
@@ -1086,7 +1087,8 @@ class Title implements LinkTarget, IDBAccessObject {
                if ( $this->isExternal() ) {
                        // This probably shouldn't even happen, except for interwiki transclusion.
                        // If possible, use the canonical name for the foreign namespace.
-                       $nsText = MWNamespace::getCanonicalName( $this->mNamespace );
+                       $nsText = MediaWikiServices::getInstance()->getNamespaceInfo()->
+                               getCanonicalName( $this->mNamespace );
                        if ( $nsText !== false ) {
                                return $nsText;
                        }
@@ -1107,8 +1109,9 @@ class Title implements LinkTarget, IDBAccessObject {
         * @return string Namespace text
         */
        public function getSubjectNsText() {
-               return MediaWikiServices::getInstance()->getContentLanguage()->
-                       getNsText( MWNamespace::getSubject( $this->mNamespace ) );
+               $services = MediaWikiServices::getInstance();
+               return $services->getContentLanguage()->
+                       getNsText( $services->getNamespaceInfo()->getSubject( $this->mNamespace ) );
        }
 
        /**
@@ -1117,20 +1120,22 @@ class Title implements LinkTarget, IDBAccessObject {
         * @return string Namespace text
         */
        public function getTalkNsText() {
-               return MediaWikiServices::getInstance()->getContentLanguage()->
-                       getNsText( MWNamespace::getTalk( $this->mNamespace ) );
+               $services = MediaWikiServices::getInstance();
+               return $services->getContentLanguage()->
+                       getNsText( $services->getNamespaceInfo()->getTalk( $this->mNamespace ) );
        }
 
        /**
         * Can this title have a corresponding talk page?
         *
-        * @see MWNamespace::hasTalkNamespace
+        * @see NamespaceInfo::hasTalkNamespace
         * @since 1.30
         *
         * @return bool True if this title either is a talk page or can have a talk page associated.
         */
        public function canHaveTalkPage() {
-               return MWNamespace::hasTalkNamespace( $this->mNamespace );
+               return MediaWikiServices::getInstance()->getNamespaceInfo()->
+                       hasTalkNamespace( $this->mNamespace );
        }
 
        /**
@@ -1148,7 +1153,8 @@ class Title implements LinkTarget, IDBAccessObject {
         * @return bool
         */
        public function isWatchable() {
-               return !$this->isExternal() && MWNamespace::isWatchable( $this->mNamespace );
+               return !$this->isExternal() && MediaWikiServices::getInstance()->getNamespaceInfo()->
+                       isWatchable( $this->mNamespace );
        }
 
        /**
@@ -1209,7 +1215,8 @@ class Title implements LinkTarget, IDBAccessObject {
         * @since 1.19
         */
        public function inNamespace( $ns ) {
-               return MWNamespace::equals( $this->mNamespace, $ns );
+               return MediaWikiServices::getInstance()->getNamespaceInfo()->
+                       equals( $this->mNamespace, $ns );
        }
 
        /**
@@ -1248,7 +1255,8 @@ class Title implements LinkTarget, IDBAccessObject {
         * @return bool
         */
        public function hasSubjectNamespace( $ns ) {
-               return MWNamespace::subjectEquals( $this->mNamespace, $ns );
+               return MediaWikiServices::getInstance()->getNamespaceInfo()->
+                       subjectEquals( $this->mNamespace, $ns );
        }
 
        /**
@@ -1259,7 +1267,8 @@ class Title implements LinkTarget, IDBAccessObject {
         * @return bool
         */
        public function isContentPage() {
-               return MWNamespace::isContent( $this->mNamespace );
+               return MediaWikiServices::getInstance()->getNamespaceInfo()->
+                       isContent( $this->mNamespace );
        }
 
        /**
@@ -1269,7 +1278,10 @@ class Title implements LinkTarget, IDBAccessObject {
         * @return bool
         */
        public function isMovable() {
-               if ( !MWNamespace::isMovable( $this->mNamespace ) || $this->isExternal() ) {
+               if (
+                       !MediaWikiServices::getInstance()->getNamespaceInfo()->
+                               isMovable( $this->mNamespace ) || $this->isExternal()
+               ) {
                        // Interwiki title or immovable namespace. Hooks don't get to override here
                        return false;
                }
@@ -1299,7 +1311,8 @@ class Title implements LinkTarget, IDBAccessObject {
         * @return bool
         */
        public function isSubpage() {
-               return MWNamespace::hasSubpages( $this->mNamespace )
+               return MediaWikiServices::getInstance()->getNamespaceInfo()->
+                       hasSubpages( $this->mNamespace )
                        ? strpos( $this->getText(), '/' ) !== false
                        : false;
        }
@@ -1495,16 +1508,19 @@ class Title implements LinkTarget, IDBAccessObject {
         * @return bool
         */
        public function isTalkPage() {
-               return MWNamespace::isTalk( $this->mNamespace );
+               return MediaWikiServices::getInstance()->getNamespaceInfo()->
+                       isTalk( $this->mNamespace );
        }
 
        /**
         * Get a Title object associated with the talk page of this article
         *
+        * @deprecated since 1.34, use NamespaceInfo::getTalkPage
         * @return Title The object for the talk page
         */
        public function getTalkPage() {
-               return self::makeTitle( MWNamespace::getTalk( $this->mNamespace ), $this->mDbkeyform );
+               return self::castFromLinkTarget(
+                       MediaWikiServices::getInstance()->getNamespaceInfo()->getTalkPage( $this ) );
        }
 
        /**
@@ -1528,37 +1544,26 @@ class Title implements LinkTarget, IDBAccessObject {
         * Get a title object associated with the subject page of this
         * talk page
         *
+        * @deprecated since 1.34, use NamespaceInfo::getSubjectPage
         * @return Title The object for the subject page
         */
        public function getSubjectPage() {
-               // Is this the same title?
-               $subjectNS = MWNamespace::getSubject( $this->mNamespace );
-               if ( $this->mNamespace == $subjectNS ) {
-                       return $this;
-               }
-               return self::makeTitle( $subjectNS, $this->mDbkeyform );
+               return self::castFromLinkTarget(
+                       MediaWikiServices::getInstance()->getNamespaceInfo()->getSubjectPage( $this ) );
        }
 
        /**
         * Get the other title for this page, if this is a subject page
         * get the talk page, if it is a subject page get the talk page
         *
+        * @deprecated since 1.34, use NamespaceInfo::getAssociatedPage
         * @since 1.25
         * @throws MWException If the page doesn't have an other page
         * @return Title
         */
        public function getOtherPage() {
-               if ( $this->isSpecialPage() ) {
-                       throw new MWException( 'Special pages cannot have other pages' );
-               }
-               if ( $this->isTalkPage() ) {
-                       return $this->getSubjectPage();
-               } else {
-                       if ( !$this->canHaveTalkPage() ) {
-                               throw new MWException( "{$this->getPrefixedText()} does not have an other page" );
-                       }
-                       return $this->getTalkPage();
-               }
+               return self::castFromLinkTarget(
+                       MediaWikiServices::getInstance()->getNamespaceInfo()->getAssociatedPage( $this ) );
        }
 
        /**
@@ -1733,7 +1738,10 @@ class Title implements LinkTarget, IDBAccessObject {
         * @since 1.20
         */
        public function getRootText() {
-               if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
+               if (
+                       !MediaWikiServices::getInstance()->getNamespaceInfo()->
+                               hasSubpages( $this->mNamespace )
+               ) {
                        return $this->getText();
                }
 
@@ -1769,7 +1777,10 @@ class Title implements LinkTarget, IDBAccessObject {
         */
        public function getBaseText() {
                $text = $this->getText();
-               if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
+               if (
+                       !MediaWikiServices::getInstance()->getNamespaceInfo()->
+                               hasSubpages( $this->mNamespace )
+               ) {
                        return $text;
                }
 
@@ -1810,7 +1821,10 @@ class Title implements LinkTarget, IDBAccessObject {
         * @return string Subpage name
         */
        public function getSubpageText() {
-               if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
+               if (
+                       !MediaWikiServices::getInstance()->getNamespaceInfo()->
+                               hasSubpages( $this->mNamespace )
+               ) {
                        return $this->mTextform;
                }
                $parts = explode( '/', $this->mTextform );
@@ -2877,7 +2891,10 @@ class Title implements LinkTarget, IDBAccessObject {
         * @return bool
         */
        public function hasSubpages() {
-               if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
+               if (
+                       !MediaWikiServices::getInstance()->getNamespaceInfo()->
+                               hasSubpages( $this->mNamespace )
+               ) {
                        # Duh
                        return false;
                }
@@ -2905,7 +2922,10 @@ class Title implements LinkTarget, IDBAccessObject {
         *  doesn't allow subpages
         */
        public function getSubpages( $limit = -1 ) {
-               if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
+               if (
+                       !MediaWikiServices::getInstance()->getNamespaceInfo()->
+                               hasSubpages( $this->mNamespace )
+               ) {
                        return [];
                }
 
@@ -3139,7 +3159,8 @@ class Title implements LinkTarget, IDBAccessObject {
         * @return string Containing capitalized title
         */
        public static function capitalize( $text, $ns = NS_MAIN ) {
-               if ( MWNamespace::isCapitalized( $ns ) ) {
+               $services = MediaWikiServices::getInstance();
+               if ( $services->getNamespaceInfo()->isCapitalized( $ns ) ) {
                        return MediaWikiServices::getInstance()->getContentLanguage()->ucfirst( $text );
                } else {
                        return $text;
@@ -3445,19 +3466,10 @@ class Title implements LinkTarget, IDBAccessObject {
                array $changeTags = []
        ) {
                global $wgUser;
-               $err = $this->isValidMoveOperation( $nt, $auth, $reason );
-               if ( is_array( $err ) ) {
-                       // Auto-block user's IP if the account was "hard" blocked
-                       $wgUser->spreadAnyEditBlock();
-                       return $err;
-               }
-               // Check suppressredirect permission
-               if ( $auth && !$wgUser->isAllowed( 'suppressredirect' ) ) {
-                       $createRedirect = true;
-               }
 
                $mp = new MovePage( $this, $nt );
-               $status = $mp->move( $wgUser, $reason, $createRedirect, $changeTags );
+               $method = $auth ? 'moveIfAllowed' : 'move';
+               $status = $mp->$method( $wgUser, $reason, $createRedirect, $changeTags );
                if ( $status->isOK() ) {
                        return true;
                } else {
@@ -3490,14 +3502,15 @@ class Title implements LinkTarget, IDBAccessObject {
                        ];
                }
                // Do the source and target namespaces support subpages?
-               if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
+               $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
+               if ( !$nsInfo->hasSubpages( $this->mNamespace ) ) {
                        return [
-                               [ 'namespace-nosubpages', MWNamespace::getCanonicalName( $this->mNamespace ) ],
+                               [ 'namespace-nosubpages', $nsInfo->getCanonicalName( $this->mNamespace ) ],
                        ];
                }
-               if ( !MWNamespace::hasSubpages( $nt->getNamespace() ) ) {
+               if ( !$nsInfo->hasSubpages( $nt->getNamespace() ) ) {
                        return [
-                               [ 'namespace-nosubpages', MWNamespace::getCanonicalName( $nt->getNamespace() ) ],
+                               [ 'namespace-nosubpages', $nsInfo->getCanonicalName( $nt->getNamespace() ) ],
                        ];
                }
 
@@ -3730,57 +3743,25 @@ class Title implements LinkTarget, IDBAccessObject {
         * @return int|bool New revision ID, or false if none exists
         */
        private function getRelativeRevisionID( $revId, $flags, $dir ) {
-               $revId = (int)$revId;
-               if ( $dir === 'next' ) {
-                       $op = '>';
-                       $sort = 'ASC';
-               } elseif ( $dir === 'prev' ) {
-                       $op = '<';
-                       $sort = 'DESC';
-               } else {
-                       throw new InvalidArgumentException( '$dir must be "next" or "prev"' );
-               }
-
-               if ( $flags & self::GAID_FOR_UPDATE ) {
-                       $db = wfGetDB( DB_MASTER );
-               } else {
-                       $db = wfGetDB( DB_REPLICA, 'contributions' );
-               }
-
-               // Intentionally not caring if the specified revision belongs to this
-               // page. We only care about the timestamp.
-               $ts = $db->selectField( 'revision', 'rev_timestamp', [ 'rev_id' => $revId ], __METHOD__ );
-               if ( $ts === false ) {
-                       $ts = $db->selectField( 'archive', 'ar_timestamp', [ 'ar_rev_id' => $revId ], __METHOD__ );
-                       if ( $ts === false ) {
-                               // Or should this throw an InvalidArgumentException or something?
-                               return false;
-                       }
+               $rl = MediaWikiServices::getInstance()->getRevisionLookup();
+               $rlFlags = $flags === self::GAID_FOR_UPDATE ? IDBAccessObject::READ_LATEST : 0;
+               $rev = $rl->getRevisionById( $revId, $rlFlags );
+               if ( !$rev ) {
+                       return false;
                }
-               $ts = $db->addQuotes( $ts );
-
-               $revId = $db->selectField( 'revision', 'rev_id',
-                       [
-                               'rev_page' => $this->getArticleID( $flags ),
-                               "rev_timestamp $op $ts OR (rev_timestamp = $ts AND rev_id $op $revId)"
-                       ],
-                       __METHOD__,
-                       [
-                               'ORDER BY' => "rev_timestamp $sort, rev_id $sort",
-                               'IGNORE INDEX' => 'rev_timestamp', // Probably needed for T159319
-                       ]
-               );
-
-               if ( $revId === false ) {
+               $oldRev = $dir === 'next'
+                       ? $rl->getNextRevision( $rev, $rlFlags )
+                       : $rl->getPreviousRevision( $rev, $rlFlags );
+               if ( !$oldRev ) {
                        return false;
-               } else {
-                       return intval( $revId );
                }
+               return $oldRev->getId();
        }
 
        /**
         * Get the revision ID of the previous revision
         *
+        * @deprecated since 1.34, use RevisionLookup::getPreviousRevision
         * @param int $revId Revision ID. Get the revision that was before this one.
         * @param int $flags Title::GAID_FOR_UPDATE
         * @return int|bool Old revision ID, or false if none exists
@@ -3792,6 +3773,7 @@ class Title implements LinkTarget, IDBAccessObject {
        /**
         * Get the revision ID of the next revision
         *
+        * @deprecated since 1.34, use RevisionLookup::getNextRevision
         * @param int $revId Revision ID. Get the revision that was after this one.
         * @param int $flags Title::GAID_FOR_UPDATE
         * @return int|bool Next revision ID, or false if none exists
@@ -4031,14 +4013,14 @@ class Title implements LinkTarget, IDBAccessObject {
        /**
         * Compare with another title.
         *
-        * @param Title $title
+        * @param LinkTarget $title
         * @return bool
         */
-       public function equals( Title $title ) {
+       public function equals( LinkTarget $title ) {
                // Note: === is necessary for proper matching of number-like titles.
-               return $this->mInterwiki === $title->mInterwiki
-                       && $this->mNamespace == $title->mNamespace
-                       && $this->mDbkeyform === $title->mDbkeyform;
+               return $this->mInterwiki === $title->getInterwiki()
+                       && $this->mNamespace == $title->getNamespace()
+                       && $this->mDbkeyform === $title->getDBkey();
        }
 
        /**
@@ -4342,9 +4324,10 @@ class Title implements LinkTarget, IDBAccessObject {
         */
        public function getNamespaceKey( $prepend = 'nstab-' ) {
                // Gets the subject namespace of this title
-               $subjectNS = MWNamespace::getSubject( $this->mNamespace );
+               $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
+               $subjectNS = $nsInfo->getSubject( $this->mNamespace );
                // Prefer canonical namespace name for HTML IDs
-               $namespaceKey = MWNamespace::getCanonicalName( $subjectNS );
+               $namespaceKey = $nsInfo->getCanonicalName( $subjectNS );
                if ( $namespaceKey === false ) {
                        // Fallback to localised text
                        $namespaceKey = $this->getSubjectNsText();
@@ -4440,7 +4423,8 @@ class Title implements LinkTarget, IDBAccessObject {
        public function canUseNoindex() {
                global $wgExemptFromUserRobotsControl;
 
-               $bannedNamespaces = $wgExemptFromUserRobotsControl ?? MWNamespace::getContentNamespaces();
+               $bannedNamespaces = $wgExemptFromUserRobotsControl ??
+                       MediaWikiServices::getInstance()->getNamespaceInfo()->getContentNamespaces();
 
                return !in_array( $this->mNamespace, $bannedNamespaces );
        }
@@ -4607,7 +4591,10 @@ class Title implements LinkTarget, IDBAccessObject {
                        }
                }
 
-               if ( MWNamespace::hasSubpages( $this->mNamespace ) ) {
+               if (
+                       MediaWikiServices::getInstance()->getNamespaceInfo()->
+                               hasSubpages( $this->mNamespace )
+               ) {
                        // Optional notice for page itself and any parent page
                        $editnotice_base = $editnotice_ns;
                        foreach ( explode( '/', $this->mDbkeyform ) as $part ) {
index b3a49c7..ebdbc42 100644 (file)
@@ -19,6 +19,8 @@
  * @ingroup Categories
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * This class performs some operations related to tracking categories, such as creating
  * a list of all such categories.
@@ -80,6 +82,7 @@ class TrackingCategories {
                }
 
                $trackingCategories = [];
+               $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
                foreach ( $categories as $catMsg ) {
                        /*
                         * Check if the tracking category varies by namespace
@@ -96,7 +99,7 @@ class TrackingCategories {
                        // Match things like {{NAMESPACE}} and {{NAMESPACENUMBER}}.
                        // False positives are ok, this is just an efficiency shortcut
                        if ( strpos( $msgObj->plain(), '{{' ) !== false ) {
-                               $ns = MWNamespace::getValidNamespaces();
+                               $ns = $nsInfo->getValidNamespaces();
                                foreach ( $ns as $namesp ) {
                                        $tempTitle = Title::makeTitleSafe( $namesp, $catMsg );
                                        if ( !$tempTitle ) {
index 49a6bb5..bfba59a 100644 (file)
@@ -399,7 +399,7 @@ class InfoAction extends FormlessAction {
                }
 
                // Subpages of this page, if subpages are enabled for the current NS
-               if ( MWNamespace::hasSubpages( $title->getNamespace() ) ) {
+               if ( $services->getNamespaceInfo()->hasSubpages( $title->getNamespace() ) ) {
                        $prefixIndex = SpecialPage::getTitleFor(
                                'Prefixindex', $title->getPrefixedText() . '/' );
                        $pageInfo['header-basic'][] = [
@@ -730,12 +730,13 @@ class InfoAction extends FormlessAction {
        protected function pageCounts( Page $page ) {
                $fname = __METHOD__;
                $config = $this->context->getConfig();
-               $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+               $services = MediaWikiServices::getInstance();
+               $cache = $services->getMainWANObjectCache();
 
                return $cache->getWithSetCallback(
                        self::getCacheKey( $cache, $page->getTitle(), $page->getLatest() ),
                        WANObjectCache::TTL_WEEK,
-                       function ( $oldValue, &$ttl, &$setOpts ) use ( $page, $config, $fname ) {
+                       function ( $oldValue, &$ttl, &$setOpts ) use ( $page, $config, $fname, $services ) {
                                global $wgActorTableSchemaMigrationStage;
 
                                $title = $page->getTitle();
@@ -759,7 +760,7 @@ class InfoAction extends FormlessAction {
                                        $joins = [];
                                }
 
-                               $watchedItemStore = MediaWikiServices::getInstance()->getWatchedItemStore();
+                               $watchedItemStore = $services->getWatchedItemStore();
 
                                $result = [];
                                $result['watchers'] = $watchedItemStore->countWatchers( $title );
@@ -824,7 +825,7 @@ class InfoAction extends FormlessAction {
                                );
 
                                // Subpages (if enabled)
-                               if ( MWNamespace::hasSubpages( $title->getNamespace() ) ) {
+                               if ( $services->getNamespaceInfo()->hasSubpages( $title->getNamespace() ) ) {
                                        $conds = [ 'page_namespace' => $title->getNamespace() ];
                                        $conds[] = 'page_title ' .
                                                $dbr->buildLike( $title->getDBkey() . '/', $dbr->anyString() );
index 8ab92af..dbf72be 100644 (file)
@@ -20,6 +20,7 @@
  * @file
  */
 
+use MediaWiki\MediaWikiServices;
 use Wikimedia\Rdbms\IDatabase;
 
 /**
@@ -36,6 +37,8 @@ use Wikimedia\Rdbms\IDatabase;
  */
 abstract class ApiBase extends ContextSource {
 
+       use ApiBlockInfoTrait;
+
        /**
         * @name Constants for ::getAllowedParams() arrays
         * These constants are keys in the arrays returned by ::getAllowedParams()
@@ -1196,7 +1199,8 @@ abstract class ApiBase extends ContextSource {
                        $provided = $this->getMain()->getCheck( $encParamName );
 
                        if ( isset( $value ) && $type == 'namespace' ) {
-                               $type = MWNamespace::getValidNamespaces();
+                               $type = MediaWikiServices::getInstance()->getNamespaceInfo()->
+                                       getValidNamespaces();
                                if ( isset( $paramSettings[self::PARAM_EXTRA_NAMESPACES] ) &&
                                        is_array( $paramSettings[self::PARAM_EXTRA_NAMESPACES] )
                                ) {
@@ -1811,7 +1815,7 @@ abstract class ApiBase extends ContextSource {
                        if ( is_string( $error[0] ) && isset( self::$blockMsgMap[$error[0]] ) && $user->getBlock() ) {
                                list( $msg, $code ) = self::$blockMsgMap[$error[0]];
                                $status->fatal( ApiMessage::create( $msg, $code,
-                                       [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ]
+                                       [ 'blockinfo' => $this->getBlockInfo( $user->getBlock() ) ]
                                ) );
                        } else {
                                $status->fatal( ...$error );
@@ -1834,7 +1838,7 @@ abstract class ApiBase extends ContextSource {
                foreach ( self::$blockMsgMap as $msg => list( $apiMsg, $code ) ) {
                        if ( $status->hasMessage( $msg ) && $user->getBlock() ) {
                                $status->replaceMessage( $msg, ApiMessage::create( $apiMsg, $code,
-                                       [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ]
+                                       [ 'blockinfo' => $this->getBlockInfo( $user->getBlock() ) ]
                                ) );
                        }
                }
@@ -2033,19 +2037,19 @@ abstract class ApiBase extends ContextSource {
                        $this->dieWithError(
                                'apierror-autoblocked',
                                'autoblocked',
-                               [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ]
+                               [ 'blockinfo' => $this->getBlockInfo( $block ) ]
                        );
                } elseif ( !$block->isSitewide() ) {
                        $this->dieWithError(
                                'apierror-blocked-partial',
                                'blocked',
-                               [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ]
+                               [ 'blockinfo' => $this->getBlockInfo( $block ) ]
                        );
                } else {
                        $this->dieWithError(
                                'apierror-blocked',
                                'blocked',
-                               [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ]
+                               [ 'blockinfo' => $this->getBlockInfo( $block ) ]
                        );
                }
        }
index b5d51aa..336943d 100644 (file)
@@ -28,6 +28,8 @@
  */
 class ApiBlock extends ApiBase {
 
+       use ApiBlockInfoTrait;
+
        /**
         * Blocks the user specified in the parameters for the given expiry, with the
         * given reason, and with all other settings provided in the params. If the block
@@ -50,7 +52,7 @@ class ApiBlock extends ApiBase {
                                $this->dieWithError(
                                        $status,
                                        null,
-                                       [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ]
+                                       [ 'blockinfo' => $this->getBlockInfo( $block ) ]
                                );
                        }
                }
diff --git a/includes/api/ApiBlockInfoTrait.php b/includes/api/ApiBlockInfoTrait.php
new file mode 100644 (file)
index 0000000..2663485
--- /dev/null
@@ -0,0 +1,53 @@
+<?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
+ */
+
+/**
+ * @ingroup API
+ */
+trait ApiBlockInfoTrait {
+
+       /**
+        * Get basic info about a given block
+        * @param Block $block
+        * @return array Array containing several keys:
+        *  - blockid - ID of the block
+        *  - blockedby - username of the blocker
+        *  - blockedbyid - user ID of the blocker
+        *  - blockreason - reason provided for the block
+        *  - blockedtimestamp - timestamp for when the block was placed/modified
+        *  - blockexpiry - expiry time of the block
+        *  - systemblocktype - system block type, if any
+        */
+       private function getBlockInfo( Block $block ) {
+               $vals = [];
+               $vals['blockid'] = $block->getId();
+               $vals['blockedby'] = $block->getByName();
+               $vals['blockedbyid'] = $block->getBy();
+               $vals['blockreason'] = $block->getReason();
+               $vals['blockedtimestamp'] = wfTimestamp( TS_ISO_8601, $block->getTimestamp() );
+               $vals['blockexpiry'] = ApiResult::formatExpiry( $block->getExpiry(), 'infinite' );
+               $vals['blockpartial'] = !$block->isSitewide();
+               if ( $block->getSystemBlockType() !== null ) {
+                       $vals['systemblocktype'] = $block->getSystemBlockType();
+               }
+               return $vals;
+       }
+
+}
index 1656e7c..78efe41 100644 (file)
@@ -583,7 +583,8 @@ class ApiHelp extends ApiBase {
                                                                        break;
 
                                                                case 'namespace':
-                                                                       $namespaces = MWNamespace::getValidNamespaces();
+                                                                       $namespaces = MediaWikiServices::getInstance()->
+                                                                               getNamespaceInfo()->getValidNamespaces();
                                                                        if ( isset( $settings[ApiBase::PARAM_EXTRA_NAMESPACES] ) &&
                                                                                is_array( $settings[ApiBase::PARAM_EXTRA_NAMESPACES] )
                                                                        ) {
index b321c7d..64c6f45 100644 (file)
@@ -866,6 +866,8 @@ class ApiPageSet extends ApiBase {
                        ApiBase::dieDebug( __METHOD__, 'Missing $processTitles parameter when $remaining is provided' );
                }
 
+               $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
+
                $usernames = [];
                if ( $res ) {
                        foreach ( $res as $row ) {
@@ -884,7 +886,7 @@ class ApiPageSet extends ApiBase {
                                $this->processDbRow( $row );
 
                                // Need gender information
-                               if ( MWNamespace::hasGenderDistinction( $row->page_namespace ) ) {
+                               if ( $nsInfo->hasGenderDistinction( $row->page_namespace ) ) {
                                        $usernames[] = $row->page_title;
                                }
                        }
@@ -907,7 +909,7 @@ class ApiPageSet extends ApiBase {
                                                $this->mTitles[] = $title;
 
                                                // need gender information
-                                               if ( MWNamespace::hasGenderDistinction( $ns ) ) {
+                                               if ( $nsInfo->hasGenderDistinction( $ns ) ) {
                                                        $usernames[] = $dbkey;
                                                }
                                        }
@@ -1249,7 +1251,10 @@ class ApiPageSet extends ApiBase {
                        }
 
                        // Need gender information
-                       if ( MWNamespace::hasGenderDistinction( $titleObj->getNamespace() ) ) {
+                       if (
+                               MediaWikiServices::getInstance()->getNamespaceInfo()->
+                                       hasGenderDistinction( $titleObj->getNamespace() )
+                       ) {
                                $usernames[] = $titleObj->getText();
                        }
                }
index bb50185..beaad43 100644 (file)
@@ -49,7 +49,8 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase {
                $user = $this->getUser();
                $db = $this->getDB();
                $params = $this->extractRequestParams( false );
-               $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
+               $services = MediaWikiServices::getInstance();
+               $revisionStore = $services->getRevisionStore();
 
                $result = $this->getResult();
 
@@ -156,7 +157,8 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase {
                $miser_ns = null;
 
                if ( $mode == 'all' ) {
-                       $namespaces = $params['namespace'] ?? MWNamespace::getValidNamespaces();
+                       $namespaces = $params['namespace'] ??
+                               $services->getNamespaceInfo()->getValidNamespaces();
                        $this->addWhereFld( 'ar_namespace', $namespaces );
 
                        // For from/to/prefix, we have to consider the potential
index 1940600..08f3ea3 100644 (file)
@@ -211,12 +211,13 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase {
                $res = $this->select( __METHOD__ );
 
                // Get gender information
-               if ( MWNamespace::hasGenderDistinction( $params['namespace'] ) ) {
+               $services = MediaWikiServices::getInstance();
+               if ( $services->getNamespaceInfo()->hasGenderDistinction( $params['namespace'] ) ) {
                        $users = [];
                        foreach ( $res as $row ) {
                                $users[] = $row->page_title;
                        }
-                       MediaWikiServices::getInstance()->getGenderCache()->doQuery( $users, __METHOD__ );
+                       $services->getGenderCache()->doQuery( $users, __METHOD__ );
                        $res->rewind(); // reset
                }
 
index 58445a1..050bc0f 100644 (file)
@@ -44,7 +44,8 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase {
 
                $db = $this->getDB();
                $params = $this->extractRequestParams( false );
-               $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
+               $services = MediaWikiServices::getInstance();
+               $revisionStore = $services->getRevisionStore();
 
                $result = $this->getResult();
 
@@ -70,7 +71,7 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase {
                if ( $params['namespace'] !== null ) {
                        $params['namespace'] = array_unique( $params['namespace'] );
                        sort( $params['namespace'] );
-                       if ( $params['namespace'] != MWNamespace::getValidNamespaces() ) {
+                       if ( $params['namespace'] != $services->getNamespaceInfo()->getValidNamespaces() ) {
                                $needPageTable = true;
                                if ( $this->getConfig()->get( 'MiserMode' ) ) {
                                        $miser_ns = $params['namespace'];
index a5437ba..276aafb 100644 (file)
@@ -708,10 +708,11 @@ class ApiQueryInfo extends ApiQueryBase {
         */
        private function getTSIDs() {
                $getTitles = $this->talkids = $this->subjectids = [];
+               $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
 
                /** @var Title $t */
                foreach ( $this->everything as $t ) {
-                       if ( MWNamespace::isTalk( $t->getNamespace() ) ) {
+                       if ( $nsInfo->isTalk( $t->getNamespace() ) ) {
                                if ( $this->fld_subjectid ) {
                                        $getTitles[] = $t->getSubjectPage();
                                }
@@ -734,12 +735,12 @@ class ApiQueryInfo extends ApiQueryBase {
                $this->addWhere( $lb->constructSet( 'page', $db ) );
                $res = $this->select( __METHOD__ );
                foreach ( $res as $row ) {
-                       if ( MWNamespace::isTalk( $row->page_namespace ) ) {
-                               $this->talkids[MWNamespace::getSubject( $row->page_namespace )][$row->page_title] =
-                                       (int)$row->page_id;
+                       if ( $nsInfo->isTalk( $row->page_namespace ) ) {
+                               $this->talkids[$nsInfo->getSubject( $row->page_namespace )][$row->page_title] =
+                                       (int)( $row->page_id );
                        } else {
-                               $this->subjectids[MWNamespace::getTalk( $row->page_namespace )][$row->page_title] =
-                                       (int)$row->page_id;
+                               $this->subjectids[$nsInfo->getTalk( $row->page_namespace )][$row->page_title] =
+                                       (int)( $row->page_id );
                        }
                }
        }
index e6403f3..98c6551 100644 (file)
@@ -67,13 +67,6 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
                $search->setFeatureData( 'rewrite', (bool)$params['enablerewrites'] );
                $search->setFeatureData( 'interwiki', (bool)$interwiki );
 
-               $nquery = $search->transformSearchTerm( $query );
-               if ( $nquery !== $query ) {
-                       $query = $nquery;
-                       wfDeprecated( 'SearchEngine::transformSearchTerm() (overridden by ' .
-                               get_class( $search ) . ')', '1.32' );
-               }
-
                $nquery = $search->replacePrefixes( $query );
                if ( $nquery !== $query ) {
                        $query = $nquery;
index 68ab725..7e4a891 100644 (file)
@@ -282,27 +282,28 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                $data = [
                        ApiResult::META_TYPE => 'assoc',
                ];
+               $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
                foreach (
                        MediaWikiServices::getInstance()->getContentLanguage()->getFormattedNamespaces()
                        as $ns => $title
                ) {
                        $data[$ns] = [
                                'id' => (int)$ns,
-                               'case' => MWNamespace::isCapitalized( $ns ) ? 'first-letter' : 'case-sensitive',
+                               'case' => $nsInfo->isCapitalized( $ns ) ? 'first-letter' : 'case-sensitive',
                        ];
                        ApiResult::setContentValue( $data[$ns], 'name', $title );
-                       $canonical = MWNamespace::getCanonicalName( $ns );
+                       $canonical = $nsInfo->getCanonicalName( $ns );
 
-                       $data[$ns]['subpages'] = MWNamespace::hasSubpages( $ns );
+                       $data[$ns]['subpages'] = $nsInfo->hasSubpages( $ns );
 
                        if ( $canonical ) {
                                $data[$ns]['canonical'] = strtr( $canonical, '_', ' ' );
                        }
 
-                       $data[$ns]['content'] = MWNamespace::isContent( $ns );
-                       $data[$ns]['nonincludable'] = MWNamespace::isNonincludable( $ns );
+                       $data[$ns]['content'] = $nsInfo->isContent( $ns );
+                       $data[$ns]['nonincludable'] = $nsInfo->isNonincludable( $ns );
 
-                       $contentmodel = MWNamespace::getNamespaceContentModel( $ns );
+                       $contentmodel = $nsInfo->getNamespaceContentModel( $ns );
                        if ( $contentmodel ) {
                                $data[$ns]['defaultcontentmodel'] = $contentmodel;
                        }
index 00d7d84..c495c6d 100644 (file)
@@ -29,6 +29,8 @@ use MediaWiki\MediaWikiServices;
  */
 class ApiQueryUserInfo extends ApiQueryBase {
 
+       use ApiBlockInfoTrait;
+
        const WL_UNREAD_LIMIT = 1000;
 
        private $params = [];
@@ -50,33 +52,6 @@ class ApiQueryUserInfo extends ApiQueryBase {
                $result->addValue( 'query', $this->getModuleName(), $r );
        }
 
-       /**
-        * Get basic info about a given block
-        * @param Block $block
-        * @return array Array containing several keys:
-        *  - blockid - ID of the block
-        *  - blockedby - username of the blocker
-        *  - blockedbyid - user ID of the blocker
-        *  - blockreason - reason provided for the block
-        *  - blockedtimestamp - timestamp for when the block was placed/modified
-        *  - blockexpiry - expiry time of the block
-        *  - systemblocktype - system block type, if any
-        */
-       public static function getBlockInfo( Block $block ) {
-               $vals = [];
-               $vals['blockid'] = $block->getId();
-               $vals['blockedby'] = $block->getByName();
-               $vals['blockedbyid'] = $block->getBy();
-               $vals['blockreason'] = $block->getReason();
-               $vals['blockedtimestamp'] = wfTimestamp( TS_ISO_8601, $block->getTimestamp() );
-               $vals['blockexpiry'] = ApiResult::formatExpiry( $block->getExpiry(), 'infinite' );
-               $vals['blockpartial'] = !$block->isSitewide();
-               if ( $block->getSystemBlockType() !== null ) {
-                       $vals['systemblocktype'] = $block->getSystemBlockType();
-               }
-               return $vals;
-       }
-
        /**
         * Get central user info
         * @param Config $config
@@ -129,7 +104,7 @@ class ApiQueryUserInfo extends ApiQueryBase {
                if ( isset( $this->prop['blockinfo'] ) ) {
                        $block = $user->getBlock();
                        if ( $block ) {
-                               $vals = array_merge( $vals, self::getBlockInfo( $block ) );
+                               $vals = array_merge( $vals, $this->getBlockInfo( $block ) );
                        }
                }
 
index ba4c6e8..d2bbe7b 100644 (file)
@@ -77,8 +77,9 @@ class ApiSetNotificationTimestamp extends ApiBase {
                        $titles = $pageSet->getGoodTitles();
                        $title = reset( $titles );
                        if ( $title ) {
+                               // XXX $title isn't actually used, can we just get rid of the previous six lines?
                                $timestamp = MediaWikiServices::getInstance()->getRevisionStore()
-                                       ->getTimestampFromId( $title, $params['torevid'], IDBAccessObject::READ_LATEST );
+                                       ->getTimestampFromId( $params['torevid'], IDBAccessObject::READ_LATEST );
                                if ( $timestamp ) {
                                        $timestamp = $dbw->timestamp( $timestamp );
                                } else {
index 3aad8f4..f038b96 100644 (file)
@@ -28,6 +28,8 @@
  */
 class ApiUnblock extends ApiBase {
 
+       use ApiBlockInfoTrait;
+
        /**
         * Unblocks the specified user or provides the reason the unblock failed.
         */
@@ -48,7 +50,7 @@ class ApiUnblock extends ApiBase {
                                $this->dieWithError(
                                        $status,
                                        null,
-                                       [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ]
+                                       [ 'blockinfo' => $this->getBlockInfo( $block ) ]
                                );
                        }
                }
index 9497d8d..ea76a45 100644 (file)
        "apihelp-edit-param-text": "문서 내용.",
        "apihelp-edit-param-summary": "편집 요약. 또한 $1section=new 및 $1sectiontitle이 설정되어 있지 않을 때 문단 제목.",
        "apihelp-edit-param-tags": "이 판에 적용할 태그를 변경합니다.",
-       "apihelp-edit-param-minor": "ì\82¬ì\86\8cí\95\9c í\8e¸ì§\91.",
+       "apihelp-edit-param-minor": "ì\9d´ í\8e¸ì§\91ì\9d\84 ì\82¬ì\86\8cí\95\9c í\8e¸ì§\91ì\9c¼ë¡\9c í\91\9cì\8b\9cí\95©ë\8b\88ë\8b¤.",
        "apihelp-edit-param-notminor": "사소하지 않은 편집.",
        "apihelp-edit-param-bot": "이 편집을 봇 편집으로 표시.",
        "apihelp-edit-param-basetimestamp": "기본 판의 타임스탬프이며, 편집 충돌을 발견하기 위해 사용됩니다. [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]]를 통해 가져올 수 있습니다.",
index 84eef72..15bc802 100644 (file)
@@ -17,7 +17,8 @@
                        "Hex",
                        "Mainframe98",
                        "Southparkfan",
-                       "Elroy"
+                       "Elroy",
+                       "Rots61"
                ]
        },
        "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Documentatie]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api E-maillijst]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API-aankondigingen]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bugs & verzoeken]\n</div>\n<strong>Status:</strong> De MediaWiki API is een stabiele interface die actief ondersteund en verbeterd wordt. Hoewel we het proberen te voorkomen, is het mogelijk dat er soms wijzigingen worden aangebracht die bepaalde API-verzoek kunnen verhinderen; abonneer u op de [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ e-maillijst mediawiki-api-announce] voor meldingen over wijzigingen.\n\n<strong>Foutieve verzoeken:</strong> als de API foutieve verzoeken ontvangt, wordt er geantwoord met een HTTP-header met de sleutel \"MediaWiki-API-Error\" en daarna worden de waarde van de header en de foutcode op dezelfde waarde ingesteld. Zie [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Foutmeldingen en waarschuwingen]] voor meer informatie.\n\n<p class=\"mw-apisandbox-link\"><strong>Testen:</strong> u kunt [[Special:ApiSandbox|eenvoudig API-verzoeken testen]].</p>",
@@ -87,7 +88,7 @@
        "apihelp-edit-param-sectiontitle": "De naam van een nieuwe sectie.",
        "apihelp-edit-param-text": "Pagina-inhoud.",
        "apihelp-edit-param-tags": "De labels voor de revisie wijzigen.",
-       "apihelp-edit-param-minor": "Kleine bewerking.",
+       "apihelp-edit-param-minor": "Mankeer deze bewerking als een kleine bewerking.",
        "apihelp-edit-param-notminor": "Niet-kleine bewerking.",
        "apihelp-edit-param-bot": "Deze bewerking markeren als een botbewerking.",
        "apihelp-edit-param-createonly": "De pagina niet bewerken als die al bestaat.",
index 2d4fc69..d36e4ea 100644 (file)
@@ -16,7 +16,8 @@
                        "Woytecr",
                        "InternerowyGołąb",
                        "CiaPan",
-                       "Vlad5250"
+                       "Vlad5250",
+                       "Railfail536"
                ]
        },
        "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Dokumentacja]]\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Lista dyskusyjna]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Ogłoszenia dotyczące API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Błędy i propozycje]\n</div>\n<strong>Stan:</strong> Wszystkie funkcje opisane na tej stronie powinny działać, ale API nadal jest aktywnie rozwijane i mogą się zmienić w dowolnym czasie. Subskrybuj [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ listę dyskusyjną mediawiki-api-announce], aby móc na bieżąco dowiadywać się o aktualizacjach.\n\n<strong>Błędne żądania:</strong> Gdy zostanie wysłane błędne żądanie do API, zostanie wysłany w odpowiedzi nagłówek HTTP z kluczem \"MediaWiki-API-Error\" i zarówno jego wartość jak i wartość kodu błędu wysłanego w odpowiedzi będą miały taką samą wartość. Aby uzyskać więcej informacji, zobacz [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Błędy i ostrzeżenia]].\n\n<strong>Testowanie:</strong> Aby łatwo testować żądania API, zobacz [[Special:ApiSandbox]].",
@@ -81,7 +82,7 @@
        "apihelp-edit-param-text": "Zawartość strony.",
        "apihelp-edit-param-summary": "Opis edycji. Także tytuł sekcji gdy użyto $1section=new, a nie ustawiono $1sectiontitle.",
        "apihelp-edit-param-tags": "Znaczniki zmian do zastosowania w tej edycji.",
-       "apihelp-edit-param-minor": "Drobna zmiana.",
+       "apihelp-edit-param-minor": "Oznacz tą zmianę jako drobną zmianę.",
        "apihelp-edit-param-notminor": "Nie oznaczaj tej zmiany jako drobną.",
        "apihelp-edit-param-bot": "Oznacz tę edycję jako edycję bota.",
        "apihelp-edit-param-basetimestamp": "Czas wersji, która jest edytowana. Służy do wykrywania konfliktów edycji. Można pobrać poprzez [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]].",
diff --git a/includes/block/AbstractBlock.php b/includes/block/AbstractBlock.php
new file mode 100644 (file)
index 0000000..f432440
--- /dev/null
@@ -0,0 +1,27 @@
+<?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\Block;
+
+/**
+ * @since 1.34
+ */
+abstract class AbstractBlock {
+}
index 7228814..eedc3c6 100644 (file)
@@ -34,6 +34,13 @@ class GenderCache {
        protected $misses = 0;
        protected $missLimit = 1000;
 
+       /** @var NamespaceInfo */
+       private $nsInfo;
+
+       public function __construct( NamespaceInfo $nsInfo = null ) {
+               $this->nsInfo = $nsInfo ?? MediaWikiServices::getInstance()->getNamespaceInfo();
+       }
+
        /**
         * @deprecated in 1.28 see MediaWikiServices::getInstance()->getGenderCache()
         * @return GenderCache
@@ -97,7 +104,7 @@ class GenderCache {
        public function doLinkBatch( $data, $caller = '' ) {
                $users = [];
                foreach ( $data as $ns => $pagenames ) {
-                       if ( !MWNamespace::hasGenderDistinction( $ns ) ) {
+                       if ( !$this->nsInfo->hasGenderDistinction( $ns ) ) {
                                continue;
                        }
                        foreach ( array_keys( $pagenames ) as $username ) {
@@ -122,7 +129,7 @@ class GenderCache {
                        if ( !$titleObj ) {
                                continue;
                        }
-                       if ( !MWNamespace::hasGenderDistinction( $titleObj->getNamespace() ) ) {
+                       if ( !$this->nsInfo->hasGenderDistinction( $titleObj->getNamespace() ) ) {
                                continue;
                        }
                        $users[] = $titleObj->getText();
index c13f95e..1bcf948 100644 (file)
@@ -45,17 +45,29 @@ class LinkCache {
        /** @var TitleFormatter */
        private $titleFormatter;
 
+       /** @var NamespaceInfo */
+       private $nsInfo;
+
        /**
         * How many Titles to store. There are two caches, so the amount actually
         * stored in memory can be up to twice this.
         */
        const MAX_SIZE = 10000;
 
-       public function __construct( TitleFormatter $titleFormatter, WANObjectCache $cache ) {
+       public function __construct(
+               TitleFormatter $titleFormatter,
+               WANObjectCache $cache,
+               NamespaceInfo $nsInfo = null
+       ) {
+               if ( !$nsInfo ) {
+                       wfDeprecated( __METHOD__ . ' with no NamespaceInfo argument', '1.34' );
+                       $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
+               }
                $this->goodLinks = new MapCacheLRU( self::MAX_SIZE );
                $this->badLinks = new MapCacheLRU( self::MAX_SIZE );
                $this->wanCache = $cache;
                $this->titleFormatter = $titleFormatter;
+               $this->nsInfo = $nsInfo;
        }
 
        /**
@@ -231,9 +243,7 @@ class LinkCache {
         */
        public function addLinkObj( LinkTarget $nt ) {
                $key = $this->titleFormatter->getPrefixedDBkey( $nt );
-               if ( $this->isBadLink( $key ) || $nt->isExternal()
-                       || $nt->inNamespace( NS_SPECIAL )
-               ) {
+               if ( $this->isBadLink( $key ) || $nt->isExternal() || $nt->getNamespace() < 0 ) {
                        return 0;
                }
                $id = $this->getGoodLinkID( $key );
@@ -300,11 +310,11 @@ class LinkCache {
                        return true;
                }
                // Focus on transcluded pages more than the main content
-               if ( MWNamespace::isContent( $ns ) ) {
+               if ( $this->nsInfo->isContent( $ns ) ) {
                        return false;
                }
                // Non-talk extension namespaces (e.g. NS_MODULE)
-               return ( $ns >= 100 && MWNamespace::isSubject( $ns ) );
+               return ( $ns >= 100 && $this->nsInfo->isSubject( $ns ) );
        }
 
        private function fetchPageRow( IDatabase $db, LinkTarget $nt ) {
index fb4c7b6..328cc2f 100644 (file)
@@ -718,8 +718,7 @@ class MessageCache {
                $this->wanCache->touchCheckKey( $this->getCheckKey( $code ) );
 
                // Purge the messages in the message blob store and fire any hook handlers
-               $resourceloader = RequestContext::getMain()->getOutput()->getResourceLoader();
-               $blobStore = $resourceloader->getMessageBlobStore();
+               $blobStore = MediaWikiServices::getInstance()->getResourceLoader()->getMessageBlobStore();
                foreach ( $replacements as list( $title, $msg ) ) {
                        $blobStore->updateMessage( $this->contLang->lcfirst( $msg ) );
                        Hooks::run( 'MessageCacheReplace', [ $title, $newTextByTitle[$title] ] );
index 8df8013..db0f380 100644 (file)
@@ -1033,9 +1033,7 @@ class LocalisationCache {
                # HACK: If using a null (i.e. disabled) storage backend, we
                # can't write to the MessageBlobStore either
                if ( $purgeBlobs && !$this->store instanceof LCStoreNull ) {
-                       $blobStore = new MessageBlobStore(
-                               MediaWikiServices::getInstance()->getResourceLoader()
-                       );
+                       $blobStore = MediaWikiServices::getInstance()->getResourceLoader()->getMessageBlobStore();
                        $blobStore->clear();
                }
        }
index 4d00fbc..bb9114a 100644 (file)
@@ -20,6 +20,8 @@
  * @file
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * Feed to Special:RecentChanges and Special:RecentChangesLinked.
  *
@@ -88,9 +90,10 @@ class ChangesFeed {
                        }
                }
 
+               $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
                foreach ( $sorted as $obj ) {
                        $title = Title::makeTitle( $obj->rc_namespace, $obj->rc_title );
-                       $talkpage = MWNamespace::hasTalkNamespace( $obj->rc_namespace )
+                       $talkpage = $nsInfo->hasTalkNamespace( $obj->rc_namespace )
                                ? $title->getTalkPage()->getFullURL()
                                : '';
 
index decbb0c..cc73dd2 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+
 /**
  * Base class for content handling.
  *
index 354cc61..103b3e5 100644 (file)
@@ -24,7 +24,7 @@
 
 namespace MediaWiki\EditPage;
 
-use MWNamespace;
+use MediaWiki\MediaWikiServices;
 use Sanitizer;
 use Title;
 use User;
@@ -75,7 +75,8 @@ class TextboxBuilder {
        public function getTextboxProtectionCSSClasses( Title $title ) {
                $classes = []; // Textarea CSS
                if ( $title->isProtected( 'edit' ) &&
-                       MWNamespace::getRestrictionLevels( $title->getNamespace() ) !== [ '' ]
+                       MediaWikiServices::getInstance()->getNamespaceInfo()->
+                       getRestrictionLevels( $title->getNamespace() ) !== [ '' ]
                ) {
                        # Is the title semi-protected?
                        if ( $title->isSemiProtected() ) {
index 6e3fa79..b4e483b 100644 (file)
@@ -74,9 +74,32 @@ class MWExceptionHandler {
         * Install handlers with PHP.
         */
        public static function installHandler() {
+               // This catches:
+               // * Exception objects that were explicitly thrown but not
+               //   caught anywhere in the application. This is rare given those
+               //   would normally be caught at a high-level like MediaWiki::run (index.php),
+               //   api.php, or ResourceLoader::respond (load.php). These high-level
+               //   catch clauses would then call MWExceptionHandler::logException
+               //   or MWExceptionHandler::handleException.
+               //   If they are not caught, then they are handled here.
+               // * Error objects (on PHP 7+), for issues that would historically
+               //   cause fatal errors but may now be caught as Throwable (not Exception).
+               //   Same as previous case, but more common to bubble to here instead of
+               //   caught locally because they tend to not be safe to recover from.
+               //   (e.g. argument TypeErorr, devision by zero, etc.)
                set_exception_handler( 'MWExceptionHandler::handleUncaughtException' );
+
+               // This catches:
+               // * Non-fatal errors (e.g. PHP Notice, PHP Warning, PHP Error) that do not
+               //   interrupt execution in any way. We log these in the background and then
+               //   continue execution.
+               // * Fatal errors (on HHVM in PHP5 mode) where PHP 7 would throw Throwable.
                set_error_handler( 'MWExceptionHandler::handleError' );
 
+               // This catches:
+               // * Fatal error for which no Throwable is thrown (PHP 7), and no Error emitted (HHVM).
+               //   This includes Out-Of-Memory and Timeout fatals.
+               //
                // Reserve 16k of memory so we can report OOM fatals
                self::$reservedMemory = str_repeat( ' ', 16384 );
                register_shutdown_function( 'MWExceptionHandler::handleFatalError' );
index c201c76..6b2a22a 100644 (file)
@@ -23,6 +23,8 @@
  * @file
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * @ingroup Dump
  */
@@ -32,6 +34,7 @@ class DumpNotalkFilter extends DumpFilter {
         * @return bool
         */
        protected function pass( $page ) {
-               return !MWNamespace::isTalk( $page->page_namespace );
+               return !MediaWikiServices::getInstance()->getNamespaceInfo()->
+                       isTalk( $page->page_namespace );
        }
 }
index 54249a8..39153cf 100644 (file)
@@ -141,6 +141,7 @@ class XmlDumpWriter {
         */
        function namespaces() {
                $spaces = "<namespaces>\n";
+               $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
                foreach (
                        MediaWikiServices::getInstance()->getContentLanguage()->getFormattedNamespaces()
                        as $ns => $title
@@ -149,7 +150,8 @@ class XmlDumpWriter {
                                Xml::element( 'namespace',
                                        [
                                                'key' => $ns,
-                                               'case' => MWNamespace::isCapitalized( $ns ) ? 'first-letter' : 'case-sensitive',
+                                               'case' => $nsInfo->isCapitalized( $ns )
+                                                       ? 'first-letter' : 'case-sensitive',
                                        ], $title ) . "\n";
                }
                $spaces .= "    </namespaces>";
index 879686f..a723557 100644 (file)
@@ -20,6 +20,8 @@
  * @file
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * Example class for HTTP accessible external objects.
  * Only supports reading, not storing.
@@ -28,7 +30,8 @@
  */
 class ExternalStoreHttp extends ExternalStoreMedium {
        public function fetchFromURL( $url ) {
-               return Http::get( $url, [], __METHOD__ );
+               return MediaWikiServices::getInstance()->getHttpRequestFactory()->
+                       get( $url, [], __METHOD__ );
        }
 
        public function store( $location, $data ) {
index 3a366c8..3e11a48 100644 (file)
@@ -176,7 +176,8 @@ class FileRepo {
                }
 
                // Optional settings that have a default
-               $this->initialCapital = $info['initialCapital'] ?? MWNamespace::isCapitalized( NS_FILE );
+               $this->initialCapital = $info['initialCapital'] ??
+                       MediaWikiServices::getInstance()->getNamespaceInfo()->isCapitalized( NS_FILE );
                $this->url = $info['url'] ?? false; // a subclass may set the URL (e.g. ForeignAPIRepo)
                if ( isset( $info['thumbUrl'] ) ) {
                        $this->thumbUrl = $info['thumbUrl'];
@@ -645,7 +646,10 @@ class FileRepo {
         * @return string
         */
        public function getNameFromTitle( Title $title ) {
-               if ( $this->initialCapital != MWNamespace::isCapitalized( NS_FILE ) ) {
+               if (
+                       $this->initialCapital !=
+                       MediaWikiServices::getInstance()->getNamespaceInfo()->isCapitalized( NS_FILE )
+               ) {
                        $name = $title->getUserCaseDBKey();
                        if ( $this->initialCapital ) {
                                $name = MediaWikiServices::getInstance()->getContentLanguage()->ucfirst( $name );
index 346ec8e..2c6f296 100644 (file)
@@ -502,8 +502,9 @@ class ForeignAPIRepo extends FileRepo {
        }
 
        /**
-        * Like a Http:get request, but with custom User-Agent.
-        * @see Http::get
+        * Like a HttpRequestFactory::get request, but with custom User-Agent.
+        * @see HttpRequestFactory::get
+        * @todo Can this use HttpRequestFactory::get() but just pass the 'userAgent' option?
         * @param string $url
         * @param string $timeout
         * @param array $options
index b6c70ab..8047835 100644 (file)
@@ -35,6 +35,9 @@ class RepoGroup {
        /** @var FileRepo[] */
        protected $foreignRepos;
 
+       /** @var WANObjectCache */
+       protected $wanCache;
+
        /** @var bool */
        protected $reposInitialised = false;
 
@@ -47,66 +50,60 @@ class RepoGroup {
        /** @var ProcessCacheLRU */
        protected $cache;
 
-       /** @var RepoGroup */
-       protected static $instance;
-
        /** Maximum number of cache items */
        const MAX_CACHE_SIZE = 500;
 
        /**
-        * Get a RepoGroup instance. At present only one instance of RepoGroup is
-        * needed in a MediaWiki invocation, this may change in the future.
+        * @deprecated since 1.34, use MediaWikiServices::getRepoGroup
         * @return RepoGroup
         */
        static function singleton() {
-               if ( self::$instance ) {
-                       return self::$instance;
-               }
-               global $wgLocalFileRepo, $wgForeignFileRepos;
-               /** @var array $wgLocalFileRepo */
-               self::$instance = new RepoGroup( $wgLocalFileRepo, $wgForeignFileRepos );
-
-               return self::$instance;
+               return MediaWikiServices::getInstance()->getRepoGroup();
        }
 
        /**
-        * Destroy the singleton instance, so that a new one will be created next
-        * time singleton() is called.
+        * @deprecated since 1.34, use MediaWikiTestCase::overrideMwServices() or similar. This will
+        * cause bugs if you don't reset all other services that depend on this one at the same time.
         */
        static function destroySingleton() {
-               self::$instance = null;
+               MediaWikiServices::getInstance()->resetServiceForTesting( 'RepoGroup' );
        }
 
        /**
-        * Set the singleton instance to a given object
-        * Used by extensions which hook into the Repo chain.
-        * It's not enough to just create a superclass ... you have
-        * to get people to call into it even though all they know is RepoGroup::singleton()
-        *
+        * @deprecated since 1.34, use MediaWikiTestCase::setService, this can mess up state of other
+        *   tests
         * @param RepoGroup $instance
         */
        static function setSingleton( $instance ) {
-               self::$instance = $instance;
+               $services = MediaWikiServices::getInstance();
+               $services->disableService( 'RepoGroup' );
+               $services->redefineService( 'RepoGroup',
+                       function () use ( $instance ) {
+                               return $instance;
+                       }
+               );
        }
 
        /**
-        * Construct a group of file repositories.
+        * Construct a group of file repositories. Do not call this -- use
+        * MediaWikiServices::getRepoGroup.
         *
         * @param array $localInfo Associative array for local repo's info
         * @param array $foreignInfo Array of repository info arrays.
         *   Each info array is an associative array with the 'class' member
         *   giving the class name. The entire array is passed to the repository
         *   constructor as the first parameter.
+        * @param WANObjectCache $wanCache
         */
-       function __construct( $localInfo, $foreignInfo ) {
+       function __construct( $localInfo, $foreignInfo, $wanCache ) {
                $this->localInfo = $localInfo;
                $this->foreignInfo = $foreignInfo;
                $this->cache = new MapCacheLRU( self::MAX_CACHE_SIZE );
+               $this->wanCache = $wanCache;
        }
 
        /**
         * Search repositories for an image.
-        * You can also use wfFindFile() to do this.
         *
         * @param Title|string $title Title object or string
         * @param array $options Associative array of options:
@@ -419,8 +416,7 @@ class RepoGroup {
        protected function newRepo( $info ) {
                $class = $info['class'];
 
-               $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
-               $info['wanCache'] = $cache;
+               $info['wanCache'] = $this->wanCache;
 
                return new $class( $info );
        }
index 7d4f4df..92be7d4 100644 (file)
@@ -2070,7 +2070,8 @@ abstract class File implements IDBAccessObject {
                                $this->repo->descriptionCacheExpiry ?: $cache::TTL_UNCACHEABLE,
                                function ( $oldValue, &$ttl, array &$setOpts ) use ( $renderUrl, $fname ) {
                                        wfDebug( "Fetching shared description from $renderUrl\n" );
-                                       $res = Http::get( $renderUrl, [], $fname );
+                                       $res = MediaWikiServices::getInstance()->getHttpRequestFactory()->
+                                               get( $renderUrl, [], $fname );
                                        if ( !$res ) {
                                                $ttl = WANObjectCache::TTL_UNCACHEABLE;
                                        }
index 3438a63..e083a4e 100644 (file)
@@ -165,7 +165,8 @@ class ForeignDBFile extends LocalFile {
                        $this->repo->descriptionCacheExpiry ?: $cache::TTL_UNCACHEABLE,
                        function ( $oldValue, &$ttl, array &$setOpts ) use ( $renderUrl, $fname ) {
                                wfDebug( "Fetching shared description from $renderUrl\n" );
-                               $res = Http::get( $renderUrl, [], $fname );
+                               $res = MediaWikiServices::getInstance()->getHttpRequestFactory()->
+                                       get( $renderUrl, [], $fname );
                                if ( !$res ) {
                                        $ttl = WANObjectCache::TTL_UNCACHEABLE;
                                }
index 9de7eb8..4d5222c 100644 (file)
@@ -169,7 +169,8 @@ class TraditionalImageGallery extends ImageGalleryBase {
 
                        // @todo Code is incomplete.
                        // $linkTarget = Title::newFromText( MediaWikiServices::getInstance()->
-                       // getContentLanguage()->getNsText( MWNamespace::getUser() ) . ":{$ut}" );
+                       // getContentLanguage()->getNsText( MediaWikiServices::getInstance()->
+                       // getNamespaceInfo()->getUser() ) . ":{$ut}" );
                        // $ul = Linker::link( $linkTarget, $ut );
 
                        $meta = [];
index 8ef9cc2..5130e36 100644 (file)
@@ -27,6 +27,18 @@ class CurlHttpRequest extends MWHttpRequest {
        protected $curlOptions = [];
        protected $headerText = "";
 
+       /**
+        * @throws RuntimeException
+        */
+       public function __construct() {
+               if ( !function_exists( 'curl_init' ) ) {
+                       throw new RuntimeException(
+                               __METHOD__ . ': curl (https://www.php.net/curl) is not installed' );
+               }
+
+               parent::__construct( ...func_get_args() );
+       }
+
        /**
         * @param resource $fh
         * @param string $content
index e6b2892..3af7f56 100644 (file)
@@ -45,7 +45,7 @@ class GuzzleHttpRequest extends MWHttpRequest {
 
        /**
         * @param string $url Url to use. If protocol-relative, will be expanded to an http:// URL
-        * @param array $options (optional) extra params to pass (see Http::request())
+        * @param array $options (optional) extra params to pass (see HttpRequestFactory::create())
         * @param string $caller The method making this request, for profiling
         * @param Profiler|null $profiler An instance of the profiler for profiling, or null
         * @throws Exception
index f0972dc..9596169 100644 (file)
  */
 
 use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
 
 /**
  * Various HTTP related functions
+ * @deprecated since 1.34
  * @ingroup HTTP
  */
 class Http {
-       public static $httpEngine = false;
+       /** @deprecated since 1.34, just use the default engine */
+       public static $httpEngine = null;
 
        /**
         * Perform an HTTP request
         *
+        * @deprecated since 1.34, use HttpRequestFactory::request()
+        *
         * @param string $method HTTP method. Usually GET/POST
         * @param string $url Full URL to act on. If protocol-relative, will be expanded to an http:// URL
-        * @param array $options Options to pass to MWHttpRequest object.
-        *      Possible keys for the array:
-        *    - timeout             Timeout length in seconds
-        *    - connectTimeout      Timeout for connection, in seconds (curl only)
-        *    - postData            An array of key-value pairs or a url-encoded form data
-        *    - proxy               The proxy to use.
-        *                          Otherwise it will use $wgHTTPProxy (if set)
-        *                          Otherwise it will use the environment variable "http_proxy" (if set)
-        *    - noProxy             Don't use any proxy at all. Takes precedence over proxy value(s).
-        *    - sslVerifyHost       Verify hostname against certificate
-        *    - sslVerifyCert       Verify SSL certificate
-        *    - caInfo              Provide CA information
-        *    - maxRedirects        Maximum number of redirects to follow (defaults to 5)
-        *    - followRedirects     Whether to follow redirects (defaults to false).
-        *                          Note: this should only be used when the target URL is trusted,
-        *                          to avoid attacks on intranet services accessible by HTTP.
-        *    - userAgent           A user agent, if you want to override the default
-        *                          MediaWiki/$wgVersion
-        *    - logger              A \Psr\Logger\LoggerInterface instance for debug logging
-        *    - username            Username for HTTP Basic Authentication
-        *    - password            Password for HTTP Basic Authentication
-        *    - originalRequest     Information about the original request (as a WebRequest object or
-        *                          an associative array with 'ip' and 'userAgent').
+        * @param array $options Options to pass to MWHttpRequest object. See HttpRequestFactory::create
+        *  docs
         * @param string $caller The method making this request, for profiling
         * @return string|bool (bool)false on failure or a string on success
         */
        public static function request( $method, $url, array $options = [], $caller = __METHOD__ ) {
-               $logger = LoggerFactory::getInstance( 'http' );
-               $logger->debug( "$method: $url" );
-
-               $options['method'] = strtoupper( $method );
-
-               if ( !isset( $options['timeout'] ) ) {
-                       $options['timeout'] = 'default';
-               }
-               if ( !isset( $options['connectTimeout'] ) ) {
-                       $options['connectTimeout'] = 'default';
-               }
-
-               $req = MWHttpRequest::factory( $url, $options, $caller );
-               $status = $req->execute();
-
-               if ( $status->isOK() ) {
-                       return $req->getContent();
-               } else {
-                       $errors = $status->getErrorsByType( 'error' );
-                       $logger->warning( Status::wrap( $status )->getWikiText( false, false, 'en' ),
-                               [ 'error' => $errors, 'caller' => $caller, 'content' => $req->getContent() ] );
-                       return false;
-               }
+               $ret = MediaWikiServices::getInstance()->getHttpRequestFactory()->request(
+                       $method, $url, $options, $caller );
+               return is_string( $ret ) ? $ret : false;
        }
 
        /**
         * Simple wrapper for Http::request( 'GET' )
-        * @see Http::request()
+        *
+        * @deprecated since 1.34, use HttpRequestFactory::get()
+        *
         * @since 1.25 Second parameter $timeout removed. Second parameter
         * is now $options which can be given a 'timeout'
         *
@@ -111,7 +77,8 @@ class Http {
 
        /**
         * Simple wrapper for Http::request( 'POST' )
-        * @see Http::request()
+        *
+        * @deprecated since 1.34, use HttpRequestFactory::post()
         *
         * @param string $url
         * @param array $options
@@ -124,11 +91,12 @@ class Http {
 
        /**
         * A standard user-agent we can use for external requests.
+        *
+        * @deprecated since 1.34, use HttpRequestFactory::getUserAgent()
         * @return string
         */
        public static function userAgent() {
-               global $wgVersion;
-               return "MediaWiki/$wgVersion";
+               return MediaWikiServices::getInstance()->getHttpRequestFactory()->getUserAgent();
        }
 
        /**
@@ -143,37 +111,37 @@ class Http {
         *
         * @todo FIXME this is wildly inaccurate and fails to actually check most stuff
         *
+        * @deprecated since 1.34, use MWHttpRequest::isValidURI
         * @param string $uri URI to check for validity
         * @return bool
         */
        public static function isValidURI( $uri ) {
-               return (bool)preg_match(
-                       '/^https?:\/\/[^\/\s]\S*$/D',
-                       $uri
-               );
+               return MWHttpRequest::isValidURI( $uri );
        }
 
        /**
         * Gets the relevant proxy from $wgHTTPProxy
         *
-        * @return mixed The proxy address or an empty string if not set.
+        * @deprecated since 1.34, use $wgHTTPProxy directly
+        * @return string The proxy address or an empty string if not set.
         */
        public static function getProxy() {
-               global $wgHTTPProxy;
+               wfDeprecated( __METHOD__, '1.34' );
 
-               if ( $wgHTTPProxy ) {
-                       return $wgHTTPProxy;
-               }
-
-               return "";
+               global $wgHTTPProxy;
+               return (string)$wgHTTPProxy;
        }
 
        /**
         * Get a configured MultiHttpClient
+        *
+        * @deprecated since 1.34, construct it directly
         * @param array $options
         * @return MultiHttpClient
         */
        public static function createMultiClient( array $options = [] ) {
+               wfDeprecated( __METHOD__, '1.34' );
+
                global $wgHTTPConnectTimeout, $wgHTTPTimeout, $wgHTTPProxy;
 
                return new MultiHttpClient( $options + [
index f155348..08520b7 100644 (file)
 namespace MediaWiki\Http;
 
 use CurlHttpRequest;
-use DomainException;
+use GuzzleHttpRequest;
 use Http;
 use MediaWiki\Logger\LoggerFactory;
 use MWHttpRequest;
 use PhpHttpRequest;
 use Profiler;
-use GuzzleHttpRequest;
+use RuntimeException;
+use Status;
 
 /**
  * Factory creating MWHttpRequest objects.
  */
 class HttpRequestFactory {
-
        /**
         * Generate a new MWHttpRequest object
         * @param string $url Url to use
-        * @param array $options (optional) extra params to pass (see Http::request())
+        * @param array $options Possible keys for the array:
+        *    - timeout             Timeout length in seconds
+        *    - connectTimeout      Timeout for connection, in seconds (curl only)
+        *    - postData            An array of key-value pairs or a url-encoded form data
+        *    - proxy               The proxy to use.
+        *                          Otherwise it will use $wgHTTPProxy (if set)
+        *                          Otherwise it will use the environment variable "http_proxy" (if set)
+        *    - noProxy             Don't use any proxy at all. Takes precedence over proxy value(s).
+        *    - sslVerifyHost       Verify hostname against certificate
+        *    - sslVerifyCert       Verify SSL certificate
+        *    - caInfo              Provide CA information
+        *    - maxRedirects        Maximum number of redirects to follow (defaults to 5)
+        *    - followRedirects     Whether to follow redirects (defaults to false).
+        *                          Note: this should only be used when the target URL is trusted,
+        *                          to avoid attacks on intranet services accessible by HTTP.
+        *    - userAgent           A user agent, if you want to override the default
+        *                          MediaWiki/$wgVersion
+        *    - logger              A \Psr\Logger\LoggerInterface instance for debug logging
+        *    - username            Username for HTTP Basic Authentication
+        *    - password            Password for HTTP Basic Authentication
+        *    - originalRequest     Information about the original request (as a WebRequest object or
+        *                          an associative array with 'ip' and 'userAgent').
         * @param string $caller The method making this request, for profiling
-        * @throws DomainException
+        * @throws RuntimeException
         * @return MWHttpRequest
         * @see MWHttpRequest::__construct
         */
        public function create( $url, array $options = [], $caller = __METHOD__ ) {
                if ( !Http::$httpEngine ) {
                        Http::$httpEngine = 'guzzle';
-               } elseif ( Http::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) {
-                       throw new DomainException( __METHOD__ . ': curl (https://www.php.net/curl) is not ' .
-                          'installed, but Http::$httpEngine is set to "curl"' );
                }
 
                if ( !isset( $options['logger'] ) ) {
@@ -60,16 +78,9 @@ class HttpRequestFactory {
                        case 'curl':
                                return new CurlHttpRequest( $url, $options, $caller, Profiler::instance() );
                        case 'php':
-                               if ( !wfIniGetBool( 'allow_url_fopen' ) ) {
-                                       throw new DomainException( __METHOD__ . ': allow_url_fopen ' .
-                                          'needs to be enabled for pure PHP http requests to ' .
-                                          'work. If possible, curl should be used instead. See ' .
-                                          'https://www.php.net/curl.'
-                                       );
-                               }
                                return new PhpHttpRequest( $url, $options, $caller, Profiler::instance() );
                        default:
-                               throw new DomainException( __METHOD__ . ': The setting of Http::$httpEngine is not valid.' );
+                               throw new RuntimeException( __METHOD__ . ': The requested engine is not valid.' );
                }
        }
 
@@ -82,4 +93,75 @@ class HttpRequestFactory {
                return function_exists( 'curl_init' ) || wfIniGetBool( 'allow_url_fopen' );
        }
 
+       /**
+        * Perform an HTTP request
+        *
+        * @since 1.34
+        * @param string $method HTTP method. Usually GET/POST
+        * @param string $url Full URL to act on. If protocol-relative, will be expanded to an http://
+        *  URL
+        * @param array $options See HttpRequestFactory::create
+        * @param string $caller The method making this request, for profiling
+        * @return string|null null on failure or a string on success
+        */
+       public function request( $method, $url, array $options = [], $caller = __METHOD__ ) {
+               $logger = LoggerFactory::getInstance( 'http' );
+               $logger->debug( "$method: $url" );
+
+               $options['method'] = strtoupper( $method );
+
+               if ( !isset( $options['timeout'] ) ) {
+                       $options['timeout'] = 'default';
+               }
+               if ( !isset( $options['connectTimeout'] ) ) {
+                       $options['connectTimeout'] = 'default';
+               }
+
+               $req = $this->create( $url, $options, $caller );
+               $status = $req->execute();
+
+               if ( $status->isOK() ) {
+                       return $req->getContent();
+               } else {
+                       $errors = $status->getErrorsByType( 'error' );
+                       $logger->warning( Status::wrap( $status )->getWikiText( false, false, 'en' ),
+                               [ 'error' => $errors, 'caller' => $caller, 'content' => $req->getContent() ] );
+                       return null;
+               }
+       }
+
+       /**
+        * Simple wrapper for request( 'GET' ), parameters have same meaning as for request()
+        *
+        * @since 1.34
+        * @param string $url
+        * @param array $options
+        * @param string $caller
+        * @return string|null
+        */
+       public function get( $url, array $options = [], $caller = __METHOD__ ) {
+               $this->request( 'GET', $url, $options, $caller );
+       }
+
+       /**
+        * Simple wrapper for request( 'POST' ), parameters have same meaning as for request()
+        *
+        * @since 1.34
+        * @param string $url
+        * @param array $options
+        * @param string $caller
+        * @return string|null
+        */
+       public function post( $url, array $options = [], $caller = __METHOD__ ) {
+               $this->request( 'POST', $url, $options, $caller );
+       }
+
+       /**
+        * @return string
+        */
+       public function getUserAgent() {
+               global $wgVersion;
+
+               return "MediaWiki/$wgVersion";
+       }
 }
index b4ac9a7..41ea1dc 100644 (file)
@@ -85,7 +85,7 @@ abstract class MWHttpRequest implements LoggerAwareInterface {
 
        /**
         * @param string $url Url to use. If protocol-relative, will be expanded to an http:// URL
-        * @param array $options (optional) extra params to pass (see Http::request())
+        * @param array $options (optional) extra params to pass (see HttpRequestFactory::create())
         * @param string $caller The method making this request, for profiling
         * @param Profiler|null $profiler An instance of the profiler for profiling, or null
         * @throws Exception
@@ -172,9 +172,9 @@ abstract class MWHttpRequest implements LoggerAwareInterface {
 
        /**
         * Generate a new request object
-        * Deprecated: @see HttpRequestFactory::create
+        * @deprecated since 1.34, use HttpRequestFactory instead
         * @param string $url Url to use
-        * @param array|null $options (optional) extra params to pass (see Http::request())
+        * @param array|null $options (optional) extra params to pass (see HttpRequestFactory::create())
         * @param string $caller The method making this request, for profiling
         * @throws DomainException
         * @return MWHttpRequest
@@ -224,7 +224,8 @@ abstract class MWHttpRequest implements LoggerAwareInterface {
                if ( self::isLocalURL( $this->url ) || $this->noProxy ) {
                        $this->proxy = '';
                } else {
-                       $this->proxy = Http::getProxy();
+                       global $wgHTTPProxy;
+                       $this->proxy = (string)$wgHTTPProxy;
                }
        }
 
@@ -662,4 +663,27 @@ abstract class MWHttpRequest implements LoggerAwareInterface {
                $this->reqHeaders['X-Forwarded-For'] = $originalRequest['ip'];
                $this->reqHeaders['X-Original-User-Agent'] = $originalRequest['userAgent'];
        }
+
+       /**
+        * Check that the given URI is a valid one.
+        *
+        * This hardcodes a small set of protocols only, because we want to
+        * deterministically reject protocols not supported by all HTTP-transport
+        * methods.
+        *
+        * "file://" specifically must not be allowed, for security reasons
+        * (see <https://www.mediawiki.org/wiki/Special:Code/MediaWiki/r67684>).
+        *
+        * @todo FIXME this is wildly inaccurate and fails to actually check most stuff
+        *
+        * @since 1.34
+        * @param string $uri URI to check for validity
+        * @return bool
+        */
+       public static function isValidURI( $uri ) {
+               return (bool)preg_match(
+                       '/^https?:\/\/[^\/\s]\S*$/D',
+                       $uri
+               );
+       }
 }
index d2af8c8..c987c62 100644 (file)
@@ -22,6 +22,17 @@ class PhpHttpRequest extends MWHttpRequest {
 
        private $fopenErrors = [];
 
+       public function __construct() {
+               if ( !wfIniGetBool( 'allow_url_fopen' ) ) {
+                       throw new RuntimeException( __METHOD__ . ': allow_url_fopen needs to be enabled for ' .
+                               'pure PHP http requests to work. If possible, curl should be used instead. See ' .
+                               'https://www.php.net/curl.'
+                       );
+               }
+
+               parent::__construct( ...func_get_args() );
+       }
+
        /**
         * @param string $url
         * @return string
index ebac200..e6936cb 100644 (file)
@@ -112,7 +112,7 @@ class ImportStreamSource implements ImportSource {
                # quicker and sorts out user-agent problems which might
                # otherwise prevent importing from large sites, such
                # as the Wikimedia cluster, etc.
-               $data = Http::request(
+               $data = MediaWikiServices::getInstance()->getHttpRequestFactory()->request(
                        $method,
                        $url,
                        [
index 4b378c1..f1ac42c 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 
+use MediaWiki\MediaWikiServices;
 use Psr\Log\LoggerInterface;
 
 /**
@@ -159,7 +160,8 @@ class ImportableUploadRevisionImporter implements UploadRevisionImporter {
 
                // @todo FIXME!
                $src = $wikiRevision->getSrc();
-               $data = Http::get( $src, [], __METHOD__ );
+               $data = MediaWikiServices::getInstance()->getHttpRequestFactory()->
+                       get( $src, [], __METHOD__ );
                if ( !$data ) {
                        $this->logger->debug( "IMPORT: couldn't fetch source $src\n" );
                        fclose( $f );
index 466e3d8..8f58344 100644 (file)
@@ -257,7 +257,7 @@ class WikiImporter {
                        return true;
                } elseif (
                        $namespace >= 0 &&
-                       MWNamespace::exists( intval( $namespace ) )
+                       MediaWikiServices::getInstance()->getNamespaceInfo()->exists( intval( $namespace ) )
                ) {
                        $namespace = intval( $namespace );
                        $this->setImportTitleFactory( new NamespaceImportTitleFactory( $namespace ) );
@@ -283,7 +283,10 @@ class WikiImporter {
 
                        if ( !$title || $title->isExternal() ) {
                                $status->fatal( 'import-rootpage-invalid' );
-                       } elseif ( !MWNamespace::hasSubpages( $title->getNamespace() ) ) {
+                       } elseif (
+                               !MediaWikiServices::getInstance()->getNamespaceInfo()->
+                               hasSubpages( $title->getNamespace() )
+                       ) {
                                $displayNSText = $title->getNamespace() == NS_MAIN
                                        ? wfMessage( 'blanknamespace' )->text()
                                        : MediaWikiServices::getInstance()->getContentLanguage()->
index 9053f8d..c231288 100644 (file)
@@ -1203,9 +1203,11 @@ abstract class Installer {
                                }
 
                                try {
-                                       $text = Http::get( $url . $file, [ 'timeout' => 3 ], __METHOD__ );
+                                       $text = MediaWikiServices::getInstance()->getHttpRequestFactory()->
+                                               get( $url . $file, [ 'timeout' => 3 ], __METHOD__ );
                                } catch ( Exception $e ) {
-                                       // Http::get throws with allow_url_fopen = false and no curl extension.
+                                       // HttpRequestFactory::get can throw with allow_url_fopen = false and no curl
+                                       // extension.
                                        $text = null;
                                }
                                unlink( $dir . $file );
index 8cc14e5..9b08510 100644 (file)
@@ -19,6 +19,8 @@
  * @ingroup JobQueue
  */
 
+use MediaWiki\Linker\LinkTarget;
+
 /**
  * Job for updating user activity like "last viewed" timestamps
  *
@@ -32,7 +34,9 @@
  * @since 1.26
  */
 class ActivityUpdateJob extends Job {
-       function __construct( Title $title, array $params ) {
+       function __construct( LinkTarget $title, array $params ) {
+               $title = Title::newFromLinkTarget( $title );
+
                parent::__construct( 'activityUpdateJob', $title, $params );
 
                static $required = [ 'type', 'userid', 'notifTime', 'curTime' ];
index 01fa46c..0cb1a52 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 
 use MediaWiki\MediaWikiServices;
+use MediaWiki\User\UserIdentity;
 
 /**
  * Job to clear a users watchlist in batches.
@@ -23,12 +24,12 @@ class ClearUserWatchlistJob extends Job implements GenericParameterJob {
        }
 
        /**
-        * @param User $user User to clear the watchlist for.
+        * @param UserIdentity $user User to clear the watchlist for.
         * @param int $maxWatchlistId The maximum wl_id at the time the job was first created.
         *
         * @return ClearUserWatchlistJob
         */
-       public static function newForUser( User $user, $maxWatchlistId ) {
+       public static function newForUser( UserIdentity $user, $maxWatchlistId ) {
                return new self( [ 'userId' => $user->getId(), 'maxWatchlistId' => $maxWatchlistId ] );
        }
 
diff --git a/includes/jobqueue/jobs/UserOptionsUpdateJob.php b/includes/jobqueue/jobs/UserOptionsUpdateJob.php
new file mode 100644 (file)
index 0000000..0e8b19f
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+/**
+ * Job that updates a user's preferences.
+ *
+ * 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 3 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
+ * @ingroup JobQueue
+ */
+
+/**
+ * Job that updates a user's preferences
+ *
+ * The following job parameters are required:
+ *   - userId: the user ID
+ *   - options: a map of (option => value)
+ *
+ * @since 1.33
+ */
+class UserOptionsUpdateJob extends Job implements GenericParameterJob {
+       public function __construct( array $params ) {
+               parent::__construct( 'userOptionsUpdate', $params );
+               $this->removeDuplicates = true;
+       }
+
+       public function run() {
+               if ( !$this->params['options'] ) {
+                       return true; // nothing to do
+               }
+
+               $user = User::newFromId( $this->params['userId'] );
+               $user->load( $user::READ_EXCLUSIVE );
+               if ( !$user->getId() ) {
+                       return true;
+               }
+
+               foreach ( $this->params['options'] as $name => $value ) {
+                       $user->setOption( $name, $value );
+               }
+
+               $user->saveSettings();
+
+               return true;
+       }
+}
index 321424f..b993626 100644 (file)
@@ -178,7 +178,7 @@ class TempFSFile extends FSFile {
         * This method should only be called internally
         */
        public static function purgeAllOnShutdown() {
-               foreach ( self::$pathsCollect as $path ) {
+               foreach ( self::$pathsCollect as $path => $unused ) {
                        Wikimedia\suppressWarnings();
                        unlink( $path );
                        Wikimedia\restoreWarnings();
index c77b156..707c382 100644 (file)
@@ -27,7 +27,7 @@ use HtmlArmor;
 use LinkCache;
 use Linker;
 use MediaWiki\MediaWikiServices;
-use MWNamespace;
+use NamespaceInfo;
 use Sanitizer;
 use Title;
 use TitleFormatter;
@@ -69,6 +69,11 @@ class LinkRenderer {
         */
        private $linkCache;
 
+       /**
+        * @var NamespaceInfo
+        */
+       private $nsInfo;
+
        /**
         * Whether to run the legacy Linker hooks
         *
@@ -79,10 +84,14 @@ class LinkRenderer {
        /**
         * @param TitleFormatter $titleFormatter
         * @param LinkCache $linkCache
+        * @param NamespaceInfo $nsInfo
         */
-       public function __construct( TitleFormatter $titleFormatter, LinkCache $linkCache ) {
+       public function __construct(
+               TitleFormatter $titleFormatter, LinkCache $linkCache, NamespaceInfo $nsInfo
+       ) {
                $this->titleFormatter = $titleFormatter;
                $this->linkCache = $linkCache;
+               $this->nsInfo = $nsInfo;
        }
 
        /**
@@ -468,8 +477,9 @@ class LinkRenderer {
                if ( $this->linkCache->getGoodLinkFieldObj( $target, 'redirect' ) ) {
                        # Page is a redirect
                        return 'mw-redirect';
-               } elseif ( $this->stubThreshold > 0 && MWNamespace::isContent( $target->getNamespace() )
-                       && $this->linkCache->getGoodLinkFieldObj( $target, 'length' ) < $this->stubThreshold
+               } elseif (
+                       $this->stubThreshold > 0 && $this->nsInfo->isContent( $target->getNamespace() ) &&
+                       $this->linkCache->getGoodLinkFieldObj( $target, 'length' ) < $this->stubThreshold
                ) {
                        # Page is a stub
                        return 'stub';
index 240ea09..eeb28b5 100644 (file)
@@ -21,6 +21,7 @@
 namespace MediaWiki\Linker;
 
 use LinkCache;
+use NamespaceInfo;
 use TitleFormatter;
 use User;
 
@@ -40,20 +41,29 @@ class LinkRendererFactory {
         */
        private $linkCache;
 
+       /**
+        * @var NamespaceInfo
+        */
+       private $nsInfo;
+
        /**
         * @param TitleFormatter $titleFormatter
         * @param LinkCache $linkCache
+        * @param NamespaceInfo $nsInfo
         */
-       public function __construct( TitleFormatter $titleFormatter, LinkCache $linkCache ) {
+       public function __construct(
+               TitleFormatter $titleFormatter, LinkCache $linkCache, NamespaceInfo $nsInfo
+       ) {
                $this->titleFormatter = $titleFormatter;
                $this->linkCache = $linkCache;
+               $this->nsInfo = $nsInfo;
        }
 
        /**
         * @return LinkRenderer
         */
        public function create() {
-               return new LinkRenderer( $this->titleFormatter, $this->linkCache );
+               return new LinkRenderer( $this->titleFormatter, $this->linkCache, $this->nsInfo );
        }
 
        /**
index 124437e..b20c83e 100644 (file)
@@ -1352,6 +1352,8 @@ class Article implements Page {
 
                $title = $this->getTitle();
 
+               $services = MediaWikiServices::getInstance();
+
                # Show info in user (talk) namespace. Does the user exist? Is he blocked?
                if ( $title->getNamespace() == NS_USER
                        || $title->getNamespace() == NS_USER_TALK
@@ -1374,7 +1376,8 @@ class Article implements Page {
                                LogEventsList::showLogExtract(
                                        $outputPage,
                                        'block',
-                                       MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
+                                       $services->getNamespaceInfo()->getCanonicalName( NS_USER ) . ':' .
+                                               $block->getTarget(),
                                        '',
                                        [
                                                'lim' => 1,
@@ -1396,7 +1399,7 @@ class Article implements Page {
                # Show delete and move logs if there were any such events.
                # The logging query can DOS the site when bots/crawlers cause 404 floods,
                # so be careful showing this. 404 pages must be cheap as they are hard to cache.
-               $cache = MediaWikiServices::getInstance()->getMainObjectStash();
+               $cache = $services->getMainObjectStash();
                $key = $cache->makeKey( 'page-recent-delete', md5( $title->getPrefixedText() ) );
                $loggedIn = $this->getContext()->getUser()->isLoggedIn();
                $sessionExists = $this->getContext()->getRequest()->getSession()->isPersistent();
index a5c8064..1f21c1b 100644 (file)
@@ -39,8 +39,8 @@ use MediaWiki\Linker\LinkRenderer;
 use MediaWiki\MediaWikiServices;
 use MessageLocalizer;
 use MWException;
-use MWNamespace;
 use MWTimestamp;
+use NamespaceInfo;
 use OutputPage;
 use Parser;
 use ParserOptions;
@@ -74,6 +74,9 @@ class DefaultPreferencesFactory implements PreferencesFactory {
        /** @var LinkRenderer */
        protected $linkRenderer;
 
+       /** @var NamespaceInfo */
+       protected $nsInfo;
+
        /**
         * TODO Make this a const when we drop HHVM support (T192166)
         *
@@ -108,16 +111,20 @@ class DefaultPreferencesFactory implements PreferencesFactory {
        ];
 
        /**
+        * Do not call this directly.  Get it from MediaWikiServices.
+        *
         * @param array|Config $options Config accepted for backwards compatibility
         * @param Language $contLang
         * @param AuthManager $authManager
         * @param LinkRenderer $linkRenderer
+        * @param NamespaceInfo|null $nsInfo
         */
        public function __construct(
                $options,
                Language $contLang,
                AuthManager $authManager,
-               LinkRenderer $linkRenderer
+               LinkRenderer $linkRenderer,
+               NamespaceInfo $nsInfo = null
        ) {
                if ( $options instanceof Config ) {
                        wfDeprecated( __METHOD__ . ' with Config parameter', '1.34' );
@@ -126,10 +133,15 @@ class DefaultPreferencesFactory implements PreferencesFactory {
 
                $options->assertRequiredOptions( self::$constructorOptions );
 
+               if ( !$nsInfo ) {
+                       wfDeprecated( __METHOD__ . ' with no NamespaceInfo argument', '1.34' );
+                       $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
+               }
                $this->options = $options;
                $this->contLang = $contLang;
                $this->authManager = $authManager;
                $this->linkRenderer = $linkRenderer;
+               $this->nsInfo = $nsInfo;
                $this->logger = new NullLogger();
        }
 
@@ -1262,7 +1274,7 @@ class DefaultPreferencesFactory implements PreferencesFactory {
         * @param array &$defaultPreferences
         */
        protected function searchPreferences( &$defaultPreferences ) {
-               foreach ( MWNamespace::getValidNamespaces() as $n ) {
+               foreach ( $this->nsInfo->getValidNamespaces() as $n ) {
                        $defaultPreferences['searchNs' . $n] = [
                                'type' => 'api',
                        ];
index 7e69a02..d142c48 100644 (file)
@@ -24,7 +24,7 @@
  */
 class UDPRCFeedEngine extends RCFeedEngine {
        /**
-        * @see RCFeedEngine::send
+        * @see FormattedRCFeed::send
         * @param array $feed
         * @param string $line
         * @return bool
index 69a6f5f..8cbde09 100644 (file)
@@ -57,9 +57,10 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
                $contLang = MediaWikiServices::getInstance()->getContentLanguage();
                $namespaceIds = $contLang->getNamespaceIds();
                $caseSensitiveNamespaces = [];
-               foreach ( MWNamespace::getCanonicalNamespaces() as $index => $name ) {
+               $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
+               foreach ( $nsInfo->getCanonicalNamespaces() as $index => $name ) {
                        $namespaceIds[$contLang->lc( $name )] = $index;
-                       if ( !MWNamespace::isCapitalized( $index ) ) {
+                       if ( !$nsInfo->isCapitalized( $index ) ) {
                                $caseSensitiveNamespaces[] = $index;
                        }
                }
@@ -92,7 +93,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
                        'wgEnableWriteAPI' => true, // Deprecated since MW 1.32
                        'wgFormattedNamespaces' => $contLang->getFormattedNamespaces(),
                        'wgNamespaceIds' => $namespaceIds,
-                       'wgContentNamespaces' => MWNamespace::getContentNamespaces(),
+                       'wgContentNamespaces' => $nsInfo->getContentNamespaces(),
                        'wgSiteName' => $conf->get( 'Sitename' ),
                        'wgDBname' => $conf->get( 'DBname' ),
                        'wgExtraSignatureNamespaces' => $conf->get( 'ExtraSignatureNamespaces' ),
index aa429b2..a7d475e 100644 (file)
@@ -41,6 +41,7 @@ abstract class PrefixSearch {
         * @return array Array of strings
         */
        public static function titleSearch( $search, $limit, $namespaces = [], $offset = 0 ) {
+               wfDeprecated( __METHOD__, '1.34' );
                $prefixSearch = new StringPrefixSearch;
                return $prefixSearch->search( $search, $limit, $namespaces, $offset );
        }
index d0912c5..65a3e6a 100644 (file)
@@ -245,6 +245,7 @@ abstract class SearchEngine {
         * search engine
         */
        public function transformSearchTerm( $term ) {
+               wfDeprecated( __METHOD__, '1.34' );
                return $term;
        }
 
index ac41c46..420c4cb 100644 (file)
@@ -755,7 +755,10 @@ abstract class Skin extends ContextSource {
                        return $subpages;
                }
 
-               if ( $out->isArticle() && MWNamespace::hasSubpages( $title->getNamespace() ) ) {
+               if (
+                       $out->isArticle() && MediaWikiServices::getInstance()->getNamespaceInfo()->
+                               hasSubpages( $title->getNamespace() )
+               ) {
                        $ptext = $title->getPrefixedText();
                        if ( strpos( $ptext, '/' ) !== false ) {
                                $links = explode( '/', $ptext );
index 25771bc..ef45d15 100644 (file)
@@ -790,7 +790,8 @@ class SkinTemplate extends Skin {
                        }
                }
 
-               $linkClass = MediaWikiServices::getInstance()->getLinkRenderer()->getLinkClasses( $title );
+               $services = MediaWikiServices::getInstance();
+               $linkClass = $services->getLinkRenderer()->getLinkClasses( $title );
 
                // wfMessageFallback will nicely accept $message as an array of fallbacks
                // or just a single key
@@ -802,8 +803,9 @@ class SkinTemplate extends Skin {
                if ( $msg->exists() ) {
                        $text = $msg->text();
                } else {
-                       $text = MediaWikiServices::getInstance()->getContentLanguage()->getConverter()->
-                               convertNamespace( MWNamespace::getSubject( $title->getNamespace() ) );
+                       $text = $services->getContentLanguage()->getConverter()->
+                               convertNamespace( $services->getNamespaceInfo()->
+                                       getSubject( $title->getNamespace() ) );
                }
 
                // Avoid PHP 7.1 warning of passing $this by reference
@@ -1086,7 +1088,8 @@ class SkinTemplate extends Skin {
                                }
 
                                if ( $title->quickUserCan( 'protect', $user ) && $title->getRestrictionTypes() &&
-                                       MWNamespace::getRestrictionLevels( $title->getNamespace(), $user ) !== [ '' ]
+                                       MediaWikiServices::getInstance()->getNamespaceInfo()->
+                                               getRestrictionLevels( $title->getNamespace(), $user ) !== [ '' ]
                                ) {
                                        $mode = $title->isProtected() ? 'unprotect' : 'protect';
                                        $content_navigation['actions'][$mode] = [
index dee31b2..f4b574b 100644 (file)
  * @file
  * @ingroup SpecialPage
  */
+
 use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
 use Wikimedia\Rdbms\DBQueryTimeoutError;
 use Wikimedia\Rdbms\IResultWrapper;
 use Wikimedia\Rdbms\FakeResultWrapper;
 use Wikimedia\Rdbms\IDatabase;
-use MediaWiki\MediaWikiServices;
 
 /**
  * Special page which uses a ChangesList to show query results.
@@ -1451,7 +1452,8 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                        if ( $opts[ 'associated' ] ) {
                                $associatedNamespaces = array_map(
                                        function ( $ns ) {
-                                               return MWNamespace::getAssociated( $ns );
+                                               return MediaWikiServices::getInstance()->getNamespaceInfo()->
+                                                       getAssociated( $ns );
                                        },
                                        $namespaces
                                );
index cea6d37..a32393e 100644 (file)
@@ -45,7 +45,8 @@ class AncientPagesPage extends QueryPage {
        public function getQueryInfo() {
                $tables = [ 'page', 'revision' ];
                $conds = [
-                       'page_namespace' => MWNamespace::getContentNamespaces(),
+                       'page_namespace' =>
+                               MediaWikiServices::getInstance()->getNamespaceInfo()->getContentNamespaces(),
                        'page_is_redirect' => 0
                ];
                $joinConds = [
index dc4d1bd..ce08392 100644 (file)
@@ -21,6 +21,7 @@
  * @ingroup SpecialPage
  */
 
+use MediaWiki\MediaWikiServices;
 use MediaWiki\Widget\DateInputWidget;
 
 /**
@@ -328,7 +329,8 @@ class SpecialContributions extends IncludableSpecialPage {
 
                                if ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) {
                                        if ( $block->getType() == Block::TYPE_RANGE ) {
-                                               $nt = MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget();
+                                               $nt = MediaWikiServices::getInstance()->getNamespaceInfo()->
+                                                       getCanonicalName( NS_USER ) . ':' . $block->getTarget();
                                        }
 
                                        $out = $this->getOutput(); // showLogExtract() wants first parameter by reference
index f13f231..2a967c5 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup SpecialPage
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * A special page that list pages that contain no link to other pages
  *
@@ -66,7 +68,8 @@ class DeadendPagesPage extends PageQueryPage {
                        ],
                        'conds' => [
                                'pl_from IS NULL',
-                               'page_namespace' => MWNamespace::getContentNamespaces(),
+                               'page_namespace' => MediaWikiServices::getInstance()->getNamespaceInfo()->
+                                       getContentNamespaces(),
                                'page_is_redirect' => 0
                        ],
                        'join_conds' => [
@@ -81,7 +84,9 @@ class DeadendPagesPage extends PageQueryPage {
        function getOrderFields() {
                // For some crazy reason ordering by a constant
                // causes a filesort
-               if ( count( MWNamespace::getContentNamespaces() ) > 1 ) {
+               if ( count( MediaWikiServices::getInstance()->getNamespaceInfo()->
+                       getContentNamespaces() ) > 1
+               ) {
                        return [ 'page_namespace', 'page_title' ];
                } else {
                        return [ 'page_title' ];
index 65cf79e..73b438c 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup SpecialPage
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * Implements Special:DeletedContributions to display archived revisions
  * @ingroup SpecialPage
@@ -160,7 +162,8 @@ class DeletedContributionsPage extends SpecialPage {
                        $block = Block::newFromTarget( $userObj, $userObj );
                        if ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) {
                                if ( $block->getType() == Block::TYPE_RANGE ) {
-                                       $nt = MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget();
+                                       $nt = MediaWikiServices::getInstance()->getNamespaceInfo()->
+                                               getCanonicalName( NS_USER ) . ':' . $block->getTarget();
                                }
 
                                // LogEventsList::showLogExtract() wants the first parameter by ref
index b05c81a..480e81a 100644 (file)
@@ -380,8 +380,9 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
         */
        protected function getWatchlistInfo() {
                $titles = [];
+               $services = MediaWikiServices::getInstance();
 
-               $watchedItems = MediaWikiServices::getInstance()->getWatchedItemStore()
+               $watchedItems = $services->getWatchedItemStore()
                        ->getWatchedItemsForUser( $this->getUser(), [ 'sort' => WatchedItemStore::SORT_ASC ] );
 
                $lb = new LinkBatch();
@@ -390,7 +391,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
                        $namespace = $watchedItem->getLinkTarget()->getNamespace();
                        $dbKey = $watchedItem->getLinkTarget()->getDBkey();
                        $lb->add( $namespace, $dbKey );
-                       if ( !MWNamespace::isTalk( $namespace ) ) {
+                       if ( !$services->getNamespaceInfo()->isTalk( $namespace ) ) {
                                $titles[$namespace][$dbKey] = 1;
                        }
                }
@@ -511,6 +512,7 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
         */
        private function getExpandedTargets( array $targets ) {
                $expandedTargets = [];
+               $services = MediaWikiServices::getInstance();
                foreach ( $targets as $target ) {
                        if ( !$target instanceof LinkTarget ) {
                                try {
@@ -523,8 +525,10 @@ class SpecialEditWatchlist extends UnlistedSpecialPage {
 
                        $ns = $target->getNamespace();
                        $dbKey = $target->getDBkey();
-                       $expandedTargets[] = new TitleValue( MWNamespace::getSubject( $ns ), $dbKey );
-                       $expandedTargets[] = new TitleValue( MWNamespace::getTalk( $ns ), $dbKey );
+                       $expandedTargets[] =
+                               new TitleValue( $services->getNamespaceInfo()->getSubject( $ns ), $dbKey );
+                       $expandedTargets[] =
+                               new TitleValue( $services->getNamespaceInfo()->getTalk( $ns ), $dbKey );
                }
                return $expandedTargets;
        }
index 84454e2..c47d87b 100644 (file)
@@ -52,7 +52,8 @@ class FewestrevisionsPage extends QueryPage {
                                'redirect' => 'page_is_redirect'
                        ],
                        'conds' => [
-                               'page_namespace' => MWNamespace::getContentNamespaces(),
+                               'page_namespace' => MediaWikiServices::getInstance()->getNamespaceInfo()->
+                                       getContentNamespaces(),
                                'page_id = rev_page' ],
                        'options' => [
                                'GROUP BY' => [ 'page_namespace', 'page_title', 'page_is_redirect' ]
index 1d10791..ae4b090 100644 (file)
@@ -84,7 +84,8 @@ class SpecialListGroupRights extends SpecialPage {
                        $groupnameLocalized = UserGroupMembership::getGroupName( $groupname );
 
                        $grouppageLocalizedTitle = UserGroupMembership::getGroupPage( $groupname )
-                               ?: Title::newFromText( MWNamespace::getCanonicalName( NS_PROJECT ) . ':' . $groupname );
+                               ?: Title::newFromText( MediaWikiServices::getInstance()->getNamespaceInfo()->
+                               getCanonicalName( NS_PROJECT ) . ':' . $groupname );
 
                        if ( $group == '*' || !$grouppageLocalizedTitle ) {
                                // Do not make a link for the generic * group or group with invalid group page
@@ -162,7 +163,8 @@ class SpecialListGroupRights extends SpecialPage {
                );
                $linkRenderer = $this->getLinkRenderer();
                ksort( $namespaceProtection );
-               $validNamespaces = MWNamespace::getValidNamespaces();
+               $validNamespaces =
+                       MediaWikiServices::getInstance()->getNamespaceInfo()->getValidNamespaces();
                $contLang = MediaWikiServices::getInstance()->getContentLanguage();
                foreach ( $namespaceProtection as $namespace => $rights ) {
                        if ( !in_array( $namespace, $validNamespaces ) ) {
index ff76a4b..ca3f4da 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup SpecialPage
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * A special page looking for articles with no article linking to them,
  * thus being lonely.
@@ -52,7 +54,8 @@ class LonelyPagesPage extends PageQueryPage {
                $tables = [ 'page', 'pagelinks', 'templatelinks' ];
                $conds = [
                        'pl_namespace IS NULL',
-                       'page_namespace' => MWNamespace::getContentNamespaces(),
+                       'page_namespace' => MediaWikiServices::getInstance()->getNamespaceInfo()->
+                               getContentNamespaces(),
                        'page_is_redirect' => 0,
                        'tl_namespace IS NULL'
                ];
@@ -89,7 +92,9 @@ class LonelyPagesPage extends PageQueryPage {
        function getOrderFields() {
                // For some crazy reason ordering by a constant
                // causes a filesort in MySQL 5
-               if ( count( MWNamespace::getContentNamespaces() ) > 1 ) {
+               if ( count( MediaWikiServices::getInstance()->getNamespaceInfo()->
+                       getContentNamespaces() ) > 1
+               ) {
                        return [ 'page_namespace', 'page_title' ];
                } else {
                        return [ 'page_title' ];
index 123c174..0dd9437 100644 (file)
@@ -24,6 +24,7 @@
  * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
  */
 
+use MediaWiki\MediaWikiServices;
 use Wikimedia\Rdbms\IResultWrapper;
 use Wikimedia\Rdbms\IDatabase;
 
@@ -53,7 +54,8 @@ class MostcategoriesPage extends QueryPage {
                                'title' => 'page_title',
                                'value' => 'COUNT(*)'
                        ],
-                       'conds' => [ 'page_namespace' => MWNamespace::getContentNamespaces() ],
+                       'conds' => [ 'page_namespace' =>
+                               MediaWikiServices::getInstance()->getNamespaceInfo()->getContentNamespaces() ],
                        'options' => [
                                'HAVING' => 'COUNT(*) > 1',
                                'GROUP BY' => [ 'page_namespace', 'page_title' ]
index c963838..0fcf842 100644 (file)
@@ -21,6 +21,7 @@
  * @ingroup SpecialPage
  */
 
+use MediaWiki\MediaWikiServices;
 use Wikimedia\Rdbms\IResultWrapper;
 use Wikimedia\Rdbms\IDatabase;
 
@@ -52,7 +53,8 @@ class MostinterwikisPage extends QueryPage {
                                'title' => 'page_title',
                                'value' => 'COUNT(*)'
                        ], 'conds' => [
-                               'page_namespace' => MWNamespace::getContentNamespaces()
+                               'page_namespace' =>
+                                       MediaWikiServices::getInstance()->getNamespaceInfo()->getContentNamespaces()
                        ], 'options' => [
                                'HAVING' => 'COUNT(*) > 1',
                                'GROUP BY' => [
index 8b5562f..b561e5b 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup SpecialPage
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * A special page that allows users to change page titles
  *
@@ -297,7 +299,7 @@ class MovePageForm extends UnlistedSpecialPage {
 
                $immovableNamespaces = [];
                foreach ( array_keys( $this->getLanguage()->getNamespaces() ) as $nsId ) {
-                       if ( !MWNamespace::isMovable( $nsId ) ) {
+                       if ( !MediaWikiServices::getInstance()->getNamespaceInfo()->isMovable( $nsId ) ) {
                                $immovableNamespaces[] = $nsId;
                        }
                }
@@ -591,21 +593,12 @@ class MovePageForm extends UnlistedSpecialPage {
 
                # Do the actual move.
                $mp = new MovePage( $ot, $nt );
-               $valid = $mp->isValidMove();
-               if ( !$valid->isOK() ) {
-                       $this->showForm( $valid->getErrorsArray() );
-                       return;
-               }
 
-               $permStatus = $mp->checkPermissions( $user, $this->reason );
-               if ( !$permStatus->isOK() ) {
-                       $this->showForm( $permStatus->getErrorsArray(), true );
-                       return;
-               }
+               $userPermitted = $mp->checkPermissions( $user, $this->reason )->isOK();
 
-               $status = $mp->move( $user, $this->reason, $createRedirect );
+               $status = $mp->moveIfAllowed( $user, $this->reason, $createRedirect );
                if ( !$status->isOK() ) {
-                       $this->showForm( $status->getErrorsArray() );
+                       $this->showForm( $status->getErrorsArray(), !$userPermitted );
                        return;
                }
 
@@ -673,11 +666,12 @@ class MovePageForm extends UnlistedSpecialPage {
                 */
 
                // @todo FIXME: Use Title::moveSubpages() here
+               $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
                $dbr = wfGetDB( DB_MASTER );
                if ( $this->moveSubpages && (
-                       MWNamespace::hasSubpages( $nt->getNamespace() ) || (
+                       $nsInfo->hasSubpages( $nt->getNamespace() ) || (
                                $this->moveTalk
-                                       && MWNamespace::hasSubpages( $nt->getTalkPage()->getNamespace() )
+                                       && $nsInfo->hasSubpages( $nt->getTalkPage()->getNamespace() )
                        )
                ) ) {
                        $conds = [
@@ -685,11 +679,11 @@ class MovePageForm extends UnlistedSpecialPage {
                                        . ' OR page_title = ' . $dbr->addQuotes( $ot->getDBkey() )
                        ];
                        $conds['page_namespace'] = [];
-                       if ( MWNamespace::hasSubpages( $nt->getNamespace() ) ) {
+                       if ( $nsInfo->hasSubpages( $nt->getNamespace() ) ) {
                                $conds['page_namespace'][] = $ot->getNamespace();
                        }
                        if ( $this->moveTalk &&
-                               MWNamespace::hasSubpages( $nt->getTalkPage()->getNamespace() )
+                               $nsInfo->hasSubpages( $nt->getTalkPage()->getNamespace() )
                        ) {
                                $conds['page_namespace'][] = $ot->getTalkPage()->getNamespace();
                        }
@@ -808,7 +802,8 @@ class MovePageForm extends UnlistedSpecialPage {
         * @param Title $title Page being moved.
         */
        function showSubpages( $title ) {
-               $nsHasSubpages = MWNamespace::hasSubpages( $title->getNamespace() );
+               $nsHasSubpages = MediaWikiServices::getInstance()->getNamespaceInfo()->
+                       hasSubpages( $title->getNamespace() );
                $subpages = $title->getSubpages();
                $count = $subpages instanceof TitleArray ? $subpages->count() : 0;
 
index d09deab..9bd855a 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup SpecialPage
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * This special page lists the defined password policies for user groups.
  * See also @ref $wgPasswordPolicy.
@@ -84,7 +86,8 @@ class SpecialPasswordPolicies extends SpecialPage {
                        $groupnameLocalized = UserGroupMembership::getGroupName( $group );
 
                        $grouppageLocalizedTitle = UserGroupMembership::getGroupPage( $group )
-                               ?: Title::newFromText( MWNamespace::getCanonicalName( NS_PROJECT ) . ':' . $group );
+                               ?: Title::newFromText( MediaWikiServices::getInstance()->getNamespaceInfo()->
+                                       getCanonicalName( NS_PROJECT ) . ':' . $group );
 
                        $grouppage = $linkRenderer->makeLink(
                                $grouppageLocalizedTitle,
index 7e5a73f..c4ea005 100644 (file)
@@ -35,7 +35,8 @@ class RandomPage extends SpecialPage {
        protected $extra = []; // Extra SQL statements
 
        public function __construct( $name = 'Randompage' ) {
-               $this->namespaces = MWNamespace::getContentNamespaces();
+               $this->namespaces = MediaWikiServices::getInstance()->getNamespaceInfo()->
+                       getContentNamespaces();
                parent::__construct( $name );
        }
 
index f27a736..4adc247 100644 (file)
@@ -330,13 +330,6 @@ class SpecialSearch extends SpecialPage {
                $showSuggestion = $title === null || !$title->isKnown();
                $search->setShowSuggestion( $showSuggestion );
 
-               $rewritten = $search->transformSearchTerm( $term );
-               if ( $rewritten !== $term ) {
-                       $term = $rewritten;
-                       wfDeprecated( 'SearchEngine::transformSearchTerm() (overridden by ' .
-                               get_class( $search ) . ')', '1.32' );
-               }
-
                $rewritten = $search->replacePrefixes( $term );
                if ( $rewritten !== $term ) {
                        wfDeprecated( 'SearchEngine::replacePrefixes() (overridden by ' .
@@ -654,7 +647,9 @@ class SpecialSearch extends SpecialPage {
                ) {
                        // Reset namespace preferences: namespaces are not searched
                        // when they're not mentioned in the URL parameters.
-                       foreach ( MWNamespace::getValidNamespaces() as $n ) {
+                       foreach ( MediaWikiServices::getInstance()->getNamespaceInfo()->getValidNamespaces()
+                               as $n
+                       ) {
                                $user->setOption( 'searchNs' . $n, false );
                        }
                        // The request parameters include all the namespaces to be searched.
index d90f72c..94da25d 100644 (file)
@@ -21,6 +21,7 @@
  * @ingroup SpecialPage
  */
 
+use MediaWiki\MediaWikiServices;
 use Wikimedia\Rdbms\IResultWrapper;
 use Wikimedia\Rdbms\IDatabase;
 
@@ -45,7 +46,10 @@ class ShortPagesPage extends QueryPage {
                $blacklist = $config->get( 'ShortPagesNamespaceBlacklist' );
                $tables = [ 'page' ];
                $conds = [
-                       'page_namespace' => array_diff( MWNamespace::getContentNamespaces(), $blacklist ),
+                       'page_namespace' => array_diff(
+                               MediaWikiServices::getInstance()->getNamespaceInfo()->getContentNamespaces(),
+                               $blacklist
+                       ),
                        'page_is_redirect' => 0
                ];
                $joinConds = [];
index d5e14d2..eff8889 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup SpecialPage
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * Special page lists various statistics, including the contents of
  * `site_stats`, plus page view details if enabled
@@ -207,8 +209,8 @@ class SpecialStatistics extends SpecialPage {
                        }
                        $msg = $this->msg( 'grouppage-' . $groupname )->inContentLanguage();
                        if ( $msg->isBlank() ) {
-                               $grouppageLocalized = MWNamespace::getCanonicalName( NS_PROJECT ) .
-                                       ':' . $groupname;
+                               $grouppageLocalized = MediaWikiServices::getInstance()->getNamespaceInfo()->
+                                       getCanonicalName( NS_PROJECT ) . ':' . $groupname;
                        } else {
                                $grouppageLocalized = $msg->text();
                        }
index 30b33cc..9efa803 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup SpecialPage
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * A special page looking for page without any category.
  *
@@ -60,7 +62,8 @@ class UncategorizedPagesPage extends PageQueryPage {
                                'cl_from IS NULL',
                                'page_namespace' => $this->requestedNamespace !== false
                                                ? $this->requestedNamespace
-                                               : MWNamespace::getContentNamespaces(),
+                                               : MediaWikiServices::getInstance()->getNamespaceInfo()->
+                                                       getContentNamespaces(),
                                'page_is_redirect' => 0
                        ],
                        'join_conds' => [
@@ -72,7 +75,10 @@ class UncategorizedPagesPage extends PageQueryPage {
        function getOrderFields() {
                // For some crazy reason ordering by a constant
                // causes a filesort
-               if ( $this->requestedNamespace === false && count( MWNamespace::getContentNamespaces() ) > 1 ) {
+               if ( $this->requestedNamespace === false &&
+                       count( MediaWikiServices::getInstance()->getNamespaceInfo()->
+                               getContentNamespaces() ) > 1
+               ) {
                        return [ 'page_namespace', 'page_title' ];
                }
 
index bac059d..812f1b0 100644 (file)
@@ -199,10 +199,7 @@ class SpecialWatchlist extends ChangesListSpecialPage {
                                        'description' => 'rcfilters-filter-watchlistactivity-unseen-description',
                                        'cssClassSuffix' => 'watchedunseen',
                                        'isRowApplicableCallable' => function ( $ctx, RecentChange $rc ) {
-                                               $changeTs = $rc->getAttribute( 'rc_timestamp' );
-                                               $lastVisitTs = $this->getLatestSeenTimestamp( $rc );
-
-                                               return $lastVisitTs !== null && $changeTs >= $lastVisitTs;
+                                               return !$this->isChangeEffectivelySeen( $rc );
                                        },
                                ],
                                [
@@ -211,10 +208,7 @@ class SpecialWatchlist extends ChangesListSpecialPage {
                                        'description' => 'rcfilters-filter-watchlistactivity-seen-description',
                                        'cssClassSuffix' => 'watchedseen',
                                        'isRowApplicableCallable' => function ( $ctx, RecentChange $rc ) {
-                                               $changeTs = $rc->getAttribute( 'rc_timestamp' );
-                                               $lastVisitTs = $this->getLatestSeenTimestamp( $rc );
-
-                                               return $lastVisitTs === null || $changeTs < $lastVisitTs;
+                                               return $this->isChangeEffectivelySeen( $rc );
                                        }
                                ],
                        ],
@@ -548,10 +542,9 @@ class SpecialWatchlist extends ChangesListSpecialPage {
                        $rc->counter = $counter++;
 
                        if ( $this->getConfig()->get( 'ShowUpdatedMarker' ) ) {
-                               $lastVisitTs = $this->getLatestSeenTimestamp( $rc );
-                               $updated = ( $lastVisitTs > $rc->getAttribute( 'timestamp' ) );
+                               $unseen = !$this->isChangeEffectivelySeen( $rc );
                        } else {
-                               $updated = false;
+                               $unseen = false;
                        }
 
                        if ( isset( $watchedItemStore ) ) {
@@ -561,7 +554,7 @@ class SpecialWatchlist extends ChangesListSpecialPage {
                                $rc->numberofWatchingusers = 0;
                        }
 
-                       $changeLine = $list->recentChangesLine( $rc, $updated, $counter );
+                       $changeLine = $list->recentChangesLine( $rc, $unseen, $counter );
                        if ( $changeLine !== false ) {
                                $s .= $changeLine;
                        }
@@ -869,9 +862,19 @@ class SpecialWatchlist extends ChangesListSpecialPage {
 
        /**
         * @param RecentChange $rc
-        * @return string TS_MW timestamp
+        * @return bool User viewed the revision or a newer one
+        */
+       protected function isChangeEffectivelySeen( RecentChange $rc ) {
+               $lastVisitTs = $this->getLatestSeenTimestampIfHasUnseen( $rc );
+
+               return $lastVisitTs === null || $lastVisitTs > $rc->getAttribute( 'rc_timestamp' );
+       }
+
+       /**
+        * @param RecentChange $rc
+        * @return string|null TS_MW timestamp or null if all revision were seen
         */
-       protected function getLatestSeenTimestamp( RecentChange $rc ) {
+       private function getLatestSeenTimestampIfHasUnseen( RecentChange $rc ) {
                return $this->watchStore->getLatestNotificationTimestamp(
                        $rc->getAttribute( 'wl_notificationtimestamp' ),
                        $rc->getPerformer(),
index a1e5156..548e921 100644 (file)
@@ -22,6 +22,8 @@
  * @author Rob Church <robchur@gmail.com>
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * Special page lists pages without language links
  *
@@ -91,7 +93,8 @@ class WithoutInterwikiPage extends PageQueryPage {
                        ],
                        'conds' => [
                                'll_title IS NULL',
-                               'page_namespace' => MWNamespace::getContentNamespaces(),
+                               'page_namespace' => MediaWikiServices::getInstance()->getNamespaceInfo()->
+                                       getContentNamespaces(),
                                'page_is_redirect' => 0
                        ],
                        'join_conds' => [ 'langlinks' => [ 'LEFT JOIN', 'll_from = page_id' ] ]
index 7a47edf..be28417 100644 (file)
@@ -415,7 +415,8 @@ class UploadForm extends HTMLForm {
                        'wgCheckFileExtensions' => $config->get( 'CheckFileExtensions' ),
                        'wgStrictFileExtensions' => $config->get( 'StrictFileExtensions' ),
                        'wgFileExtensions' => array_values( array_unique( $config->get( 'FileExtensions' ) ) ),
-                       'wgCapitalizeUploads' => MWNamespace::isCapitalized( NS_FILE ),
+                       'wgCapitalizeUploads' => MediaWikiServices::getInstance()->getNamespaceInfo()->
+                               isCapitalized( NS_FILE ),
                        'wgMaxUploadSize' => $this->mMaxUploadSize,
                        'wgFileCanRotate' => SpecialUpload::rotationEnabled(),
                ];
index a187a44..e0db715 100644 (file)
@@ -386,7 +386,7 @@ class ContribsPager extends RangeChronologicalPager {
                        }
 
                        $associatedNS = $this->mDb->addQuotes(
-                               MWNamespace::getAssociated( $this->namespace )
+                               MediaWikiServices::getInstance()->getAssociated( $this->namespace )
                        );
 
                        return [
index 4da9395..1a6ba72 100644 (file)
@@ -46,7 +46,10 @@ class NaiveImportTitleFactory implements ImportTitleFactory {
 
                        // For built-in namespaces (0 <= ID < 100), we try to find a local NS with
                        // the same namespace ID
-                       if ( $foreignNs < 100 && MWNamespace::exists( $foreignNs ) ) {
+                       if (
+                               $foreignNs < 100 &&
+                               MediaWikiServices::getInstance()->getNamespaceInfo()->exists( $foreignNs )
+                       ) {
                                return Title::makeTitleSafe( $foreignNs, $foreignTitle->getText() );
                        }
                }
index 7c756aa..7a44d0a 100644 (file)
@@ -18,6 +18,8 @@
  * @file
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * A class to convert page titles on a foreign wiki (ForeignTitle objects) into
  * page titles on the local wiki (Title objects), placing all pages in a fixed
@@ -31,7 +33,7 @@ class NamespaceImportTitleFactory implements ImportTitleFactory {
         * @param int $ns The namespace to use for all pages
         */
        public function __construct( $ns ) {
-               if ( !MWNamespace::exists( $ns ) ) {
+               if ( !MediaWikiServices::getInstance()->getNamespaceInfo()->exists( $ns ) ) {
                        throw new MWException( "Namespace $ns doesn't exist on this wiki" );
                }
                $this->ns = $ns;
index f9cab24..7cfadc0 100644 (file)
@@ -20,6 +20,9 @@
  * @file
  */
 
+use MediaWiki\Config\ServiceOptions;
+use MediaWiki\Linker\LinkTarget;
+
 /**
  * This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of
  * them based on index.  The textual names of the namespaces are handled by Language.php.
@@ -44,14 +47,36 @@ class NamespaceInfo {
        /** @var int[]|null Valid namespaces cache */
        private $validNamespaces = null;
 
-       /** @var Config */
-       private $config;
+       /** @var ServiceOptions */
+       private $options;
+
+       /**
+        * TODO Make this const when HHVM support is dropped (T192166)
+        *
+        * @since 1.34
+        * @var array
+        */
+       public static $constructorOptions = [
+               'AllowImageMoving',
+               'CanonicalNamespaceNames',
+               'CapitalLinkOverrides',
+               'CapitalLinks',
+               'ContentNamespaces',
+               'ExtraNamespaces',
+               'ExtraSignatureNamespaces',
+               'NamespaceContentModels',
+               'NamespaceProtection',
+               'NamespacesWithSubpages',
+               'NonincludableNamespaces',
+               'RestrictionLevels',
+       ];
 
        /**
-        * @param Config $config
+        * @param ServiceOptions $options
         */
-       public function __construct( Config $config ) {
-               $this->config = $config;
+       public function __construct( ServiceOptions $options ) {
+               $options->assertRequiredOptions( self::$constructorOptions );
+               $this->options = $options;
        }
 
        /**
@@ -80,8 +105,8 @@ class NamespaceInfo {
         * @return bool
         */
        public function isMovable( $index ) {
-               $result = !( $index < NS_MAIN ||
-                       ( $index == NS_FILE && !$this->config->get( 'AllowImageMoving' ) ) );
+               $result = $index >= NS_MAIN &&
+                       ( $index != NS_FILE || $this->options->get( 'AllowImageMoving' ) );
 
                /**
                 * @since 1.20
@@ -125,6 +150,18 @@ class NamespaceInfo {
                        : $index + 1;
        }
 
+       /**
+        * @param LinkTarget $target
+        * @return LinkTarget Talk page for $target
+        * @throws MWException if $target's namespace doesn't have talk pages (e.g., NS_SPECIAL)
+        */
+       public function getTalkPage( LinkTarget $target ) : LinkTarget {
+               if ( $this->isTalk( $target->getNamespace() ) ) {
+                       return $target;
+               }
+               return new TitleValue( $this->getTalk( $target->getNamespace() ), $target->getDbKey() );
+       }
+
        /**
         * Get the subject namespace index for a given namespace
         * Special namespaces (NS_MEDIA, NS_SPECIAL) are always the subject.
@@ -143,24 +180,44 @@ class NamespaceInfo {
                        : $index;
        }
 
+       /**
+        * @param LinkTarget $target
+        * @return LinkTarget Subject page for $target
+        */
+       public function getSubjectPage( LinkTarget $target ) : LinkTarget {
+               if ( $this->isSubject( $target->getNamespace() ) ) {
+                       return $target;
+               }
+               return new TitleValue( $this->getSubject( $target->getNamespace() ), $target->getDbKey() );
+       }
+
        /**
         * Get the associated namespace.
         * For talk namespaces, returns the subject (non-talk) namespace
         * For subject (non-talk) namespaces, returns the talk namespace
         *
         * @param int $index Namespace index
-        * @return int|null If no associated namespace could be found
+        * @return int
+        * @throws MWException if called on a namespace that has no talk pages (e.g., NS_SPECIAL)
         */
        public function getAssociated( $index ) {
                $this->isMethodValidFor( $index, __METHOD__ );
 
                if ( $this->isSubject( $index ) ) {
                        return $this->getTalk( $index );
-               } elseif ( $this->isTalk( $index ) ) {
-                       return $this->getSubject( $index );
-               } else {
-                       return null;
                }
+               return $this->getSubject( $index );
+       }
+
+       /**
+        * @param LinkTarget $target
+        * @return LinkTarget Talk page for $target if it's a subject page, subject page if it's a talk
+        *   page
+        * @throws MWException if $target's namespace doesn't have talk pages (e.g., NS_SPECIAL)
+        */
+       public function getAssociatedPage( LinkTarget $target ) : LinkTarget {
+               return new TitleValue(
+                       $this->getAssociated( $target->getNamespace() ), $target->getDbKey() );
        }
 
        /**
@@ -215,11 +272,11 @@ class NamespaceInfo {
        public function getCanonicalNamespaces() {
                if ( $this->canonicalNamespaces === null ) {
                        $this->canonicalNamespaces =
-                               [ NS_MAIN => '' ] + $this->config->get( 'CanonicalNamespaceNames' );
+                               [ NS_MAIN => '' ] + $this->options->get( 'CanonicalNamespaceNames' );
                        $this->canonicalNamespaces +=
                                ExtensionRegistry::getInstance()->getAttribute( 'ExtensionNamespaces' );
-                       if ( is_array( $this->config->get( 'ExtraNamespaces' ) ) ) {
-                               $this->canonicalNamespaces += $this->config->get( 'ExtraNamespaces' );
+                       if ( is_array( $this->options->get( 'ExtraNamespaces' ) ) ) {
+                               $this->canonicalNamespaces += $this->options->get( 'ExtraNamespaces' );
                        }
                        Hooks::run( 'CanonicalNamespaces', [ &$this->canonicalNamespaces ] );
                }
@@ -242,7 +299,7 @@ class NamespaceInfo {
         * The input *must* be converted to lower case first
         *
         * @param string $name Namespace name
-        * @return int
+        * @return int|null
         */
        public function getCanonicalIndex( $name ) {
                if ( $this->namespaceIndexes === false ) {
@@ -259,8 +316,8 @@ class NamespaceInfo {
        }
 
        /**
-        * Returns an array of the namespaces (by integer id) that exist on the
-        * wiki. Used primarily by the api in help documentation.
+        * Returns an array of the namespaces (by integer id) that exist on the wiki. Used primarily by
+        * the API in help documentation. The array is sorted numerically and omits negative namespaces.
         * @return array
         */
        public function getValidNamespaces() {
@@ -297,7 +354,7 @@ class NamespaceInfo {
         * @return bool
         */
        public function isContent( $index ) {
-               return $index == NS_MAIN || in_array( $index, $this->config->get( 'ContentNamespaces' ) );
+               return $index == NS_MAIN || in_array( $index, $this->options->get( 'ContentNamespaces' ) );
        }
 
        /**
@@ -309,7 +366,7 @@ class NamespaceInfo {
         */
        public function wantSignatures( $index ) {
                return $this->isTalk( $index ) ||
-                       in_array( $index, $this->config->get( 'ExtraSignatureNamespaces' ) );
+                       in_array( $index, $this->options->get( 'ExtraSignatureNamespaces' ) );
        }
 
        /**
@@ -329,7 +386,7 @@ class NamespaceInfo {
         * @return bool
         */
        public function hasSubpages( $index ) {
-               return !empty( $this->config->get( 'NamespacesWithSubpages' )[$index] );
+               return !empty( $this->options->get( 'NamespacesWithSubpages' )[$index] );
        }
 
        /**
@@ -337,7 +394,7 @@ class NamespaceInfo {
         * @return array Array of namespace indices
         */
        public function getContentNamespaces() {
-               $contentNamespaces = $this->config->get( 'ContentNamespaces' );
+               $contentNamespaces = $this->options->get( 'ContentNamespaces' );
                if ( !is_array( $contentNamespaces ) || $contentNamespaces === [] ) {
                        return [ NS_MAIN ];
                } elseif ( !in_array( NS_MAIN, $contentNamespaces ) ) {
@@ -391,13 +448,13 @@ class NamespaceInfo {
                if ( in_array( $index, $this->alwaysCapitalizedNamespaces ) ) {
                        return true;
                }
-               $overrides = $this->config->get( 'CapitalLinkOverrides' );
+               $overrides = $this->options->get( 'CapitalLinkOverrides' );
                if ( isset( $overrides[$index] ) ) {
                        // CapitalLinkOverrides is explicitly set
                        return $overrides[$index];
                }
                // Default to the global setting
-               return $this->config->get( 'CapitalLinks' );
+               return $this->options->get( 'CapitalLinks' );
        }
 
        /**
@@ -418,7 +475,7 @@ class NamespaceInfo {
         * @return bool
         */
        public function isNonincludable( $index ) {
-               $namespaces = $this->config->get( 'NonincludableNamespaces' );
+               $namespaces = $this->options->get( 'NonincludableNamespaces' );
                return $namespaces && in_array( $index, $namespaces );
        }
 
@@ -433,22 +490,25 @@ class NamespaceInfo {
         * @return null|string Default model name for the given namespace, if set
         */
        public function getNamespaceContentModel( $index ) {
-               return $this->config->get( 'NamespaceContentModels' )[$index] ?? null;
+               return $this->options->get( 'NamespaceContentModels' )[$index] ?? null;
        }
 
        /**
         * Determine which restriction levels it makes sense to use in a namespace,
         * optionally filtered by a user's rights.
         *
+        * @todo Move this to PermissionManager and remove the dependency here on permissions-related
+        * config settings.
+        *
         * @param int $index Index to check
         * @param User|null $user User to check
         * @return array
         */
        public function getRestrictionLevels( $index, User $user = null ) {
-               if ( !isset( $this->config->get( 'NamespaceProtection' )[$index] ) ) {
+               if ( !isset( $this->options->get( 'NamespaceProtection' )[$index] ) ) {
                        // All levels are valid if there's no namespace restriction.
                        // But still filter by user, if necessary
-                       $levels = $this->config->get( 'RestrictionLevels' );
+                       $levels = $this->options->get( 'RestrictionLevels' );
                        if ( $user ) {
                                $levels = array_values( array_filter( $levels, function ( $level ) use ( $user ) {
                                        $right = $level;
@@ -467,7 +527,7 @@ class NamespaceInfo {
                // First, get the list of groups that can edit this namespace.
                $namespaceGroups = [];
                $combine = 'array_merge';
-               foreach ( (array)$this->config->get( 'NamespaceProtection' )[$index] as $right ) {
+               foreach ( (array)$this->options->get( 'NamespaceProtection' )[$index] as $right ) {
                        if ( $right == 'sysop' ) {
                                $right = 'editprotected'; // BC
                        }
@@ -485,7 +545,7 @@ class NamespaceInfo {
                // group that can edit the namespace but would be blocked by the
                // restriction.
                $usableLevels = [ '' ];
-               foreach ( $this->config->get( 'RestrictionLevels' ) as $level ) {
+               foreach ( $this->options->get( 'RestrictionLevels' ) as $level ) {
                        $right = $level;
                        if ( $right == 'sysop' ) {
                                $right = 'editprotected'; // BC
index 4244350..415196a 100644 (file)
@@ -18,6 +18,8 @@
  * @file
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * A class to convert page titles on a foreign wiki (ForeignTitle objects) into
  * page titles on the local wiki (Title objects), placing all pages as subpages
@@ -32,7 +34,10 @@ class SubpageImportTitleFactory implements ImportTitleFactory {
         * created
         */
        public function __construct( Title $rootPage ) {
-               if ( !MWNamespace::hasSubpages( $rootPage->getNamespace() ) ) {
+               if (
+                       !MediaWikiServices::getInstance()->getNamespaceInfo()->
+                               hasSubpages( $rootPage->getNamespace() )
+               ) {
                        throw new MWException( "The root page you specified, $rootPage, is in a " .
                                "namespace where subpages are not allowed" );
                }
index 66674dc..41facc7 100644 (file)
@@ -64,9 +64,11 @@ class ExternalUserNames {
                if ( $pos !== false ) {
                        $iw = explode( ':', substr( $userName, 0, $pos ) );
                        $firstIw = array_shift( $iw );
-                       $interwikiLookup = MediaWikiServices::getInstance()->getInterwikiLookup();
+                       $services = MediaWikiServices::getInstance();
+                       $interwikiLookup = $services->getInterwikiLookup();
                        if ( $interwikiLookup->isValidInterwiki( $firstIw ) ) {
-                               $title = MWNamespace::getCanonicalName( NS_USER ) . ':' . substr( $userName, $pos + 1 );
+                               $title = $services->getNamespaceInfo()->getCanonicalName( NS_USER ) .
+                                       ':' . substr( $userName, $pos + 1 );
                                if ( $iw ) {
                                        $title = implode( ':', $iw ) . ':' . $title;
                                }
index cdbbcc5..2cea712 100644 (file)
@@ -673,11 +673,20 @@ class User implements IDBAccessObject, UserIdentity {
         * @param int|null $userId User ID, if known
         * @param string|null $userName User name, if known
         * @param int|null $actorId Actor ID, if known
+        * @param bool|string $wikiId remote wiki to which the User/Actor ID applies, or false if none
         * @return User
         */
-       public static function newFromAnyId( $userId, $userName, $actorId ) {
+       public static function newFromAnyId( $userId, $userName, $actorId, $wikiId = false ) {
                global $wgActorTableSchemaMigrationStage;
 
+               // Stop-gap solution for the problem described in T222212.
+               // Force the User ID and Actor ID to zero for users loaded from the database
+               // of another wiki, to prevent subtle data corruption and confusing failure modes.
+               if ( $wikiId !== false ) {
+                       $userId = 0;
+                       $actorId = 0;
+               }
+
                $user = new User;
                $user->mFrom = 'defaults';
 
@@ -914,7 +923,7 @@ class User implements IDBAccessObject, UserIdentity {
                }
 
                if ( !( $flags & self::READ_LATEST ) && array_key_exists( $name, self::$idCacheByName ) ) {
-                       return self::$idCacheByName[$name];
+                       return is_null( self::$idCacheByName[$name] ) ? null : (int)self::$idCacheByName[$name];
                }
 
                list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
@@ -3665,12 +3674,25 @@ class User implements IDBAccessObject, UserIdentity {
                return true;
        }
 
+       /**
+        * Alias of isLoggedIn() with a name that describes its actual functionality. UserIdentity has
+        * only this new name and not the old isLoggedIn() variant.
+        *
+        * @return bool True if user is registered on this wiki, i.e., has a user ID. False if user is
+        *   anonymous or has no local account (which can happen when importing). This is equivalent to
+        *   getId() != 0 and is provided for code readability.
+        * @since 1.34
+        */
+       public function isRegistered() {
+               return $this->getId() != 0;
+       }
+
        /**
         * Get whether the user is logged in
         * @return bool
         */
        public function isLoggedIn() {
-               return $this->getId() != 0;
+               return $this->isRegistered();
        }
 
        /**
@@ -3678,7 +3700,7 @@ class User implements IDBAccessObject, UserIdentity {
         * @return bool
         */
        public function isAnon() {
-               return !$this->isLoggedIn();
+               return !$this->isRegistered();
        }
 
        /**
index ac9bbec..64c61fe 100644 (file)
@@ -62,4 +62,12 @@ interface UserIdentity {
         */
        public function equals( UserIdentity $user );
 
+       /**
+        * @since 1.34
+        *
+        * @return bool True if user is registered on this wiki, i.e., has a user ID. False if user is
+        *   anonymous or has no local account (which can happen when importing). This must be
+        *   equivalent to getId() != 0 and is provided for code readability.
+        */
+       public function isRegistered();
 }
index d1fd19d..800ac76 100644 (file)
@@ -93,4 +93,14 @@ class UserIdentityValue implements UserIdentity {
                return $this->getName() === $user->getName();
        }
 
+       /**
+        * @since 1.34
+        *
+        * @return bool True if user is registered on this wiki, i.e., has a user ID. False if user is
+        *   anonymous or has no local account (which can happen when importing). This is equivalent to
+        *   getId() != 0 and is provided for code readability.
+        */
+       public function isRegistered() {
+               return $this->getId() != 0;
+       }
 }
index fc95ebc..72f6086 100644 (file)
@@ -18,7 +18,9 @@
  * @file
  * @ingroup Watchlist
  */
+
 use MediaWiki\Linker\LinkTarget;
+use MediaWiki\User\UserIdentity;
 use Wikimedia\Rdbms\DBReadOnlyError;
 
 /**
@@ -42,7 +44,7 @@ class NoWriteWatchedItemStore implements WatchedItemStoreInterface {
                $this->actualStore = $actualStore;
        }
 
-       public function countWatchedItems( User $user ) {
+       public function countWatchedItems( UserIdentity $user ) {
                return $this->actualStore->countWatchedItems( $user );
        }
 
@@ -68,27 +70,27 @@ class NoWriteWatchedItemStore implements WatchedItemStoreInterface {
                );
        }
 
-       public function getWatchedItem( User $user, LinkTarget $target ) {
+       public function getWatchedItem( UserIdentity $user, LinkTarget $target ) {
                return $this->actualStore->getWatchedItem( $user, $target );
        }
 
-       public function loadWatchedItem( User $user, LinkTarget $target ) {
+       public function loadWatchedItem( UserIdentity $user, LinkTarget $target ) {
                return $this->actualStore->loadWatchedItem( $user, $target );
        }
 
-       public function getWatchedItemsForUser( User $user, array $options = [] ) {
+       public function getWatchedItemsForUser( UserIdentity $user, array $options = [] ) {
                return $this->actualStore->getWatchedItemsForUser( $user, $options );
        }
 
-       public function isWatched( User $user, LinkTarget $target ) {
+       public function isWatched( UserIdentity $user, LinkTarget $target ) {
                return $this->actualStore->isWatched( $user, $target );
        }
 
-       public function getNotificationTimestampsBatch( User $user, array $targets ) {
+       public function getNotificationTimestampsBatch( UserIdentity $user, array $targets ) {
                return $this->actualStore->getNotificationTimestampsBatch( $user, $targets );
        }
 
-       public function countUnreadNotifications( User $user, $unreadLimit = null ) {
+       public function countUnreadNotifications( UserIdentity $user, $unreadLimit = null ) {
                return $this->actualStore->countUnreadNotifications( $user, $unreadLimit );
        }
 
@@ -100,56 +102,60 @@ class NoWriteWatchedItemStore implements WatchedItemStoreInterface {
                throw new DBReadOnlyError( null, self::DB_READONLY_ERROR );
        }
 
-       public function addWatch( User $user, LinkTarget $target ) {
+       public function addWatch( UserIdentity $user, LinkTarget $target ) {
                throw new DBReadOnlyError( null, self::DB_READONLY_ERROR );
        }
 
-       public function addWatchBatchForUser( User $user, array $targets ) {
+       public function addWatchBatchForUser( UserIdentity $user, array $targets ) {
                throw new DBReadOnlyError( null, self::DB_READONLY_ERROR );
        }
 
-       public function removeWatch( User $user, LinkTarget $target ) {
+       public function removeWatch( UserIdentity $user, LinkTarget $target ) {
                throw new DBReadOnlyError( null, self::DB_READONLY_ERROR );
        }
 
        public function setNotificationTimestampsForUser(
-               User $user,
+               UserIdentity $user,
                $timestamp,
                array $targets = []
        ) {
                throw new DBReadOnlyError( null, self::DB_READONLY_ERROR );
        }
 
-       public function updateNotificationTimestamp( User $editor, LinkTarget $target, $timestamp ) {
+       public function updateNotificationTimestamp(
+               UserIdentity $editor, LinkTarget $target, $timestamp
+       ) {
                throw new DBReadOnlyError( null, self::DB_READONLY_ERROR );
        }
 
-       public function resetAllNotificationTimestampsForUser( User $user ) {
+       public function resetAllNotificationTimestampsForUser( UserIdentity $user ) {
                throw new DBReadOnlyError( null, self::DB_READONLY_ERROR );
        }
 
        public function resetNotificationTimestamp(
-               User $user,
-               Title $title,
+               UserIdentity $user,
+               LinkTarget $title,
                $force = '',
                $oldid = 0
        ) {
                throw new DBReadOnlyError( null, self::DB_READONLY_ERROR );
        }
 
-       public function clearUserWatchedItems( User $user ) {
+       public function clearUserWatchedItems( UserIdentity $user ) {
                throw new DBReadOnlyError( null, self::DB_READONLY_ERROR );
        }
 
-       public function clearUserWatchedItemsUsingJobQueue( User $user ) {
+       public function clearUserWatchedItemsUsingJobQueue( UserIdentity $user ) {
                throw new DBReadOnlyError( null, self::DB_READONLY_ERROR );
        }
 
-       public function removeWatchBatchForUser( User $user, array $titles ) {
+       public function removeWatchBatchForUser( UserIdentity $user, array $titles ) {
                throw new DBReadOnlyError( null, self::DB_READONLY_ERROR );
        }
 
-       public function getLatestNotificationTimestamp( $timestamp, User $user, LinkTarget $target ) {
+       public function getLatestNotificationTimestamp(
+               $timestamp, UserIdentity $user, LinkTarget $target
+       ) {
                return wfTimestampOrNull( TS_MW, $timestamp );
        }
 }
index 43a9c4e..4bf7f0c 100644 (file)
@@ -20,6 +20,7 @@
  */
 
 use MediaWiki\Linker\LinkTarget;
+use MediaWiki\User\UserIdentity;
 
 /**
  * Representation of a pair of user and title for watchlist entries.
@@ -36,7 +37,7 @@ class WatchedItem {
        private $linkTarget;
 
        /**
-        * @var User
+        * @var UserIdentity
         */
        private $user;
 
@@ -46,12 +47,12 @@ class WatchedItem {
        private $notificationTimestamp;
 
        /**
-        * @param User $user
+        * @param UserIdentity $user
         * @param LinkTarget $linkTarget
         * @param null|string $notificationTimestamp the value of the wl_notificationtimestamp field
         */
        public function __construct(
-               User $user,
+               UserIdentity $user,
                LinkTarget $linkTarget,
                $notificationTimestamp
        ) {
@@ -61,9 +62,17 @@ class WatchedItem {
        }
 
        /**
+        * @deprecated since 1.34, use getUserIdentity()
         * @return User
         */
        public function getUser() {
+               return User::newFromIdentity( $this->user );
+       }
+
+       /**
+        * @return UserIdentity
+        */
+       public function getUserIdentity() {
                return $this->user;
        }
 
index 6094f41..30e3cbe 100644 (file)
@@ -1,8 +1,9 @@
 <?php
 
-use Wikimedia\Rdbms\IDatabase;
 use MediaWiki\Linker\LinkTarget;
+use MediaWiki\User\UserIdentity;
 use Wikimedia\Assert\Assert;
+use Wikimedia\Rdbms\IDatabase;
 use Wikimedia\Rdbms\LoadBalancer;
 
 /**
@@ -121,8 +122,8 @@ class WatchedItemQueryService {
         *        'end'                 => string (format accepted by wfTimestamp) requires 'dir' option,
         *                                 timestamp to end enumerating
         *        'watchlistOwner'      => User user whose watchlist items should be listed if different
-        *                                 than the one specified with $user param,
-        *                                 requires 'watchlistOwnerToken' option
+        *                                 than the one specified with $user param, requires
+        *                                 'watchlistOwnerToken' option
         *        'watchlistOwnerToken' => string a watchlist token used to access another user's
         *                                 watchlist, used with 'watchlistOwnerToken' option
         *        'limit'               => int maximum numbers of items to return
@@ -256,7 +257,7 @@ class WatchedItemQueryService {
        /**
         * For simple listing of user's watchlist items, see WatchedItemStore::getWatchedItemsForUser
         *
-        * @param User $user
+        * @param UserIdentity $user
         * @param array $options Allowed keys:
         *        'sort'         => string optional sorting by namespace ID and title
         *                          one of the self::SORT_* constants
@@ -272,8 +273,8 @@ class WatchedItemQueryService {
         *                          specified using the form option
         * @return WatchedItem[]
         */
-       public function getWatchedItemsForUser( User $user, array $options = [] ) {
-               if ( $user->isAnon() ) {
+       public function getWatchedItemsForUser( UserIdentity $user, array $options = [] ) {
+               if ( !$user->isRegistered() ) {
                        // TODO: should this just return an empty array or rather complain loud at this point
                        // as e.g. ApiBase::getWatchlistUser does?
                        return [];
@@ -460,11 +461,12 @@ class WatchedItemQueryService {
                return $conds;
        }
 
-       private function getWatchlistOwnerId( User $user, array $options ) {
+       private function getWatchlistOwnerId( UserIdentity $user, array $options ) {
                if ( array_key_exists( 'watchlistOwner', $options ) ) {
                        /** @var User $watchlistOwner */
                        $watchlistOwner = $options['watchlistOwner'];
-                       $ownersToken = $watchlistOwner->getOption( 'watchlisttoken' );
+                       $ownersToken =
+                               $watchlistOwner->getOption( 'watchlisttoken' );
                        $token = $options['watchlistOwnerToken'];
                        if ( $ownersToken == '' || !hash_equals( $ownersToken, $token ) ) {
                                throw ApiUsageException::newWithMessage( null, 'apierror-bad-watchlist-token', 'bad_wltoken' );
@@ -613,7 +615,9 @@ class WatchedItemQueryService {
                );
        }
 
-       private function getWatchedItemsForUserQueryConds( IDatabase $db, User $user, array $options ) {
+       private function getWatchedItemsForUserQueryConds(
+               IDatabase $db, UserIdentity $user, array $options
+       ) {
                $conds = [ 'wl_user' => $user->getId() ];
                if ( $options['namespaceIds'] ) {
                        $conds['wl_namespace'] = array_map( 'intval', $options['namespaceIds'] );
index a0e64c5..00770ea 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 
+use MediaWiki\User\UserIdentity;
 use Wikimedia\Rdbms\IResultWrapper;
 use Wikimedia\Rdbms\IDatabase;
 
@@ -21,7 +22,7 @@ interface WatchedItemQueryServiceExtension {
         *
         * @warning Any joins added *must* join on a unique key of the target table
         *  unless you really know what you're doing.
-        * @param User $user
+        * @param UserIdentity $user
         * @param array $options Options from
         *  WatchedItemQueryService::getWatchedItemsWithRecentChangeInfo()
         * @param IDatabase $db Database connection being used for the query
@@ -31,15 +32,16 @@ interface WatchedItemQueryServiceExtension {
         * @param array &$dbOptions Options for Database::select()
         * @param array &$joinConds Join conditions for Database::select()
         */
-       public function modifyWatchedItemsWithRCInfoQuery( User $user, array $options, IDatabase $db,
-               array &$tables, array &$fields, array &$conds, array &$dbOptions, array &$joinConds
+       public function modifyWatchedItemsWithRCInfoQuery( UserIdentity $user, array $options,
+               IDatabase $db, array &$tables, array &$fields, array &$conds, array &$dbOptions,
+               array &$joinConds
        );
 
        /**
         * Modify the results from WatchedItemQueryService::getWatchedItemsWithRecentChangeInfo()
         * before they're returned.
         *
-        * @param User $user
+        * @param UserIdentity $user
         * @param array $options Options from
         *  WatchedItemQueryService::getWatchedItemsWithRecentChangeInfo()
         * @param IDatabase $db Database connection being used for the query
@@ -50,7 +52,7 @@ interface WatchedItemQueryServiceExtension {
         *  [ $recentChangeInfo['rc_timestamp'], $recentChangeInfo['rc_id'] ] from the first item
         *  removed.
         */
-       public function modifyWatchedItemsWithRCInfo( User $user, array $options, IDatabase $db,
+       public function modifyWatchedItemsWithRCInfo( UserIdentity $user, array $options, IDatabase $db,
                array &$items, $res, &$startFrom
        );
 
index e287a35..bd4360e 100644 (file)
@@ -1,12 +1,14 @@
 <?php
 
-use Wikimedia\Rdbms\IDatabase;
 use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
 use MediaWiki\Linker\LinkTarget;
+use MediaWiki\Revision\RevisionLookup;
+use MediaWiki\User\UserIdentity;
 use Wikimedia\Assert\Assert;
-use Wikimedia\ScopedCallback;
+use Wikimedia\Rdbms\IDatabase;
 use Wikimedia\Rdbms\ILBFactory;
 use Wikimedia\Rdbms\LoadBalancer;
+use Wikimedia\ScopedCallback;
 
 /**
  * Storage layer class for WatchedItems.
@@ -67,14 +69,19 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
        private $deferredUpdatesAddCallableUpdateCallback;
 
        /**
-        * @var callable|null
+        * @var int
         */
-       private $revisionGetTimestampFromIdCallback;
+       private $updateRowsPerQuery;
 
        /**
-        * @var int
+        * @var NamespaceInfo
         */
-       private $updateRowsPerQuery;
+       private $nsInfo;
+
+       /**
+        * @var RevisionLookup
+        */
+       private $revisionLookup;
 
        /**
         * @var StatsdDataFactoryInterface
@@ -88,6 +95,8 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
         * @param HashBagOStuff $cache
         * @param ReadOnlyMode $readOnlyMode
         * @param int $updateRowsPerQuery
+        * @param NamespaceInfo $nsInfo
+        * @param RevisionLookup $revisionLookup
         */
        public function __construct(
                ILBFactory $lbFactory,
@@ -95,7 +104,9 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
                BagOStuff $stash,
                HashBagOStuff $cache,
                ReadOnlyMode $readOnlyMode,
-               $updateRowsPerQuery
+               $updateRowsPerQuery,
+               NamespaceInfo $nsInfo,
+               RevisionLookup $revisionLookup
        ) {
                $this->lbFactory = $lbFactory;
                $this->loadBalancer = $lbFactory->getMainLB();
@@ -106,9 +117,9 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
                $this->stats = new NullStatsdDataFactory();
                $this->deferredUpdatesAddCallableUpdateCallback =
                        [ DeferredUpdates::class, 'addCallableUpdate' ];
-               $this->revisionGetTimestampFromIdCallback =
-                       [ Revision::class, 'getTimestampFromId' ];
                $this->updateRowsPerQuery = $updateRowsPerQuery;
+               $this->nsInfo = $nsInfo;
+               $this->revisionLookup = $revisionLookup;
 
                $this->latestUpdateCache = new HashBagOStuff( [ 'maxKeys' => 3 ] );
        }
@@ -144,30 +155,7 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
                } );
        }
 
-       /**
-        * Overrides the Revision::getTimestampFromId callback
-        * This is intended for use while testing and will fail if MW_PHPUNIT_TEST is not defined.
-        *
-        * @param callable $callback
-        * @see Revision::getTimestampFromId for callback signiture
-        *
-        * @return ScopedCallback to reset the overridden value
-        * @throws MWException
-        */
-       public function overrideRevisionGetTimestampFromIdCallback( callable $callback ) {
-               if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
-                       throw new MWException(
-                               'Cannot override Revision::getTimestampFromId callback in operation.'
-                       );
-               }
-               $previousValue = $this->revisionGetTimestampFromIdCallback;
-               $this->revisionGetTimestampFromIdCallback = $callback;
-               return new ScopedCallback( function () use ( $previousValue ) {
-                       $this->revisionGetTimestampFromIdCallback = $previousValue;
-               } );
-       }
-
-       private function getCacheKey( User $user, LinkTarget $target ) {
+       private function getCacheKey( UserIdentity $user, LinkTarget $target ) {
                return $this->cache->makeKey(
                        (string)$target->getNamespace(),
                        $target->getDBkey(),
@@ -176,7 +164,7 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
        }
 
        private function cache( WatchedItem $item ) {
-               $user = $item->getUser();
+               $user = $item->getUserIdentity();
                $target = $item->getLinkTarget();
                $key = $this->getCacheKey( $user, $target );
                $this->cache->set( $key, $item );
@@ -184,7 +172,7 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
                $this->stats->increment( 'WatchedItemStore.cache' );
        }
 
-       private function uncache( User $user, LinkTarget $target ) {
+       private function uncache( UserIdentity $user, LinkTarget $target ) {
                $this->cache->delete( $this->getCacheKey( $user, $target ) );
                unset( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()][$user->getId()] );
                $this->stats->increment( 'WatchedItemStore.uncache' );
@@ -201,7 +189,7 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
                }
        }
 
-       private function uncacheUser( User $user ) {
+       private function uncacheUser( UserIdentity $user ) {
                $this->stats->increment( 'WatchedItemStore.uncacheUser' );
                foreach ( $this->cacheIndex as $ns => $dbKeyArray ) {
                        foreach ( $dbKeyArray as $dbKey => $userArray ) {
@@ -218,12 +206,12 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
        }
 
        /**
-        * @param User $user
+        * @param UserIdentity $user
         * @param LinkTarget $target
         *
         * @return WatchedItem|false
         */
-       private function getCached( User $user, LinkTarget $target ) {
+       private function getCached( UserIdentity $user, LinkTarget $target ) {
                return $this->cache->get( $this->getCacheKey( $user, $target ) );
        }
 
@@ -231,12 +219,12 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
         * Return an array of conditions to select or update the appropriate database
         * row.
         *
-        * @param User $user
+        * @param UserIdentity $user
         * @param LinkTarget $target
         *
         * @return array
         */
-       private function dbCond( User $user, LinkTarget $target ) {
+       private function dbCond( UserIdentity $user, LinkTarget $target ) {
                return [
                        'wl_user' => $user->getId(),
                        'wl_namespace' => $target->getNamespace(),
@@ -260,11 +248,11 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
         *
         * @since 1.30
         *
-        * @param User $user
+        * @param UserIdentity $user
         *
         * @return bool true on success, false when too many items are watched
         */
-       public function clearUserWatchedItems( User $user ) {
+       public function clearUserWatchedItems( UserIdentity $user ) {
                if ( $this->countWatchedItems( $user ) > $this->updateRowsPerQuery ) {
                        return false;
                }
@@ -280,7 +268,7 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
                return true;
        }
 
-       private function uncacheAllItemsForUser( User $user ) {
+       private function uncacheAllItemsForUser( UserIdentity $user ) {
                $userId = $user->getId();
                foreach ( $this->cacheIndex as $ns => $dbKeyIndex ) {
                        foreach ( $dbKeyIndex as $dbKey => $userIndex ) {
@@ -309,9 +297,9 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
         *
         * @since 1.31
         *
-        * @param User $user
+        * @param UserIdentity $user
         */
-       public function clearUserWatchedItemsUsingJobQueue( User $user ) {
+       public function clearUserWatchedItemsUsingJobQueue( UserIdentity $user ) {
                $job = ClearUserWatchlistJob::newForUser( $user, $this->getMaxId() );
                $this->queueGroup->push( $job );
        }
@@ -332,10 +320,10 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
 
        /**
         * @since 1.31
-        * @param User $user
+        * @param UserIdentity $user
         * @return int
         */
-       public function countWatchedItems( User $user ) {
+       public function countWatchedItems( UserIdentity $user ) {
                $dbr = $this->getConnectionRef( DB_REPLICA );
                $return = (int)$dbr->selectField(
                        'watchlist',
@@ -394,16 +382,16 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
        }
 
        /**
-        * @param User $user
+        * @param UserIdentity $user
         * @param TitleValue[] $titles
         * @return bool
         * @throws MWException
         */
-       public function removeWatchBatchForUser( User $user, array $titles ) {
+       public function removeWatchBatchForUser( UserIdentity $user, array $titles ) {
                if ( $this->readOnlyMode->isReadOnly() ) {
                        return false;
                }
-               if ( $user->isAnon() ) {
+               if ( !$user->isRegistered() ) {
                        return false;
                }
                if ( !$titles ) {
@@ -563,12 +551,12 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
 
        /**
         * @since 1.27
-        * @param User $user
+        * @param UserIdentity $user
         * @param LinkTarget $target
         * @return bool
         */
-       public function getWatchedItem( User $user, LinkTarget $target ) {
-               if ( $user->isAnon() ) {
+       public function getWatchedItem( UserIdentity $user, LinkTarget $target ) {
+               if ( !$user->isRegistered() ) {
                        return false;
                }
 
@@ -583,13 +571,13 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
 
        /**
         * @since 1.27
-        * @param User $user
+        * @param UserIdentity $user
         * @param LinkTarget $target
         * @return WatchedItem|bool
         */
-       public function loadWatchedItem( User $user, LinkTarget $target ) {
-               // Only loggedin user can have a watchlist
-               if ( $user->isAnon() ) {
+       public function loadWatchedItem( UserIdentity $user, LinkTarget $target ) {
+               // Only registered user can have a watchlist
+               if ( !$user->isRegistered() ) {
                        return false;
                }
 
@@ -618,11 +606,11 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
 
        /**
         * @since 1.27
-        * @param User $user
+        * @param UserIdentity $user
         * @param array $options
         * @return WatchedItem[]
         */
-       public function getWatchedItemsForUser( User $user, array $options = [] ) {
+       public function getWatchedItemsForUser( UserIdentity $user, array $options = [] ) {
                $options += [ 'forWrite' => false ];
 
                $dbOptions = [];
@@ -664,27 +652,27 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
 
        /**
         * @since 1.27
-        * @param User $user
+        * @param UserIdentity $user
         * @param LinkTarget $target
         * @return bool
         */
-       public function isWatched( User $user, LinkTarget $target ) {
+       public function isWatched( UserIdentity $user, LinkTarget $target ) {
                return (bool)$this->getWatchedItem( $user, $target );
        }
 
        /**
         * @since 1.27
-        * @param User $user
+        * @param UserIdentity $user
         * @param LinkTarget[] $targets
         * @return array
         */
-       public function getNotificationTimestampsBatch( User $user, array $targets ) {
+       public function getNotificationTimestampsBatch( UserIdentity $user, array $targets ) {
                $timestamps = [];
                foreach ( $targets as $target ) {
                        $timestamps[$target->getNamespace()][$target->getDBkey()] = false;
                }
 
-               if ( $user->isAnon() ) {
+               if ( !$user->isRegistered() ) {
                        return $timestamps;
                }
 
@@ -728,27 +716,27 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
 
        /**
         * @since 1.27
-        * @param User $user
+        * @param UserIdentity $user
         * @param LinkTarget $target
         * @throws MWException
         */
-       public function addWatch( User $user, LinkTarget $target ) {
+       public function addWatch( UserIdentity $user, LinkTarget $target ) {
                $this->addWatchBatchForUser( $user, [ $target ] );
        }
 
        /**
         * @since 1.27
-        * @param User $user
+        * @param UserIdentity $user
         * @param LinkTarget[] $targets
         * @return bool
         * @throws MWException
         */
-       public function addWatchBatchForUser( User $user, array $targets ) {
+       public function addWatchBatchForUser( UserIdentity $user, array $targets ) {
                if ( $this->readOnlyMode->isReadOnly() ) {
                        return false;
                }
-               // Only logged-in user can have a watchlist
-               if ( $user->isAnon() ) {
+               // Only registered user can have a watchlist
+               if ( !$user->isRegistered() ) {
                        return false;
                }
 
@@ -799,12 +787,12 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
 
        /**
         * @since 1.27
-        * @param User $user
+        * @param UserIdentity $user
         * @param LinkTarget $target
         * @return bool
         * @throws MWException
         */
-       public function removeWatch( User $user, LinkTarget $target ) {
+       public function removeWatch( UserIdentity $user, LinkTarget $target ) {
                return $this->removeWatchBatchForUser( $user, [ $target ] );
        }
 
@@ -820,14 +808,16 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
         * only the specified titles will be updated, and this will be done immediately (not deferred).
         *
         * @since 1.27
-        * @param User $user
+        * @param UserIdentity $user
         * @param string|int $timestamp Value to set the "last viewed" timestamp to (null to clear)
         * @param LinkTarget[] $targets Titles to set the timestamp for; [] means the entire watchlist
         * @return bool
         */
-       public function setNotificationTimestampsForUser( User $user, $timestamp, array $targets = [] ) {
-               // Only loggedin user can have a watchlist
-               if ( $user->isAnon() || $this->readOnlyMode->isReadOnly() ) {
+       public function setNotificationTimestampsForUser(
+               UserIdentity $user, $timestamp, array $targets = []
+       ) {
+               // Only registered user can have a watchlist
+               if ( !$user->isRegistered() || $this->readOnlyMode->isReadOnly() ) {
                        return false;
                }
 
@@ -873,7 +863,9 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
                return true;
        }
 
-       public function getLatestNotificationTimestamp( $timestamp, User $user, LinkTarget $target ) {
+       public function getLatestNotificationTimestamp(
+               $timestamp, UserIdentity $user, LinkTarget $target
+       ) {
                $timestamp = wfTimestampOrNull( TS_MW, $timestamp );
                if ( $timestamp === null ) {
                        return null; // no notification
@@ -894,12 +886,12 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
        /**
         * Schedule a DeferredUpdate that sets all of the "last viewed" timestamps for a given user
         * to the same value.
-        * @param User $user
+        * @param UserIdentity $user
         * @param string|int|null $timestamp Value to set all timestamps to, null to clear them
         */
-       public function resetAllNotificationTimestampsForUser( User $user, $timestamp = null ) {
-               // Only loggedin user can have a watchlist
-               if ( $user->isAnon() ) {
+       public function resetAllNotificationTimestampsForUser( UserIdentity $user, $timestamp = null ) {
+               // Only registered user can have a watchlist
+               if ( !$user->isRegistered() ) {
                        return;
                }
 
@@ -920,12 +912,14 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
 
        /**
         * @since 1.27
-        * @param User $editor
+        * @param UserIdentity $editor
         * @param LinkTarget $target
         * @param string|int $timestamp
         * @return int[]
         */
-       public function updateNotificationTimestamp( User $editor, LinkTarget $target, $timestamp ) {
+       public function updateNotificationTimestamp(
+               UserIdentity $editor, LinkTarget $target, $timestamp
+       ) {
                $dbw = $this->getConnectionRef( DB_MASTER );
                $uids = $dbw->selectFieldValues(
                        'watchlist',
@@ -977,23 +971,36 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
 
        /**
         * @since 1.27
-        * @param User $user
-        * @param Title $title
+        * @param UserIdentity $user
+        * @param LinkTarget $title
         * @param string $force
         * @param int $oldid
         * @return bool
         */
-       public function resetNotificationTimestamp( User $user, Title $title, $force = '', $oldid = 0 ) {
+       public function resetNotificationTimestamp(
+               UserIdentity $user, LinkTarget $title, $force = '', $oldid = 0
+       ) {
                $time = time();
 
-               // Only loggedin user can have a watchlist
-               if ( $this->readOnlyMode->isReadOnly() || $user->isAnon() ) {
+               // Only registered user can have a watchlist
+               if ( $this->readOnlyMode->isReadOnly() || !$user->isRegistered() ) {
                        return false;
                }
 
-               if ( !Hooks::run( 'BeforeResetNotificationTimestamp', [ &$user, &$title, $force, &$oldid ] ) ) {
+               // Hook expects User and Title, not UserIdentity and LinkTarget
+               $userObj = User::newFromId( $user->getId() );
+               $titleObj = Title::castFromLinkTarget( $title );
+               if ( !Hooks::run( 'BeforeResetNotificationTimestamp',
+                       [ &$userObj, &$titleObj, $force, &$oldid ] )
+               ) {
                        return false;
                }
+               if ( !$userObj->equals( $user ) ) {
+                       $user = $userObj;
+               }
+               if ( !$titleObj->equals( $title ) ) {
+                       $title = $titleObj;
+               }
 
                $item = null;
                if ( $force != 'force' ) {
@@ -1004,11 +1011,19 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
                }
 
                // Get the timestamp (TS_MW) of this revision to track the latest one seen
-               $seenTime = call_user_func(
-                       $this->revisionGetTimestampFromIdCallback,
-                       $title,
-                       $oldid ?: $title->getLatestRevID()
-               );
+               $id = $oldid;
+               $seenTime = null;
+               if ( !$id ) {
+                       $latestRev = $this->revisionLookup->getRevisionByTitle( $title );
+                       if ( $latestRev ) {
+                               $id = $latestRev->getId();
+                               // Save a DB query
+                               $seenTime = $latestRev->getTimestamp();
+                       }
+               }
+               if ( $seenTime === null ) {
+                       $seenTime = $this->revisionLookup->getTimestampFromId( $id );
+               }
 
                // Mark the item as read immediately in lightweight storage
                $this->stash->merge(
@@ -1053,10 +1068,10 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
        }
 
        /**
-        * @param User $user
+        * @param UserIdentity $user
         * @return MapCacheLRU|null The map contains prefixed title keys and TS_MW values
         */
-       private function getPageSeenTimestamps( User $user ) {
+       private function getPageSeenTimestamps( UserIdentity $user ) {
                $key = $this->getPageSeenTimestampsKey( $user );
 
                return $this->latestUpdateCache->getWithSetCallback(
@@ -1069,10 +1084,10 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
        }
 
        /**
-        * @param User $user
+        * @param UserIdentity $user
         * @return string
         */
-       private function getPageSeenTimestampsKey( User $user ) {
+       private function getPageSeenTimestampsKey( UserIdentity $user ) {
                return $this->stash->makeGlobalKey(
                        'watchlist-recent-updates',
                        $this->lbFactory->getLocalDomainID(),
@@ -1088,13 +1103,16 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
                return "{$target->getNamespace()}:{$target->getDBkey()}";
        }
 
-       private function getNotificationTimestamp( User $user, Title $title, $item, $force, $oldid ) {
+       private function getNotificationTimestamp(
+               UserIdentity $user, LinkTarget $title, $item, $force, $oldid
+       ) {
                if ( !$oldid ) {
                        // No oldid given, assuming latest revision; clear the timestamp.
                        return null;
                }
 
-               if ( !$title->getNextRevisionID( $oldid ) ) {
+               $oldRev = $this->revisionLookup->getRevisionById( $oldid );
+               if ( !$this->revisionLookup->getNextRevision( $oldRev, $title ) ) {
                        // Oldid given and is the latest revision for this title; clear the timestamp.
                        return null;
                }
@@ -1110,12 +1128,7 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
 
                // Oldid given and isn't the latest; update the timestamp.
                // This will result in no further notification emails being sent!
-               // Calls Revision::getTimestampFromId in normal operation
-               $notificationTimestamp = call_user_func(
-                       $this->revisionGetTimestampFromIdCallback,
-                       $title,
-                       $oldid
-               );
+               $notificationTimestamp = $this->revisionLookup->getTimestampFromId( $oldid );
 
                // We need to go one second to the future because of various strict comparisons
                // throughout the codebase
@@ -1137,11 +1150,11 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
 
        /**
         * @since 1.27
-        * @param User $user
+        * @param UserIdentity $user
         * @param int|null $unreadLimit
         * @return int|bool
         */
-       public function countUnreadNotifications( User $user, $unreadLimit = null ) {
+       public function countUnreadNotifications( UserIdentity $user, $unreadLimit = null ) {
                $dbr = $this->getConnectionRef( DB_REPLICA );
 
                $queryOptions = [];
@@ -1174,11 +1187,15 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
         * @param LinkTarget $newTarget
         */
        public function duplicateAllAssociatedEntries( LinkTarget $oldTarget, LinkTarget $newTarget ) {
-               $oldTarget = Title::newFromLinkTarget( $oldTarget );
-               $newTarget = Title::newFromLinkTarget( $newTarget );
-
-               $this->duplicateEntry( $oldTarget->getSubjectPage(), $newTarget->getSubjectPage() );
-               $this->duplicateEntry( $oldTarget->getTalkPage(), $newTarget->getTalkPage() );
+               // Duplicate first the subject page, then the talk page
+               $this->duplicateEntry(
+                       $this->nsInfo->getSubjectPage( $oldTarget ),
+                       $this->nsInfo->getSubjectPage( $newTarget )
+               );
+               $this->duplicateEntry(
+                       $this->nsInfo->getTalkPage( $oldTarget ),
+                       $this->nsInfo->getTalkPage( $newTarget )
+               );
        }
 
        /**
@@ -1241,10 +1258,10 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
        }
 
        /**
-        * @param User $user
-        * @param Title[] $titles
+        * @param UserIdentity $user
+        * @param LinkTarget[] $titles
         */
-       private function uncacheTitlesForUser( User $user, array $titles ) {
+       private function uncacheTitlesForUser( UserIdentity $user, array $titles ) {
                foreach ( $titles as $title ) {
                        $this->uncache( $user, $title );
                }
index 349d98a..5ff29d0 100644 (file)
@@ -18,7 +18,9 @@
  * @file
  * @ingroup Watchlist
  */
+
 use MediaWiki\Linker\LinkTarget;
+use MediaWiki\User\UserIdentity;
 use Wikimedia\Rdbms\DBUnexpectedError;
 
 /**
@@ -43,11 +45,11 @@ interface WatchedItemStoreInterface {
         *
         * @since 1.31
         *
-        * @param User $user
+        * @param UserIdentity $user
         *
         * @return int
         */
-       public function countWatchedItems( User $user );
+       public function countWatchedItems( UserIdentity $user );
 
        /**
         * @since 1.31
@@ -115,29 +117,29 @@ interface WatchedItemStoreInterface {
         *
         * @since 1.31
         *
-        * @param User $user
+        * @param UserIdentity $user
         * @param LinkTarget $target
         *
         * @return WatchedItem|false
         */
-       public function getWatchedItem( User $user, LinkTarget $target );
+       public function getWatchedItem( UserIdentity $user, LinkTarget $target );
 
        /**
         * Loads an item from the db
         *
         * @since 1.31
         *
-        * @param User $user
+        * @param UserIdentity $user
         * @param LinkTarget $target
         *
         * @return WatchedItem|false
         */
-       public function loadWatchedItem( User $user, LinkTarget $target );
+       public function loadWatchedItem( UserIdentity $user, LinkTarget $target );
 
        /**
         * @since 1.31
         *
-        * @param User $user
+        * @param UserIdentity $user
         * @param array $options Allowed keys:
         *        'forWrite' => bool defaults to false
         *        'sort' => string optional sorting by namespace ID and title
@@ -145,24 +147,24 @@ interface WatchedItemStoreInterface {
         *
         * @return WatchedItem[]
         */
-       public function getWatchedItemsForUser( User $user, array $options = [] );
+       public function getWatchedItemsForUser( UserIdentity $user, array $options = [] );
 
        /**
         * Must be called separately for Subject & Talk namespaces
         *
         * @since 1.31
         *
-        * @param User $user
+        * @param UserIdentity $user
         * @param LinkTarget $target
         *
         * @return bool
         */
-       public function isWatched( User $user, LinkTarget $target );
+       public function isWatched( UserIdentity $user, LinkTarget $target );
 
        /**
         * @since 1.31
         *
-        * @param User $user
+        * @param UserIdentity $user
         * @param LinkTarget[] $targets
         *
         * @return array multi-dimensional like $return[$namespaceId][$titleString] = $timestamp,
@@ -170,54 +172,54 @@ interface WatchedItemStoreInterface {
         *         - string|null value of wl_notificationtimestamp,
         *         - false if $target is not watched by $user.
         */
-       public function getNotificationTimestampsBatch( User $user, array $targets );
+       public function getNotificationTimestampsBatch( UserIdentity $user, array $targets );
 
        /**
         * Must be called separately for Subject & Talk namespaces
         *
         * @since 1.31
         *
-        * @param User $user
+        * @param UserIdentity $user
         * @param LinkTarget $target
         */
-       public function addWatch( User $user, LinkTarget $target );
+       public function addWatch( UserIdentity $user, LinkTarget $target );
 
        /**
         * @since 1.31
         *
-        * @param User $user
+        * @param UserIdentity $user
         * @param LinkTarget[] $targets
         *
         * @return bool success
         */
-       public function addWatchBatchForUser( User $user, array $targets );
+       public function addWatchBatchForUser( UserIdentity $user, array $targets );
 
        /**
-        * Removes an entry for the User watching the LinkTarget
+        * Removes an entry for the UserIdentity watching the LinkTarget
         * Must be called separately for Subject & Talk namespaces
         *
         * @since 1.31
         *
-        * @param User $user
+        * @param UserIdentity $user
         * @param LinkTarget $target
         *
         * @return bool success
         * @throws DBUnexpectedError
         * @throws MWException
         */
-       public function removeWatch( User $user, LinkTarget $target );
+       public function removeWatch( UserIdentity $user, LinkTarget $target );
 
        /**
         * @since 1.31
         *
-        * @param User $user The user to set the timestamps for
+        * @param UserIdentity $user The user to set the timestamps for
         * @param string|null $timestamp Set the update timestamp to this value
         * @param LinkTarget[] $targets List of targets to update. Default to all targets
         *
         * @return bool success
         */
        public function setNotificationTimestampsForUser(
-               User $user,
+               UserIdentity $user,
                $timestamp,
                array $targets = []
        );
@@ -227,29 +229,30 @@ interface WatchedItemStoreInterface {
         *
         * @since 1.31
         *
-        * @param User $user The user to reset the timestamps for
+        * @param UserIdentity $user The user to reset the timestamps for
         */
-       public function resetAllNotificationTimestampsForUser( User $user );
+       public function resetAllNotificationTimestampsForUser( UserIdentity $user );
 
        /**
         * @since 1.31
         *
-        * @param User $editor The editor that triggered the update. Their notification
+        * @param UserIdentity $editor The editor that triggered the update. Their notification
         *  timestamp will not be updated(they have already seen it)
         * @param LinkTarget $target The target to update timestamps for
         * @param string $timestamp Set the update timestamp to this value
         *
         * @return int[] Array of user IDs the timestamp has been updated for
         */
-       public function updateNotificationTimestamp( User $editor, LinkTarget $target, $timestamp );
+       public function updateNotificationTimestamp(
+               UserIdentity $editor, LinkTarget $target, $timestamp );
 
        /**
         * Reset the notification timestamp of this entry
         *
         * @since 1.31
         *
-        * @param User $user
-        * @param Title $title
+        * @param UserIdentity $user
+        * @param LinkTarget $title
         * @param string $force Whether to force the write query to be executed even if the
         *    page is not watched or the notification timestamp is already NULL.
         *    'force' in order to force
@@ -258,18 +261,19 @@ interface WatchedItemStoreInterface {
         *
         * @return bool success Whether a job was enqueued
         */
-       public function resetNotificationTimestamp( User $user, Title $title, $force = '', $oldid = 0 );
+       public function resetNotificationTimestamp(
+               UserIdentity $user, LinkTarget $title, $force = '', $oldid = 0 );
 
        /**
         * @since 1.31
         *
-        * @param User $user
+        * @param UserIdentity $user
         * @param int|null $unreadLimit
         *
         * @return int|bool The number of unread notifications
         *                  true if greater than or equal to $unreadLimit
         */
-       public function countUnreadNotifications( User $user, $unreadLimit = null );
+       public function countUnreadNotifications( UserIdentity $user, $unreadLimit = null );
 
        /**
         * Check if the given title already is watched by the user, and if so
@@ -303,28 +307,28 @@ interface WatchedItemStoreInterface {
         *
         * @since 1.31
         *
-        * @param User $user
+        * @param UserIdentity $user
         */
-       public function clearUserWatchedItems( User $user );
+       public function clearUserWatchedItems( UserIdentity $user );
 
        /**
         * Queues a job that will clear the users watchlist using the Job Queue.
         *
         * @since 1.31
         *
-        * @param User $user
+        * @param UserIdentity $user
         */
-       public function clearUserWatchedItemsUsingJobQueue( User $user );
+       public function clearUserWatchedItemsUsingJobQueue( UserIdentity $user );
 
        /**
         * @since 1.32
         *
-        * @param User $user
+        * @param UserIdentity $user
         * @param LinkTarget[] $targets
         *
         * @return bool success
         */
-       public function removeWatchBatchForUser( User $user, array $targets );
+       public function removeWatchBatchForUser( UserIdentity $user, array $targets );
 
        /**
         * Convert $timestamp to TS_MW or return null if the page was visited since then by $user
@@ -335,9 +339,10 @@ interface WatchedItemStoreInterface {
         * Usage of this method should be limited to WatchedItem* classes
         *
         * @param string|null $timestamp Value of wl_notificationtimestamp from the DB
-        * @param User $user
+        * @param UserIdentity $user
         * @param LinkTarget $target
-        * @return string TS_MW timestamp or null
+        * @return string|null TS_MW timestamp or null if all revision were seen
         */
-       public function getLatestNotificationTimestamp( $timestamp, User $user, LinkTarget $target );
+       public function getLatestNotificationTimestamp(
+               $timestamp, UserIdentity $user, LinkTarget $target );
 }
index 5106c17..66b6566 100644 (file)
@@ -6,7 +6,6 @@ use Hooks;
 use Html;
 use MediaWiki\MediaWikiServices;
 use MediaWiki\Widget\SearchInputWidget;
-use MWNamespace;
 use SearchEngineConfig;
 use SpecialSearch;
 use Xml;
@@ -240,7 +239,8 @@ class SearchFormWidget {
                $activeNamespaces = $this->specialSearch->getNamespaces();
                $langConverter = $this->specialSearch->getLanguage();
                foreach ( $this->searchConfig->searchableNamespaces() as $namespace => $name ) {
-                       $subject = MWNamespace::getSubject( $namespace );
+                       $subject = MediaWikiServices::getInstance()->getNamespaceInfo()->
+                               getSubject( $namespace );
                        if ( !isset( $rows[$subject] ) ) {
                                $rows[$subject] = "";
                        }
index 539bdf4..7b89a9c 100644 (file)
@@ -27,6 +27,7 @@
  */
 
 use CLDRPluralRuleParser\Evaluator;
+use MediaWiki\MediaWikiServices;
 use Wikimedia\Assert\Assert;
 
 /**
@@ -506,7 +507,8 @@ class Language {
                if ( is_null( $this->namespaceNames ) ) {
                        global $wgMetaNamespace, $wgMetaNamespaceTalk, $wgExtraNamespaces;
 
-                       $validNamespaces = MWNamespace::getCanonicalNamespaces();
+                       $validNamespaces = MediaWikiServices::getInstance()->getNamespaceInfo()->
+                               getCanonicalNamespaces();
 
                        $this->namespaceNames = $wgExtraNamespaces +
                                self::$dataCache->getItem( $this->mCode, 'namespaceNames' );
@@ -744,7 +746,8 @@ class Language {
         */
        public function getNsIndex( $text ) {
                $lctext = $this->lc( $text );
-               $ns = MWNamespace::getCanonicalIndex( $lctext );
+               $ns = MediaWikiServices::getInstance()->getNamespaceInfo()->
+                       getCanonicalIndex( $lctext );
                if ( $ns !== null ) {
                        return $ns;
                }
index 91191b7..b3df9ec 100644 (file)
        "error": "Wōh",
        "databaseerror": "Cȳþþuhordes wōh",
        "databaseerror-textcl": "Gecyþneshordfræge misgedwild belamp",
+       "databaseerror-query": "Æsce: $1",
+       "databaseerror-function": "Wice: $1",
        "databaseerror-error": "Wōg: $1",
        "laggedslavemode": "'''Warnung:''' Wēnunga næbbe se tramet nīwlīca nīwunga.",
        "readonly": "Ġifhord locen",
index 3742b59..137e7ea 100644 (file)
        "botpasswords-editexisting": "تعديل كلمة سر موجودة للبوت",
        "botpasswords-label-needsreset": "(تحتاج كلمة المرور إلى إعادة الضبط)",
        "botpasswords-label-appid": "اسم البوت:",
-       "botpasswords-label-create": "Ø£Ù\86شأ",
+       "botpasswords-label-create": "Ø¥Ù\86شاء",
        "botpasswords-label-update": "تحديث",
        "botpasswords-label-cancel": "ألغ",
        "botpasswords-label-delete": "احذف",
index 9fb6049..522ac69 100644 (file)
        "actions": "Parilaksana",
        "namespaces": "Genah pesengan",
        "variants": "kawentenan sane lianan",
-       "navigation-heading": "menu navigasi",
+       "navigation-heading": "Menu navigasi",
        "errorpagetitle": "kaluputan",
        "returnto": "mabalik ring $1",
        "tagline": "Saka {{SITENAME}}",
        "talkpagelinktext": "Wicara",
        "specialpage": "Lembar sane kautamayang",
        "personaltools": "pekakas pribadi",
-       "talk": "rembug\n\nngarembug (kata kerja)",
+       "talk": "Rembug",
        "views": "Pekantenan",
        "toolbox": "Pekakas",
        "viewhelppage": "cingak lembar pamitutlung",
        "savearticle": "simpen lembar",
        "preview": "tayangan sadurungnyane",
        "showpreview": "cingak sane lintang",
-       "showdiff": "cingak pagentosan",
+       "showdiff": "Cingak pagentosan",
        "anoneditwarning": "<strong>Pingetan:</strong> Ida dané nénten kacatet ngranjing. Alamat IP ida dané jagi kacatet ring sejarah (indik sané dumunan) ring lembar puniki. Yening ida dane <strong>[$1 log in]</strong> utawi <strong>[$2 create an account]</strong>, your edits will be attributed to your username, along with other benefits.",
        "newarticle": "(Anyar)",
        "newarticletext": "ida dane ngiring pranala nuju lembar sane durung wenten. yening jagi ngaryanang lembar punika, ketik daging lembar ring kotak sane wenten ring beten puniki. (cingak [$1 lembar wantuan] anggen wacana salanturnyane). yening ida dane nenten nyelapang neked ring lembar puniki, klik tombol \"back\" ring \"penjelajah web\" ida dane.",
        "action-edit": "benahang lembar puniki",
        "nchanges": "$1{{PLURAL:$1|panguwahan|uwah-uwahan}}",
        "enhancedrc-history": "babad",
-       "recentchanges": "pagentosan sane anyar",
+       "recentchanges": "Pagentosan anyar",
        "recentchanges-legend": "pilihan panguwahan sane anyar",
        "recentchanges-feed-description": "molihang pagentosan anyar ring wiki ring \"umpan\" puniki",
        "recentchanges-label-newpage": "panguwahan puniki ngaryanin lembar anyar",
        "recentchanges-label-minor": "niki panguwahan kidik",
        "recentchanges-label-bot": "penguwahan puniki kalaksanayang antuk bot",
        "recentchanges-label-unpatrolled": "panguwahan puniki durung kapatroli",
-       "rcnotefrom": "Ring beten puniki inggih punika {{PLURAL:$5|panguwahan|panguwahan}} saking <strong>$3, $4</strong> (kaedengang ngantos <strong>$1</strong> panguwahan).",
+       "rcnotefrom": "Ring beten puniki inggih punika {{PLURAL:$5|panguwahan}} saking <strong>$3, $4</strong> (kaedengang ngantos <strong>$1</strong> panguwahan).",
        "rclistfrom": "edengang  penguwahan sane anyar wit saking $3 $2",
        "rcshowhideminor": "$1 uwahan kidik",
        "rcshowhideminor-show": "Edengang",
        "namespace": "Genah pesengan",
        "invert": "uliang pilihan",
        "tooltip-invert": "Centang kotak puniki mangdané ngengkebang lembar sané kauwah ring genah wastan sané kapilih (miwah genah wastan sané mapaiketan yéning kacentang)",
-       "blanknamespace": "utama",
+       "blanknamespace": "(Utama)",
        "contributions": "kawigunan {{GENDER:$1|penganggo}}",
        "contributions-title": "Kontribusi pangangge anggen $1",
        "mycontris": "kawigunan",
        "tooltip-n-randompage": "edengang polah-palih lembar",
        "tooltip-n-help": "genah anggen ngarereh",
        "tooltip-t-whatlinkshere": "kepahan sami lembar wiki sane maduwe pranala nuju lembar puniki",
-       "tooltip-t-recentchangeslinked": "pagentosan sane anyar lembar-lembar sane maduwe pranala nuju lembar puniki",
+       "tooltip-t-recentchangeslinked": "Pagentosan anyar lembar sane maduwe pranala nuju lembar puniki",
        "tooltip-feed-atom": "\"atom feed\" anggen lembar puniki",
-       "tooltip-t-contributions": "Daptar kepahan kawigunan {{GENDER:$1|penganggo niki}",
+       "tooltip-t-contributions": "Daptar kepahan kawigunan {{GENDER:$1|penganggo niki}}",
        "tooltip-t-emailuser": "Ngirim surel majeng ring {{GENDER:$1|penganggo puniki}}",
        "tooltip-t-upload": "ngunggahang file",
-       "tooltip-t-specialpages": "kepahan sami lembar istimewa",
+       "tooltip-t-specialpages": "Kepahan sami lembar istimewa",
        "tooltip-t-print": "kawentenan lian sane macetak ring lembar puniki",
        "tooltip-t-permalink": "Pranala ajeg kaanggen ngubah lembar puniki",
        "tooltip-ca-nstab-main": "cingak dagingnyane lembar puniki",
        "tooltip-ca-nstab-help": "cingak lembar pamitutlung",
        "tooltip-ca-nstab-category": "cingak lembar kategori",
        "tooltip-minoredit": "pingetin puniki dados panguwahan kidik",
-       "tooltip-save": "simpen pagentosan ida dane",
-       "tooltip-preview": "pagentosan sane dumun duwen ida dane, mangda anggen niki sadurung jagi nyimpen!",
-       "tooltip-diff": "cingak pagentosan sane sampun ida dane laksanayang",
+       "tooltip-save": "Nyimpen pagentosan ida dane",
+       "tooltip-preview": "Pagentosan sane dumun duwen ida dane, mangda anggen niki sadurung jagi nyimpen!",
+       "tooltip-diff": "Cingak pagentosan sane sampun ida dane laksanayang",
        "tooltip-compareselectedversions": "cingak binane makekalih kepahan lembar sane kasudi",
        "tooltip-watch": "imbuhin lembar niki ring daftar paninjoan ida dane",
        "tooltip-rollback": "\"nguliang\" muwungan jagi ngabecikang ring lembar puniki nuju haturan sane untat ngangge apisan klik",
index c665142..63a4659 100644 (file)
        "action-hideuser": "блякаваньне імя ўдзельніка і яго хаваньне",
        "action-ipblock-exempt": "абыход блякаваньняў IP-адрасоў, аўтаблякаваньняў і блякаваньняў дыяпазонаў",
        "action-unblockself": "разблякаваньне самога сябе",
+       "action-noratelimit": "адсутнасьць абмежаваньня хуткасьці",
+       "action-reupload-own": "перазапіс уласных існых файлаў",
        "nchanges": "$1 {{PLURAL:$1|зьмена|зьмены|зьменаў}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|з апошняга візыту}}",
        "enhancedrc-history": "гісторыя",
        "linksearch-pat": "Узор для пошуку:",
        "linksearch-ns": "Прастора назваў:",
        "linksearch-ok": "Шукаць",
-       "linksearch-text": "Ð\9cожна Ñ\9eжÑ\8bваÑ\86Ñ\8c Ñ\81Ñ\8bмбалÑ\96 Ð¿Ð°Ð´Ñ\81Ñ\82аноÑ\9eкÑ\96, Ð½Ð°Ð¿Ñ\80Ñ\8bклад, Â«*.wikipedia.org».\nÐ\9dеабÑ\85однÑ\8b Ð´Ð°Ð¼Ñ\8dн Ð¿ÐµÑ\80Ñ\88ага Ñ\9eзÑ\80оÑ\9eнÑ\8e, Ð½Ð°Ð¿Ñ\80Ñ\8bклад, Â«*.org».<br />\n{{PLURAL:$2|1=Ð\9fÑ\80аÑ\82акол, Ñ\8fкÑ\96 Ð¿Ð°Ð´Ñ\82Ñ\80Ñ\8bмлÑ\96ваеÑ\86Ñ\86а|Ð\9fÑ\80аÑ\82аколÑ\8b, Ñ\8fкÑ\96Ñ\8f Ð¿Ð°Ð´Ñ\82Ñ\80Ñ\8bмлÑ\96ваÑ\8eÑ\86Ñ\86а}}: $1 (дапомна http://, калі пратакол не пазначаны).",
+       "linksearch-text": "Ð\9cожна Ñ\9eжÑ\8bваÑ\86Ñ\8c Ñ\81Ñ\8bмбалÑ\96 Ð¿Ð°Ð´Ñ\81Ñ\82аноÑ\9eкÑ\96, Ð½Ð°Ð¿Ñ\80Ñ\8bклад, Â«*.wikipedia.org».\nÐ\9dеабÑ\85однÑ\8b Ð´Ð°Ð¼Ñ\8dн Ð¿ÐµÑ\80Ñ\88ага Ñ\9eзÑ\80оÑ\9eнÑ\8e, Ð½Ð°Ð¿Ñ\80Ñ\8bклад, Â«*.org».<br />\n{{PLURAL:$2|1=Ð\9fÑ\80аÑ\82акол, Ñ\8fкÑ\96 Ð¿Ð°Ð´Ñ\82Ñ\80Ñ\8bмлÑ\96ваеÑ\86Ñ\86а|Ð\9fÑ\80аÑ\82аколÑ\8b, Ñ\8fкÑ\96Ñ\8f Ð¿Ð°Ð´Ñ\82Ñ\80Ñ\8bмлÑ\96ваÑ\8eÑ\86Ñ\86а}}: $1 (па Ð·Ð¼Ð¾Ñ\9eÑ\87анÑ\8cнÑ\96 http://, калі пратакол не пазначаны).",
        "linksearch-line": "Спасылка на $1 з $2",
        "linksearch-error": "Сымбалі падстаноўкі могуць ужывацца толькі ў пачатку адрасоў.",
        "listusersfrom": "Паказаць удзельнікаў ад:",
index 5eeaf93..15edfc0 100644 (file)
        "blocklink": "block",
        "unblocklink": "unblock",
        "change-blocklink": "change block",
+       "empty-username": "(no username available)",
        "contribslink": "contribs",
        "emaillink": "send email",
        "autoblocker": "Autoblocked because your IP address has been recently used by \"[[User:$1|$1]]\".\nThe reason given for $1's block is \"$2\"",
index 21a3ef0..1a6b65b 100644 (file)
@@ -55,7 +55,8 @@
                        "Joao Xavier",
                        "Surfo",
                        "YvesNevelsteen",
-                       "Vlad5250"
+                       "Vlad5250",
+                       "Mirin"
                ]
        },
        "tog-underline": "Substrekado de ligiloj:",
        "histfirst": "plej malnova",
        "histlast": "plej nova",
        "historysize": "({{PLURAL:$1|1 bajto|$1 bajtoj}})",
-       "historyempty": "(malplena)",
+       "historyempty": "malplena",
        "history-feed-title": "Historio de redaktoj",
        "history-feed-description": "Revizia historio por ĉi tiu paĝo en la vikio",
        "history-feed-item-nocomment": "$1 ĉe $2",
        "rcfilters-watchlist-markseen-button": "Marku ĉiujn ŝanĝojn viditaj",
        "rcfilters-watchlist-edit-watchlist-button": "Redakti vian atentaron",
        "rcfilters-watchlist-showupdated": "Ŝanĝoj en paĝoj, kiujn vi ne vizitis post la ŝanĝo, aperas <strong>grase</strong>, kun plenigitaj buletoj.",
+       "rcfilters-watchlist-preference-label": "Uzi fasadon ne uzantan JavaScript",
        "rcfilters-target-page-placeholder": "Enigu nomon de paĝo (aŭ kategorio)",
        "rcnotefrom": "Malsupre estas la {{PLURAL:$5|ŝanĝo|ŝanĝoj}} ekde <strong>$3, $4</strong> (montrante ĝis <strong>$1</strong>).",
        "rclistfrom": "Montri novajn ŝanĝojn ekde \"$3 $2\"",
        "uploadstash-thumbnail": "Vidi bildeton",
        "uploadstash-exception": "Ne eblas alŝuti en kaŝkonservejon ($1): \"$2\".",
        "uploadstash-bad-path-unrecognized-thumb-name": "Nerekonita miniatura nomo.",
+       "uploadstash-zero-length": "Longo de dosiero estas nul.",
        "invalid-chunk-offset": "Malvalida deŝovo de dosierpeco",
        "img-auth-accessdenied": "Atingo malpermisita",
        "img-auth-nopathinfo": "Mankas informo pri vojo.\nVia servilo estu agordita por sendi la variablojn REQUEST_URI kaj/aŭ PATH_INFO.\nSe ĝi jam estas, provu aktivigon de $wgUsePathInfo.\nVidu https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
        "speciallogtitlelabel": "Celo (titolo aŭ  {{ns:user}}:salutnomo por uzanto):",
        "log": "Protokoloj",
        "logeventslist-submit": "Montri",
+       "logeventslist-tag-log": "Protokolo de etikedoj",
        "all-logs-page": "Ĉiuj publikaj protokoloj",
        "alllogstext": "Suma kompilaĵo de ĉiuj protokoloj de {{SITENAME}}.\nVi povas plistrikti la mendon per selektado de protokola speco, la salutnomo (inkluzivante uskladon) aŭ la efika paĝo (ankaŭ inkluzivas uskladon).",
        "logempty": "Neniaj artikoloj en la protokolo.",
        "deleteprotected": "Vi ne povas forigi ĉi tiun paĝon ĉar ĝi estis protektita.",
        "deleting-backlinks-warning": "<strong>Atentigo:</strong>\n[[Special:WhatLinksHere/{{FULLPAGENAME}}|Aliaj paĝoj]] ligas al aŭ transkludas tiun ĉi forigotan paĝon.",
        "rollback": "Restarigi antaŭan redakton",
+       "rollback-confirmation-confirm": "Bonvolu konfirmi:",
+       "rollback-confirmation-yes": "Amasmalfari",
+       "rollback-confirmation-no": "Nuligi",
        "rollbacklink": "malfari",
        "rollbacklinkcount": "nuligi $1 {{PLURAL:$1|redakton|redaktojn}}",
        "rollbacklinkcount-morethan": "nuligi pli ol $1 {{PLURAL:$1|redakton|redaktojn}}",
        "ipb-sitewide": "Tutreteja",
        "ipb-partial": "Parta",
        "ipb-pages-label": "Paĝoj",
+       "ipb-namespaces-label": "Nomspacoj",
        "badipaddress": "Neniu uzanto, aŭ la IP-adreso estas misformita.",
        "blockipsuccesssub": "Forbaro sukcesis.",
        "blockipsuccesstext": "[[Special:Contributions/$1|$1]] estas forbarita. <br />\nVidu la [[Special:BlockList|liston de forbaroj]] por kontroli.",
        "ipb-blocklist-contribs": "Kontribuoj de {{GENDER:$1|$1}}",
        "ipb-blocklist-duration-left": "$1 restas",
        "block-expiry": "Blokdaŭro",
+       "block-prevent-edit": "Redaktado",
+       "block-reason": "Kialo:",
        "unblockip": "Malforbari IP-adreson/nomon",
        "unblockiptext": "Per la jena formulo vi povas repovigi al iu\nforbarita IP-adreso/nomo la povon enskribi en la vikio.",
        "ipusubmit": "Forigi ĉi tiun forbaron",
        "blocklist-userblocks": "Kaŝi konto-forbarojn",
        "blocklist-tempblocks": "Kaŝi provizorajn forbarojn",
        "blocklist-addressblocks": "Kaŝi unuopajn IP-adresajn forbarojn",
+       "blocklist-type": "Tipo:",
        "blocklist-rangeblocks": "Kaŝi blokojn de intervalo",
        "blocklist-timestamp": "Tempindiko",
        "blocklist-target": "Celo",
        "pageinfo-display-title": "Montrita titolo",
        "pageinfo-default-sort": "Pravaloro de ordiga ŝlosilo",
        "pageinfo-length": "Paĝgrandeco (en bajtoj)",
+       "pageinfo-namespace": "Nomspaco",
        "pageinfo-article-id": "Paĝa identigo",
        "pageinfo-language": "Lingvo de paĝa enhavo",
        "pageinfo-language-change": "ŝanĝi",
        "confirm-unwatch-top": "Ĉu forigi tiun ĉi paĝon el via atentaro?",
        "confirm-rollback-button": "Bone",
        "confirm-rollback-top": "Malfaru redaktojn al ĉi tiu paĝo?",
+       "confirm-mcrundo-title": "Malfari ŝanĝon",
+       "mcrundofailed": "Malfaro malsukcesis",
        "quotation-marks": "„$1“",
        "imgmultipageprev": "← antaŭa paĝo",
        "imgmultipagenext": "sekva paĝo →",
        "tag-list-wrapper": "[[Special:Tags|{{PLURAL:$1|Etikedo|Etikedoj}}]]: $2",
        "tag-mw-contentmodelchange": "ŝanĝo de enhavomodelo",
        "tag-mw-contentmodelchange-description": "Redaktoj kiuj [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel ŝanĝas la enhavmodelon] de paĝo",
+       "tag-mw-undo": "Malfari",
        "tags-title": "Etikedoj",
        "tags-intro": "Ĉi tiu paĝo montras la etikedojn kun kiuj la programaro markus redakton, kaj iliaj signifoj.",
        "tags-tag": "Etikeda nomo",
        "compare-title-not-exists": "La titolo kiun vi specifis ne ekzistas.",
        "compare-revision-not-exists": "La revizio kiun vi specifis ne ekzistas.",
        "diff-form": "Malsamoj",
+       "diff-form-submit": "Montri diferencojn",
        "permanentlink": "Konstanta ligilo",
+       "permanentlink-revid": "Identigilo de revizio",
+       "permanentlink-submit": "Iri al revizio",
        "dberr-problems": "Bedaŭrinde, ĉi tiu retejo suferas pro teknikaj problemoj.",
        "dberr-again": "Bonvolu atendi kelkajn minutojn kaj reŝargi.",
        "dberr-info": "(Ne eblas konekti la datumbazon: $1)",
        "special-characters-group-thai": "Taja",
        "special-characters-group-lao": "laŭa",
        "special-characters-group-khmer": "kmera",
+       "special-characters-group-canadianaboriginal": "Kanada Indiĝena",
        "special-characters-title-endash": "mallonga streketo",
        "special-characters-title-emdash": "longa streketo",
        "special-characters-title-minus": "minus-signo",
        "mw-widgets-categoryselector-add-category-placeholder": "Aldoni kategorion",
        "mw-widgets-usersmultiselect-placeholder": "Aldoni pliajn...",
        "mw-widgets-titlesmultiselect-placeholder": "Aldoni pliajn...",
+       "date-range-from": "De dato:",
+       "date-range-to": "Ĝis dato:",
        "sessionmanager-tie": "Kombini diversajn tipojn de ensaluta peto ne estas permisita: $1.",
        "sessionprovider-generic": "$1 seancoj",
        "sessionprovider-mediawiki-session-cookiesessionprovider": "kuketaj seancoj",
        "log-action-filter-suppress-reblock": "Forigi uzanton per reforbari",
        "log-action-filter-upload-upload": "Novalŝuta",
        "log-action-filter-upload-overwrite": "Realŝuta",
+       "log-action-filter-upload-revert": "Restarigi",
        "authmanager-authn-not-in-progress": "Aŭtentigado ne estas progresanta aŭ la seancaj datumoj perdiĝis. Bonvolu provi denove ekde la komenco.",
        "authmanager-authn-no-primary": "La provizita legitimaĵo ne povus esti aŭtentikigita.",
        "authmanager-authn-no-local-user": "La provizitaj legitimaĵoj ne estas asociitaj kun ajna uzanto de ĉi tiu vikio.",
        "revid": "revizio $1",
        "pageid": "Identigilo de paĝo $1",
        "pagedata-title": "Paĝaj datumoj",
+       "pagedata-bad-title": "Nevalida titolo: \"$1\".",
+       "passwordpolicies": "Reguloj pri pasvortoj",
        "passwordpolicies-group": "Grupo",
        "passwordpolicies-policies": "Politiko",
        "passwordpolicies-policy-minimalpasswordlength": "Pasvortoj devas esti longaj almenaŭ  $1 {{PLURAL:$1|1 signon|$1 signojn}}.",
index ff0c96d..028ebae 100644 (file)
        "createacct-reason": "Razlog",
        "createacct-reason-ph": "Zašto stvarate još jedan račun?",
        "createacct-reason-help": "Poruka koja se prikazuje u evidenciji stvaranja suradničkih računa",
-       "createacct-submit": "Stvorite svoj suradnički račun",
+       "createacct-submit": "Stvori svoj suradnički račun",
        "createacct-another-submit": "Otvori račun",
        "createacct-continue-submit": "Pritisni za stvaranje računa",
        "createacct-another-continue-submit": "Nastavi za stvaranje računa",
        "sp-contributions-newonly": "Pokaži samo stranice koje je suradnik započeo",
        "sp-contributions-hideminor": "Sakrij manje izmjene",
        "sp-contributions-submit": "Traži",
+       "sp-contributions-outofrange": "Nije moguće pokazati rezultate. Traženi raspon IP adresa veći je od CIDR limita /$1.",
        "whatlinkshere": "Što vodi ovamo",
        "whatlinkshere-title": "Stranice koje vode na »$1«",
        "whatlinkshere-page": "Stranica:",
index dce7cba..4200f17 100644 (file)
        "action-editmyusercss": "saját szerkesztői CSS-fájlok szerkesztése",
        "action-editmyuserjson": "saját szerkesztői JSON-fájlok szerkesztése",
        "action-editmyuserjs": "saját szerkesztői JavaScript-fájlok szerkesztése",
+       "action-viewsuppressed": "minden felhasználó elől elrejtett változtatások megtekintése",
+       "action-hideuser": "felhasználói név blokkolása és elrejtése a külvilág elől",
        "action-ipblock-exempt": "IP-, auto- és tartományblokkok megkerülése",
        "action-unblockself": "saját felhasználói fiók blokkjának feloldása",
        "action-noratelimit": "sebességkorlát figyelmen kívül hagyása",
        "action-reupload-own": "a saját maga által feltöltött fájlok felülírása",
+       "action-nominornewtalk": "vitalapok apró szerkesztése új üzenetről való értesítés kiküldése nélkül",
        "action-markbotedits": "visszaállított szerkesztések botként való jelölése",
        "action-patrolmarks": "járőrök jelzéseinek megtekintése a friss változásokban",
        "action-override-export-depth": "lapok exportálása a hivatkozott lapokkal együtt, legfeljebb 5-ös mélységig",
+       "action-suppressredirect": "átirányítások készítésének kihagyása a lapok régi nevén átnevezéskor",
        "nchanges": "$1 változtatás",
        "enhancedrc-since-last-visit": "$1 az utolsó látogatás óta",
        "enhancedrc-history": "történet",
        "passwordpolicies-policyflag-forcechange": "lecserélés követelése bejelentkezéskor",
        "passwordpolicies-policyflag-suggestchangeonlogin": "lecserélés ajánlása bejelentkezéskor",
        "unprotected-js": "Biztonsági okokból JavaScript nem tölthető be védtelen lapokról. Kérlek egyedül a MediaWiki névtérben készíts JavaScriptet, vagy szerkesztői allapként.",
-       "userlogout-continue": "Amennyiben ki szeretnél jelentkezni, [$1 használd a kijelentkezési oldalt]."
+       "userlogout-continue": "Amennyiben ki szeretnél jelentkezni, [$1 használd a kijelentkezési oldalt].",
+       "userlogout-sessionerror": "Sikertelen kijelentkezés munkamenethiba miatt. Kérlek [$1 próbáld újra]."
 }
index 17a3470..06256b9 100644 (file)
        "badaccess-group0": "Արտունութիւն չունիք այս գործողութիւնը կատարել:",
        "badaccess-groups": "Տուեալ գործողութիւնը միայն $1 {{PLURAL:$2|խումբի|խումբերի}} մասնակիցները կ՛րնան կատարել։",
        "ok": "Լաւ",
-       "pagetitle": "Միացէ՛ք {{SITENAME}} նախագիծին",
+       "pagetitle": "",
        "retrievedfrom": "Վերցուած է «$1» էջէն",
        "youhavenewmessages": "{{PLURAL:$3|Դուք ունիք}} $1 ($2)։",
        "youhavenewmessagesfromusers": "{{PLURAL:$4|Դուք ունիք}} $1 {{PLURAL:$3|այլ մասնակից|$3 մասնակիցէն}} ($2):",
index 643fb5f..a348240 100644 (file)
        "download": "undhuh",
        "unwatchedpages": "Kaca kang ora ingawasan",
        "listredirects": "Pratélan alihan",
-       "unusedtemplates": "Cithakan kang ora kanggo",
+       "unusedtemplates": "Cithakan kang ora kaanggo",
        "unusedtemplatestext": "Kaca iki isi kabèh kaca ing mandala aran {{ns:template}} kang ora kaanggo ing kaca liya.\nAja lali mesthèkaké ana-orané pranala liya kang ngener cithakané sadurungé panjenengan mbusek.",
        "unusedtemplateswlh": "pranala liya-liyané",
        "randompage": "Kaca sembarang",
        "withoutinterwiki-summary": "Kaca-kaca ing ngisor iki ora nggayut menyang vèrsi basa liyané.",
        "withoutinterwiki-legend": "Préfiks",
        "withoutinterwiki-submit": "Tuduhna",
-       "fewestrevisions": "Artikel kang owahé sithik dhéwé",
+       "fewestrevisions": "Artikel kang owahé sathithik dhéwé",
        "nbytes": "$1 {{PLURAL:$1|bét|bét}}",
        "ncategories": "$1 {{PLURAL:$1|kategori|kategori}}",
        "ninterwikis": "$1 {{PLURAL:$1|interwiki|interwiki}}",
        "uncategorizedcategories": "Kategori kang tanpa kategori",
        "uncategorizedimages": "Barkas kang tanpa kategori",
        "uncategorizedtemplates": "Cithakan kang durung kawènèhan kategori",
-       "unusedcategories": "Kategori kang ora kanggo",
-       "unusedimages": "Barkas kang ora kanggo",
+       "unusedcategories": "Kategori kang ora kaanggo",
+       "unusedimages": "Barkas kang ora kaanggo",
        "wantedcategories": "Kategori kang kapéngini",
        "wantedpages": "Kaca kang kapéngini",
        "wantedpages-badtitle": "Sesirah ora sah ing omboyakan kasil: $1",
index 9bd432d..85bea6f 100644 (file)
        "rcfilters-savedqueries-already-saved": "Disse filtrene er allerede lagret. Endre innstillingene dine for å opprette et nytt lagret filter.",
        "rcfilters-restore-default-filters": "Gjenopprett standardfiltre",
        "rcfilters-clear-all-filters": "Nullstill alle filtre",
-       "rcfilters-show-new-changes": "Vis nye endringer siden $1",
+       "rcfilters-show-new-changes": "Vis nye endringer etter $1",
        "rcfilters-search-placeholder": "Filtrer endringer (bruk menyen eller søk etter et filternavn)",
        "rcfilters-invalid-filter": "Ugyldig filter",
        "rcfilters-empty-filter": "Ingen aktive filtre. Alle bidrag vises.",
index b8857e6..d75881d 100644 (file)
        "revid": "versjon $1",
        "interfaceadmin-info": "$1\n\nLøyva for endring av CSS/JS/JSON-filer som gjeld heile nettstaden vart nyleg skilde ut frå <code>editinterface</code>-retten. Om du ikkje skjøner kvifor du får denne feilmeldinga, sjå [[mw:MediaWiki_1.32/interface-admin]].",
        "passwordpolicies-policy-passwordcannotmatchusername": "Passordet kan ikkje vera det same som brukarnamnet",
-       "passwordpolicies-policy-passwordcannotmatchblacklist": "Passordet kan ikkje passa med svartelista passord"
+       "passwordpolicies-policy-passwordcannotmatchblacklist": "Passordet kan ikkje passa med svartelista passord",
+       "userlogout-sessionerror": "Utlogging gjekk ikkje grunna ein øktfeil. [$1 Freist om att]."
 }
index 9ef3fe7..063bae3 100644 (file)
@@ -5,7 +5,8 @@
                        "Lancine.kounfantoh.fofana",
                        "Lanciné.kounfantoh.fofana",
                        "Youssoufkadialy",
-                       "Amire80"
+                       "Amire80",
+                       "Nafadji Mory Diané"
                ]
        },
        "sunday": "ߞߊ߯ߙߌߟߏ߲",
@@ -96,7 +97,7 @@
        "navigation-heading": "ߛߏ߲߯ߓߊߟߌ߫ ߓߏߟߏ߲ߘߊ",
        "errorpagetitle": "ߝߎ߬ߕߎ߲߬ߕߌ",
        "returnto": "ߌ ߞߐߛߊ߬ߦߌ߲߬ ߦߊ߲߬ ߡߊ߬$1",
-       "tagline": "ߞߊ߬ ߝߘߊ߫ {{SITENAMEP}}",
+       "tagline": "ߞߊ߬ ߝߘߊ߫{{SITENAMEP}}",
        "help": "ߘߍ߬ߡߍ߲߬ߠߌ",
        "help-mediawiki": "ߘߍ߬ߡߍ߲߬ߠߌ߲ ߞߊ߬ ߓߍ߲߬ ߥߞߌ-ߟߊߛߋߢߊߥߙߍ ߡߊ߬",
        "search": "ߢߌߣߌ߲ߠߌ",
        "nstab-category": "ߦߌߟߡߊ",
        "mainpage-nstab": "ߓߏ߬ߟߏ߲߬ߘߊ",
        "nosuchspecialpage": "ߘߐߜߍ߫ ߓߟߏߡߊߞߊ߬ߣߍ߲߬ ߛߎ߮ ߏ߬ ߝߋ߲߫ ߕߍ߫ ߦߊ߲߬",
+       "nospecialpagetext": "<strong>ߊߟߎ߫ ߓߘߊ߫ ߞߐߜߍ߫ ߓߟߏߡߊߞߊ߬ߣߍ߲ ߘߏ߫ ߢߌߣߌ߲߫ ߡߍ߲ ߕߺߴߦߋ߲߬.</strong>\nߞߐߜߍ߫ ߓߟߏߡߊߞߊ߬ߣߍ߲߫ ߓߘߍ߬ߡߊ ߟߎ߬ ߛߙߍߘߍ ߦߋ߫ ߢߌ߲߬ ߠߋ߫ ߞߊ߲߬ [[Special:SpecialPages|{{int:specialpages}}]].",
        "badtitle": "ߞߎ߲߬ߕߐ߰ ߖߎ߮",
        "viewsource": "ߊ߬ ߛߎ߲ ߘߐߜߍ߫",
        "viewsource-title": "ߣߌ߲߬ $1 ߛߎ߲ ߘߐߜߍ߫",
        "content-model-wikitext": "ߥߞߌ߫ ߞߟߏߜߍ",
        "viewpagelogs": "ߞߐߜߍ ߣߌ߲߬ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߠߎ߬ ߦߋ߫",
        "currentrev-asof": "$1 ߟߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߕߊ߬ߡߌ߲߬ߣߍ߲",
-       "revisionasof": "ߊ߬ ߡߊߛߊ߬ߦߌ߲ ߦߊ߲߬ ߓߊ߫ $1",
+       "revisionasof": "ߊ߬ ߡߊߛߊ߬ߦߌ߲ ߦߊ߲߬ ߓߊ߫ 1$",
        "revision-info": "{{GENDER:$6|$2}} ߟߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ $2",
        "previousrevision": "→ ߡߊ߬ߛߊ߬ߦߌ߲߬ߠߌ߲ ߞߘߐ߬ߡߊ߲",
        "nextrevision": "ߡߊ߬ߛߋ߬ߦߌ߲߬ߣߍ߲߬ ߞߎߘߊ →",
        "currentrevisionlink": "ߡߊ߬ߛߊ߬ߦߌ߲߬ߠߌ߲ ߕߊ߬ߡߌ߲߬ߣߍ߲",
        "cur": "ߞߍߞߎߘߊ",
        "last": "ߢߍߕߊ",
+       "history-fieldset-title": "ߣߐ߬ߡߊ߬ߛߊߦߌ߲ ߠߎ߬ ߛߍ߲ߛߍ߲߫",
        "histfirst": "ߞߘߐ߬ߡߊ߲ ߠߎ߬",
        "histlast": "ߞߎߘߊ ߟߎ߬",
        "history-feed-title": "ߡߊ߬ߛߊ߬ߦߌ߲߬ߠߌ߲ ߘߐ߬ߝߐ",
        "history-feed-description": "ߞߐߜߍ ߣߌ߲߬ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߘߐ߬ߝߐ߸ ߥߞߌ ߘߐ߫",
        "rev-delundel": "ߊ߬ ߦߋߢߊ ߡߊߦߟߍ߬ߡߊ߲߫",
        "history-title": "$1 ߡߛߊ߬ߦߌ߲߬ߠߌ߲ ߘߐ߬ߝߐ",
-       "lineno": "ߛߌ߬ߕߊߙߌ $1:",
+       "lineno": "$1 ߛߌ߬ߕߊߙߌ",
+       "compareselectedversions": "ߘߟߊߡߌߘߊ߫ ߛߎߥߊ߲ߘߌߣߍ߲ ߠߎ߬ ߟߊߢߐ߲߯ߡߊ߫",
        "editundo": "ߊ߬ ߘߐߛߊ߬߸ ߊ߬ ߓߟߏߞߊ߬߸ ߊ߬ ߓߙߐߕߐ߫",
        "diff-empty": "ߝߊߙߊ߲ߝߊ߯ߛߌ߫ ߕߴߊ߬ߟߎ߬ ߕߍ߫",
        "searchresults": "ߢߌߣߌ߲ߠߌ߲ ߞߐߝߟߌ ߟߎ߬",
        "searchall": "ߊ߬ ߓߍ߯",
        "search-nonefound": "ߖߋ߬ߓߟߌ߬ ߛߌ߫ ߕߍ߫ ߢߌ߬ߣߌ߲߬ߞߊ߬ߟߌ ߣߌ߲߫ ߞߊ߲߬.",
        "mypreferences": "ߟߊ߬ߝߌ߬ߛߦߊ߬ߟߌ",
+       "group-sysop": "ߡߙߊ߬ߟߌ߬ߟߊ",
        "right-writeapi": "ߛߓߍߟߌ API ߟߊߓߊ߯ߙߊ߫",
        "newuserlogpage": "ߖߊ߬ߕߋ߬ߘߊ߬ ߓߘߊ߫ ߟߊߞߊ߬ ߌ ߜߊ߲߬ߞߎ߲߬",
        "action-edit": "ߞߐߜߍ ߣߌ߲߬ ߡߊߦߟߍ߬ߡߊ߲߬",
        "recentchanges": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߫ ߞߎߘߊ",
        "recentchanges-legend": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߎߘߊ ߟߎ߫ ߟߊ߬ߓߍ߲߬ߢߐ߰ߡߦߊ߬ߘߊ",
        "recentchanges-summary": "ߥߞߌ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߎ߲ߓߊ ߡߍ߲ ߠߎ߬ ߞߍߣߍ߲߫ ߞߐߜߍ ߣߌ߲߬ ߞߊ߲߬߸ ߏ߬ ߟߎ߫ ߣߐ߬ߣߐ߬.",
+       "recentchanges-noresult": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߬ ߛߌ߫ ߓߍ߲߬ߢߐ߲߰ߦߊ߬ߣߍ߲߬ ߕߍ߫ ߛߎߡߊ߲ߡߕߊ ߢߌ߲߬ ߠߎ߫ ߡߊ߬ ߕߎ߬ߡߊ߬ ߟߊߕߍ߰ߣߍ߲ ߦߌ߬ߘߊ ߘߐ߫.",
        "recentchanges-label-newpage": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߣߌ߲߬ ߓߘߊ߫ ߘߐߜߍ߫ ߞߎߘߊ ߟߊߘߊ߲߫",
        "recentchanges-label-minor": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߘߋ߬ߣߍ߲ ߠߋ߫ ߦߋ߫",
        "recentchanges-label-bot": "ߡߐ߰ߡߐ߮ ߟߋ߫ ߣߐ߬ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ ߣߌ߲߬ ߞߍ߫ ߟߊ߫",
        "randompage": "ߓߍ߲߬ߛߋ߲߬ߡߊ߬ ߞߐߜߍ",
        "statistics": "ߖߊ߬ߕߋ߬ߛߎ߬ߓߐ ߟߎ߬",
        "nbytes": "$1 {{PLURAL:$1|byte|bytes}}",
+       "nmembers": "$1 {{PLURAL:$1|ߛߌ߲߬ߝߏ߲ |members}}",
        "prefixindex": "ߞߐߜߍ߫ ߡߍ߲ ߠߎ߬ ߓߍ߯ ߟߊߝߟߐߣߍ߲߫...",
        "listusers": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߛߙߍߘߍ",
        "newpages": "ߘߐߜߍ߫ ߞߎߘߊ",
        "booksources-search": "ߢߌߣߌ߲ߠߌ߲",
        "specialloguserlabel": "ߞߍߓߊ߮ :",
        "log": "ߘߏ߲߬",
+       "logempty": "ߦߙߍߞߍߟߌ߫ ߛߌ߫ ߓߍ߲߬ߢߐ߲߰ߦߊ߬ߣߍ߲߬ ߕߍ߫ ߝߐ߰ߓߍ ߟߎ߬ ߘߐ߫",
        "allpages": "ߞߐߜߍ ߟߎ߬ ߓߍ߯",
        "allarticles": "ߞߐߜߍ ߟߎ߬ ߓߍ߯",
        "allpagessubmit": "ߥߊ߫",
        "tooltip-t-recentchangeslinked": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߬ ߞߎߘߊ ߟߎ߬ ߞߐߜߍ߫ ߘߐ߫ ߡߍ߲ ߣߌ߫ ߞߐߜߍ ߣߌ߲߬ ߕߎ߲߰ߣߍ߲߫",
        "tooltip-feed-atom": "ߞߐߜߍ ߣߌ߲߬ ߝߕߌ߫ ߓߊߟߏ",
        "tooltip-t-contributions": "{{GENDER:$1|ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ}} ߟߊ߫ ߓߟߏߓߌߟߊߢߐ߲߮ߞߊ߲ ߛߙߍߘߍ",
+       "tooltip-t-emailuser": " ߢߎߡߍߙߋ߲ߞߏ߲ߘߏ ߟߊߕߊ߯ ߟߊߓߊ߯ߙߟߊ ߣߌ߲߬ ߡߊ߬{{GENDER:$1|ߟߊߓߊ߯ߙߟߊ(ߡߏ߬ߛߏ) }}",
        "tooltip-t-upload": "ߞߐߕߐ߮ ߟߎ߫ ߟߊߦߟߍ߬",
        "tooltip-t-specialpages": "ߘߎ߲߬ߘߎ߬ߡߊ߬ ߞߐߜߍ߫ ߟߎ߫ ߛߙߍߘߍ",
        "tooltip-t-print": " ߞߐߜߍ ߣߌ߲߬  ߜߌ߬ߙߌ߲߬ߘߌ߬ߕߊ߬ߡߊ ߛߎ߮",
index 7148e91..712ce22 100644 (file)
        "revdelete-text-file": "ړنگې شوې بڼې به لا تر اوسه پورې د مخ پېښليک کې ښکاري، خو د هغو ځينو برخو ته به عام خلک لاسرسی و نه لري.",
        "logdelete-text": "ړنگې شوې بڼې به لا تر اوسه پورې د مخ پېښليک کې ښکاري، خو د هغو ځينو برخو ته به عام خلک لاسرسی و نه لري.",
        "revdelete-text-others": "نور پازوالان به لا هم د پټ راز محتوياتو ته لاسرسی ومومي او دا یې له منځه یوسي، مګر که نه بل ډول مشخص شوی.",
-       "revdelete-confirm": "Ù\84Ø·Ù\81ا Ø¯Ø§ ØªØ§Û\8cÛ\8cد Ú©Ú\93ئ Ú\86Û\90 ØªØ§Ø³Ù\88 Ø¯Ø§ Ú©Ø§Ø± Ú©Ù\88Ù\84 ØºÙ\88اÚ\93ئØ\8c Ø¯Ø§ Ú\86Û\90 ØªØ§Ø³Ù\88 Ù¾Ø§Û\8cÙ\84Û\90 Ù¾Ù\87 Ù¾Ø§Ù\85 Ú©Û\90 Ù\84رئ Ø§Ù\88 ØªØ§Ø³Ù\88 Û\8cÛ\90 Ø³Ø±Ù\87 Ù\85طابÙ\82ت Ú©Ù\88ئ[[{{MediaWiki:Policy-url}}|پاÙ\84Û\8cسÛ\8d]].",
+       "revdelete-confirm": "Ù\84Ø·Ù\81ا Ø¯Ø§ ØªØ§Û\8cÛ\8cد Ú©Ú\93ئ Ú\86Û\90 ØªØ§Ø³Ù\88 Ø¯Ø§ Ú©Ø§Ø± Ú©Ù\88Ù\84 ØºÙ\88اÚ\93ئØ\8c ØªØ§Ø³Ù\88 Ù¾Ø§Û\8cÙ\84Û\90 Ù¾Ù\87 Ù¾Ø§Ù\85 Ú©Û\90 Ù\84رئ Ø§Ù\88 [[{{MediaWiki:Policy-url}}|پاÙ\84Û\8cسÛ\8d]] ØªÙ\87 Ù\85Ù\88 Ù\87Ù\85 Ù\81کر Ø¯Û\8c.",
        "revdelete-legend": "د ښکارېدنې محدوديتونه ټاکل",
        "revdelete-hide-text": "د مخکتنې متن",
        "revdelete-hide-image": "د دوتنې مېنځپانگه پټول",
index 8a776dc..9dfa861 100644 (file)
        "blocklink": "Display name for a link that, when selected, leads to a form where a user can be blocked. Used in page history and recent changes pages. Example: \"''UserName (Talk | contribs | '''block''')''\".\n\nUsed as link title in [[Special:Contributions]] and in [[Special:DeletedContributions]].\n\nSee also:\n* {{msg-mw|Sp-contributions-talk}}\n* {{msg-mw|Change-blocklink}}\n* {{msg-mw|Unblocklink}}\n* {{msg-mw|Sp-contributions-blocklog}}\n* {{msg-mw|Sp-contributions-uploads}}\n* {{msg-mw|Sp-contributions-logs}}\n* {{msg-mw|Sp-contributions-deleted}}\n* {{msg-mw|Sp-contributions-userrights}}\n{{Identical|Block}}",
        "unblocklink": "Used as link title in [[Special:Contributions]] and in [[Special:DeletedContributions]].\n\nSee also:\n* {{msg-mw|Sp-contributions-talk}}\n* {{msg-mw|change-blocklink}}\n* {{msg-mw|blocklink}}\n* {{msg-mw|sp-contributions-blocklog}}\n* {{msg-mw|sp-contributions-uploads}}\n* {{msg-mw|sp-contributions-logs}}\n* {{msg-mw|sp-contributions-deleted}}\n* {{msg-mw|sp-contributions-userrights}}\n{{Identical|Unblock}}",
        "change-blocklink": "Used to name the link on [[Special:Log]].\n\nAlso used as link title in [[Special:Contributions]] and in [[Special:DeletedContributions]].\n\nSee also:\n* {{msg-mw|Sp-contributions-talk}}\n* {{msg-mw|unblocklink}}\n* {{msg-mw|blocklink}}\n* {{msg-mw|sp-contributions-blocklog}}\n* {{msg-mw|sp-contributions-uploads}}\n* {{msg-mw|sp-contributions-logs}}\n* {{msg-mw|sp-contributions-deleted}}\n* {{msg-mw|sp-contributions-userrights}}",
+       "empty-username": "If no username is available to display for a log or history entry, such as because of an incorrect database entry, this message is displayed in place of the links to the user page/talk/contribs/etc.",
        "contribslink": "Short for \"contributions\". Used as display name for a link to user contributions on history pages, [[Special:RecentChanges]], [[Special:Watchlist]], etc.\n{{Identical|Contribution}}",
        "emaillink": "Used as display name for a link to send an e-mail to a user in the user tool links. Example: \"(Talk | contribs | block | send e-mail)\".\n\n{{Identical|E-mail}}",
        "autoblocker": "Used in [[Special:Block]].\n* $1 - target username\n* $2 - reason",
index 0ad8cb8..2fc69e2 100644 (file)
@@ -64,6 +64,7 @@
        "tog-norollbackdiff": "Төннөрүү кэнниттэн барыллар уратыларын көрдөрүмэ",
        "tog-useeditwarning": "Уларытыыларбын бигэргэппэккэ сирэйтэн тахсаары гыннахпына сэрэтээр",
        "tog-prefershttps": "Манна киирэргэ куруук көмүскэллээх холбонууну туттарга",
+       "tog-showrollbackconfirmation": "Сигэни баттаатахха дьайыыга бигэргэтиини көрдөр",
        "underline-always": "Куруук",
        "underline-never": "Аннынан тардыма",
        "underline-default": "Браузер туруоруутунан",
        "badretype": "Аһарыктарыҥ сөп түбэспэтилэр.",
        "usernameinprogress": "Бу аатынан бэлиэтэнии бара турар.\nБука диэн кэтэһэ түс.",
        "userexists": "Суруйбут аатыҥ бэлиэр баар.\nБука диэн, атын аатта тал.",
+       "createacct-normalization": "Эн бэлиэтэммит аатыҥ техника хааччаҕын учуоттаан маннык буолуо «$2».",
        "loginerror": "Ааккын система билбэтэ",
        "createacct-error": "Бэлиэтэнии кэмигэр алҕас таҕыста",
        "createaccounterror": "Саҥа аат бэлиэтиир кыах суох: $1",
        "resetpass-abort-generic": "Аһарыгы уларытыыны кэҥэтии тохтотто.",
        "resetpass-expired": "Аһарыгыҥ болдьоҕо ааспыт эбит. Бука диэн, саҥа аһарыкта туруорун.",
        "resetpass-expired-soft": "Аһарыгыҥ болдьоҕо бүппүт, онон уларытыллыахтаах эбит. Бука диэн атын аһарыкта суруй эбэтэр маны баттаан кэлин киллэрээр \"{{int:authprovider-resetpass-skip-label}}\".",
-       "resetpass-validity-soft": "Аһарыгыҥ алҕастаах: $1\n\nБука диэн саҥа аһарыкта суруй эбэтэр кэлин киллэриэххин баҕарар буоллаххына маны баттаа \"{{int:authprovider-resetpass-skip-label}}\"",
+       "resetpass-validity": "Аһарыгыҥ алҕастаах: $1\n\nСаҥа аһарыкта туруорун дуу.",
+       "resetpass-validity-soft": "Аһарыгыҥ алҕастаах: $1\n\nБука диэн саҥа аһарыкта суруй эбэтэр кэлин суруйуоххун баҕарар буоллаххына маны баттаа \n\"{{int:authprovider-resetpass-skip-label}}\"",
        "passwordreset": "Аһарыгы саҥаттан",
        "passwordreset-text-one": "Урукку аһарыгы уларытарга бу форманы толор.",
        "passwordreset-text-many": "{{PLURAL:$1|Быстах аһарыгы электрон почтаҕар ыыттарарга түннүктэртэн биирдэстэригэр суруй.}}",
        "diff-paragraph-moved-toold": "Параграф көһөрүллүбүт. Баттаан урукку сиригэр көс.",
        "difference-missing-revision": "$2 барыл бу тэҥнээһиҥҥэ ($1) көстүбэтэ.\n\nБу үксүн хайыы-үйэ сотуллубут сирэйи кытта тэҥнээри эргэрбит сигэнэн кэллэххэ баар буолааччы.\nСиһилии баҕар [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} сотуу сурунаалыгар] баара буолуо.",
        "searchresults": "Булулунна",
+       "search-filter-title-prefix": "Мантан саҕаланар «$1» сирэйдэри эрэ көрдөө",
        "search-filter-title-prefix-reset": "Сирэйдэри барытын көрдөөһүн",
        "searchresults-title": "Көрдөөһүн түмүгэ \"$1\"",
        "titlematches": "Ыстатыйалар ааттара хоһулаһар",
        "stub-threshold-disabled": "Арахсыбыт",
        "recentchangesdays": "Хас хонук иһинэн уларытыылары көрдөрөргө:",
        "recentchangesdays-max": "(улааппыта $1 күн)",
-       "recentchangescount": "Саҥа Ñ\83лаÑ\80Ñ\8bÑ\82Ñ\8bÑ\8bлаÑ\80 ÐºÓ©Ñ\80дөÑ\80үллÑ\8dр ахсааннара:",
-       "prefs-help-recentchangescount": "Ð\91Ñ\83 Ñ\81аҥа ÐºÓ©Ð½Ð½Ó©Ñ\80үүлÑ\8dÑ\80и, Ñ\81иÑ\80Ñ\8dй Ñ\83Ñ\81Ñ\82Ñ\83оÑ\80Ñ\83йалаÑ\80Ñ\8bн Ñ\83онна Ñ\81Ñ\83Ñ\80Ñ\83нааллаÑ\80Ñ\8b ÐºÓ©Ñ\80дөÑ\80Ó©Ñ\80.",
+       "recentchangescount": "Саҥа Ñ\83лаÑ\80Ñ\8bÑ\82Ñ\8bÑ\8bлаÑ\80 Ð¸Ñ\81пииһÑ\8dкÑ\82Ñ\8dÑ\80игÑ\8dÑ\80, Ñ\81иÑ\80Ñ\8dй Ñ\83Ñ\81Ñ\82Ñ\83оÑ\80Ñ\83йаÑ\82Ñ\8bгаÑ\80 Ñ\83онна Ñ\81Ñ\83Ñ\80Ñ\83нааллаÑ\80га ÐºÓ©Ñ\80дөÑ\80үллÑ\8dÑ\80 Ñ\83лаÑ\80Ñ\8bÑ\82Ñ\8bÑ\8bлар ахсааннара:",
+       "prefs-help-recentchangescount": "УлааппÑ\8bÑ\82а: 1000",
        "prefs-help-watchlist-token2": "Бу кэтиир испииһэгиҥ ситим-ханаалын кистэлэҥ күлүүһэ.\nБу күлүүһүнэн ким баҕарар эн испииһэккин көрүөн сөп, онон кимиэхэ да биэримэ. Хаһан баҕарар [[Special:ResetTokens|маны баттаан уларытыаххын]] сөп.",
        "savedprefs": "Эн туруорууларыҥ олохтоннулар.",
        "savedrights": "{{GENDER:$1|$1}} кыттааччы бөлөҕө бигэргэннэ.",
        "default": "чопчу ыйыллыбатаҕына маннык",
        "prefs-files": "Билэлэр",
        "prefs-custom-css": "Бэйэ CSS",
+       "prefs-custom-json": "Тус бэйэ JSON-а",
        "prefs-custom-js": "Бэйэ JS",
        "prefs-common-config": "Бары тиэмэлэргэ биир CSS/JS",
        "prefs-reset-intro": "Бу сирэй көмөтүнэн туруорууларгын саҥаттан туруорар турукка төннөрүөххүн сөп.\nМаны бигэргэттэххинэ билигин баар туруоруулары дэбигис сөргүппэккин.",
        "prefs-displaywatchlist": "Көстүүтүн туруоруулара",
        "prefs-changesrc": "Көстүбүт уларытыылар",
        "prefs-changeswatchlist": "Көрдөр;ллэр уларытыылар",
+       "prefs-pageswatchlist": "Кэтэбилгэ сылдьар сирэйдэр",
        "prefs-tokenwatchlist": "Токен",
        "prefs-diffs": "Уратылара",
        "prefs-help-prefershttps": "Аныгыскы киириигэр үлэлиир буолуо.",
        "group-autoconfirmed": "Аптамаатынан бигэргэтиллибит кыттааччылар",
        "group-bot": "Роботтар",
        "group-sysop": "Дьаһабыллар",
+       "group-interface-admin": "Алтыһаан дьаһабыллара",
        "group-bureaucrat": "Бюрокрааттар",
        "group-suppress": "Ревизордар",
        "group-all": "(бары)",
        "group-autoconfirmed-member": "{{GENDER:$1|аптамаатынан бигэргэтиллибит кыттааччы}}",
        "group-bot-member": "{{GENDER:$1|робот}}",
        "group-sysop-member": "{{GENDER:$1|дьаһабыл}}",
+       "group-interface-admin-member": "{{GENDER:$1|алтыһаан дьаһабыла}}",
        "group-bureaucrat-member": "{{GENDER:$1|бүрэкирээт}}",
        "group-suppress-member": "{{GENDER:$1|ревизор}}",
        "grouppage-user": "{{ns:project}}:Кыттааччылар",
        "grouppage-autoconfirmed": "{{ns:project}}:Аптамаатынан бигэргэммит кыттааччылар",
        "grouppage-bot": "{{ns:project}}:Роботтар",
        "grouppage-sysop": "{{ns:project}}:Дьаһабыллар",
+       "grouppage-interface-admin": "{{ns:project}}:Алтыһаан дьаһабыллара",
        "grouppage-bureaucrat": "{{ns:project}}:Бюрокрааттар",
        "grouppage-suppress": "{{ns:project}}:Ревизордар",
        "right-read": "Сирэйдэри көрүү",
-       "right-edit": "СиÑ\80Ñ\8dйдÑ\8dÑ\80и Ñ\83ларытыы",
+       "right-edit": "Уларытыы",
        "right-createpage": "Сирэйдэри оҥоруу (ырытыы сирэйдэриттэн ураты)",
        "right-createtalk": "Ырытыы сирэйдэрин оҥоруу",
        "right-createaccount": "Саҥа кыттааччыны бэлиэтээһин",
        "right-reupload-own": "Билэлэри суруттарбыт киһи бэйэтэ иккистээн суруттарыыта",
        "right-reupload-shared": "Уопсай ыскылаат билэлэрин локальнай ыскылаат билэлэринэн уларытыы",
        "right-upload_by_url": "URL аадырыстан билэлэри киллэрии",
-       "right-purge": "Ð\9aÑ\8dÑ\8dһи Ð±Ð¸Ð³Ñ\8dÑ\80гÑ\8dÑ\82Ñ\8dÑ\80 Ñ\81иÑ\80Ñ\8dйÑ\8d Ñ\81Ñ\83оÑ\85 ыраастааһын",
+       "right-purge": "СиÑ\80Ñ\8dй ÐºÑ\8dÑ\8dһин ыраастааһын",
        "right-autoconfirmed": "IP түргэнигэр олоҕурбут хааччахтан тутулуктаныма",
        "right-bot": "аптамаат быһыытынан ааҕыллар",
        "right-nominornewtalk": "Ырытыы сирэйдэригэр кыра көннөрүүлэр суох буоллахтарына саҥа этии эрэсиимэ холбонор",
        "right-editusercss": "Атын кыттааччылар CSS-билэлэрин уларытыы",
        "right-edituserjson": "Атын кыттааччылар JSON-билэлэрин уларытыы",
        "right-edituserjs": "Атын кыттааччылар JS-билэлэрин уларытыы",
+       "right-editsitecss": "CSS-билэлэри уларытыы",
+       "right-editsitejson": "JSON-билэлэри уларытыы",
+       "right-editsitejs": "JavaScript-билэлэри уларытыы",
        "right-editmyusercss": "Кыттааччы CSS-билэтин уларытыы",
+       "right-editmyuserjson": "Тус  бэйэ JSON-билэлэрин уларытыы",
        "right-editmyuserjs": "Бэйэ JavaScript-билэлэрин уларытыы",
        "right-viewmywatchlist": "Бэйэ кэтиир тиһигин көрүү",
        "right-editmywatchlist": "Бэйэ кэтиир тиһигин уларытыы. Болҕой, сорох дьайыыларыҥ бу быраабы биэрбэтэҕиҥ да иһин сирэйдэри тиһиккэ эбиэхтэрин сөп.",
        "action-changetags": "ханнык баҕарар тиэктэри сурунаал биирдиилээн уларытыыларыгар уонна суруктарыгар эбэри уонна сотору көҥүллээ",
        "action-deletechangetags": "тиэктэри билии олоҕуттан сотуу",
        "action-purge": "сирэй кээһин ыраастааһын",
+       "action-bigdelete": "уһун устуоруйалаах сирэйдэри сотуу",
+       "action-blockemail": "эл. суругу ыытары бобуу",
+       "action-bot": "аптамаат быһыытынан ааҕыллар",
+       "action-editinterface": "кыттааччы алтыһаанын уларытыы",
+       "action-editusercss": "атын кыттааччылар CSS-билэлэрин уларытыы",
+       "action-edituserjson": "атын кыттааччылар JSON-билэлэрин уларытыы",
+       "action-edituserjs": "Атын кыттааччылар JavaScript-билэлэрин уларытыы",
+       "action-editsitecss": "ситим-сир CSS-билэлэрин уларытыы",
        "nchanges": "$1 {{PLURAL:$1|уларытыы|уларытыылар}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|тиһэх сылдьыыгыттан}}",
        "enhancedrc-history": "устуоруйата",
index 5b4b6f2..c262c70 100644 (file)
        "yourpasswordagain": "Поново унеси лозинку:",
        "createacct-yourpasswordagain": "Потврдите лозинку",
        "createacct-yourpasswordagain-ph": "Поново унесите лозинку",
-       "userlogin-remembermypassword": "Ð\9eÑ\81Ñ\82ави Ð¼Ðµ Ð¿Ñ\80иÑ\98авÑ\99еног/Ñ\83",
+       "userlogin-remembermypassword": "Ð\9dе Ð¾Ð´Ñ\98авÑ\99Ñ\83Ñ\98 Ð¼Ðµ",
        "userlogin-signwithsecure": "Користите безбедну везу",
        "cannotlogin-title": "Пријава није могућа",
        "cannotlogin-text": "Пријава није могућа",
        "copyrightwarning": "Имајте на уму да се сви доприноси на овом викију сматрају као објављени под лиценцом $2 (више на $1).\nАко не желите да се ваши текстови мењају и размењују без ограничења, онда их не шаљите овде.<br />\nИсто тако обећавате да сте Ви аутор текста, или да сте га умножили са извора који је у јавном власништву.\n<strong>Не шаљите радове заштићене ауторским правима без дозволе!</strong>",
        "copyrightwarning2": "Имајте на уму да се сви доприноси на овом викију могу мењати, враћати или брисати од других корисника.\nАко не желите да се ваши текстови слободно мењају и расподељују, не шаљите их овде.<br />\nИсто тако обећавате да сте ви аутор текста, или да сте га умножили с извора који је у јавном власништву (више на $1).\n<strong>Не шаљите радове заштићене ауторским правима без дозволе!</strong>",
        "editpage-cannot-use-custom-model": "Модел садржаја ове странице се не може променити.",
-       "longpageerror": "<strong>Грешка: текст који сте унели је величине {{PLURAL:$1|један килобајт|$1 килобајта}}, што је веће од {{PLURAL:$2|дозвољеног једног килобајта|дозвољена $2 килобајта|дозвољених $2 килобајта}}.</strong>\nСтраница не може бити сачувана.",
+       "longpageerror": "<strong>Грешка: текст који сте проследили је величине {{PLURAL:$1|један килобајт|$1 килобајта}}, што је веће од {{PLURAL:$2|дозвољеног једног килобајта|дозвољена $2 килобајта|дозвољених $2 килобајта}}.</strong>\nСтраница не може бити сачувана.",
        "readonlywarning": "<strong>Упозорење: база података је закључана ради одржавања, тако да тренутно нећете моћи да сачувате измене.</strong>\nМожда бисте желели сачувати текст за касније у некој текстуалној датотеци.\n\nСистемски администратор је навео следеће објашњење: $1",
        "protectedpagewarning": "<strong>Упозорење: Ова страница је заштићена, тако да само корисници са администраторским овлашћењима могу да је уређују.</strong>\nНајновији унос у дневнику је наведен испод као референца:",
        "semiprotectedpagewarning": "<strong>Напомена:</strong> Ова страница је заштићена, тако да само аутоматски потврђени корисници могу да је уређују.\nНајновији унос у дневнику је наведен испод као референца:",
        "upload": "Отпремање датотеке",
        "uploadbtn": "Отпреми датотеку",
        "reuploaddesc": "Назад на образац за отпремање",
-       "upload-tryagain": "Пошаљи измењени опис датотеке",
+       "upload-tryagain": "Проследи измењени опис датотеке",
        "upload-tryagain-nostash": "Пошаљите ре-отпремљену датотеку и измењен опис",
        "uploadnologin": "Нисте пријављени",
        "uploadnologintext": "$1 да бисте отпремали датотеке.",
        "filetype-unwanted-type": "<strong>„.$1“</strong> је непожељан тип датотеке.\n{{PLURAL:$3|Пожељан тип датотеке је|Пожељни типови датотека су}} $2.",
        "filetype-banned-type": "<strong>„.$1“</strong> {{PLURAL:$4|није допуштен тип датотеке|нису допуштени типови датотека}}.\n{{PLURAL:$3|Дозвољен тип датотеке је|Дозвољени типови датотека су}} $2.",
        "filetype-missing": "Ова датотека нема проширење (нпр. „.jpg“).",
-       "empty-file": "Ð\9fоÑ\81лаÑ\82а Ð´Ð°Ñ\82оÑ\82ека је празна.",
+       "empty-file": "Ð\94аÑ\82оÑ\82ека ÐºÐ¾Ñ\98Ñ\83 Ñ\81Ñ\82е Ð¿Ñ\80оÑ\81ледили је празна.",
        "file-too-large": "Послата датотека је превелика.",
        "filename-tooshort": "Назив датотеке је прекратак.",
        "filetype-banned": "Овај тип датотеке је забрањен.",
        "emailnotarget": "Непостојеће или наважеће корисничко име примаоца.",
        "emailtarget": "Унос корисничког имена примаоца",
        "emailusername": "Корисничко име:",
-       "emailusernamesubmit": "Пошаљи",
+       "emailusernamesubmit": "Проследи",
        "email-legend": "Слање е-поруке кориснику/ци пројекта {{SITENAME}}",
        "emailfrom": "Од:",
        "emailto": "За:",
        "deleting-subpages-warning": "<strong>Упозорење:</strong> Страница коју желите избрисати има [[Special:PrefixIndex/{{FULLPAGENAME}}/|{{PLURAL:$1|подстраницу|$1 подстранице|$1 подстраница|51=преко 50 подстраница}}]].",
        "rollback": "Врати измене",
        "rollback-confirmation-confirm": "Потврдите:",
+       "rollback-confirmation-yes": "Врати",
+       "rollback-confirmation-no": "Откажи",
        "rollbacklink": "врати",
        "rollbacklinkcount": "врати $1 {{PLURAL:$1|измену|измене|измена}}",
        "rollbacklinkcount-morethan": "врати више од $1 {{PLURAL:$1|измене|измене|измена}}",
        "mycontris": "Доприноси",
        "anoncontribs": "Доприноси",
        "contribsub2": "За {{GENDER:$3|$1}} ($2)",
+       "contributions-subtitle": "За {{GENDER:$3|$1}}",
        "contributions-userdoesnotexist": "Кориснички налог „$1“ није отворен.",
+       "negative-namespace-not-supported": "Именски простори са негативним вредностима нису подржани.",
        "nocontribs": "Нису пронађене промене које одговарају овим критеријумима.",
        "uctop": "тренутна",
        "month": "од месеца (и раније):",
        "blocklist-userblocks": "Сакриј блокаде налога",
        "blocklist-tempblocks": "Сакриј привремене блокаде",
        "blocklist-addressblocks": "Сакриј појединачне блокаде IP-а",
+       "blocklist-type": "Тип:",
+       "blocklist-type-opt-all": "Све",
        "blocklist-type-opt-sitewide": "На нивоу сајта",
        "blocklist-type-opt-partial": "Делимично",
        "blocklist-rangeblocks": "Сакриј блокаде опсега",
        "watchlistedit-normal-done": "{{PLURAL:$1|1=Једна страница је уклоњена|$1 странице су уклоњене|$1 страница је уклоњено}} с вашег списка надгледања:",
        "watchlistedit-raw-title": "Уређивање необрађеног списка надгледања",
        "watchlistedit-raw-legend": "Уређивање необрађеног списка надгледања",
-       "watchlistedit-raw-explain": "Ð\9dаÑ\81лови Ñ\81а Ñ\81пиÑ\81ка Ð½Ð°Ð´Ð³Ð»ÐµÐ´Ð°Ñ\9aа Ñ\81Ñ\83 Ð¿Ñ\80иказани Ð¸Ñ\81под Ð¸ Ð¼Ð¾Ð³Ñ\83 Ñ\81е Ñ\83Ñ\80еÑ\92иваÑ\82и Ð´Ð¾Ð´Ð°Ð²Ð°Ñ\9aем Ð¸Ð»Ð¸ Ñ\83клаÑ\9aаÑ\9aем Ñ\81Ñ\82авки Ñ\81а Ñ\81пиÑ\81ка;\nÑ\98едан Ð½Ð°Ñ\81лов Ð¿Ð¾ Ñ\80едÑ\83.\nÐ\9aада Ð·Ð°Ð²Ñ\80Ñ\88иÑ\82е, ÐºÐ»Ð¸ÐºÐ½Ð¸Ñ\82е Ð½Ð° â\80\9e{{int:Watchlistedit-raw-submit}}â\80\9c.\nÐ\9cожеÑ\82е Ð´Ð° [[Special:EditWatchlist|коÑ\80иÑ\81Ñ\82иÑ\82е Ð¸ Ð¾Ð±Ð¸Ñ\87ан уређивач]].",
+       "watchlistedit-raw-explain": "Ð\9dаÑ\81лови Ñ\81а Ñ\81пиÑ\81ка Ð½Ð°Ð´Ð³Ð»ÐµÐ´Ð°Ñ\9aа Ñ\81Ñ\83 Ð¿Ñ\80иказани Ð¸Ñ\81под Ð¸ Ð¼Ð¾Ð³Ñ\83 Ñ\81е Ñ\83Ñ\80еÑ\92иваÑ\82и Ð´Ð¾Ð´Ð°Ð²Ð°Ñ\9aем Ð¸Ð»Ð¸ Ñ\83клаÑ\9aаÑ\9aем Ñ\81Ñ\82авки Ñ\81а Ñ\81пиÑ\81ка;\nÑ\98едан Ð½Ð°Ñ\81лов Ð¿Ð¾ Ñ\80едÑ\83.\nÐ\9aада Ð·Ð°Ð²Ñ\80Ñ\88иÑ\82е, ÐºÐ»Ð¸ÐºÐ½Ð¸Ñ\82е Ð½Ð° â\80\9e{{int:Watchlistedit-raw-submit}}â\80\9d.\nÐ\9cожеÑ\82е Ð´Ð° [[Special:EditWatchlist|коÑ\80иÑ\81Ñ\82иÑ\82е Ð¸ Ñ\81Ñ\82андаÑ\80дни уређивач]].",
        "watchlistedit-raw-titles": "Наслови:",
        "watchlistedit-raw-submit": "Ажурирај списак",
        "watchlistedit-raw-done": "Ваш списак надгледања је ажуриран.",
        "htmlform-int-toolow": "Наведена вредност је испод минимума од $1",
        "htmlform-int-toohigh": "Наведена вредност је изнад максимума од $1",
        "htmlform-required": "Ова вредност је обавезна.",
-       "htmlform-submit": "Постави",
+       "htmlform-submit": "Проследи",
        "htmlform-reset": "Врати промене",
        "htmlform-selectorother-other": "Друго",
        "htmlform-no": "Не",
index 7373150..ff452ee 100644 (file)
        "page_first": "ilk",
        "page_last": "son",
        "histlegend": "Fark seçimi: Karşılaştırmayı istediğiniz 2 sürümün önündeki daireleri işaretleyip, \"{{int:Compareselectedversions}}\" düğmesine basın.<br />\nTanımlar: '''({{int:cur}})''' = son revizyon ile arasındaki fark, '''({{int:last}})''' = bir önceki revizyon ile arasındaki fark, '''{{int:minoreditletter}}''' = küçük değişiklik.",
-       "history-fieldset-title": "Geçmişe gözat",
+       "history-fieldset-title": "Revizyonları filtrele",
        "history-show-deleted": "Sadece silinen sürümler",
        "histfirst": "en eski",
        "histlast": "en yeni",
        "historysize": "({{PLURAL:$1|1 bayt|$1 bayt}})",
-       "historyempty": "(boş)",
+       "historyempty": "boş",
        "history-feed-title": "Değişiklik geçmişi",
        "history-feed-description": "Viki üzerindeki bu sayfanın değişiklik geçmişi.",
        "history-feed-item-nocomment": "$1, $2'de",
        "right-reupload-own": "Kendisinin yüklediği bir dosyanın üzerine yaz",
        "right-reupload-shared": "Paylaşılan ortam deposundaki dosyaları yerel olarak geçersiz kıl",
        "right-upload_by_url": "Bir URL adresinden dosya yükle",
-       "right-purge": "Doğrulama yapmadan bir sayfa için site belleğini temizle",
+       "right-purge": "Bir sayfa için site önbelleğini temizle",
        "right-autoconfirmed": "IP-tabanlı hız limitleri etkilenme",
        "right-bot": "Otomatik bir işlem gibi muamele gör",
        "right-nominornewtalk": "Kullanıcı tartışma sayfalarında yaptığı küçük değişiklikler kullanıcıya yeni mesaj bildirimiyle bildirilmez",
        "grant-delete": "Sayfaları, sürümleri ve günlük girdileri sil",
        "grant-editinterface": "MediaWiki alanadını, sitewide'ı ve kullanıcı JSON'unu düzenle",
        "grant-editmycssjs": "Kullanıcı CSS/JSON/JavaScript'ini düzenle",
-       "grant-editmyoptions": "Kullanıcı tercihlerini Düzenle",
+       "grant-editmyoptions": "Kullanıcı tercihlerinizi ve JSON yapılandırmanızı düzenleyin",
        "grant-editmywatchlist": "İzleme listeni düzenle",
        "grant-editsiteconfig": "Sitewide ve kullanıcı CSS/JS değiştir",
        "grant-editpage": "Mevcut sayfaları düzenle",
        "rcfilters-savedqueries-already-saved": "Bu filtreler zaten kaydedildi. Yeni bir Kayıtlı Filtre oluşturmak için ayarlarınızı değiştirin.",
        "rcfilters-restore-default-filters": "Varsayılan süzgeçleri geri getir",
        "rcfilters-clear-all-filters": "Tüm süzgeçleri temizle",
-       "rcfilters-show-new-changes": "Yeni değişiklikleri görüntüle",
+       "rcfilters-show-new-changes": "$1 tarihinden bu yana yapılan yeni değişiklikleri görüntüleyin",
        "rcfilters-search-placeholder": "Son değişiklikleri filtrele (menüyü kullanın veya süzgeç adını arayın)",
        "rcfilters-invalid-filter": "Geçersiz süzgeç",
        "rcfilters-empty-filter": "Etkin süzgeç bulunmuyor. Tüm katkıları gösteriliyor.",
        "rcfilters-watchlist-markseen-button": "Tüm değişiklikleri görüldü olarak işaretle",
        "rcfilters-watchlist-edit-watchlist-button": "İzlenen sayfaların listesini düzenle",
        "rcfilters-watchlist-showupdated": "Gerçekleştirilen değişikliklerden bu yana ziyaret etmediğiniz sayfalarda yapılan değişiklikler <strong>koyu</strong> renktedir.",
-       "rcfilters-preference-label": "Son değişikliklerin geliştirilmiş sürümünü gizle",
-       "rcfilters-preference-help": "2017 arayüz tasarımını ve bu andan sonra eklenen tüm araçları geri alır.",
-       "rcfilters-watchlist-preference-label": "İzleme listesinin geliştirilmiş sürümünü gizle",
-       "rcfilters-watchlist-preference-help": "2017 arayüz tasarımını ve bu andan sonra eklenen tüm araçları geri alır.",
+       "rcfilters-preference-label": "JavaScript olmayan bir arayüz kullanın",
+       "rcfilters-preference-help": "Filtre olmadan arama yapma veya işlevselliği vurgulamadan SonDeğişiklikler'i yükler.",
+       "rcfilters-watchlist-preference-label": "JavaScript olmayan bir arayüz kullanın",
+       "rcfilters-watchlist-preference-help": "Filtre Listesini arama olmadan veya işlevselliği vurgulayarak İzleme Listesi'ni yükler.",
        "rcfilters-target-page-placeholder": "Bir sayfa (ya da kategori) adı girin",
        "rcnotefrom": "<strong>$3, $4</strong> tarihinden itibaren yapılan {{PLURAL:$5|değişiklik|değişiklik}} aşağıdadır (<strong>$1</strong> tarhine kadar olanlar gösterilmektedir).",
        "rclistfromreset": "Tarih seçimini sıfırla",
        "apisandbox-loading-results": "API sonuçları alınıyor...",
        "apisandbox-results-error": "API sorgusu yanıtı yüklenirken bir hata oluştu: $1.",
        "apisandbox-request-url-label": "İstek URL:",
-       "apisandbox-request-time": "İstek zamanı: $1",
+       "apisandbox-request-time": "İstek zamanı: {{PLURAL:$1|$1 ms}}",
        "apisandbox-continue": "Devam et",
        "apisandbox-continue-clear": "Temizle",
        "apisandbox-multivalue-all-namespaces": "$1 (Tüm isim alanları)",
        "enotif_body_intro_moved": "{{SITENAME}} sayfası $1, $2 tarafından $PAGEEDITDATE tarihinde {{GENDER:$2|taşındı}}, mevcut revizyon için bakınız: $3.",
        "enotif_body_intro_restored": "{{SITENAME}} sayfası $1, $2 tarafından $PAGEEDITDATE tarihinde {{GENDER:$2|geri getirildi}}, mevcut revizyon için bakınız: $3.",
        "enotif_body_intro_changed": "{{SITENAME}} sayfası $1, $2 tarafından $PAGEEDITDATE tarihinde {{GENDER:$2|değiştirildi}}, mevcut revizyon için bakınız: $3.",
-       "enotif_lastvisited": "Son ziyaretinizden bu yana olan tüm değişiklikleri görmek için $1'e bakın.",
+       "enotif_lastvisited": "Son ziyaretinizden bu yana yapılan tüm değişiklikler için bakınız: $1",
        "enotif_lastdiff": "Bu değişikliği görmek için, $1 sayfasına bakınız.",
        "enotif_anon_editor": "anonim kullanıcı $1",
        "enotif_body": "Sayın $WATCHINGUSERNAME,\n\n$PAGEINTRO $NEWPAGE\n\nEditörün girdiği özet: $PAGESUMMARY $PAGEMINOREDIT\n\nEditörün iletişim bilgileri:\ne-posta: $PAGEEDITOR_EMAIL\nviki: $PAGEEDITOR_WIKI\n\nBahsi geçen sayfayı oturum açarak ziyaret edinceye kadar sayfayla ilgili başka bildirim gönderilmeyecektir. Ayrıca izleme listenizdeki tüm sayfaların bildirim durumlarını sıfırlayabilirsiniz.\n\n{{SITENAME}} bildirim sistemi\n\n--\nE-posta bildirim ayarlarınızı değiştirmek için aşağıdaki sayfayı ziyaret ediniz:\n{{canonicalurl:{{#special:Preferences}}}}\n\nİzleme listesi ayarlarınızı değiştirmek için aşağıdaki sayfayı ziyaret ediniz:\n{{canonicalurl:{{#special:EditWatchlist}}}}\n\nSayfayı izleme listenizden silmek için aşağıdaki sayfayı ziyaret ediniz:\n$UNWATCHURL\n\nGeri bildirim ve daha fazla yardım için:\n$HELPPAGE",
        "deletepage": "Sayfayı sil",
        "confirm": "Onayla",
        "excontent": "eski içerik: '$1'",
-       "excontentauthor": "eski içerik: '$1' ('[[Special:Contributions/$2|$2]]' katkıda bulunmuş olan tek kullanıcı)",
+       "excontentauthor": "eski içerik: '$1' ve katkıda bulunmuş olan tek kullanıcı \"[[Special:Contributions/$2|$2]]\" ([[User talk:$2|mesaj]])",
        "exbeforeblank": "Silinmeden önceki içerik: '$1'",
        "delete-confirm": "\"$1\" sayfasını sil",
        "delete-legend": "Sil",
        "historywarning": "<strong>Uyarı:</strong> Silmek üzere olduğunuz sayfanın yaklaşık olarak $1 sürüme sahip bir geçmişi var:",
-       "historyaction-submit": "Göster",
+       "historyaction-submit": "Revizyonları göster",
        "confirmdeletetext": "Bu sayfayı veya dosyayı tüm geçmişi ile birlikte veritabanından kalıcı olarak silmek üzeresiniz.\nBu işlemden kaynaklı doğabilecek sonuçların farkında iseniz ve işlemin [[{{MediaWiki:Policy-url}}|Silme kurallarına]] uygun olduğuna eminseniz, işlemi onaylayın.",
        "actioncomplete": "İşlem tamamlandı",
        "actionfailed": "İşlem başarısız oldu",
        "mycontris": "Katkılar",
        "anoncontribs": "Katkılar",
        "contribsub2": "{{GENDER:$3|$1}} ($2) tarafından",
+       "contributions-subtitle": "{{GENDER:$3|$1}} için",
        "contributions-userdoesnotexist": "\"$1\" kullanıcı hesabı kayıtlı değil.",
        "nocontribs": "Bu kriterlere uyan değişiklik bulunamadı",
        "uctop": "güncel",
        "sp-contributions-newbies-sub": "Yeni kullanıcılar için",
        "sp-contributions-newbies-title": "Yeni hesaplar için kullanıcı katkıları",
        "sp-contributions-blocklog": "engelleme günlüğü",
-       "sp-contributions-suppresslog": "kullanıcının silinen katkıları",
-       "sp-contributions-deleted": "kullanıcının silinen katkıları",
+       "sp-contributions-suppresslog": "{{GENDER:$1|kullanıcının}} baskılanmış katkıları",
+       "sp-contributions-deleted": "{{GENDER:$1|kullanıcının}} silinen katkıları",
        "sp-contributions-uploads": "yüklenenler",
        "sp-contributions-logs": "günlükler",
        "sp-contributions-talk": "mesaj",
index e40a5f9..b7dc9c4 100644 (file)
@@ -35,7 +35,8 @@
                        "Hello903hello",
                        "Fitoschido",
                        "Kanashimi",
-                       "Roy17"
+                       "Roy17",
+                       "Tang891228"
                ]
        },
        "tog-underline": "連結加底線:",
        "title-invalid-talk-namespace": "所請求嘅版面標題指去未開嘅討論版。",
        "title-invalid-characters": "所請求嘅版面標題有「$1」呢個無效字符。",
        "title-invalid-relative": "標題有相對路徑。因為用戶嘅瀏覽器經常處理唔到相對路徑(./, ../),所以相對路徑無效。",
-       "title-invalid-magic-tilde": "所請求嘅版面標題有無效嘅波浪線魔字(<nowiki>~~~</nowiki>)。",
+       "title-invalid-magic-tilde": "所請求嘅版面標題有無效嘅波浪線魔字(<nowiki>~~~</nowiki>)。",
        "title-invalid-too-long": "所請求嘅版面標題太長。標題用UTF-8編碼嗰時嘅長度唔應該超過 $1 {{PLURAL:$1|字節}}",
        "title-invalid-leading-colon": "所請求嘅版面標題開頭有無效冒號。",
        "perfcached": "以下嘅資料係嚟自快取,可能唔係最新嘅。 最多有{{PLURAL:$1|一個結果|$1個結果}}響快取度。",
index 1eab155..1542eda 100644 (file)
                        "Hello903hello",
                        "Luuva",
                        "Davidzdh",
-                       "WQL"
+                       "WQL",
+                       "Tang891228"
                ]
        },
        "tog-underline": "底線標示連結:",
        "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] 的如何遷移。",
+       "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": "日誌",
index 5e1feb7..b7d584a 100644 (file)
@@ -24,6 +24,8 @@
  * @author  Platonides
  */
 
+use MediaWiki\MediaWikiServices;
+
 require_once __DIR__ . '/Benchmarker.php';
 
 /**
@@ -45,7 +47,8 @@ class BenchHttpHttps extends Benchmarker {
        }
 
        private function doRequest( $proto ) {
-               Http::get( "$proto://localhost/", [], __METHOD__ );
+               MediaWikiServices::getInstance()->getHttpRequestFactory()->
+                       get( "$proto://localhost/", [], __METHOD__ );
        }
 
        // bench function 1
index 1b49f0e..20be9fd 100644 (file)
@@ -55,7 +55,10 @@ class CleanupCaps extends TableCleanup {
 
                $this->namespace = intval( $this->getOption( 'namespace', 0 ) );
 
-               if ( MWNamespace::isCapitalized( $this->namespace ) ) {
+               if (
+                       MediaWikiServices::getInstance()->getNamespaceInfo()->
+                               isCapitalized( $this->namespace )
+               ) {
                        $this->output( "Will be moving pages to first letter capitalized titles" );
                        $callback = 'processRowToUppercase';
                } else {
index 7f0e340..cad6122 100644 (file)
@@ -152,7 +152,7 @@ class TitleCleanup extends TableCleanup {
 
                        # Namespace which no longer exists. Put the page in the main namespace
                        # since we don't have any idea of the old namespace name. See T70501.
-                       if ( !MWNamespace::exists( $ns ) ) {
+                       if ( !MediaWikiServices::getInstance()->getNamespaceInfo()->exists( $ns ) ) {
                                $ns = 0;
                        }
 
index 61d1e5d..f7fd9d5 100644 (file)
@@ -166,13 +166,13 @@ class CleanupUsersWithNoId extends LoggedUpdateMaintenance {
 
                                $id = 0;
                                if ( $this->assign ) {
-                                       $id = (int)User::idFromName( $name );
+                                       $id = User::idFromName( $name );
                                        if ( !$id ) {
                                                // See if any extension wants to create it.
                                                if ( !isset( $this->triedCreations[$name] ) ) {
                                                        $this->triedCreations[$name] = true;
                                                        if ( !Hooks::run( 'ImportHandleUnknownUser', [ $name ] ) ) {
-                                                               $id = (int)User::idFromName( $name, User::READ_LATEST );
+                                                               $id = User::idFromName( $name, User::READ_LATEST );
                                                        }
                                                }
                                        }
index 900752f..b57db8f 100644 (file)
@@ -34,6 +34,8 @@
  * @author Antoine Musso <hashar at free dot fr>
  */
 
+use MediaWiki\MediaWikiServices;
+
 require_once __DIR__ . '/Maintenance.php';
 
 /**
@@ -216,7 +218,7 @@ class FindHooks extends Maintenance {
 
                $retval = [];
                while ( true ) {
-                       $json = Http::get(
+                       $json = MediaWikiServices::getInstance()->getHttpRequestFactory()->get(
                                wfAppendQuery( 'https://www.mediawiki.org/w/api.php', $params ),
                                [],
                                __METHOD__
index af5373c..7d43f21 100644 (file)
@@ -290,7 +290,7 @@ class GenerateSitemap extends Maintenance {
         * @return string
         */
        function guessPriority( $namespace ) {
-               return MWNamespace::isSubject( $namespace )
+               return MediaWikiServices::getInstance()->getNamespaceInfo()->isSubject( $namespace )
                        ? $this->priorities[self::GS_MAIN]
                        : $this->priorities[self::GS_TALK];
        }
index e60e776..1d4b496 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup Maintenance
  */
 
+use MediaWiki\MediaWikiServices;
+
 require_once __DIR__ . '/Maintenance.php';
 
 /**
@@ -64,7 +66,8 @@ class ImportSiteScripts extends Maintenance {
                        $url = wfAppendQuery( $baseUrl, [
                                'action' => 'raw',
                                'title' => "MediaWiki:{$page}" ] );
-                       $text = Http::get( $url, [], __METHOD__ );
+                       $text = MediaWikiServices::getInstance()->getHttpRequestFactory()->
+                               get( $url, [], __METHOD__ );
 
                        $wikiPage = WikiPage::factory( $title );
                        $content = ContentHandler::makeContent( $text, $wikiPage->getTitle() );
@@ -86,7 +89,8 @@ class ImportSiteScripts extends Maintenance {
 
                while ( true ) {
                        $url = wfAppendQuery( $baseUrl, $data );
-                       $strResult = Http::get( $url, [], __METHOD__ );
+                       $strResult = MediaWikiServices::getInstance()->getHttpRequestFactory()->
+                               get( $url, [], __METHOD__ );
                        $result = FormatJson::decode( $strResult, true );
 
                        $page = null;
index 075d6f2..3c73306 100644 (file)
@@ -117,7 +117,10 @@ class NamespaceDupes extends Maintenance {
                }
 
                // Now pull in all canonical and alias namespaces...
-               foreach ( MWNamespace::getCanonicalNamespaces() as $ns => $name ) {
+               foreach (
+                       MediaWikiServices::getInstance()->getNamespaceInfo()->getCanonicalNamespaces()
+                       as $ns => $name
+               ) {
                        // This includes $wgExtraNamespaces
                        if ( $name !== '' ) {
                                $spaces[$name] = $ns;
@@ -429,7 +432,10 @@ class NamespaceDupes extends Maintenance {
         * @return ResultWrapper
         */
        private function getTargetList( $ns, $name, $options ) {
-               if ( $options['move-talk'] && MWNamespace::isSubject( $ns ) ) {
+               if (
+                       $options['move-talk'] &&
+                       MediaWikiServices::getInstance()->getNamespaceInfo()->isSubject( $ns )
+               ) {
                        $checkNamespaces = [ NS_MAIN, NS_TALK ];
                } else {
                        $checkNamespaces = NS_MAIN;
@@ -465,9 +471,10 @@ class NamespaceDupes extends Maintenance {
                        $dbk = "$name-" . $dbk;
                }
                $destNS = $ns;
-               if ( $sourceNs == NS_TALK && MWNamespace::isSubject( $ns ) ) {
+               $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
+               if ( $sourceNs == NS_TALK && $nsInfo->isSubject( $ns ) ) {
                        // This is an associated talk page moved with the --move-talk feature.
-                       $destNS = MWNamespace::getTalk( $destNS );
+                       $destNS = $nsInfo->getTalk( $destNS );
                }
                $newTitle = Title::makeTitleSafe( $destNS, $dbk );
                if ( !$newTitle || !$newTitle->canExist() ) {
index acc66c5..a654a1f 100644 (file)
@@ -86,7 +86,7 @@ TEXT
                        $url = rtrim( $this->source, '?' ) . '?' . $url;
                }
 
-               $json = Http::get( $url );
+               $json = MediaWikiServices::getInstance()->getHttpRequestFactory()->get( $url );
                $data = json_decode( $json, true );
 
                if ( is_array( $data ) ) {
index cf398ff..2cdf418 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup Maintenance
  */
 
+use MediaWiki\MediaWikiServices;
+
 require_once __DIR__ . '/Maintenance.php';
 
 /**
@@ -102,7 +104,8 @@ class RebuildFileCache extends Maintenance {
                        // Get the pages
                        $res = $dbr->select( 'page',
                                [ 'page_namespace', 'page_title', 'page_id' ],
-                               [ 'page_namespace' => MWNamespace::getContentNamespaces(),
+                               [ 'page_namespace' => MediaWikiServices::getInstance()->getNamespaceInfo()->
+                                       getContentNamespaces(),
                                        "page_id BETWEEN " . (int)$blockStart . " AND " . (int)$blockEnd ],
                                __METHOD__,
                                [ 'ORDER BY' => 'page_id ASC', 'USE INDEX' => 'PRIMARY' ]
index 2105e0f..b8ac75e 100644 (file)
     "selenium-test": "wdio ./tests/selenium/wdio.conf.js"
   },
   "devDependencies": {
-    "eslint-config-wikimedia": "0.11.0",
+    "eslint-config-wikimedia": "0.12.0",
     "grunt": "1.0.4",
     "grunt-banana-checker": "0.7.0",
     "grunt-contrib-copy": "1.0.0",
     "grunt-contrib-watch": "1.1.0",
     "grunt-eslint": "21.0.0",
-    "grunt-jsonlint": "1.1.0",
     "grunt-karma": "3.0.0",
     "grunt-stylelint": "0.10.1",
     "grunt-svgmin": "5.0.0",
index a3f51da..c28ac4a 100644 (file)
@@ -1439,6 +1439,7 @@ return [
                'dependencies' => [ 'jquery.makeCollapsible' ],
                'scripts' => 'resources/src/mediawiki.action/mediawiki.action.history.js',
                'styles' => 'resources/src/mediawiki.action/mediawiki.action.history.css',
+               'targets' => [ 'desktop', 'mobile' ]
        ],
        'mediawiki.action.history.styles' => [
                'skinStyles' => [
index c6f5b49..f5019a7 100644 (file)
@@ -16,7 +16,8 @@
 #pagehistory li.selected {
        background-color: #f8f9fa;
        color: #222;
-       border: 1px dashed #a2a9b1;
+       border-color: #f8f9fa;
+       outline: 1px dashed #a2a9b1;
 }
 
 .mw-history-revisionactions {
diff --git a/resources/src/mediawiki.less/mediawiki.ui/mixins.buttons.less b/resources/src/mediawiki.less/mediawiki.ui/mixins.buttons.less
new file mode 100644 (file)
index 0000000..9ee92b2
--- /dev/null
@@ -0,0 +1,46 @@
+// Common button mixins for MediaWiki
+//
+// Helper mixins used to create button styles. this file is importable
+// by all less files via `@import 'mediawiki.mixins.buttons';`.
+
+/* stylelint-disable selector-class-pattern */
+
+// Primary buttons mixin
+.button-colors-primary( @bgColor, @highlightColor, @activeColor ) {
+       background-color: @bgColor;
+       color: #fff;
+       // border of the same color as background so that light background and
+       // dark background buttons are the same height and width
+       border: 1px solid @bgColor;
+
+       &:hover {
+               background-color: @highlightColor;
+               border-color: @highlightColor;
+       }
+
+       &:focus {
+               box-shadow: inset 0 0 0 1px @bgColor, inset 0 0 0 2px #fff;
+       }
+
+       &:active,
+       &.is-on {
+               background-color: @activeColor;
+               border-color: @activeColor;
+               box-shadow: none;
+       }
+
+       &:disabled {
+               background-color: @colorGray12;
+               color: #fff;
+               border-color: @colorGray12;
+
+               // Make sure disabled buttons don't have hover and active states
+               &:hover,
+               &:active {
+                       background-color: @colorGray12;
+                       color: #fff;
+                       border-color: @colorGray12;
+                       box-shadow: none;
+               }
+       }
+}
index a85ecd7..e58cb1e 100644 (file)
@@ -1,50 +1,9 @@
 @import 'mediawiki.mixins';
+@import 'mediawiki.ui/mixins.buttons';
 @import 'mediawiki.ui/variables';
 
 /* stylelint-disable selector-class-pattern */
 
-// Buttons
-// Helper mixins
-// Primary buttons mixin
-.button-colors-primary( @bgColor, @highlightColor, @activeColor ) {
-       background-color: @bgColor;
-       color: #fff;
-       // border of the same color as background so that light background and
-       // dark background buttons are the same height and width
-       border: 1px solid @bgColor;
-
-       &:hover {
-               background-color: @highlightColor;
-               border-color: @highlightColor;
-       }
-
-       &:focus {
-               box-shadow: inset 0 0 0 1px @bgColor, inset 0 0 0 2px #fff;
-       }
-
-       &:active,
-       &.is-on {
-               background-color: @activeColor;
-               border-color: @activeColor;
-               box-shadow: none;
-       }
-
-       &:disabled {
-               background-color: @colorGray12;
-               color: #fff;
-               border-color: @colorGray12;
-
-               // Make sure disabled buttons don't have hover and active states
-               &:hover,
-               &:active {
-                       background-color: @colorGray12;
-                       color: #fff;
-                       border-color: @colorGray12;
-                       box-shadow: none;
-               }
-       }
-}
-
 // All buttons start with `.mw-ui-button` class, modified by other classes.
 // It can be any element.  Due to a lack of a CSS reset, the exact styling of
 // the button depends on what type of element is used.
index 603f4c2..f7a4cc4 100644 (file)
@@ -1,19 +1,25 @@
 <?php
 
+use MediaWiki\MediaWikiServices;
 use Wikimedia\TestingAccessWrapper;
 
 abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase {
        protected static $httpEngine;
        protected $oldHttpEngine;
 
+       /** @var HttpRequestFactory */
+       private $factory;
+
        public function setUp() {
                parent::setUp();
                $this->oldHttpEngine = Http::$httpEngine;
                Http::$httpEngine = static::$httpEngine;
 
+               $this->factory = MediaWikiServices::getInstance()->getHttpRequestFactory();
+
                try {
-                       $request = MWHttpRequest::factory( 'null:' );
-               } catch ( DomainException $e ) {
+                       $request = $factory->create( 'null:' );
+               } catch ( RuntimeException $e ) {
                        $this->markTestSkipped( static::$httpEngine . ' engine not supported' );
                }
 
@@ -32,19 +38,19 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase {
        // --------------------
 
        public function testIsRedirect() {
-               $request = MWHttpRequest::factory( 'http://httpbin.org/get' );
+               $request = $this->factory->create( 'http://httpbin.org/get' );
                $status = $request->execute();
                $this->assertTrue( $status->isGood() );
                $this->assertFalse( $request->isRedirect() );
 
-               $request = MWHttpRequest::factory( 'http://httpbin.org/redirect/1' );
+               $request = $this->factory->create( 'http://httpbin.org/redirect/1' );
                $status = $request->execute();
                $this->assertTrue( $status->isGood() );
                $this->assertTrue( $request->isRedirect() );
        }
 
        public function testgetFinalUrl() {
-               $request = MWHttpRequest::factory( 'http://httpbin.org/redirect/3' );
+               $request = $this->factory->create( 'http://httpbin.org/redirect/3' );
                if ( !$request->canFollowRedirects() ) {
                        $this->markTestSkipped( 'cannot follow redirects' );
                }
@@ -52,14 +58,14 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase {
                $this->assertTrue( $status->isGood() );
                $this->assertNotSame( 'http://httpbin.org/get', $request->getFinalUrl() );
 
-               $request = MWHttpRequest::factory( 'http://httpbin.org/redirect/3', [ 'followRedirects'
+               $request = $this->factory->create( 'http://httpbin.org/redirect/3', [ 'followRedirects'
                        => true ] );
                $status = $request->execute();
                $this->assertTrue( $status->isGood() );
                $this->assertSame( 'http://httpbin.org/get', $request->getFinalUrl() );
                $this->assertResponseFieldValue( 'url', 'http://httpbin.org/get', $request );
 
-               $request = MWHttpRequest::factory( 'http://httpbin.org/redirect/3', [ 'followRedirects'
+               $request = $this->factory->create( 'http://httpbin.org/redirect/3', [ 'followRedirects'
                => true ] );
                $status = $request->execute();
                $this->assertTrue( $status->isGood() );
@@ -71,7 +77,7 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase {
                        return;
                }
 
-               $request = MWHttpRequest::factory( 'http://httpbin.org/redirect/3', [ 'followRedirects'
+               $request = $this->factory->create( 'http://httpbin.org/redirect/3', [ 'followRedirects'
                => true, 'maxRedirects' => 1 ] );
                $status = $request->execute();
                $this->assertTrue( $status->isGood() );
@@ -79,7 +85,7 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase {
        }
 
        public function testSetCookie() {
-               $request = MWHttpRequest::factory( 'http://httpbin.org/cookies' );
+               $request = $this->factory->create( 'http://httpbin.org/cookies' );
                $request->setCookie( 'foo', 'bar' );
                $request->setCookie( 'foo2', 'bar2', [ 'domain' => 'example.com' ] );
                $status = $request->execute();
@@ -88,7 +94,7 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase {
        }
 
        public function testSetCookieJar() {
-               $request = MWHttpRequest::factory( 'http://httpbin.org/cookies' );
+               $request = $this->factory->create( 'http://httpbin.org/cookies' );
                $cookieJar = new CookieJar();
                $cookieJar->setCookie( 'foo', 'bar', [ 'domain' => 'httpbin.org' ] );
                $cookieJar->setCookie( 'foo2', 'bar2', [ 'domain' => 'example.com' ] );
@@ -97,7 +103,7 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase {
                $this->assertTrue( $status->isGood() );
                $this->assertResponseFieldValue( 'cookies', [ 'foo' => 'bar' ], $request );
 
-               $request = MWHttpRequest::factory( 'http://httpbin.org/cookies/set?foo=bar' );
+               $request = $this->factory->create( 'http://httpbin.org/cookies/set?foo=bar' );
                $cookieJar = new CookieJar();
                $request->setCookieJar( $cookieJar );
                $status = $request->execute();
@@ -106,7 +112,7 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase {
 
                $this->markTestIncomplete( 'CookieJar does not handle deletion' );
 
-               // $request = MWHttpRequest::factory( 'http://httpbin.org/cookies/delete?foo' );
+               // $request = $this->factory->create( 'http://httpbin.org/cookies/delete?foo' );
                // $cookieJar = new CookieJar();
                // $cookieJar->setCookie( 'foo', 'bar', [ 'domain' => 'httpbin.org' ] );
                // $cookieJar->setCookie( 'foo2', 'bar2', [ 'domain' => 'httpbin.org' ] );
@@ -118,7 +124,7 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase {
        }
 
        public function testGetResponseHeaders() {
-               $request = MWHttpRequest::factory( 'http://httpbin.org/response-headers?Foo=bar' );
+               $request = $this->factory->create( 'http://httpbin.org/response-headers?Foo=bar' );
                $status = $request->execute();
                $this->assertTrue( $status->isGood() );
                $headers = array_change_key_case( $request->getResponseHeaders(), CASE_LOWER );
@@ -127,7 +133,7 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase {
        }
 
        public function testSetHeader() {
-               $request = MWHttpRequest::factory( 'http://httpbin.org/headers' );
+               $request = $this->factory->create( 'http://httpbin.org/headers' );
                $request->setHeader( 'Foo', 'bar' );
                $status = $request->execute();
                $this->assertTrue( $status->isGood() );
@@ -135,14 +141,14 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase {
        }
 
        public function testGetStatus() {
-               $request = MWHttpRequest::factory( 'http://httpbin.org/status/418' );
+               $request = $this->factory->create( 'http://httpbin.org/status/418' );
                $status = $request->execute();
                $this->assertFalse( $status->isOK() );
                $this->assertSame( $request->getStatus(), 418 );
        }
 
        public function testSetUserAgent() {
-               $request = MWHttpRequest::factory( 'http://httpbin.org/user-agent' );
+               $request = $this->factory->create( 'http://httpbin.org/user-agent' );
                $request->setUserAgent( 'foo' );
                $status = $request->execute();
                $this->assertTrue( $status->isGood() );
@@ -150,7 +156,7 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase {
        }
 
        public function testSetData() {
-               $request = MWHttpRequest::factory( 'http://httpbin.org/post', [ 'method' => 'POST' ] );
+               $request = $this->factory->create( 'http://httpbin.org/post', [ 'method' => 'POST' ] );
                $request->setData( [ 'foo' => 'bar', 'foo2' => 'bar2' ] );
                $status = $request->execute();
                $this->assertTrue( $status->isGood() );
@@ -163,7 +169,7 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase {
                        return;
                }
 
-               $request = MWHttpRequest::factory( 'http://httpbin.org/ip' );
+               $request = $this->factory->create( 'http://httpbin.org/ip' );
                $data = '';
                $request->setCallback( function ( $fh, $content ) use ( &$data ) {
                        $data .= $content;
@@ -177,7 +183,7 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase {
        }
 
        public function testBasicAuthentication() {
-               $request = MWHttpRequest::factory( 'http://httpbin.org/basic-auth/user/pass', [
+               $request = $this->factory->create( 'http://httpbin.org/basic-auth/user/pass', [
                        'username' => 'user',
                        'password' => 'pass',
                ] );
@@ -185,7 +191,7 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase {
                $this->assertTrue( $status->isGood() );
                $this->assertResponseFieldValue( 'authenticated', true, $request );
 
-               $request = MWHttpRequest::factory( 'http://httpbin.org/basic-auth/user/pass', [
+               $request = $this->factory->create( 'http://httpbin.org/basic-auth/user/pass', [
                        'username' => 'user',
                        'password' => 'wrongpass',
                ] );
@@ -195,7 +201,7 @@ abstract class MWHttpRequestTestCase extends PHPUnit\Framework\TestCase {
        }
 
        public function testFactoryDefaults() {
-               $request = MWHttpRequest::factory( 'http://acme.test' );
+               $request = $this->factory->create( 'http://acme.test' );
                $this->assertInstanceOf( MWHttpRequest::class, $request );
        }
 
index fddee3d..34f8cd5 100644 (file)
@@ -168,14 +168,10 @@ class ParserTestPrinter extends TestRecorder {
                        $output = strtr( $output, $pairs );
                }
 
-               # Windows, or at least the fc utility, is retarded
-               $slash = wfIsWindows() ? '\\' : '/';
-               $prefix = wfTempDir() . "{$slash}mwParser-" . mt_rand();
-
-               $infile = "$prefix-$inFileTail";
+               $infile = tempnam( wfTempDir(), "mwParser-$inFileTail" );
                $this->dumpToFile( $input, $infile );
 
-               $outfile = "$prefix-$outFileTail";
+               $outfile = tempnam( wfTempDir(), "mwParser-$outFileTail" );
                $this->dumpToFile( $output, $outfile );
 
                global $wgDiff3;
index 3eb25a9..3b63c19 100644 (file)
@@ -289,9 +289,14 @@ class ParserTestRunner {
 
                // All FileRepo changes should be done here by injecting services,
                // there should be no need to change global variables.
-               RepoGroup::setSingleton( $this->createRepoGroup() );
+               MediaWikiServices::getInstance()->disableService( 'RepoGroup' );
+               MediaWikiServices::getInstance()->redefineService( 'RepoGroup',
+                       function () {
+                               return $this->createRepoGroup();
+                       }
+               );
                $teardown[] = function () {
-                       RepoGroup::destroySingleton();
+                       MediaWikiServices::getInstance()->resetServiceForTesting( 'RepoGroup' );
                };
 
                // Set up null lock managers
@@ -449,7 +454,8 @@ class ParserTestRunner {
                                'transformVia404' => false,
                                'backend' => $backend
                        ],
-                       []
+                       [],
+                       MediaWikiServices::getInstance()->getMainWANObjectCache()
                );
        }
 
@@ -635,6 +641,8 @@ class ParserTestRunner {
        /**
         * Reset the Title-related services that need resetting
         * for each test
+        *
+        * @todo We need to reset all services on every test
         */
        private function resetTitleServices() {
                $services = MediaWikiServices::getInstance();
@@ -643,6 +651,7 @@ class ParserTestRunner {
                $services->resetServiceForTesting( '_MediaWikiTitleCodec' );
                $services->resetServiceForTesting( 'LinkRenderer' );
                $services->resetServiceForTesting( 'LinkRendererFactory' );
+               $services->resetServiceForTesting( 'NamespaceInfo' );
        }
 
        /**
@@ -1071,7 +1080,8 @@ class ParserTestRunner {
                        'wgLanguageCode' => $langCode,
                        'wgRawHtml' => self::getOptionValue( 'wgRawHtml', $opts, false ),
                        'wgNamespacesWithSubpages' => array_fill_keys(
-                               MWNamespace::getValidNamespaces(), isset( $opts['subpage'] )
+                               MediaWikiServices::getInstance()->getNamespaceInfo()->getValidNamespaces(),
+                               isset( $opts['subpage'] )
                        ),
                        'wgMaxTocLevel' => $maxtoclevel,
                        'wgAllowExternalImages' => self::getOptionValue( 'wgAllowExternalImages', $opts, true ),
index ebc3b79..1e70c57 100644 (file)
@@ -472,7 +472,17 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase {
         * @return string Absolute name of the temporary file
         */
        protected function getNewTempFile() {
-               $fileName = tempnam( wfTempDir(), 'MW_PHPUnit_' . static::class . '_' );
+               $fileName = tempnam(
+                       wfTempDir(),
+                       // Avoid backslashes here as they result in inconsistent results
+                       // between Windows and other OS, as well as between functions
+                       // that try to normalise these in one or both directions.
+                       // For example, tempnam rejects directory separators in the prefix which
+                       // means it rejects any namespaced class on Windows.
+                       // And then there is, wfMkdirParents which normalises paths always
+                       // whereas most other PHP and MW functions do not.
+                       'MW_PHPUnit_' . strtr( static::class, [ '\\' => '_' ] ) . '_'
+               );
                $this->tmpFiles[] = $fileName;
 
                return $fileName;
@@ -489,14 +499,15 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase {
         * @return string Absolute name of the temporary directory
         */
        protected function getNewTempDirectory() {
-               // Starting of with a temporary /file/.
+               // Starting of with a temporary *file*.
                $fileName = $this->getNewTempFile();
 
-               // Converting the temporary /file/ to a /directory/
+               // Converting the temporary file to a *directory*.
                // The following is not atomic, but at least we now have a single place,
-               // where temporary directory creation is bundled and can be improved
+               // where temporary directory creation is bundled and can be improved.
                unlink( $fileName );
-               $this->assertTrue( wfMkdirParents( $fileName ) );
+               // If this fails for some reason, PHP will warn and fail the test.
+               mkdir( $fileName, 0777, /* recursive = */ true );
 
                return $fileName;
        }
@@ -2233,18 +2244,19 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase {
                }
 
                // NOTE: prefer content namespaces
+               $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
                $namespaces = array_unique( array_merge(
-                       MWNamespace::getContentNamespaces(),
+                       $nsInfo->getContentNamespaces(),
                        [ NS_MAIN, NS_HELP, NS_PROJECT ], // prefer these
-                       MWNamespace::getValidNamespaces()
+                       $nsInfo->getValidNamespaces()
                ) );
 
                $namespaces = array_diff( $namespaces, [
                        NS_FILE, NS_CATEGORY, NS_MEDIAWIKI, NS_USER // don't mess with magic namespaces
                ] );
 
-               $talk = array_filter( $namespaces, function ( $ns ) {
-                       return MWNamespace::isTalk( $ns );
+               $talk = array_filter( $namespaces, function ( $ns ) use ( $nsInfo ) {
+                       return $nsInfo->isTalk( $ns );
                } );
 
                // prefer non-talk pages
@@ -2408,4 +2420,18 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase {
                        'comment' => $comment,
                ] );
        }
+
+       /**
+        * Returns a PHPUnit constraint that matches anything other than a fixed set of values. This can
+        * be used to whitelist values, e.g.
+        *   $mock->expects( $this->never() )->method( $this->anythingBut( 'foo', 'bar' ) );
+        * which will throw if any unexpected method is called.
+        *
+        * @param mixed ...$values Values that are not matched
+        */
+       protected function anythingBut( ...$values ) {
+               return $this->logicalNot( $this->logicalOr(
+                       ...array_map( [ $this, 'matches' ], $values )
+               ) );
+       }
 }
index df3de4a..61e3e7c 100644 (file)
@@ -131,6 +131,68 @@ class BlockTest extends MediaWikiLangTestCase {
                ];
        }
 
+       /**
+        * @dataProvider provideNewFromTargetRangeBlocks
+        * @covers Block::newFromTarget
+        */
+       public function testNewFromTargetRangeBlocks( $targets, $ip, $expectedTarget ) {
+               $blocker = $this->getTestSysop()->getUser();
+
+               foreach ( $targets as $target ) {
+                       $block = new Block();
+                       $block->setTarget( $target );
+                       $block->setBlocker( $blocker );
+                       $block->insert();
+               }
+
+               // Should find the block with the narrowest range
+               $blockTarget = Block::newFromTarget( $this->getTestUser()->getUser(), $ip )->getTarget();
+               $this->assertSame(
+                       $blockTarget instanceof User ? $blockTarget->getName() : $blockTarget,
+                       $expectedTarget
+               );
+
+               foreach ( $targets as $target ) {
+                       $block = Block::newFromTarget( $target );
+                       $block->delete();
+               }
+       }
+
+       function provideNewFromTargetRangeBlocks() {
+               return [
+                       'Blocks to IPv4 ranges' => [
+                               [ '0.0.0.0/20', '0.0.0.0/30', '0.0.0.0/25' ],
+                               '0.0.0.0',
+                               '0.0.0.0/30'
+                       ],
+                       'Blocks to IPv6 ranges' => [
+                               [ '0:0:0:0:0:0:0:0/20', '0:0:0:0:0:0:0:0/30', '0:0:0:0:0:0:0:0/25' ],
+                               '0:0:0:0:0:0:0:0',
+                               '0:0:0:0:0:0:0:0/30'
+                       ],
+                       'Blocks to wide IPv4 range and IP' => [
+                               [ '0.0.0.0/16', '0.0.0.0' ],
+                               '0.0.0.0',
+                               '0.0.0.0'
+                       ],
+                       'Blocks to wide IPv6 range and IP' => [
+                               [ '0:0:0:0:0:0:0:0/19', '0:0:0:0:0:0:0:0' ],
+                               '0:0:0:0:0:0:0:0',
+                               '0:0:0:0:0:0:0:0'
+                       ],
+                       'Blocks to narrow IPv4 range and IP' => [
+                               [ '0.0.0.0/31', '0.0.0.0' ],
+                               '0.0.0.0',
+                               '0.0.0.0'
+                       ],
+                       'Blocks to narrow IPv6 range and IP' => [
+                               [ '0:0:0:0:0:0:0:0/127', '0:0:0:0:0:0:0:0' ],
+                               '0:0:0:0:0:0:0:0',
+                               '0:0:0:0:0:0:0:0'
+                       ],
+               ];
+       }
+
        /**
         * @covers Block::appliesToRight
         */
index 5f0200d..a758f99 100644 (file)
@@ -37,7 +37,7 @@ class ContentSecurityPolicyTest extends MediaWikiTestCase {
                // Note, there are some obscure globals which
                // could affect the results which aren't included above.
 
-               RepoGroup::destroySingleton();
+               $this->overrideMwServices();
                $context = RequestContext::getMain();
                $resp = $context->getRequest()->response();
                $conf = $context->getConfig();
index 9443b19..1210a50 100644 (file)
@@ -74,12 +74,8 @@ class GlobalTest extends MediaWikiTestCase {
                $this->assertFalse(
                        wfRandomString() == wfRandomString()
                );
-               $this->assertEquals(
-                       strlen( wfRandomString( 10 ) ), 10
-               );
-               $this->assertTrue(
-                       preg_match( '/^[0-9a-f]+$/i', wfRandomString() ) === 1
-               );
+               $this->assertSame( 10, strlen( wfRandomString( 10 ) ), 'length' );
+               $this->assertSame( 1, preg_match( '/^[0-9a-f]+$/i', wfRandomString() ), 'pattern' );
        }
 
        /**
index 438d3e7..2362961 100644 (file)
@@ -14,11 +14,16 @@ class LinkerTest extends MediaWikiLangTestCase {
                        'wgArticlePath' => '/wiki/$1',
                ] );
 
-               $this->assertEquals(
-                       $expected,
-                       Linker::userLink( $userId, $userName, $altUserName ),
-                       $msg
-               );
+               // We'd also test the warning, but injecting a mock logger into a static method is tricky.
+               if ( $userName === '' ) {
+                       Wikimedia\suppressWarnings();
+               }
+               $actual = Linker::userLink( $userId, $userName, $altUserName );
+               if ( $userName === '' ) {
+                       Wikimedia\restoreWarnings();
+               }
+
+               $this->assertEquals( $expected, $actual, $msg );
        }
 
        public static function provideCasesForUserLink() {
@@ -29,6 +34,9 @@ class LinkerTest extends MediaWikiLangTestCase {
                # - optional altUserName
                # - optional message
                return [
+                       # Empty name (T222529)
+                       'Empty username, userid 0' => [ '(no username available)', 0, '' ],
+                       'Empty username, userid > 0' => [ '(no username available)', 73, '' ],
 
                        # ## ANONYMOUS USER ########################################
                        [
@@ -87,6 +95,118 @@ class LinkerTest extends MediaWikiLangTestCase {
                ];
        }
 
+       /**
+        * @dataProvider provideUserToolLinks
+        * @covers Linker::userToolLinks
+        * @param string $expected
+        * @param int $userId
+        * @param string $userText
+        */
+       public function testUserToolLinks( $expected, $userId, $userText ) {
+               // We'd also test the warning, but injecting a mock logger into a static method is tricky.
+               if ( $userText === '' ) {
+                       Wikimedia\suppressWarnings();
+               }
+               $actual = Linker::userToolLinks( $userId, $userText );
+               if ( $userText === '' ) {
+                       Wikimedia\restoreWarnings();
+               }
+
+               $this->assertSame( $expected, $actual );
+       }
+
+       public static function provideUserToolLinks() {
+               return [
+                       // Empty name (T222529)
+                       'Empty username, userid 0' => [ ' (no username available)', 0, '' ],
+                       'Empty username, userid > 0' => [ ' (no username available)', 73, '' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideUserTalkLink
+        * @covers Linker::userTalkLink
+        * @param string $expected
+        * @param int $userId
+        * @param string $userText
+        */
+       public function testUserTalkLink( $expected, $userId, $userText ) {
+               // We'd also test the warning, but injecting a mock logger into a static method is tricky.
+               if ( $userText === '' ) {
+                       Wikimedia\suppressWarnings();
+               }
+               $actual = Linker::userTalkLink( $userId, $userText );
+               if ( $userText === '' ) {
+                       Wikimedia\restoreWarnings();
+               }
+
+               $this->assertSame( $expected, $actual );
+       }
+
+       public static function provideUserTalkLink() {
+               return [
+                       // Empty name (T222529)
+                       'Empty username, userid 0' => [ '(no username available)', 0, '' ],
+                       'Empty username, userid > 0' => [ '(no username available)', 73, '' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideBlockLink
+        * @covers Linker::blockLink
+        * @param string $expected
+        * @param int $userId
+        * @param string $userText
+        */
+       public function testBlockLink( $expected, $userId, $userText ) {
+               // We'd also test the warning, but injecting a mock logger into a static method is tricky.
+               if ( $userText === '' ) {
+                       Wikimedia\suppressWarnings();
+               }
+               $actual = Linker::blockLink( $userId, $userText );
+               if ( $userText === '' ) {
+                       Wikimedia\restoreWarnings();
+               }
+
+               $this->assertSame( $expected, $actual );
+       }
+
+       public static function provideBlockLink() {
+               return [
+                       // Empty name (T222529)
+                       'Empty username, userid 0' => [ '(no username available)', 0, '' ],
+                       'Empty username, userid > 0' => [ '(no username available)', 73, '' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideEmailLink
+        * @covers Linker::emailLink
+        * @param string $expected
+        * @param int $userId
+        * @param string $userText
+        */
+       public function testEmailLink( $expected, $userId, $userText ) {
+               // We'd also test the warning, but injecting a mock logger into a static method is tricky.
+               if ( $userText === '' ) {
+                       Wikimedia\suppressWarnings();
+               }
+               $actual = Linker::emailLink( $userId, $userText );
+               if ( $userText === '' ) {
+                       Wikimedia\restoreWarnings();
+               }
+
+               $this->assertSame( $expected, $actual );
+       }
+
+       public static function provideEmailLink() {
+               return [
+                       // Empty name (T222529)
+                       'Empty username, userid 0' => [ '(no username available)', 0, '' ],
+                       'Empty username, userid > 0' => [ '(no username available)', 73, '' ],
+               ];
+       }
+
        /**
         * @dataProvider provideCasesForFormatComment
         * @covers Linker::formatComment
index 646b487..222b3e3 100644 (file)
@@ -1,5 +1,7 @@
 <?php
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * @covers PageProps
  *
@@ -234,7 +236,8 @@ class PagePropsTest extends MediaWikiLangTestCase {
                                ( $model === null || $model === CONTENT_MODEL_WIKITEXT )
                        ) {
                                $ns = $this->getDefaultWikitextNS();
-                               $page = MWNamespace::getCanonicalName( $ns ) . ':' . $page;
+                               $page = MediaWikiServices::getInstance()->getNamespaceInfo()->
+                                       getCanonicalName( $ns ) . ':' . $page;
                        }
 
                        $page = Title::newFromText( $page );
index b183fca..3467153 100644 (file)
@@ -1416,10 +1416,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                        ->value['revision'];
 
                $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $result = $store->getTimestampFromId(
-                       $page->getTitle(),
-                       $rev->getId()
-               );
+               $result = $store->getTimestampFromId( $rev->getId() );
 
                $this->assertSame( $rev->getTimestamp(), $result );
        }
@@ -1434,10 +1431,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                        ->value['revision'];
 
                $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $result = $store->getTimestampFromId(
-                       $page->getTitle(),
-                       $rev->getId() + 1
-               );
+               $result = $store->getTimestampFromId( $rev->getId() + 1 );
 
                $this->assertFalse( $result );
        }
index 13ddffa..7501167 100644 (file)
@@ -182,7 +182,8 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                        ( $model === null || $model === CONTENT_MODEL_WIKITEXT )
                ) {
                        $ns = $this->getDefaultWikitextNS();
-                       $titleString = MWNamespace::getCanonicalName( $ns ) . ':' . $titleString;
+                       $titleString = MediaWikiServices::getInstance()->getNamespaceInfo()->
+                               getCanonicalName( $ns ) . ':' . $titleString;
                }
 
                $title = Title::newFromText( $titleString );
@@ -625,6 +626,34 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                $this->assertEquals( $latestRevision, $newRevision->getPrevious()->getId() );
        }
 
+       /**
+        * @covers Title::getPreviousRevisionID
+        * @covers Title::getRelativeRevisionID
+        * @covers MediaWiki\Revision\RevisionStore::getPreviousRevision
+        * @covers MediaWiki\Revision\RevisionStore::getRelativeRevision
+        */
+       public function testTitleGetPreviousRevisionID() {
+               $oldestId = $this->testPage->getOldestRevision()->getId();
+               $latestId = $this->testPage->getLatest();
+
+               $title = $this->testPage->getTitle();
+
+               $this->assertFalse( $title->getPreviousRevisionID( $oldestId ) );
+
+               $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
+               $newId = $this->testPage->getRevision()->getId();
+
+               $this->assertEquals( $latestId, $title->getPreviousRevisionID( $newId ) );
+       }
+
+       /**
+        * @covers Title::getPreviousRevisionID
+        * @covers Title::getRelativeRevisionID
+        */
+       public function testTitleGetPreviousRevisionID_invalid() {
+               $this->assertFalse( $this->testPage->getTitle()->getPreviousRevisionID( 123456789 ) );
+       }
+
        /**
         * @covers Revision::getNext
         */
@@ -640,6 +669,33 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                $this->assertEquals( $rev2->getId(), $rev1->getNext()->getId() );
        }
 
+       /**
+        * @covers Title::getNextRevisionID
+        * @covers Title::getRelativeRevisionID
+        * @covers MediaWiki\Revision\RevisionStore::getNextRevision
+        * @covers MediaWiki\Revision\RevisionStore::getRelativeRevision
+        */
+       public function testTitleGetNextRevisionID() {
+               $title = $this->testPage->getTitle();
+
+               $origId = $this->testPage->getLatest();
+
+               $this->assertFalse( $title->getNextRevisionID( $origId ) );
+
+               $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
+               $newId = $this->testPage->getLatest();
+
+               $this->assertSame( $this->testPage->getLatest(), $title->getNextRevisionID( $origId ) );
+       }
+
+       /**
+        * @covers Title::getNextRevisionID
+        * @covers Title::getRelativeRevisionID
+        */
+       public function testTitleGetNextRevisionID_invalid() {
+               $this->assertFalse( $this->testPage->getTitle()->getNextRevisionID( 123456789 ) );
+       }
+
        /**
         * @covers Revision::newNullRevision
         */
index 3064a3d..40a5dc5 100644 (file)
@@ -33,7 +33,7 @@ class TestUserRegistry {
         */
        public static function getMutableTestUser( $testName, $groups = [] ) {
                $id = self::getNextId();
-               $password = wfRandomString( 20 );
+               $password = "password_for_test_user_id_{$id}";
                $testUser = new TestUser(
                        "TestUser $testName $id",  // username
                        "Name $id",                // real name
@@ -75,7 +75,7 @@ class TestUserRegistry {
                                $password = 'UTSysopPassword';
                        } else {
                                $username = "TestUser $id";
-                               $password = wfRandomString( 20 );
+                               $password = "password_for_test_user_id_{$id}";
                        }
                        self::$testUsers[$key] = $testUser = new TestUser(
                                $username,            // username
index c0de1bf..c46f69b 100644 (file)
@@ -157,6 +157,7 @@ class TitleTest extends MediaWikiTestCase {
                        ]
                ] );
 
+               // Reset services since we modified $wgLocalInterwikis
                $this->overrideMwServices();
        }
 
@@ -785,19 +786,6 @@ class TitleTest extends MediaWikiTestCase {
                ];
        }
 
-       /**
-        * @dataProvider provideGetTalkPage_good
-        * @covers Title::getTalkPage
-        */
-       public function testGetTalkPage_good( Title $title, Title $expected ) {
-               $talk = $title->getTalkPage();
-               $this->assertSame(
-                       $expected->getPrefixedDBKey(),
-                       $talk->getPrefixedDBKey(),
-                       $title->getPrefixedDBKey()
-               );
-       }
-
        /**
         * @dataProvider provideGetTalkPage_good
         * @covers Title::getTalkPageIfDefined
index 0dc64df..4e19822 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 
+use MediaWiki\MediaWikiServices;
 use Wikimedia\TestingAccessWrapper;
 
 /**
@@ -643,7 +644,7 @@ class ApiBaseTest extends ApiTestCase {
                                        ApiBase::PARAM_ISMULTI => true,
                                        ApiBase::PARAM_TYPE => 'namespace',
                                ],
-                               MWNamespace::getValidNamespaces(),
+                               MediaWikiServices::getInstance()->getNamespaceInfo()->getValidNamespaces(),
                                [],
                        ],
                        // PARAM_ALL is ignored with namespace types.
@@ -654,7 +655,7 @@ class ApiBaseTest extends ApiTestCase {
                                        ApiBase::PARAM_TYPE => 'namespace',
                                        ApiBase::PARAM_ALL => false,
                                ],
-                               MWNamespace::getValidNamespaces(),
+                               MediaWikiServices::getInstance()->getNamespaceInfo()->getValidNamespaces(),
                                [],
                        ],
                        'Namespace with wildcard "x"' => [
@@ -1332,7 +1333,10 @@ class ApiBaseTest extends ApiTestCase {
                        'expiry' => time() + 100500,
                ] );
                $block->insert();
-               $blockinfo = [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ];
+               $userInfoTrait = TestingAccessWrapper::newFromObject(
+                       $this->getMockForTrait( ApiBlockInfoTrait::class )
+               );
+               $blockinfo = [ 'blockinfo' => $userInfoTrait->getBlockInfo( $block ) ];
 
                $expect = Status::newGood();
                $expect->fatal( ApiMessage::create( 'apierror-blocked', 'blocked', $blockinfo ) );
@@ -1387,7 +1391,10 @@ class ApiBaseTest extends ApiTestCase {
                        'expiry' => time() + 100500,
                ] );
                $block->insert();
-               $blockinfo = [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ];
+               $userInfoTrait = TestingAccessWrapper::newFromObject(
+                       $this->getObjectForTrait( ApiBlockInfoTrait::class )
+               );
+               $blockinfo = [ 'blockinfo' => $userInfoTrait->getBlockInfo( $block ) ];
 
                $expect = Status::newGood();
                $expect->fatal( ApiMessage::create( 'apierror-blocked', 'blocked', $blockinfo ) );
diff --git a/tests/phpunit/includes/api/ApiBlockInfoTraitTest.php b/tests/phpunit/includes/api/ApiBlockInfoTraitTest.php
new file mode 100644 (file)
index 0000000..f05cfbc
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @covers ApiBlockInfoTrait
+ */
+class ApiBlockInfoTraitTest extends MediaWikiTestCase {
+
+       public function testGetBlockInfo() {
+               $block = new Block();
+               $mock = $this->getMockForTrait( ApiBlockInfoTrait::class );
+               $info = TestingAccessWrapper::newFromObject( $mock )->getBlockInfo( $block );
+               $subset = [
+                       'blockid' => null,
+                       'blockedby' => '',
+                       'blockedbyid' => 0,
+                       'blockreason' => '',
+                       'blockexpiry' => 'infinite',
+                       'blockpartial' => false,
+               ];
+               $this->assertArraySubset( $subset, $info );
+       }
+
+       public function testGetBlockInfoPartial() {
+               $mock = $this->getMockForTrait( ApiBlockInfoTrait::class );
+
+               $block = new Block( [
+                       'sitewide' => false,
+               ] );
+               $info = TestingAccessWrapper::newFromObject( $mock )->getBlockInfo( $block );
+               $subset = [
+                       'blockid' => null,
+                       'blockedby' => '',
+                       'blockedbyid' => 0,
+                       'blockreason' => '',
+                       'blockexpiry' => 'infinite',
+                       'blockpartial' => true,
+               ];
+               $this->assertArraySubset( $subset, $info );
+       }
+
+}
diff --git a/tests/phpunit/includes/api/ApiQueryUserInfoTest.php b/tests/phpunit/includes/api/ApiQueryUserInfoTest.php
deleted file mode 100644 (file)
index 7dcb75c..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-<?php
-
-/**
- * @group medium
- * @covers ApiQueryUserInfo
- */
-class ApiQueryUserInfoTest extends ApiTestCase {
-       public function testGetBlockInfo() {
-               $apiQueryUserInfo = new ApiQueryUserInfo(
-                       new ApiQuery( new ApiMain( $this->apiContext ), 'userinfo' ),
-                       'userinfo'
-               );
-
-               $block = new Block();
-               $info = $apiQueryUserInfo->getBlockInfo( $block );
-               $subset = [
-                       'blockid' => null,
-                       'blockedby' => '',
-                       'blockedbyid' => 0,
-                       'blockreason' => '',
-                       'blockexpiry' => 'infinite',
-                       'blockpartial' => false,
-               ];
-               $this->assertArraySubset( $subset, $info );
-       }
-
-       public function testGetBlockInfoPartial() {
-               $apiQueryUserInfo = new ApiQueryUserInfo(
-                       new ApiQuery( new ApiMain( $this->apiContext ), 'userinfo' ),
-                       'userinfo'
-               );
-
-               $block = new Block( [
-                       'sitewide' => false,
-               ] );
-               $info = $apiQueryUserInfo->getBlockInfo( $block );
-               $subset = [
-                       'blockid' => null,
-                       'blockedby' => '',
-                       'blockedbyid' => 0,
-                       'blockreason' => '',
-                       'blockexpiry' => 'infinite',
-                       'blockpartial' => true,
-               ];
-               $this->assertArraySubset( $subset, $info );
-       }
-}
index ec443e7..591f27d 100644 (file)
@@ -19,11 +19,10 @@ class GlobalVarConfigTest extends MediaWikiTestCase {
         */
        public function testConstructor( $prefix ) {
                $var = $prefix . 'GlobalVarConfigTest';
-               $rand = wfRandomString();
-               $this->setMwGlobals( $var, $rand );
+               $this->setMwGlobals( $var, 'testvalue' );
                $config = new GlobalVarConfig( $prefix );
                $this->assertInstanceOf( GlobalVarConfig::class, $config );
-               $this->assertEquals( $rand, $config->get( 'GlobalVarConfigTest' ) );
+               $this->assertEquals( 'testvalue', $config->get( 'GlobalVarConfigTest' ) );
        }
 
        public static function provideConstructor() {
@@ -41,7 +40,7 @@ class GlobalVarConfigTest extends MediaWikiTestCase {
         * @covers GlobalVarConfig::hasWithPrefix
         */
        public function testHas() {
-               $this->setMwGlobals( 'wgGlobalVarConfigTestHas', wfRandomString() );
+               $this->setMwGlobals( 'wgGlobalVarConfigTestHas', 'testvalue' );
                $config = new GlobalVarConfig();
                $this->assertTrue( $config->has( 'GlobalVarConfigTestHas' ) );
                $this->assertFalse( $config->has( 'GlobalVarConfigTestNotHas' ) );
index b79cdf3..106a13b 100644 (file)
@@ -30,7 +30,6 @@ use Wikimedia\Rdbms\LBFactorySimple;
 use Wikimedia\Rdbms\LBFactoryMulti;
 use Wikimedia\Rdbms\LoadBalancer;
 use Wikimedia\Rdbms\ChronologyProtector;
-use Wikimedia\Rdbms\DatabaseMysqli;
 use Wikimedia\Rdbms\MySQLMasterPos;
 use Wikimedia\Rdbms\DatabaseDomain;
 
@@ -47,7 +46,7 @@ class LBFactoryTest extends MediaWikiTestCase {
         * @dataProvider getLBFactoryClassProvider
         */
        public function testGetLBFactoryClass( $expected, $deprecated ) {
-               $mockDB = $this->getMockBuilder( DatabaseMysqli::class )
+               $mockDB = $this->getMockBuilder( IDatabase::class )
                        ->disableOriginalConstructor()
                        ->getMock();
 
@@ -291,7 +290,7 @@ class LBFactoryTest extends MediaWikiTestCase {
                $m2Pos = new MySQLMasterPos( 'db1064-bin.002400/794074907', $now );
 
                // Master DB 1
-               $mockDB1 = $this->getMockBuilder( DatabaseMysqli::class )
+               $mockDB1 = $this->getMockBuilder( IDatabase::class )
                        ->disableOriginalConstructor()
                        ->getMock();
                $mockDB1->method( 'writesOrCallbacksPending' )->willReturn( true );
@@ -316,7 +315,7 @@ class LBFactoryTest extends MediaWikiTestCase {
                $lb1->method( 'getMasterPos' )->willReturn( $m1Pos );
                $lb1->method( 'getServerName' )->with( 0 )->willReturn( 'master1' );
                // Master DB 2
-               $mockDB2 = $this->getMockBuilder( DatabaseMysqli::class )
+               $mockDB2 = $this->getMockBuilder( IDatabase::class )
                        ->disableOriginalConstructor()
                        ->getMock();
                $mockDB2->method( 'writesOrCallbacksPending' )->willReturn( true );
index 4195f96..a63521a 100644 (file)
@@ -177,7 +177,7 @@ class TextboxBuilderTest extends MediaWikiTestCase {
                $expected
        ) {
                $this->setMwGlobals( [
-                       // set to trick MWNamespace::getRestrictionLevels
+                       // set to trick NamespaceInfo::getRestrictionLevels
                        'wgRestrictionLevels' => $restrictionLevels
                ] );
 
index 4dc2f9e..8548fde 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 
+use MediaWiki\MediaWikiServices;
 use Wikimedia\TestingAccessWrapper;
 
 /**
@@ -97,7 +98,7 @@ class FileBackendTest extends MediaWikiTestCase {
                        'name' => 'localtesting',
                        'lockManager' => LockManagerGroup::singleton()->get( 'fsLockManager' ),
                        'parallelize' => 'implicit',
-                       'wikiId' => wfWikiID() . wfRandomString(),
+                       'wikiId' => 'testdb',
                        'backends' => [
                                [
                                        'name' => 'localmultitesting1',
@@ -1538,7 +1539,8 @@ class FileBackendTest extends MediaWikiTestCase {
                $url = $this->backend->getFileHttpUrl( [ 'src' => $source ] );
 
                if ( $url !== null ) { // supported
-                       $data = Http::request( "GET", $url, [], __METHOD__ );
+                       $data = MediaWikiServices::getInstance()->getHttpRequestFactory()->
+                               get( $url, [], __METHOD__ );
                        $this->assertEquals( $content, $data,
                                "HTTP GET of URL has right contents ($backendName)." );
                }
@@ -2567,11 +2569,9 @@ class FileBackendTest extends MediaWikiTestCase {
                        'wikiId' => wfWikiID()
                ] ) );
 
-               $name = wfRandomString( 300 );
-
                $input = [
                        'headers' => [
-                               'content-Disposition' => FileBackend::makeContentDisposition( 'inline', $name ),
+                               'content-Disposition' => FileBackend::makeContentDisposition( 'inline', 'name' ),
                                'Content-dUration' => 25.6,
                                'X-LONG-VALUE' => str_pad( '0', 300 ),
                                'CONTENT-LENGTH' => 855055,
@@ -2579,7 +2579,7 @@ class FileBackendTest extends MediaWikiTestCase {
                ];
                $expected = [
                        'headers' => [
-                               'content-disposition' => FileBackend::makeContentDisposition( 'inline', $name ),
+                               'content-disposition' => FileBackend::makeContentDisposition( 'inline', 'name' ),
                                'content-duration' => 25.6,
                                'content-length' => 855055
                        ]
index 4c9855b..346be7a 100644 (file)
@@ -112,7 +112,7 @@ class FileBackendDBRepoWrapperTest extends MediaWikiTestCase {
        }
 
        protected function getMocks() {
-               $dbMock = $this->getMockBuilder( Wikimedia\Rdbms\DatabaseMysqli::class )
+               $dbMock = $this->getMockBuilder( Wikimedia\Rdbms\IDatabase::class )
                        ->disableOriginalClone()
                        ->disableOriginalConstructor()
                        ->getMock();
index 9beea5b..0c78c2b 100644 (file)
@@ -28,7 +28,7 @@ class MigrateFileRepoLayoutTest extends MediaWikiTestCase {
                        ]
                ] );
 
-               $dbMock = $this->getMockBuilder( Wikimedia\Rdbms\DatabaseMysqli::class )
+               $dbMock = $this->getMockBuilder( Wikimedia\Rdbms\IDatabase::class )
                        ->disableOriginalConstructor()
                        ->getMock();
 
index 5a343f6..67de698 100644 (file)
@@ -7,7 +7,7 @@ class RepoGroupTest extends MediaWikiTestCase {
 
        function testHasForeignRepoNegative() {
                $this->setMwGlobals( 'wgForeignFileRepos', [] );
-               RepoGroup::destroySingleton();
+               $this->overrideMwServices();
                FileBackendGroup::destroySingleton();
                $this->assertFalse( RepoGroup::singleton()->hasForeignRepos() );
        }
@@ -27,7 +27,7 @@ class RepoGroupTest extends MediaWikiTestCase {
 
        function testForEachForeignRepoNone() {
                $this->setMwGlobals( 'wgForeignFileRepos', [] );
-               RepoGroup::destroySingleton();
+               $this->overrideMwServices();
                FileBackendGroup::destroySingleton();
                $fakeCallback = $this->createMock( RepoGroupTestHelper::class );
                $fakeCallback->expects( $this->never() )->method( 'callback' );
@@ -48,7 +48,7 @@ class RepoGroupTest extends MediaWikiTestCase {
                        'apiThumbCacheExpiry' => 86400,
                        'directory' => $wgUploadDirectory
                ] ] );
-               RepoGroup::destroySingleton();
+               $this->overrideMwServices();
                FileBackendGroup::destroySingleton();
        }
 }
index eee4296..a8c53d9 100644 (file)
@@ -67,6 +67,8 @@ class HttpTest extends MediaWikiTestCase {
         * @covers Http::getProxy
         */
        public function testGetProxy() {
+               $this->hideDeprecated( 'Http::getProxy' );
+
                $this->setMwGlobals( 'wgHTTPProxy', false );
                $this->assertEquals(
                        '',
index 1baaa54..ce07f78 100644 (file)
@@ -259,8 +259,7 @@ class JobQueueTest extends MediaWikiTestCase {
                $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" );
                $this->assertEquals( 0, $queue->getAcquiredCount(), "Queue is empty ($desc)" );
 
-               $id = wfRandomString( 32 );
-               $root1 = Job::newRootJobParams( "nulljobspam:$id" ); // task ID/timestamp
+               $root1 = Job::newRootJobParams( "nulljobspam:testId" ); // task ID/timestamp
                for ( $i = 0; $i < 5; ++$i ) {
                        $this->assertNull( $queue->push( $this->newJob( 0, $root1 ) ), "Push worked ($desc)" );
                }
index ef333f9..4c93789 100644 (file)
@@ -85,9 +85,9 @@ class CSSMinTest extends MediaWikiTestCase {
         * @covers CSSMin::getMimeType
         */
        public function testGetMimeType( $fileContents, $fileExtension, $expected ) {
-               $fileName = wfTempDir() . DIRECTORY_SEPARATOR . uniqid( 'MW_PHPUnit_CSSMinTest_' ) . '.'
-                       . $fileExtension;
-               $this->addTmpFiles( $fileName );
+               // Automatically removed when it falls out of scope (including if the test fails)
+               $file = TempFSFile::factory( 'PHPUnit_CSSMinTest_', $fileExtension, wfTempDir() );
+               $fileName = $file->getPath();
                file_put_contents( $fileName, $fileContents );
                $this->assertSame( $expected, CSSMin::getMimeType( $fileName ) );
        }
index 0376803..9f88474 100644 (file)
@@ -28,8 +28,8 @@ class MultiWriteBagOStuffTest extends MediaWikiTestCase {
         * @covers MultiWriteBagOStuff::doWrite
         */
        public function testSetImmediate() {
-               $key = wfRandomString();
-               $value = wfRandomString();
+               $key = 'key';
+               $value = 'value';
                $this->cache->set( $key, $value );
 
                // Set in tier 1
@@ -42,8 +42,8 @@ class MultiWriteBagOStuffTest extends MediaWikiTestCase {
         * @covers MultiWriteBagOStuff
         */
        public function testSyncMerge() {
-               $key = wfRandomString();
-               $value = wfRandomString();
+               $key = 'keyA';
+               $value = 'value';
                $func = function () use ( $value ) {
                        return $value;
                };
@@ -56,14 +56,14 @@ class MultiWriteBagOStuffTest extends MediaWikiTestCase {
                // Set in tier 1
                $this->assertEquals( $value, $this->cache1->get( $key ), 'Written to tier 1' );
                // Not yet set in tier 2
-               $this->assertEquals( false, $this->cache2->get( $key ), 'Not written to tier 2' );
+               $this->assertFalse( $this->cache2->get( $key ), 'Not written to tier 2' );
 
                $dbw->commit();
 
                // Set in tier 2
                $this->assertEquals( $value, $this->cache2->get( $key ), 'Written to tier 2' );
 
-               $key = wfRandomString();
+               $key = 'keyB';
 
                $dbw->begin();
                $this->cache->merge( $key, $func, 0, 1, BagOStuff::WRITE_SYNC );
@@ -80,8 +80,8 @@ class MultiWriteBagOStuffTest extends MediaWikiTestCase {
         * @covers MultiWriteBagOStuff::set
         */
        public function testSetDelayed() {
-               $key = wfRandomString();
-               $value = (object)[ 'v' => wfRandomString() ];
+               $key = 'key';
+               $value = (object)[ 'v' => 'saved value' ];
                $expectValue = clone $value;
 
                // XXX: DeferredUpdates bound to transactions in CLI mode
@@ -90,12 +90,12 @@ class MultiWriteBagOStuffTest extends MediaWikiTestCase {
                $this->cache->set( $key, $value );
 
                // Test that later changes to $value don't affect the saved value (e.g. T168040)
-               $value->v = 'bogus';
+               $value->v = 'other value';
 
                // Set in tier 1
                $this->assertEquals( $expectValue, $this->cache1->get( $key ), 'Written to tier 1' );
                // Not yet set in tier 2
-               $this->assertEquals( false, $this->cache2->get( $key ), 'Not written to tier 2' );
+               $this->assertFalse( $this->cache2->get( $key ), 'Not written to tier 2' );
 
                $dbw->commit();
 
index b7f22ec..550ec0b 100644 (file)
@@ -23,40 +23,40 @@ class ReplicatedBagOStuffTest extends MediaWikiTestCase {
         * @covers ReplicatedBagOStuff::set
         */
        public function testSet() {
-               $key = wfRandomString();
-               $value = wfRandomString();
+               $key = 'a key';
+               $value = 'a value';
                $this->cache->set( $key, $value );
 
                // Write to master.
-               $this->assertEquals( $this->writeCache->get( $key ), $value );
+               $this->assertEquals( $value, $this->writeCache->get( $key ) );
                // Don't write to replica. Replication is deferred to backend.
-               $this->assertEquals( $this->readCache->get( $key ), false );
+               $this->assertFalse( $this->readCache->get( $key ) );
        }
 
        /**
         * @covers ReplicatedBagOStuff::get
         */
        public function testGet() {
-               $key = wfRandomString();
+               $key = 'a key';
 
-               $write = wfRandomString();
+               $write = 'one value';
                $this->writeCache->set( $key, $write );
-               $read = wfRandomString();
+               $read = 'another value';
                $this->readCache->set( $key, $read );
 
                // Read from replica.
-               $this->assertEquals( $this->cache->get( $key ), $read );
+               $this->assertEquals( $read, $this->cache->get( $key ) );
        }
 
        /**
         * @covers ReplicatedBagOStuff::get
         */
        public function testGetAbsent() {
-               $key = wfRandomString();
-               $value = wfRandomString();
+               $key = 'a key';
+               $value = 'a value';
                $this->writeCache->set( $key, $value );
 
                // Don't read from master. No failover if value is absent.
-               $this->assertEquals( $this->cache->get( $key ), false );
+               $this->assertFalse( $this->cache->get( $key ) );
        }
 }
index 27e5a65..83ce3d2 100644 (file)
@@ -19,10 +19,18 @@ class LinkRendererFactoryTest extends MediaWikiLangTestCase {
         */
        private $linkCache;
 
+       /**
+        * @var NamespaceInfo
+        */
+       private $nsInfo;
+
        public function setUp() {
                parent::setUp();
-               $this->titleFormatter = MediaWikiServices::getInstance()->getTitleFormatter();
-               $this->linkCache = MediaWikiServices::getInstance()->getLinkCache();
+
+               $services = MediaWikiServices::getInstance();
+               $this->titleFormatter = $services->getTitleFormatter();
+               $this->linkCache = $services->getLinkCache();
+               $this->nsInfo = $services->getNamespaceInfo();
        }
 
        public static function provideCreateFromLegacyOptions() {
@@ -54,7 +62,8 @@ class LinkRendererFactoryTest extends MediaWikiLangTestCase {
         * @dataProvider provideCreateFromLegacyOptions
         */
        public function testCreateFromLegacyOptions( $options, $func, $val ) {
-               $factory = new LinkRendererFactory( $this->titleFormatter, $this->linkCache );
+               $factory =
+                       new LinkRendererFactory( $this->titleFormatter, $this->linkCache, $this->nsInfo );
                $linkRenderer = $factory->createFromLegacyOptions(
                        $options
                );
@@ -63,7 +72,8 @@ class LinkRendererFactoryTest extends MediaWikiLangTestCase {
        }
 
        public function testCreate() {
-               $factory = new LinkRendererFactory( $this->titleFormatter, $this->linkCache );
+               $factory =
+                       new LinkRendererFactory( $this->titleFormatter, $this->linkCache, $this->nsInfo );
                $this->assertInstanceOf( LinkRenderer::class, $factory->create() );
        }
 
@@ -74,7 +84,8 @@ class LinkRendererFactoryTest extends MediaWikiLangTestCase {
                $user->expects( $this->once() )
                        ->method( 'getStubThreshold' )
                        ->willReturn( 15 );
-               $factory = new LinkRendererFactory( $this->titleFormatter, $this->linkCache );
+               $factory =
+                       new LinkRendererFactory( $this->titleFormatter, $this->linkCache, $this->nsInfo );
                $linkRenderer = $factory->createForUser( $user );
                $this->assertInstanceOf( LinkRenderer::class, $linkRenderer );
                $this->assertEquals( 15, $linkRenderer->getStubThreshold() );
index 91ee276..d4e1961 100644 (file)
@@ -140,7 +140,8 @@ class LinkRendererTest extends MediaWikiLangTestCase {
        public function testGetLinkClasses() {
                $wanCache = ObjectCache::getMainWANInstance();
                $titleFormatter = MediaWikiServices::getInstance()->getTitleFormatter();
-               $linkCache = new LinkCache( $titleFormatter, $wanCache );
+               $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
+               $linkCache = new LinkCache( $titleFormatter, $wanCache, $nsInfo );
                $foobarTitle = new TitleValue( NS_MAIN, 'FooBar' );
                $redirectTitle = new TitleValue( NS_MAIN, 'Redirect' );
                $userTitle = new TitleValue( NS_USER, 'Someuser' );
@@ -164,7 +165,7 @@ class LinkRendererTest extends MediaWikiLangTestCase {
                        0 // redir
                );
 
-               $linkRenderer = new LinkRenderer( $titleFormatter, $linkCache );
+               $linkRenderer = new LinkRenderer( $titleFormatter, $linkCache, $nsInfo );
                $linkRenderer->setStubThreshold( 0 );
                $this->assertEquals(
                        '',
index bcd5c37..a00eb3f 100644 (file)
@@ -52,11 +52,19 @@ class DefaultPreferencesFactoryTest extends \MediaWikiTestCase {
         * @return DefaultPreferencesFactory
         */
        protected function getPreferencesFactory() {
+               $mockNsInfo = $this->createMock( NamespaceInfo::class );
+               $mockNsInfo->method( 'getValidNamespaces' )->willReturn( [
+                       NS_MAIN, NS_TALK, NS_USER, NS_USER_TALK
+               ] );
+               $mockNsInfo->expects( $this->never() )
+                       ->method( $this->anythingBut( 'getValidNamespaces', '__destruct' ) );
+
                return new DefaultPreferencesFactory(
                        new ServiceOptions( DefaultPreferencesFactory::$constructorOptions, $this->config ),
                        new Language(),
                        AuthManager::singleton(),
-                       MediaWikiServices::getInstance()->getLinkRenderer()
+                       MediaWikiServices::getInstance()->getLinkRenderer(),
+                       $mockNsInfo
                );
        }
 
index 21b6468..556c640 100644 (file)
  * @file
  */
 
-use MediaWiki\MediaWikiServices;
+use MediaWiki\Config\ServiceOptions;
 
 class NamespaceInfoTest extends MediaWikiTestCase {
+       /**********************************************************************************************
+        * Shared code
+        * %{
+        */
+       private $scopedCallback;
 
-       /** @var NamespaceInfo */
-       private $obj;
-
-       protected function setUp() {
+       public function setUp() {
                parent::setUp();
 
-               $this->setMwGlobals( [
-                       'wgContentNamespaces' => [ NS_MAIN ],
-                       'wgNamespacesWithSubpages' => [
-                               NS_TALK => true,
-                               NS_USER => true,
-                               NS_USER_TALK => true,
-                       ],
-                       'wgCapitalLinks' => true,
-                       'wgCapitalLinkOverrides' => [],
-                       'wgNonincludableNamespaces' => [],
-               ] );
-
-               $this->obj = MediaWikiServices::getInstance()->getNamespaceInfo();
-       }
+               // Boo, there's still some global state in the class :(
+               global $wgHooks;
+               $hooks = $wgHooks;
+               unset( $hooks['CanonicalNamespaces'] );
+               $this->setMwGlobals( 'wgHooks', $hooks );
 
-       /**
-        * @todo Write more texts, handle $wgAllowImageMoving setting
-        * @covers NamespaceInfo::isMovable
-        */
-       public function testIsMovable() {
-               $this->assertFalse( $this->obj->isMovable( NS_SPECIAL ) );
+               $this->scopedCallback =
+                       ExtensionRegistry::getInstance()->setAttributeForTest( 'ExtensionNamespaces', [] );
        }
 
-       private function assertIsSubject( $ns ) {
-               $this->assertTrue( $this->obj->isSubject( $ns ) );
-       }
+       public function tearDown() {
+               $this->scopedCallback = null;
 
-       private function assertIsNotSubject( $ns ) {
-               $this->assertFalse( $this->obj->isSubject( $ns ) );
+               parent::tearDown();
        }
 
        /**
-        * Please make sure to change testIsTalk() if you change the assertions below
-        * @covers NamespaceInfo::isSubject
+        * TODO Make this a const once HHVM support is dropped (T192166)
         */
-       public function testIsSubject() {
-               // Special namespaces
-               $this->assertIsSubject( NS_MEDIA );
-               $this->assertIsSubject( NS_SPECIAL );
-
-               // Subject pages
-               $this->assertIsSubject( NS_MAIN );
-               $this->assertIsSubject( NS_USER );
-               $this->assertIsSubject( 100 ); # user defined
-
-               // Talk pages
-               $this->assertIsNotSubject( NS_TALK );
-               $this->assertIsNotSubject( NS_USER_TALK );
-               $this->assertIsNotSubject( 101 ); # user defined
+       private static $defaultOptions = [
+               'AllowImageMoving' => true,
+               'CanonicalNamespaceNames' => [
+                       NS_TALK => 'Talk',
+                       NS_USER => 'User',
+                       NS_USER_TALK => 'User_talk',
+                       NS_SPECIAL => 'Special',
+                       NS_MEDIA => 'Media',
+               ],
+               'CapitalLinkOverrides' => [],
+               'CapitalLinks' => true,
+               'ContentNamespaces' => [ NS_MAIN ],
+               'ExtraNamespaces' => [],
+               'ExtraSignatureNamespaces' => [],
+               'NamespaceContentModels' => [],
+               'NamespaceProtection' => [],
+               'NamespacesWithSubpages' => [
+                       NS_TALK => true,
+                       NS_USER => true,
+                       NS_USER_TALK => true,
+               ],
+               'NonincludableNamespaces' => [],
+               'RestrictionLevels' => [ '', 'autoconfirmed', 'sysop' ],
+       ];
+
+       private function newObj( array $options = [] ) : NamespaceInfo {
+               return new NamespaceInfo( new ServiceOptions( NamespaceInfo::$constructorOptions,
+                       $options, self::$defaultOptions ) );
        }
 
-       private function assertIsTalk( $ns ) {
-               $this->assertTrue( $this->obj->isTalk( $ns ) );
-       }
+       // %} End shared code
 
-       private function assertIsNotTalk( $ns ) {
-               $this->assertFalse( $this->obj->isTalk( $ns ) );
-       }
+       /**********************************************************************************************
+        * Basic methods
+        * %{
+        */
 
        /**
-        * Reverse of testIsSubject().
-        * Please update testIsSubject() if you change assertions below
-        * @covers NamespaceInfo::isTalk
+        * @covers NamespaceInfo::__construct
+        * @dataProvider provideConstructor
+        * @param ServiceOptions $options
+        * @param string|null $expectedExceptionText
         */
-       public function testIsTalk() {
-               // Special namespaces
-               $this->assertIsNotTalk( NS_MEDIA );
-               $this->assertIsNotTalk( NS_SPECIAL );
-
-               // Subject pages
-               $this->assertIsNotTalk( NS_MAIN );
-               $this->assertIsNotTalk( NS_USER );
-               $this->assertIsNotTalk( 100 ); # user defined
+       public function testConstructor( ServiceOptions $options, $expectedExceptionText = null ) {
+               if ( $expectedExceptionText !== null ) {
+                       $this->setExpectedException( \Wikimedia\Assert\PreconditionException::class,
+                               $expectedExceptionText );
+               }
+               new NamespaceInfo( $options );
+               $this->assertTrue( true );
+       }
 
-               // Talk pages
-               $this->assertIsTalk( NS_TALK );
-               $this->assertIsTalk( NS_USER_TALK );
-               $this->assertIsTalk( 101 ); # user defined
+       public function provideConstructor() {
+               return [
+                       [ new ServiceOptions( NamespaceInfo::$constructorOptions, self::$defaultOptions ) ],
+                       [ new ServiceOptions( [], [] ), 'Required options missing: ' ],
+                       [ new ServiceOptions(
+                               array_merge( NamespaceInfo::$constructorOptions, [ 'invalid' ] ),
+                               self::$defaultOptions,
+                               [ 'invalid' => '' ]
+                       ), 'Unsupported options passed: invalid' ],
+               ];
        }
 
        /**
-        * @covers NamespaceInfo::getSubject
+        * @dataProvider provideIsMovable
+        * @covers NamespaceInfo::isMovable
+        *
+        * @param bool $expected
+        * @param int $ns
+        * @param bool $allowImageMoving
         */
-       public function testGetSubject() {
-               // Special namespaces are their own subjects
-               $this->assertEquals( NS_MEDIA, $this->obj->getSubject( NS_MEDIA ) );
-               $this->assertEquals( NS_SPECIAL, $this->obj->getSubject( NS_SPECIAL ) );
-
-               $this->assertEquals( NS_MAIN, $this->obj->getSubject( NS_TALK ) );
-               $this->assertEquals( NS_USER, $this->obj->getSubject( NS_USER_TALK ) );
+       public function testIsMovable( $expected, $ns, $allowImageMoving = true ) {
+               $obj = $this->newObj( [ 'AllowImageMoving' => $allowImageMoving ] );
+               $this->assertSame( $expected, $obj->isMovable( $ns ) );
        }
 
-       /**
-        * Regular getTalk() calls
-        * Namespaces without a talk page (NS_MEDIA, NS_SPECIAL) are tested in
-        * the function testGetTalkExceptions()
-        * @covers NamespaceInfo::getTalk
-        */
-       public function testGetTalk() {
-               $this->assertEquals( NS_TALK, $this->obj->getTalk( NS_MAIN ) );
-               $this->assertEquals( NS_TALK, $this->obj->getTalk( NS_TALK ) );
-               $this->assertEquals( NS_USER_TALK, $this->obj->getTalk( NS_USER ) );
-               $this->assertEquals( NS_USER_TALK, $this->obj->getTalk( NS_USER_TALK ) );
+       public function provideIsMovable() {
+               return [
+                       'Main' => [ true, NS_MAIN ],
+                       'Talk' => [ true, NS_TALK ],
+                       'Special' => [ false, NS_SPECIAL ],
+                       'Nonexistent even namespace' => [ true, 1234 ],
+                       'Nonexistent odd namespace' => [ true, 12345 ],
+
+                       'Media with image moving' => [ false, NS_MEDIA, true ],
+                       'Media with no image moving' => [ false, NS_MEDIA, false ],
+                       'File with image moving' => [ true, NS_FILE, true ],
+                       'File with no image moving' => [ false, NS_FILE, false ],
+               ];
        }
 
        /**
-        * Exceptions with getTalk()
-        * NS_MEDIA does not have talk pages. MediaWiki raise an exception for them.
-        * @expectedException MWException
-        * @covers NamespaceInfo::getTalk
+        * @param int $ns
+        * @param bool $expected
+        * @dataProvider provideIsSubject
+        * @covers NamespaceInfo::isSubject
         */
-       public function testGetTalkExceptionsForNsMedia() {
-               $this->assertNull( $this->obj->getTalk( NS_MEDIA ) );
+       public function testIsSubject( $ns, $expected ) {
+               $this->assertSame( $expected, $this->newObj()->isSubject( $ns ) );
        }
 
        /**
-        * Exceptions with getTalk()
-        * NS_SPECIAL does not have talk pages. MediaWiki raise an exception for them.
-        * @expectedException MWException
-        * @covers NamespaceInfo::getTalk
+        * @param int $ns
+        * @param bool $expected
+        * @dataProvider provideIsSubject
+        * @covers NamespaceInfo::isTalk
         */
-       public function testGetTalkExceptionsForNsSpecial() {
-               $this->assertNull( $this->obj->getTalk( NS_SPECIAL ) );
+       public function testIsTalk( $ns, $expected ) {
+               $this->assertSame( !$expected, $this->newObj()->isTalk( $ns ) );
        }
 
-       /**
-        * Regular getAssociated() calls
-        * Namespaces without an associated page (NS_MEDIA, NS_SPECIAL) are tested in
-        * the function testGetAssociatedExceptions()
-        * @covers NamespaceInfo::getAssociated
-        */
-       public function testGetAssociated() {
-               $this->assertEquals( NS_TALK, $this->obj->getAssociated( NS_MAIN ) );
-               $this->assertEquals( NS_MAIN, $this->obj->getAssociated( NS_TALK ) );
+       public function provideIsSubject() {
+               return [
+                       // Special namespaces
+                       [ NS_MEDIA, true ],
+                       [ NS_SPECIAL, true ],
+
+                       // Subject pages
+                       [ NS_MAIN, true ],
+                       [ NS_USER, true ],
+                       [ 100, true ],
+
+                       // Talk pages
+                       [ NS_TALK, false ],
+                       [ NS_USER_TALK, false ],
+                       [ 101, false ],
+               ];
        }
 
-       # ## Exceptions with getAssociated()
-       # ## NS_MEDIA and NS_SPECIAL do not have talk pages. MediaWiki raises
-       # ## an exception for them.
        /**
-        * @expectedException MWException
-        * @covers NamespaceInfo::getAssociated
+        * @covers NamespaceInfo::exists
+        * @dataProvider provideExists
+        * @param int $ns
+        * @param bool $expected
         */
-       public function testGetAssociatedExceptionsForNsMedia() {
-               $this->assertNull( $this->obj->getAssociated( NS_MEDIA ) );
+       public function testExists( $ns, $expected ) {
+               $this->assertSame( $expected, $this->newObj()->exists( $ns ) );
        }
 
-       /**
-        * @expectedException MWException
-        * @covers NamespaceInfo::getAssociated
-        */
-       public function testGetAssociatedExceptionsForNsSpecial() {
-               $this->assertNull( $this->obj->getAssociated( NS_SPECIAL ) );
+       public function provideExists() {
+               return [
+                       'Main' => [ NS_MAIN, true ],
+                       'Talk' => [ NS_TALK, true ],
+                       'Media' => [ NS_MEDIA, true ],
+                       'Special' => [ NS_SPECIAL, true ],
+                       'Nonexistent' => [ 12345, false ],
+                       'Negative nonexistent' => [ -12345, false ],
+               ];
        }
 
        /**
         * Note if we add a namespace registration system with keys like 'MAIN'
-        * we should add tests here for equivilance on things like 'MAIN' == 0
+        * we should add tests here for equivalence on things like 'MAIN' == 0
         * and 'MAIN' == NS_MAIN.
         * @covers NamespaceInfo::equals
         */
        public function testEquals() {
-               $this->assertTrue( $this->obj->equals( NS_MAIN, NS_MAIN ) );
-               $this->assertTrue( $this->obj->equals( NS_MAIN, 0 ) ); // In case we make NS_MAIN 'MAIN'
-               $this->assertTrue( $this->obj->equals( NS_USER, NS_USER ) );
-               $this->assertTrue( $this->obj->equals( NS_USER, 2 ) );
-               $this->assertTrue( $this->obj->equals( NS_USER_TALK, NS_USER_TALK ) );
-               $this->assertTrue( $this->obj->equals( NS_SPECIAL, NS_SPECIAL ) );
-               $this->assertFalse( $this->obj->equals( NS_MAIN, NS_TALK ) );
-               $this->assertFalse( $this->obj->equals( NS_USER, NS_USER_TALK ) );
-               $this->assertFalse( $this->obj->equals( NS_PROJECT, NS_TEMPLATE ) );
+               $obj = $this->newObj();
+               $this->assertTrue( $obj->equals( NS_MAIN, NS_MAIN ) );
+               $this->assertTrue( $obj->equals( NS_MAIN, 0 ) ); // In case we make NS_MAIN 'MAIN'
+               $this->assertTrue( $obj->equals( NS_USER, NS_USER ) );
+               $this->assertTrue( $obj->equals( NS_USER, 2 ) );
+               $this->assertTrue( $obj->equals( NS_USER_TALK, NS_USER_TALK ) );
+               $this->assertTrue( $obj->equals( NS_SPECIAL, NS_SPECIAL ) );
+               $this->assertFalse( $obj->equals( NS_MAIN, NS_TALK ) );
+               $this->assertFalse( $obj->equals( NS_USER, NS_USER_TALK ) );
+               $this->assertFalse( $obj->equals( NS_PROJECT, NS_TEMPLATE ) );
        }
 
        /**
+        * @param int $ns1
+        * @param int $ns2
+        * @param bool $expected
+        * @dataProvider provideSubjectEquals
         * @covers NamespaceInfo::subjectEquals
         */
-       public function testSubjectEquals() {
-               $this->assertSameSubject( NS_MAIN, NS_MAIN );
-               $this->assertSameSubject( NS_MAIN, 0 ); // In case we make NS_MAIN 'MAIN'
-               $this->assertSameSubject( NS_USER, NS_USER );
-               $this->assertSameSubject( NS_USER, 2 );
-               $this->assertSameSubject( NS_USER_TALK, NS_USER_TALK );
-               $this->assertSameSubject( NS_SPECIAL, NS_SPECIAL );
-               $this->assertSameSubject( NS_MAIN, NS_TALK );
-               $this->assertSameSubject( NS_USER, NS_USER_TALK );
+       public function testSubjectEquals( $ns1, $ns2, $expected ) {
+               $this->assertSame( $expected, $this->newObj()->subjectEquals( $ns1, $ns2 ) );
+       }
 
-               $this->assertDifferentSubject( NS_PROJECT, NS_TEMPLATE );
-               $this->assertDifferentSubject( NS_SPECIAL, NS_MAIN );
+       public function provideSubjectEquals() {
+               return [
+                       [ NS_MAIN, NS_MAIN, true ],
+                       // In case we make NS_MAIN 'MAIN'
+                       [ NS_MAIN, 0, true ],
+                       [ NS_USER, NS_USER, true ],
+                       [ NS_USER, 2, true ],
+                       [ NS_USER_TALK, NS_USER_TALK, true ],
+                       [ NS_SPECIAL, NS_SPECIAL, true ],
+                       [ NS_MAIN, NS_TALK, true ],
+                       [ NS_USER, NS_USER_TALK, true ],
+
+                       [ NS_PROJECT, NS_TEMPLATE, false ],
+                       [ NS_SPECIAL, NS_MAIN, false ],
+                       [ NS_MEDIA, NS_SPECIAL, false ],
+                       [ NS_SPECIAL, NS_MEDIA, false ],
+               ];
        }
 
        /**
-        * @covers NamespaceInfo::subjectEquals
+        * @dataProvider provideHasTalkNamespace
+        * @covers NamespaceInfo::hasTalkNamespace
+        *
+        * @param int $ns
+        * @param bool $expected
         */
-       public function testSpecialAndMediaAreDifferentSubjects() {
-               $this->assertDifferentSubject(
-                       NS_MEDIA, NS_SPECIAL,
-                       "NS_MEDIA and NS_SPECIAL are different subject namespaces"
-               );
-               $this->assertDifferentSubject(
-                       NS_SPECIAL, NS_MEDIA,
-                       "NS_SPECIAL and NS_MEDIA are different subject namespaces"
-               );
+       public function testHasTalkNamespace( $ns, $expected ) {
+               $this->assertSame( $expected, $this->newObj()->hasTalkNamespace( $ns ) );
        }
 
        public function provideHasTalkNamespace() {
@@ -235,178 +263,180 @@ class NamespaceInfoTest extends MediaWikiTestCase {
        }
 
        /**
-        * @dataProvider provideHasTalkNamespace
-        * @covers NamespaceInfo::hasTalkNamespace
-        *
-        * @param int $index
+        * @param int $ns
         * @param bool $expected
+        * @param array $contentNamespaces
+        * @covers NamespaceInfo::isContent
+        * @dataProvider provideIsContent
         */
-       public function testHasTalkNamespace( $index, $expected ) {
-               $actual = $this->obj->hasTalkNamespace( $index );
-               $this->assertSame( $actual, $expected, "NS $index" );
-       }
-
-       private function assertIsContent( $ns ) {
-               $this->assertTrue( $this->obj->isContent( $ns ) );
+       public function testIsContent( $ns, $expected, $contentNamespaces = [ NS_MAIN ] ) {
+               $obj = $this->newObj( [ 'ContentNamespaces' => $contentNamespaces ] );
+               $this->assertSame( $expected, $obj->isContent( $ns ) );
        }
 
-       private function assertIsNotContent( $ns ) {
-               $this->assertFalse( $this->obj->isContent( $ns ) );
+       public function provideIsContent() {
+               return [
+                       [ NS_MAIN, true ],
+                       [ NS_MEDIA, false ],
+                       [ NS_SPECIAL, false ],
+                       [ NS_TALK, false ],
+                       [ NS_USER, false ],
+                       [ NS_CATEGORY, false ],
+                       [ 100, false ],
+                       [ 100, true, [ NS_MAIN, 100, 252 ] ],
+                       [ 252, true, [ NS_MAIN, 100, 252 ] ],
+                       [ NS_MAIN, true, [ NS_MAIN, 100, 252 ] ],
+                       // NS_MAIN is always content
+                       [ NS_MAIN, true, [] ],
+               ];
        }
 
        /**
-        * @covers NamespaceInfo::isContent
+        * @dataProvider provideWantSignatures
+        * @covers NamespaceInfo::wantSignatures
+        *
+        * @param int $index
+        * @param bool $expected
         */
-       public function testIsContent() {
-               // NS_MAIN is a content namespace per DefaultSettings.php
-               // and per function definition.
-
-               $this->assertIsContent( NS_MAIN );
-
-               // Other namespaces which are not expected to be content
+       public function testWantSignatures( $index, $expected ) {
+               $this->assertSame( $expected, $this->newObj()->wantSignatures( $index ) );
+       }
 
-               $this->assertIsNotContent( NS_MEDIA );
-               $this->assertIsNotContent( NS_SPECIAL );
-               $this->assertIsNotContent( NS_TALK );
-               $this->assertIsNotContent( NS_USER );
-               $this->assertIsNotContent( NS_CATEGORY );
-               $this->assertIsNotContent( 100 );
+       public function provideWantSignatures() {
+               return [
+                       'Main' => [ NS_MAIN, false ],
+                       'Talk' => [ NS_TALK, true ],
+                       'User' => [ NS_USER, false ],
+                       'User talk' => [ NS_USER_TALK, true ],
+                       'Special' => [ NS_SPECIAL, false ],
+                       'Media' => [ NS_MEDIA, false ],
+                       'Nonexistent talk' => [ 12345, true ],
+                       'Nonexistent subject' => [ 123456, false ],
+                       'Nonexistent negative odd' => [ -12345, false ],
+               ];
        }
 
        /**
-        * Similar to testIsContent() but alters the $wgContentNamespaces
-        * global variable.
-        * @covers NamespaceInfo::isContent
+        * @dataProvider provideWantSignatures_ExtraSignatureNamespaces
+        * @covers NamespaceInfo::wantSignatures
+        *
+        * @param int $index
+        * @param int $expected
         */
-       public function testIsContentAdvanced() {
-               global $wgContentNamespaces;
-
-               // Test that user defined namespace #252 is not content
-               $this->assertIsNotContent( 252 );
-
-               // Bless namespace # 252 as a content namespace
-               $wgContentNamespaces[] = 252;
-
-               $this->assertIsContent( 252 );
-
-               // Makes sure NS_MAIN was not impacted
-               $this->assertIsContent( NS_MAIN );
+       public function testWantSignatures_ExtraSignatureNamespaces( $index, $expected ) {
+               $obj = $this->newObj( [ 'ExtraSignatureNamespaces' =>
+                       [ NS_MAIN, NS_USER, NS_SPECIAL, NS_MEDIA, 123456, -12345 ] ] );
+               $this->assertSame( $expected, $obj->wantSignatures( $index ) );
        }
 
-       private function assertIsWatchable( $ns ) {
-               $this->assertTrue( $this->obj->isWatchable( $ns ) );
-       }
+       public function provideWantSignatures_ExtraSignatureNamespaces() {
+               $ret = array_map(
+                       function ( $arr ) {
+                               // We've added all these as extra signature namespaces, so expect true
+                               return [ $arr[0], true ];
+                       },
+                       self::provideWantSignatures()
+               );
 
-       private function assertIsNotWatchable( $ns ) {
-               $this->assertFalse( $this->obj->isWatchable( $ns ) );
+               // Add one more that's false
+               $ret['Another nonexistent subject'] = [ 12345678, false ];
+               return $ret;
        }
 
        /**
+        * @param int $ns
+        * @param bool $expected
         * @covers NamespaceInfo::isWatchable
+        * @dataProvider provideIsWatchable
         */
-       public function testIsWatchable() {
-               // Specials namespaces are not watchable
-               $this->assertIsNotWatchable( NS_MEDIA );
-               $this->assertIsNotWatchable( NS_SPECIAL );
-
-               // Core defined namespaces are watchables
-               $this->assertIsWatchable( NS_MAIN );
-               $this->assertIsWatchable( NS_TALK );
-
-               // Additional, user defined namespaces are watchables
-               $this->assertIsWatchable( 100 );
-               $this->assertIsWatchable( 101 );
+       public function testIsWatchable( $ns, $expected ) {
+               $this->assertSame( $expected, $this->newObj()->isWatchable( $ns ) );
        }
 
-       private function assertHasSubpages( $ns ) {
-               $this->assertTrue( $this->obj->hasSubpages( $ns ) );
-       }
+       public function provideIsWatchable() {
+               return [
+                       // Specials namespaces are not watchable
+                       [ NS_MEDIA, false ],
+                       [ NS_SPECIAL, false ],
 
-       private function assertHasNotSubpages( $ns ) {
-               $this->assertFalse( $this->obj->hasSubpages( $ns ) );
+                       // Core defined namespaces are watchables
+                       [ NS_MAIN, true ],
+                       [ NS_TALK, true ],
+
+                       // Additional, user defined namespaces are watchables
+                       [ 100, true ],
+                       [ 101, true ],
+               ];
        }
 
        /**
+        * @param int $ns
+        * @param int $expected
+        * @param array|null $namespacesWithSubpages To pass to constructor
         * @covers NamespaceInfo::hasSubpages
+        * @dataProvider provideHasSubpages
         */
-       public function testHasSubpages() {
-               global $wgNamespacesWithSubpages;
-
-               // Special namespaces:
-               $this->assertHasNotSubpages( NS_MEDIA );
-               $this->assertHasNotSubpages( NS_SPECIAL );
-
-               // Namespaces without subpages
-               $this->assertHasNotSubpages( NS_MAIN );
+       public function testHasSubpages( $ns, $expected, array $namespacesWithSubpages = null ) {
+               $obj = $this->newObj( $namespacesWithSubpages
+                       ? [ 'NamespacesWithSubpages' => $namespacesWithSubpages ]
+                       : [] );
+               $this->assertSame( $expected, $obj->hasSubpages( $ns ) );
+       }
 
-               $wgNamespacesWithSubpages[NS_MAIN] = true;
-               $this->assertHasSubpages( NS_MAIN );
+       public function provideHasSubpages() {
+               return [
+                       // Special namespaces:
+                       [ NS_MEDIA, false ],
+                       [ NS_SPECIAL, false ],
 
-               $wgNamespacesWithSubpages[NS_MAIN] = false;
-               $this->assertHasNotSubpages( NS_MAIN );
+                       // Namespaces without subpages
+                       [ NS_MAIN, false ],
+                       [ NS_MAIN, true, [ NS_MAIN => true ] ],
+                       [ NS_MAIN, false, [ NS_MAIN => false ] ],
 
-               // Some namespaces with subpages
-               $this->assertHasSubpages( NS_TALK );
-               $this->assertHasSubpages( NS_USER );
-               $this->assertHasSubpages( NS_USER_TALK );
+                       // Some namespaces with subpages
+                       [ NS_TALK, true ],
+                       [ NS_USER, true ],
+                       [ NS_USER_TALK, true ],
+               ];
        }
 
        /**
+        * @param $contentNamespaces To pass to constructor
+        * @param array $expected
+        * @dataProvider provideGetContentNamespaces
         * @covers NamespaceInfo::getContentNamespaces
         */
-       public function testGetContentNamespaces() {
-               global $wgContentNamespaces;
-
-               $this->assertEquals(
-                       [ NS_MAIN ],
-                       $this->obj->getContentNamespaces(),
-                       '$wgContentNamespaces is an array with only NS_MAIN by default'
-               );
-
-               # test !is_array( $wgcontentNamespaces )
-               $wgContentNamespaces = '';
-               $this->assertEquals( [ NS_MAIN ], $this->obj->getContentNamespaces() );
-
-               $wgContentNamespaces = false;
-               $this->assertEquals( [ NS_MAIN ], $this->obj->getContentNamespaces() );
-
-               $wgContentNamespaces = null;
-               $this->assertEquals( [ NS_MAIN ], $this->obj->getContentNamespaces() );
+       public function testGetContentNamespaces( $contentNamespaces, array $expected ) {
+               $obj = $this->newObj( [ 'ContentNamespaces' => $contentNamespaces ] );
+               $this->assertSame( $expected, $obj->getContentNamespaces() );
+       }
 
-               $wgContentNamespaces = 5;
-               $this->assertEquals( [ NS_MAIN ], $this->obj->getContentNamespaces() );
+       public function provideGetContentNamespaces() {
+               return [
+                       // Non-array
+                       [ '', [ NS_MAIN ] ],
+                       [ false, [ NS_MAIN ] ],
+                       [ null, [ NS_MAIN ] ],
+                       [ 5, [ NS_MAIN ] ],
 
-               # test $wgContentNamespaces === []
-               $wgContentNamespaces = [];
-               $this->assertEquals( [ NS_MAIN ], $this->obj->getContentNamespaces() );
+                       // Empty array
+                       [ [], [ NS_MAIN ] ],
 
-               # test !in_array( NS_MAIN, $wgContentNamespaces )
-               $wgContentNamespaces = [ NS_USER, NS_CATEGORY ];
-               $this->assertEquals(
-                       [ NS_MAIN, NS_USER, NS_CATEGORY ],
-                       $this->obj->getContentNamespaces(),
-                       'NS_MAIN is forced in $wgContentNamespaces even if unwanted'
-               );
+                       // NS_MAIN is forced to be content even if unwanted
+                       [ [ NS_USER, NS_CATEGORY ], [ NS_MAIN, NS_USER, NS_CATEGORY ] ],
 
-               # test other cases, return $wgcontentNamespaces as is
-               $wgContentNamespaces = [ NS_MAIN ];
-               $this->assertEquals(
-                       [ NS_MAIN ],
-                       $this->obj->getContentNamespaces()
-               );
-
-               $wgContentNamespaces = [ NS_MAIN, NS_USER, NS_CATEGORY ];
-               $this->assertEquals(
-                       [ NS_MAIN, NS_USER, NS_CATEGORY ],
-                       $this->obj->getContentNamespaces()
-               );
+                       // In other cases, return as-is
+                       [ [ NS_MAIN ], [ NS_MAIN ] ],
+                       [ [ NS_MAIN, NS_USER, NS_CATEGORY ], [ NS_MAIN, NS_USER, NS_CATEGORY ] ],
+               ];
        }
 
        /**
         * @covers NamespaceInfo::getSubjectNamespaces
         */
        public function testGetSubjectNamespaces() {
-               $subjectsNS = $this->obj->getSubjectNamespaces();
+               $subjectsNS = $this->newObj()->getSubjectNamespaces();
                $this->assertContains( NS_MAIN, $subjectsNS,
                        "Talk namespaces should have NS_MAIN" );
                $this->assertNotContains( NS_TALK, $subjectsNS,
@@ -422,7 +452,7 @@ class NamespaceInfoTest extends MediaWikiTestCase {
         * @covers NamespaceInfo::getTalkNamespaces
         */
        public function testGetTalkNamespaces() {
-               $talkNS = $this->obj->getTalkNamespaces();
+               $talkNS = $this->newObj()->getTalkNamespaces();
                $this->assertContains( NS_TALK, $talkNS,
                        "Subject namespaces should have NS_TALK" );
                $this->assertNotContains( NS_MAIN, $talkNS,
@@ -434,167 +464,870 @@ class NamespaceInfoTest extends MediaWikiTestCase {
                        "Subject namespaces should not have NS_SPECIAL" );
        }
 
-       private function assertIsCapitalized( $ns ) {
-               $this->assertTrue( $this->obj->isCapitalized( $ns ) );
+       /**
+        * @param int $ns
+        * @param bool $expected
+        * @param bool $capitalLinks To pass to constructor
+        * @param array $capitalLinkOverrides To pass to constructor
+        * @dataProvider provideIsCapitalized
+        * @covers NamespaceInfo::isCapitalized
+        */
+       public function testIsCapitalized(
+               $ns, $expected, $capitalLinks = true, array $capitalLinkOverrides = []
+       ) {
+               $obj = $this->newObj( [
+                       'CapitalLinks' => $capitalLinks,
+                       'CapitalLinkOverrides' => $capitalLinkOverrides,
+               ] );
+               $this->assertSame( $expected, $obj->isCapitalized( $ns ) );
        }
 
-       private function assertIsNotCapitalized( $ns ) {
-               $this->assertFalse( $this->obj->isCapitalized( $ns ) );
+       public function provideIsCapitalized() {
+               return [
+                       // Test default settings
+                       [ NS_PROJECT, true ],
+                       [ NS_PROJECT_TALK, true ],
+                       [ NS_MEDIA, true ],
+                       [ NS_FILE, true ],
+
+                       // Always capitalized no matter what
+                       [ NS_SPECIAL, true, false ],
+                       [ NS_USER, true, false ],
+                       [ NS_MEDIAWIKI, true, false ],
+
+                       // Even with an override too
+                       [ NS_SPECIAL, true, false, [ NS_SPECIAL => false ] ],
+                       [ NS_USER, true, false, [ NS_USER => false ] ],
+                       [ NS_MEDIAWIKI, true, false, [ NS_MEDIAWIKI => false ] ],
+
+                       // Overrides work for other namespaces
+                       [ NS_PROJECT, false, true, [ NS_PROJECT => false ] ],
+                       [ NS_PROJECT, true, false, [ NS_PROJECT => true ] ],
+
+                       // NS_MEDIA is treated like NS_FILE, and ignores NS_MEDIA overrides
+                       [ NS_MEDIA, false, true, [ NS_FILE => false, NS_MEDIA => true ] ],
+                       [ NS_MEDIA, true, false, [ NS_FILE => true, NS_MEDIA => false ] ],
+                       [ NS_FILE, false, true, [ NS_FILE => false, NS_MEDIA => true ] ],
+                       [ NS_FILE, true, false, [ NS_FILE => true, NS_MEDIA => false ] ],
+               ];
        }
 
        /**
-        * Some namespaces are always capitalized per code definition
-        * in NamespaceInfo::$alwaysCapitalizedNamespaces
-        * @covers NamespaceInfo::isCapitalized
+        * @covers NamespaceInfo::hasGenderDistinction
         */
-       public function testIsCapitalizedHardcodedAssertions() {
-               // NS_MEDIA and NS_FILE are treated the same
-               $this->assertEquals(
-                       $this->obj->isCapitalized( NS_MEDIA ),
-                       $this->obj->isCapitalized( NS_FILE ),
-                       'NS_MEDIA and NS_FILE have same capitalization rendering'
-               );
+       public function testHasGenderDistinction() {
+               $obj = $this->newObj();
 
-               // Boths are capitalized by default
-               $this->assertIsCapitalized( NS_MEDIA );
-               $this->assertIsCapitalized( NS_FILE );
+               // Namespaces with gender distinctions
+               $this->assertTrue( $obj->hasGenderDistinction( NS_USER ) );
+               $this->assertTrue( $obj->hasGenderDistinction( NS_USER_TALK ) );
+
+               // Other ones, "genderless"
+               $this->assertFalse( $obj->hasGenderDistinction( NS_MEDIA ) );
+               $this->assertFalse( $obj->hasGenderDistinction( NS_SPECIAL ) );
+               $this->assertFalse( $obj->hasGenderDistinction( NS_MAIN ) );
+               $this->assertFalse( $obj->hasGenderDistinction( NS_TALK ) );
+       }
 
-               // Always capitalized namespaces
-               // @see NamespaceInfo::$alwaysCapitalizedNamespaces
-               $this->assertIsCapitalized( NS_SPECIAL );
-               $this->assertIsCapitalized( NS_USER );
-               $this->assertIsCapitalized( NS_MEDIAWIKI );
+       /**
+        * @covers NamespaceInfo::isNonincludable
+        */
+       public function testIsNonincludable() {
+               $obj = $this->newObj( [ 'NonincludableNamespaces' => [ NS_USER ] ] );
+               $this->assertTrue( $obj->isNonincludable( NS_USER ) );
+               $this->assertFalse( $obj->isNonincludable( NS_TEMPLATE ) );
        }
 
        /**
-        * Follows up for testIsCapitalizedHardcodedAssertions() but alter the
-        * global $wgCapitalLink setting to have extended coverage.
+        * @dataProvider provideGetNamespaceContentModel
+        * @covers NamespaceInfo::getNamespaceContentModel
         *
-        * NamespaceInfo::isCapitalized() rely on two global settings:
-        *   $wgCapitalLinkOverrides = []; by default
-        *   $wgCapitalLinks = true; by default
-        * This function test $wgCapitalLinks
+        * @param int $ns
+        * @param string $expected
+        */
+       public function testGetNamespaceContentModel( $ns, $expected ) {
+               $obj = $this->newObj( [ 'NamespaceContentModels' =>
+                       [ NS_USER => CONTENT_MODEL_WIKITEXT, 123 => CONTENT_MODEL_JSON, 1234 => 'abcdef' ],
+               ] );
+               $this->assertSame( $expected, $obj->getNamespaceContentModel( $ns ) );
+       }
+
+       public function provideGetNamespaceContentModel() {
+               return [
+                       [ NS_MAIN, null ],
+                       [ NS_TALK, null ],
+                       [ NS_USER, CONTENT_MODEL_WIKITEXT ],
+                       [ NS_USER_TALK, null ],
+                       [ NS_SPECIAL, null ],
+                       [ 122, null ],
+                       [ 123, CONTENT_MODEL_JSON ],
+                       [ 1234, 'abcdef' ],
+                       [ 1235, null ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetCategoryLinkType
+        * @covers NamespaceInfo::getCategoryLinkType
         *
-        * Global setting correctness is tested against the NS_PROJECT and
-        * NS_PROJECT_TALK namespaces since they are not hardcoded nor specials
-        * @covers NamespaceInfo::isCapitalized
+        * @param int $ns
+        * @param string $expected
         */
-       public function testIsCapitalizedWithWgCapitalLinks() {
-               $this->assertIsCapitalized( NS_PROJECT );
-               $this->assertIsCapitalized( NS_PROJECT_TALK );
+       public function testGetCategoryLinkType( $ns, $expected ) {
+               $this->assertSame( $expected, $this->newObj()->getCategoryLinkType( $ns ) );
+       }
 
-               $this->setMwGlobals( 'wgCapitalLinks', false );
+       public function provideGetCategoryLinkType() {
+               return [
+                       [ NS_MAIN, 'page' ],
+                       [ NS_TALK, 'page' ],
+                       [ NS_USER, 'page' ],
+                       [ NS_USER_TALK, 'page' ],
+
+                       [ NS_FILE, 'file' ],
+                       [ NS_FILE_TALK, 'page' ],
 
-               // hardcoded namespaces (see above function) are still capitalized:
-               $this->assertIsCapitalized( NS_SPECIAL );
-               $this->assertIsCapitalized( NS_USER );
-               $this->assertIsCapitalized( NS_MEDIAWIKI );
+                       [ NS_CATEGORY, 'subcat' ],
+                       [ NS_CATEGORY_TALK, 'page' ],
 
-               // setting is correctly applied
-               $this->assertIsNotCapitalized( NS_PROJECT );
-               $this->assertIsNotCapitalized( NS_PROJECT_TALK );
+                       [ 100, 'page' ],
+                       [ 101, 'page' ],
+               ];
        }
 
+       // %} End basic methods
+
+       /**********************************************************************************************
+        * getSubject/Talk/Associated
+        * %{
+        */
        /**
-        * Counter part for NamespaceInfo::testIsCapitalizedWithWgCapitalLinks() now
-        * testing the $wgCapitalLinkOverrides global.
+        * @dataProvider provideSubjectTalk
+        * @covers NamespaceInfo::getSubject
+        * @covers NamespaceInfo::getSubjectPage
+        * @covers NamespaceInfo::isMethodValidFor
+        * @covers Title::getSubjectPage
         *
-        * @todo split groups of assertions in autonomous testing functions
-        * @covers NamespaceInfo::isCapitalized
+        * @param int $subject
+        * @param int $talk
         */
-       public function testIsCapitalizedWithWgCapitalLinkOverrides() {
-               global $wgCapitalLinkOverrides;
+       public function testGetSubject( $subject, $talk ) {
+               $obj = $this->newObj();
+               $this->assertSame( $subject, $obj->getSubject( $subject ) );
+               $this->assertSame( $subject, $obj->getSubject( $talk ) );
+
+               $subjectTitleVal = new TitleValue( $subject, 'A' );
+               $talkTitleVal = new TitleValue( $talk, 'A' );
+               // Object will be the same one passed in if it's a subject, different but equal object if
+               // it's talk
+               $this->assertSame( $subjectTitleVal, $obj->getSubjectPage( $subjectTitleVal ) );
+               $this->assertEquals( $subjectTitleVal, $obj->getSubjectPage( $talkTitleVal ) );
+
+               $subjectTitle = Title::makeTitle( $subject, 'A' );
+               $talkTitle = Title::makeTitle( $talk, 'A' );
+               $this->assertSame( $subjectTitle, $subjectTitle->getSubjectPage() );
+               $this->assertEquals( $subjectTitle, $talkTitle->getSubjectPage() );
+       }
 
-               // Test default settings
-               $this->assertIsCapitalized( NS_PROJECT );
-               $this->assertIsCapitalized( NS_PROJECT_TALK );
+       /**
+        * @dataProvider provideSpecialNamespaces
+        * @covers NamespaceInfo::getSubject
+        * @covers NamespaceInfo::getSubjectPage
+        *
+        * @param int $ns
+        */
+       public function testGetSubject_special( $ns ) {
+               $obj = $this->newObj();
+               $this->assertSame( $ns, $obj->getSubject( $ns ) );
 
-               // hardcoded namespaces (see above function) are capitalized:
-               $this->assertIsCapitalized( NS_SPECIAL );
-               $this->assertIsCapitalized( NS_USER );
-               $this->assertIsCapitalized( NS_MEDIAWIKI );
+               $title = new TitleValue( $ns, 'A' );
+               $this->assertSame( $title, $obj->getSubjectPage( $title ) );
+       }
 
-               // Hardcoded namespaces remains capitalized
-               $wgCapitalLinkOverrides[NS_SPECIAL] = false;
-               $wgCapitalLinkOverrides[NS_USER] = false;
-               $wgCapitalLinkOverrides[NS_MEDIAWIKI] = false;
+       /**
+        * @dataProvider provideSubjectTalk
+        * @covers NamespaceInfo::getTalk
+        * @covers NamespaceInfo::getTalkPage
+        * @covers NamespaceInfo::isMethodValidFor
+        * @covers Title::getTalkPage
+        *
+        * @param int $subject
+        * @param int $talk
+        */
+       public function testGetTalk( $subject, $talk ) {
+               $obj = $this->newObj();
+               $this->assertSame( $talk, $obj->getTalk( $subject ) );
+               $this->assertSame( $talk, $obj->getTalk( $talk ) );
+
+               $subjectTitleVal = new TitleValue( $subject, 'A' );
+               $talkTitleVal = new TitleValue( $talk, 'A' );
+               // Object will be the same one passed in if it's a talk, different but equal object if it's
+               // subject
+               $this->assertEquals( $talkTitleVal, $obj->getTalkPage( $subjectTitleVal ) );
+               $this->assertSame( $talkTitleVal, $obj->getTalkPage( $talkTitleVal ) );
+
+               $subjectTitle = Title::makeTitle( $subject, 'A' );
+               $talkTitle = Title::makeTitle( $talk, 'A' );
+               $this->assertEquals( $talkTitle, $subjectTitle->getTalkPage() );
+               $this->assertSame( $talkTitle, $talkTitle->getTalkPage() );
+       }
 
-               $this->assertIsCapitalized( NS_SPECIAL );
-               $this->assertIsCapitalized( NS_USER );
-               $this->assertIsCapitalized( NS_MEDIAWIKI );
+       /**
+        * @dataProvider provideSpecialNamespaces
+        * @covers NamespaceInfo::getTalk
+        * @covers NamespaceInfo::isMethodValidFor
+        *
+        * @param int $ns
+        */
+       public function testGetTalk_special( $ns ) {
+               $this->setExpectedException( MWException::class,
+                       "NamespaceInfo::getTalk does not make any sense for given namespace $ns" );
+               $this->newObj()->getTalk( $ns );
+       }
 
-               $wgCapitalLinkOverrides[NS_PROJECT] = false;
-               $this->assertIsNotCapitalized( NS_PROJECT );
+       /**
+        * @dataProvider provideSpecialNamespaces
+        * @covers NamespaceInfo::getTalk
+        * @covers NamespaceInfo::getTalkPage
+        * @covers NamespaceInfo::isMethodValidFor
+        *
+        * @param int $ns
+        */
+       public function testGetTalkPage_special( $ns ) {
+               $this->setExpectedException( MWException::class,
+                       "NamespaceInfo::getTalk does not make any sense for given namespace $ns" );
+               $this->newObj()->getTalkPage( new TitleValue( $ns, 'A' ) );
+       }
 
-               $wgCapitalLinkOverrides[NS_PROJECT] = true;
-               $this->assertIsCapitalized( NS_PROJECT );
+       /**
+        * @dataProvider provideSpecialNamespaces
+        * @covers NamespaceInfo::getTalk
+        * @covers NamespaceInfo::getTalkPage
+        * @covers NamespaceInfo::isMethodValidFor
+        * @covers Title::getTalkPage
+        *
+        * @param int $ns
+        */
+       public function testTitleGetTalkPage_special( $ns ) {
+               $this->setExpectedException( MWException::class,
+                       "NamespaceInfo::getTalk does not make any sense for given namespace $ns" );
+               Title::makeTitle( $ns, 'A' )->getTalkPage();
+       }
 
-               unset( $wgCapitalLinkOverrides[NS_PROJECT] );
-               $this->assertIsCapitalized( NS_PROJECT );
+       /**
+        * @dataProvider provideSpecialNamespaces
+        * @covers NamespaceInfo::getAssociated
+        * @covers NamespaceInfo::isMethodValidFor
+        *
+        * @param int $ns
+        */
+       public function testGetAssociated_special( $ns ) {
+               $this->setExpectedException( MWException::class,
+                       "NamespaceInfo::getAssociated does not make any sense for given namespace $ns" );
+               $this->newObj()->getAssociated( $ns );
        }
 
        /**
-        * @covers NamespaceInfo::hasGenderDistinction
+        * @dataProvider provideSpecialNamespaces
+        * @covers NamespaceInfo::getAssociated
+        * @covers NamespaceInfo::getAssociatedPage
+        * @covers NamespaceInfo::isMethodValidFor
+        *
+        * @param int $ns
         */
-       public function testHasGenderDistinction() {
-               // Namespaces with gender distinctions
-               $this->assertTrue( $this->obj->hasGenderDistinction( NS_USER ) );
-               $this->assertTrue( $this->obj->hasGenderDistinction( NS_USER_TALK ) );
+       public function testGetAssociatedPage_special( $ns ) {
+               $this->setExpectedException( MWException::class,
+                       "NamespaceInfo::getAssociated does not make any sense for given namespace $ns" );
+               $this->newObj()->getAssociatedPage( new TitleValue( $ns, 'A' ) );
+       }
 
-               // Other ones, "genderless"
-               $this->assertFalse( $this->obj->hasGenderDistinction( NS_MEDIA ) );
-               $this->assertFalse( $this->obj->hasGenderDistinction( NS_SPECIAL ) );
-               $this->assertFalse( $this->obj->hasGenderDistinction( NS_MAIN ) );
-               $this->assertFalse( $this->obj->hasGenderDistinction( NS_TALK ) );
+       /**
+        * @dataProvider provideSpecialNamespaces
+        * @covers NamespaceInfo::getAssociated
+        * @covers NamespaceInfo::getAssociatedPage
+        * @covers NamespaceInfo::isMethodValidFor
+        * @covers Title::getOtherPage
+        *
+        * @param int $ns
+        */
+       public function testTitleGetOtherPage_special( $ns ) {
+               $this->setExpectedException( MWException::class,
+                       "NamespaceInfo::getAssociated does not make any sense for given namespace $ns" );
+               Title::makeTitle( $ns, 'A' )->getOtherPage();
        }
 
        /**
-        * @covers NamespaceInfo::isNonincludable
+        * @dataProvider provideSubjectTalk
+        * @covers NamespaceInfo::getAssociated
+        * @covers NamespaceInfo::getAssociatedPage
+        * @covers Title::getOtherPage
+        *
+        * @param int $subject
+        * @param int $talk
         */
-       public function testIsNonincludable() {
-               global $wgNonincludableNamespaces;
+       public function testGetAssociated( $subject, $talk ) {
+               $obj = $this->newObj();
+               $this->assertSame( $talk, $obj->getAssociated( $subject ) );
+               $this->assertSame( $subject, $obj->getAssociated( $talk ) );
+
+               $subjectTitle = new TitleValue( $subject, 'A' );
+               $talkTitle = new TitleValue( $talk, 'A' );
+               // Object will not be the same
+               $this->assertEquals( $talkTitle, $obj->getAssociatedPage( $subjectTitle ) );
+               $this->assertEquals( $subjectTitle, $obj->getAssociatedPage( $talkTitle ) );
+
+               $subjectTitle = Title::makeTitle( $subject, 'A' );
+               $talkTitle = Title::makeTitle( $talk, 'A' );
+               $this->assertEquals( $talkTitle, $subjectTitle->getOtherPage() );
+               $this->assertEquals( $subjectTitle, $talkTitle->getOtherPage() );
+       }
+
+       public static function provideSubjectTalk() {
+               return [
+                       // Format: [ subject, talk ]
+                       'Main/talk' => [ NS_MAIN, NS_TALK ],
+                       'User/user talk' => [ NS_USER, NS_USER_TALK ],
+                       'Unknown namespaces also supported' => [ 106, 107 ],
+               ];
+       }
+
+       public static function provideSpecialNamespaces() {
+               return [
+                       'Special' => [ NS_SPECIAL ],
+                       'Media' => [ NS_MEDIA ],
+                       'Unknown negative index' => [ -613 ],
+               ];
+       }
 
-               $wgNonincludableNamespaces = [ NS_USER ];
+       // %} End getSubject/Talk/Associated
 
-               $this->assertTrue( $this->obj->isNonincludable( NS_USER ) );
-               $this->assertFalse( $this->obj->isNonincludable( NS_TEMPLATE ) );
+       /**********************************************************************************************
+        * Canonical namespaces
+        * %{
+        */
+
+       // Default canonical namespaces
+       // %{
+       private function getDefaultNamespaces() {
+               return [ NS_MAIN => '' ] + self::$defaultOptions['CanonicalNamespaceNames'];
        }
 
-       private function assertSameSubject( $ns1, $ns2, $msg = '' ) {
-               $this->assertTrue( $this->obj->subjectEquals( $ns1, $ns2 ), $msg );
+       /**
+        * @covers NamespaceInfo::getCanonicalNamespaces
+        */
+       public function testGetCanonicalNamespaces() {
+               $this->assertSame(
+                       $this->getDefaultNamespaces(),
+                       $this->newObj()->getCanonicalNamespaces()
+               );
        }
 
-       private function assertDifferentSubject( $ns1, $ns2, $msg = '' ) {
-               $this->assertFalse( $this->obj->subjectEquals( $ns1, $ns2 ), $msg );
+       /**
+        * @dataProvider provideGetCanonicalName
+        * @covers NamespaceInfo::getCanonicalName
+        *
+        * @param int $index
+        * @param string|bool $expected
+        */
+       public function testGetCanonicalName( $index, $expected ) {
+               $this->assertSame( $expected, $this->newObj()->getCanonicalName( $index ) );
        }
 
-       public function provideGetCategoryLinkType() {
+       public function provideGetCanonicalName() {
                return [
-                       [ NS_MAIN, 'page' ],
-                       [ NS_TALK, 'page' ],
-                       [ NS_USER, 'page' ],
-                       [ NS_USER_TALK, 'page' ],
+                       'Main' => [ NS_MAIN, '' ],
+                       'Talk' => [ NS_TALK, 'Talk' ],
+                       'With underscore not space' => [ NS_USER_TALK, 'User_talk' ],
+                       'Special' => [ NS_SPECIAL, 'Special' ],
+                       'Nonexistent' => [ 12345, false ],
+                       'Nonexistent negative' => [ -12345, false ],
+               ];
+       }
 
-                       [ NS_FILE, 'file' ],
-                       [ NS_FILE_TALK, 'page' ],
+       /**
+        * @dataProvider provideGetCanonicalIndex
+        * @covers NamespaceInfo::getCanonicalIndex
+        *
+        * @param string $name
+        * @param int|null $expected
+        */
+       public function testGetCanonicalIndex( $name, $expected ) {
+               $this->assertSame( $expected, $this->newObj()->getCanonicalIndex( $name ) );
+       }
 
-                       [ NS_CATEGORY, 'subcat' ],
-                       [ NS_CATEGORY_TALK, 'page' ],
+       public function provideGetCanonicalIndex() {
+               return [
+                       'Main' => [ '', NS_MAIN ],
+                       'Talk' => [ 'talk', NS_TALK ],
+                       'Not lowercase' => [ 'Talk', null ],
+                       'With underscore' => [ 'user_talk', NS_USER_TALK ],
+                       'Space is not recognized for underscore' => [ 'user talk', null ],
+                       '0' => [ '0', null ],
+               ];
+       }
 
-                       [ 100, 'page' ],
-                       [ 101, 'page' ],
+       /**
+        * @covers NamespaceInfo::getValidNamespaces
+        */
+       public function testGetValidNamespaces() {
+               $this->assertSame(
+                       [ NS_MAIN, NS_TALK, NS_USER, NS_USER_TALK ],
+                       $this->newObj()->getValidNamespaces()
+               );
+       }
+
+       // %} End default canonical namespaces
+
+       // No canonical namespace names
+       // %{
+       /**
+        * @covers NamespaceInfo::getCanonicalNamespaces
+        */
+       public function testGetCanonicalNamespaces_NoCanonicalNamespaceNames() {
+               $obj = $this->newObj( [ 'CanonicalNamespaceNames' => [] ] );
+
+               $this->assertSame( [ NS_MAIN => '' ], $obj->getCanonicalNamespaces() );
+       }
+
+       /**
+        * @covers NamespaceInfo::getCanonicalName
+        */
+       public function testGetCanonicalName_NoCanonicalNamespaceNames() {
+               $obj = $this->newObj( [ 'CanonicalNamespaceNames' => [] ] );
+
+               $this->assertSame( '', $obj->getCanonicalName( NS_MAIN ) );
+               $this->assertFalse( $obj->getCanonicalName( NS_TALK ) );
+       }
+
+       /**
+        * @covers NamespaceInfo::getCanonicalIndex
+        */
+       public function testGetCanonicalIndex_NoCanonicalNamespaceNames() {
+               $obj = $this->newObj( [ 'CanonicalNamespaceNames' => [] ] );
+
+               $this->assertSame( NS_MAIN, $obj->getCanonicalIndex( '' ) );
+               $this->assertNull( $obj->getCanonicalIndex( 'talk' ) );
+       }
+
+       /**
+        * @covers NamespaceInfo::getValidNamespaces
+        */
+       public function testGetValidNamespaces_NoCanonicalNamespaceNames() {
+               $obj = $this->newObj( [ 'CanonicalNamespaceNames' => [] ] );
+
+               $this->assertSame( [ NS_MAIN ], $obj->getValidNamespaces() );
+       }
+
+       // %} End no canonical namespace names
+
+       // Test extension namespaces
+       // %{
+       private function setupExtensionNamespaces() {
+               $this->scopedCallback = null;
+               $this->scopedCallback = ExtensionRegistry::getInstance()->setAttributeForTest(
+                       'ExtensionNamespaces',
+                       [ NS_MAIN => 'No effect', NS_TALK => 'No effect', 12345 => 'Extended' ]
+               );
+       }
+
+       /**
+        * @covers NamespaceInfo::getCanonicalNamespaces
+        */
+       public function testGetCanonicalNamespaces_ExtensionNamespaces() {
+               $this->setupExtensionNamespaces();
+
+               $this->assertSame(
+                       $this->getDefaultNamespaces() + [ 12345 => 'Extended' ],
+                       $this->newObj()->getCanonicalNamespaces()
+               );
+       }
+
+       /**
+        * @covers NamespaceInfo::getCanonicalName
+        */
+       public function testGetCanonicalName_ExtensionNamespaces() {
+               $this->setupExtensionNamespaces();
+               $obj = $this->newObj();
+
+               $this->assertSame( '', $obj->getCanonicalName( NS_MAIN ) );
+               $this->assertSame( 'Talk', $obj->getCanonicalName( NS_TALK ) );
+               $this->assertSame( 'Extended', $obj->getCanonicalName( 12345 ) );
+       }
+
+       /**
+        * @covers NamespaceInfo::getCanonicalIndex
+        */
+       public function testGetCanonicalIndex_ExtensionNamespaces() {
+               $this->setupExtensionNamespaces();
+               $obj = $this->newObj();
+
+               $this->assertSame( NS_MAIN, $obj->getCanonicalIndex( '' ) );
+               $this->assertSame( NS_TALK, $obj->getCanonicalIndex( 'talk' ) );
+               $this->assertSame( 12345, $obj->getCanonicalIndex( 'extended' ) );
+       }
+
+       /**
+        * @covers NamespaceInfo::getValidNamespaces
+        */
+       public function testGetValidNamespaces_ExtensionNamespaces() {
+               $this->setupExtensionNamespaces();
+
+               $this->assertSame(
+                       [ NS_MAIN, NS_TALK, NS_USER, NS_USER_TALK, 12345 ],
+                       $this->newObj()->getValidNamespaces()
+               );
+       }
+
+       // %} End extension namespaces
+
+       // Hook namespaces
+       // %{
+       /**
+        * @return array Expected canonical namespaces
+        */
+       private function setupHookNamespaces() {
+               $callback =
+                       function ( &$canonicalNamespaces ) {
+                               $canonicalNamespaces[NS_MAIN] = 'Main';
+                               unset( $canonicalNamespaces[NS_MEDIA] );
+                               $canonicalNamespaces[123456] = 'Hooked';
+                       };
+               $this->setTemporaryHook( 'CanonicalNamespaces', $callback );
+               $expected = $this->getDefaultNamespaces();
+               ( $callback )( $expected );
+               return $expected;
+       }
+
+       /**
+        * @covers NamespaceInfo::getCanonicalNamespaces
+        */
+       public function testGetCanonicalNamespaces_HookNamespaces() {
+               $expected = $this->setupHookNamespaces();
+
+               $this->assertSame( $expected, $this->newObj()->getCanonicalNamespaces() );
+       }
+
+       /**
+        * @covers NamespaceInfo::getCanonicalName
+        */
+       public function testGetCanonicalName_HookNamespaces() {
+               $this->setupHookNamespaces();
+               $obj = $this->newObj();
+
+               $this->assertSame( 'Main', $obj->getCanonicalName( NS_MAIN ) );
+               $this->assertFalse( $obj->getCanonicalName( NS_MEDIA ) );
+               $this->assertSame( 'Hooked', $obj->getCanonicalName( 123456 ) );
+       }
+
+       /**
+        * @covers NamespaceInfo::getCanonicalIndex
+        */
+       public function testGetCanonicalIndex_HookNamespaces() {
+               $this->setupHookNamespaces();
+               $obj = $this->newObj();
+
+               $this->assertSame( NS_MAIN, $obj->getCanonicalIndex( 'main' ) );
+               $this->assertNull( $obj->getCanonicalIndex( 'media' ) );
+               $this->assertSame( 123456, $obj->getCanonicalIndex( 'hooked' ) );
+       }
+
+       /**
+        * @covers NamespaceInfo::getValidNamespaces
+        */
+       public function testGetValidNamespaces_HookNamespaces() {
+               $this->setupHookNamespaces();
+
+               $this->assertSame(
+                       [ NS_MAIN, NS_TALK, NS_USER, NS_USER_TALK, 123456 ],
+                       $this->newObj()->getValidNamespaces()
+               );
+       }
+
+       // %} End hook namespaces
+
+       // Extra namespaces
+       // %{
+       /**
+        * @return NamespaceInfo
+        */
+       private function setupExtraNamespaces() {
+               return $this->newObj( [ 'ExtraNamespaces' =>
+                       [ NS_MAIN => 'No effect', NS_TALK => 'No effect', 1234567 => 'Extra' ]
+               ] );
+       }
+
+       /**
+        * @covers NamespaceInfo::getCanonicalNamespaces
+        */
+       public function testGetCanonicalNamespaces_ExtraNamespaces() {
+               $this->assertSame(
+                       $this->getDefaultNamespaces() + [ 1234567 => 'Extra' ],
+                       $this->setupExtraNamespaces()->getCanonicalNamespaces()
+               );
+       }
+
+       /**
+        * @covers NamespaceInfo::getCanonicalName
+        */
+       public function testGetCanonicalName_ExtraNamespaces() {
+               $obj = $this->setupExtraNamespaces();
+
+               $this->assertSame( '', $obj->getCanonicalName( NS_MAIN ) );
+               $this->assertSame( 'Talk', $obj->getCanonicalName( NS_TALK ) );
+               $this->assertSame( 'Extra', $obj->getCanonicalName( 1234567 ) );
+       }
+
+       /**
+        * @covers NamespaceInfo::getCanonicalIndex
+        */
+       public function testGetCanonicalIndex_ExtraNamespaces() {
+               $obj = $this->setupExtraNamespaces();
+
+               $this->assertNull( $obj->getCanonicalIndex( 'no effect' ) );
+               $this->assertNull( $obj->getCanonicalIndex( 'no_effect' ) );
+               $this->assertSame( 1234567, $obj->getCanonicalIndex( 'extra' ) );
+       }
+
+       /**
+        * @covers NamespaceInfo::getValidNamespaces
+        */
+       public function testGetValidNamespaces_ExtraNamespaces() {
+               $this->assertSame(
+                       [ NS_MAIN, NS_TALK, NS_USER, NS_USER_TALK, 1234567 ],
+                       $this->setupExtraNamespaces()->getValidNamespaces()
+               );
+       }
+
+       // %} End extra namespaces
+
+       // Canonical namespace caching
+       // %{
+       /**
+        * @covers NamespaceInfo::getCanonicalNamespaces
+        */
+       public function testGetCanonicalNamespaces_caching() {
+               $obj = $this->newObj();
+
+               // This should cache the values
+               $obj->getCanonicalNamespaces();
+
+               // Now try to alter them through nefarious means
+               $this->setupExtensionNamespaces();
+               $this->setupHookNamespaces();
+
+               // Should have no effect
+               $this->assertSame( $this->getDefaultNamespaces(), $obj->getCanonicalNamespaces() );
+       }
+
+       /**
+        * @covers NamespaceInfo::getCanonicalName
+        */
+       public function testGetCanonicalName_caching() {
+               $obj = $this->newObj();
+
+               // This should cache the values
+               $obj->getCanonicalName( NS_MAIN );
+
+               // Now try to alter them through nefarious means
+               $this->setupExtensionNamespaces();
+               $this->setupHookNamespaces();
+
+               // Should have no effect
+               $this->assertSame( '', $obj->getCanonicalName( NS_MAIN ) );
+               $this->assertSame( 'Media', $obj->getCanonicalName( NS_MEDIA ) );
+               $this->assertFalse( $obj->getCanonicalName( 12345 ) );
+               $this->assertFalse( $obj->getCanonicalName( 123456 ) );
+       }
+
+       /**
+        * @covers NamespaceInfo::getCanonicalIndex
+        */
+       public function testGetCanonicalIndex_caching() {
+               $obj = $this->newObj();
+
+               // This should cache the values
+               $obj->getCanonicalIndex( '' );
+
+               // Now try to alter them through nefarious means
+               $this->setupExtensionNamespaces();
+               $this->setupHookNamespaces();
+
+               // Should have no effect
+               $this->assertSame( NS_MAIN, $obj->getCanonicalIndex( '' ) );
+               $this->assertSame( NS_MEDIA, $obj->getCanonicalIndex( 'media' ) );
+               $this->assertNull( $obj->getCanonicalIndex( 'extended' ) );
+               $this->assertNull( $obj->getCanonicalIndex( 'hooked' ) );
+       }
+
+       /**
+        * @covers NamespaceInfo::getValidNamespaces
+        */
+       public function testGetValidNamespaces_caching() {
+               $obj = $this->newObj();
+
+               // This should cache the values
+               $obj->getValidNamespaces();
+
+               // Now try to alter through nefarious means
+               $this->setupExtensionNamespaces();
+               $this->setupHookNamespaces();
+
+               // Should have no effect
+               $this->assertSame(
+                       [ NS_MAIN, NS_TALK, NS_USER, NS_USER_TALK ],
+                       $obj->getValidNamespaces()
+               );
+       }
+
+       // %} End canonical namespace caching
+
+       // Miscellaneous
+       // %{
+
+       /**
+        * @dataProvider provideGetValidNamespaces_misc
+        * @covers NamespaceInfo::getValidNamespaces
+        *
+        * @param array $namespaces List of namespace indices to return from getCanonicalNamespaces()
+        *   (list is overwritten by a hook, so NS_MAIN doesn't have to be present)
+        * @param array $expected
+        */
+       public function testGetValidNamespaces_misc( array $namespaces, array $expected ) {
+               // Each namespace's name is just its index
+               $this->setTemporaryHook( 'CanonicalNamespaces',
+                       function ( &$canonicalNamespaces ) use ( $namespaces ) {
+                               $canonicalNamespaces = array_combine( $namespaces, $namespaces );
+                       }
+               );
+               $this->assertSame( $expected, $this->newObj()->getValidNamespaces() );
+       }
+
+       public function provideGetValidNamespaces_misc() {
+               return [
+                       'Out of order (T109137)' => [ [ 1, 0 ], [ 0, 1 ] ],
+                       'Alphabetical order' => [ [ 10, 2 ], [ 2, 10 ] ],
+                       'Negative' => [ [ -1000, -500, -2, 0 ], [ 0 ] ],
                ];
        }
 
+       // %} End miscellaneous
+       // %} End canonical namespaces
+
+       /**********************************************************************************************
+        * Restriction levels
+        * %{
+        */
+
+       /**
+        * This mock user can only have isAllowed() called on it.
+        *
+        * @param array $groups Groups for the mock user to have
+        * @return User
+        */
+       private function getMockUser( array $groups = [] ) : User {
+               $groups[] = '*';
+
+               $mock = $this->createMock( User::class );
+               $mock->method( 'isAllowed' )->will( $this->returnCallback(
+                       function ( $action ) use ( $groups ) {
+                               global $wgGroupPermissions, $wgRevokePermissions;
+                               if ( $action == '' ) {
+                                       return true;
+                               }
+                               foreach ( $wgRevokePermissions as $group => $rights ) {
+                                       if ( !in_array( $group, $groups ) ) {
+                                               continue;
+                                       }
+                                       if ( isset( $rights[$action] ) && $rights[$action] ) {
+                                               return false;
+                                       }
+                               }
+                               foreach ( $wgGroupPermissions as $group => $rights ) {
+                                       if ( !in_array( $group, $groups ) ) {
+                                               continue;
+                                       }
+                                       if ( isset( $rights[$action] ) && $rights[$action] ) {
+                                               return true;
+                                       }
+                               }
+                               return false;
+                       }
+               ) );
+               $mock->expects( $this->never() )->method( $this->anythingBut( 'isAllowed' ) );
+               return $mock;
+       }
+
        /**
-        * @dataProvider provideGetCategoryLinkType
-        * @covers NamespaceInfo::getCategoryLinkType
+        * @dataProvider provideGetRestrictionLevels
+        * @covers NamespaceInfo::getRestrictionLevels
         *
-        * @param int $index
-        * @param string $expected
+        * @param array $expected
+        * @param int $ns
+        * @param User|null $user
         */
-       public function testGetCategoryLinkType( $index, $expected ) {
-               $actual = $this->obj->getCategoryLinkType( $index );
-               $this->assertSame( $expected, $actual, "NS $index" );
+       public function testGetRestrictionLevels( array $expected, $ns, User $user = null ) {
+               $this->setMwGlobals( [
+                       'wgGroupPermissions' => [
+                               '*' => [ 'edit' => true ],
+                               'autoconfirmed' => [ 'editsemiprotected' => true ],
+                               'sysop' => [
+                                       'editsemiprotected' => true,
+                                       'editprotected' => true,
+                               ],
+                               'privileged' => [ 'privileged' => true ],
+                       ],
+                       'wgRevokePermissions' => [
+                               'noeditsemiprotected' => [ 'editsemiprotected' => true ],
+                       ],
+               ] );
+               $obj = $this->newObj( [
+                       'NamespaceProtection' => [
+                               NS_MAIN => 'autoconfirmed',
+                               NS_USER => 'sysop',
+                               101 => [ 'editsemiprotected', 'privileged' ],
+                       ],
+               ] );
+               $this->assertSame( $expected, $obj->getRestrictionLevels( $ns, $user ) );
        }
+
+       public function provideGetRestrictionLevels() {
+               return [
+                       'No namespace restriction' => [ [ '', 'autoconfirmed', 'sysop' ], NS_TALK ],
+                       'Restricted to autoconfirmed' => [ [ '', 'sysop' ], NS_MAIN ],
+                       'Restricted to sysop' => [ [ '' ], NS_USER ],
+                       // @todo Bug -- 'sysop' protection should be allowed in this case. Someone who's
+                       // autoconfirmed and also privileged can edit this namespace, and would be blocked by
+                       // the sysop protection.
+                       'Restricted to someone in two groups' => [ [ '' ], 101 ],
+
+                       'No special permissions' => [ [ '' ], NS_TALK, $this->getMockUser() ],
+                       'autoconfirmed' => [
+                               [ '', 'autoconfirmed' ],
+                               NS_TALK,
+                               $this->getMockUser( [ 'autoconfirmed' ] )
+                       ],
+                       'autoconfirmed revoked' => [
+                               [ '' ],
+                               NS_TALK,
+                               $this->getMockUser( [ 'autoconfirmed', 'noeditsemiprotected' ] )
+                       ],
+                       'sysop' => [
+                               [ '', 'autoconfirmed', 'sysop' ],
+                               NS_TALK,
+                               $this->getMockUser( [ 'sysop' ] )
+                       ],
+                       'sysop with autoconfirmed revoked (a bit silly)' => [
+                               [ '', 'sysop' ],
+                               NS_TALK,
+                               $this->getMockUser( [ 'sysop', 'noeditsemiprotected' ] )
+                       ],
+               ];
+       }
+
+       // %} End restriction levels
 }
+
+/**
+ * For really cool vim folding this needs to be at the end:
+ * vim: foldmarker=%{,%} foldmethod=marker
+ */
index 481da75..48c8a95 100644 (file)
@@ -504,20 +504,24 @@ class UserTest extends MediaWikiTestCase {
        }
 
        /**
+        * @covers User::isRegistered
         * @covers User::isLoggedIn
         * @covers User::isAnon
         */
        public function testLoggedIn() {
                $user = $this->getMutableTestUser()->getUser();
+               $this->assertTrue( $user->isRegistered() );
                $this->assertTrue( $user->isLoggedIn() );
                $this->assertFalse( $user->isAnon() );
 
                // Non-existent users are perceived as anonymous
                $user = User::newFromName( 'UTNonexistent' );
+               $this->assertFalse( $user->isRegistered() );
                $this->assertFalse( $user->isLoggedIn() );
                $this->assertTrue( $user->isAnon() );
 
                $user = new User;
+               $this->assertFalse( $user->isRegistered() );
                $this->assertFalse( $user->isLoggedIn() );
                $this->assertTrue( $user->isAnon() );
        }
@@ -1201,6 +1205,15 @@ class UserTest extends MediaWikiTestCase {
                $this->assertSame( 'Bogus', $test->getName() );
                $this->assertSame( 654321, $test->getActorId() );
 
+               // Loading remote user by name from remote wiki should succeed
+               $test = User::newFromAnyId( null, 'Bogus', null, 'foo' );
+               $this->assertSame( 0, $test->getId() );
+               $this->assertSame( 'Bogus', $test->getName() );
+               $this->assertSame( 0, $test->getActorId() );
+               $test = User::newFromAnyId( 123456, 'Bogus', 654321, 'foo' );
+               $this->assertSame( 0, $test->getId() );
+               $this->assertSame( 0, $test->getActorId() );
+
                // Exceptional cases
                try {
                        User::newFromAnyId( null, null, null );
@@ -1212,6 +1225,13 @@ class UserTest extends MediaWikiTestCase {
                        $this->fail( 'Expected exception not thrown' );
                } catch ( InvalidArgumentException $ex ) {
                }
+
+               // Loading remote user by id from remote wiki should fail
+               try {
+                       User::newFromAnyId( 123456, null, 654321, 'foo' );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( InvalidArgumentException $ex ) {
+               }
        }
 
        /**
@@ -1578,4 +1598,38 @@ class UserTest extends MediaWikiTestCase {
                $updater->setContent( 'main', $content );
                return $updater->saveRevision( CommentStoreComment::newUnsavedComment( $comment ) );
        }
+
+       /**
+        * @covers User::idFromName
+        */
+       public function testExistingIdFromName() {
+               $this->assertTrue(
+                       array_key_exists( $this->user->getName(), User::$idCacheByName ),
+                       'Test user should already be in the id cache.'
+               );
+               $this->assertSame(
+                       $this->user->getId(), User::idFromName( $this->user->getName() ),
+                       'Id is correctly retreived from the cache.'
+               );
+               $this->assertSame(
+                       $this->user->getId(), User::idFromName( $this->user->getName(), User::READ_LATEST ),
+                       'Id is correctly retreived from the database.'
+               );
+       }
+
+       /**
+        * @covers User::idFromName
+        */
+       public function testNonExistingIdFromName() {
+               $this->assertFalse(
+                       array_key_exists( 'NotExisitngUser', User::$idCacheByName ),
+                       'Non exisitng user should not be in the id cache.'
+               );
+               $this->assertSame( null, User::idFromName( 'NotExisitngUser' ) );
+               $this->assertTrue(
+                       array_key_exists( 'NotExisitngUser', User::$idCacheByName ),
+                       'Username will be cached when requested once.'
+               );
+               $this->assertSame( null, User::idFromName( 'NotExisitngUser' ) );
+       }
 }
index a8761e3..f424b21 100644 (file)
@@ -1,5 +1,7 @@
 <?php
 
+use MediaWiki\User\UserIdentityValue;
+
 /**
  * @author Addshore
  *
@@ -14,7 +16,8 @@ class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase {
                $noWriteService = new NoWriteWatchedItemStore( $innerService );
 
                $this->setExpectedException( DBReadOnlyError::class );
-               $noWriteService->addWatch( $this->getTestSysop()->getUser(), new TitleValue( 0, 'Foo' ) );
+               $noWriteService->addWatch(
+                       new UserIdentityValue( 1, 'MockUser', 0 ), new TitleValue( 0, 'Foo' ) );
        }
 
        public function testAddWatchBatchForUser() {
@@ -24,7 +27,7 @@ class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase {
                $noWriteService = new NoWriteWatchedItemStore( $innerService );
 
                $this->setExpectedException( DBReadOnlyError::class );
-               $noWriteService->addWatchBatchForUser( $this->getTestSysop()->getUser(), [] );
+               $noWriteService->addWatchBatchForUser( new UserIdentityValue( 1, 'MockUser', 0 ), [] );
        }
 
        public function testRemoveWatch() {
@@ -34,7 +37,8 @@ class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase {
                $noWriteService = new NoWriteWatchedItemStore( $innerService );
 
                $this->setExpectedException( DBReadOnlyError::class );
-               $noWriteService->removeWatch( $this->getTestSysop()->getUser(), new TitleValue( 0, 'Foo' ) );
+               $noWriteService->removeWatch(
+                       new UserIdentityValue( 1, 'MockUser', 0 ), new TitleValue( 0, 'Foo' ) );
        }
 
        public function testSetNotificationTimestampsForUser() {
@@ -45,7 +49,7 @@ class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase {
 
                $this->setExpectedException( DBReadOnlyError::class );
                $noWriteService->setNotificationTimestampsForUser(
-                       $this->getTestSysop()->getUser(),
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
                        'timestamp',
                        []
                );
@@ -59,7 +63,7 @@ class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase {
 
                $this->setExpectedException( DBReadOnlyError::class );
                $noWriteService->updateNotificationTimestamp(
-                       $this->getTestSysop()->getUser(),
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
                        new TitleValue( 0, 'Foo' ),
                        'timestamp'
                );
@@ -73,8 +77,8 @@ class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase {
 
                $this->setExpectedException( DBReadOnlyError::class );
                $noWriteService->resetNotificationTimestamp(
-                       $this->getTestSysop()->getUser(),
-                       Title::newFromText( 'Foo' )
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
+                       new TitleValue( 0, 'Foo' )
                );
        }
 
@@ -85,7 +89,7 @@ class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase {
                $noWriteService = new NoWriteWatchedItemStore( $innerService );
 
                $return = $noWriteService->countWatchedItems(
-                       $this->getTestSysop()->getUser()
+                       new UserIdentityValue( 1, 'MockUser', 0 )
                );
                $this->assertEquals( __METHOD__, $return );
        }
@@ -154,7 +158,7 @@ class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase {
                $noWriteService = new NoWriteWatchedItemStore( $innerService );
 
                $return = $noWriteService->getWatchedItem(
-                       $this->getTestSysop()->getUser(),
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
                        new TitleValue( 0, 'Foo' )
                );
                $this->assertEquals( __METHOD__, $return );
@@ -167,7 +171,7 @@ class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase {
                $noWriteService = new NoWriteWatchedItemStore( $innerService );
 
                $return = $noWriteService->loadWatchedItem(
-                       $this->getTestSysop()->getUser(),
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
                        new TitleValue( 0, 'Foo' )
                );
                $this->assertEquals( __METHOD__, $return );
@@ -182,7 +186,7 @@ class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase {
                $noWriteService = new NoWriteWatchedItemStore( $innerService );
 
                $return = $noWriteService->getWatchedItemsForUser(
-                       $this->getTestSysop()->getUser(),
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
                        []
                );
                $this->assertEquals( __METHOD__, $return );
@@ -195,7 +199,7 @@ class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase {
                $noWriteService = new NoWriteWatchedItemStore( $innerService );
 
                $return = $noWriteService->isWatched(
-                       $this->getTestSysop()->getUser(),
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
                        new TitleValue( 0, 'Foo' )
                );
                $this->assertEquals( __METHOD__, $return );
@@ -210,7 +214,7 @@ class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase {
                $noWriteService = new NoWriteWatchedItemStore( $innerService );
 
                $return = $noWriteService->getNotificationTimestampsBatch(
-                       $this->getTestSysop()->getUser(),
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
                        [ new TitleValue( 0, 'Foo' ) ]
                );
                $this->assertEquals( __METHOD__, $return );
@@ -225,7 +229,7 @@ class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase {
                $noWriteService = new NoWriteWatchedItemStore( $innerService );
 
                $return = $noWriteService->countUnreadNotifications(
-                       $this->getTestSysop()->getUser(),
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
                        88
                );
                $this->assertEquals( __METHOD__, $return );
index b22b7f8..3ba8773 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 
+use MediaWiki\User\UserIdentityValue;
 use Wikimedia\Rdbms\IDatabase;
 use Wikimedia\Rdbms\LoadBalancer;
 use Wikimedia\TestingAccessWrapper;
@@ -156,34 +157,32 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase {
 
        /**
         * @param int $id
+        * @param string[] $extraMethods Extra methods that are expected might be called
         * @return PHPUnit_Framework_MockObject_MockObject|User
         */
-       private function getMockNonAnonUserWithId( $id ) {
+       private function getMockNonAnonUserWithId( $id, array $extraMethods = [] ) {
                $mock = $this->getMockBuilder( User::class )->getMock();
-               $mock->expects( $this->any() )
-                       ->method( 'isAnon' )
-                       ->will( $this->returnValue( false ) );
-               $mock->expects( $this->any() )
-                       ->method( 'getId' )
-                       ->will( $this->returnValue( $id ) );
+               $mock->method( 'isRegistered' )->willReturn( true );
+               $mock->method( 'getId' )->willReturn( $id );
+               $methods = array_merge( [
+                       'isRegistered',
+                       'getId',
+               ], $extraMethods );
+               $mock->expects( $this->never() )->method( $this->anythingBut( ...$methods ) );
                return $mock;
        }
 
        /**
         * @param int $id
+        * @param string[] $extraMethods Extra methods that are expected might be called
         * @return PHPUnit_Framework_MockObject_MockObject|User
         */
-       private function getMockUnrestrictedNonAnonUserWithId( $id ) {
-               $mock = $this->getMockNonAnonUserWithId( $id );
-               $mock->expects( $this->any() )
-                       ->method( 'isAllowed' )
-                       ->will( $this->returnValue( true ) );
-               $mock->expects( $this->any() )
-                       ->method( 'isAllowedAny' )
-                       ->will( $this->returnValue( true ) );
-               $mock->expects( $this->any() )
-                       ->method( 'useRCPatrol' )
-                       ->will( $this->returnValue( true ) );
+       private function getMockUnrestrictedNonAnonUserWithId( $id, array $extraMethods = [] ) {
+               $mock = $this->getMockNonAnonUserWithId( $id,
+                       array_merge( [ 'isAllowed', 'isAllowedAny', 'useRCPatrol' ], $extraMethods ) );
+               $mock->method( 'isAllowed' )->willReturn( true );
+               $mock->method( 'isAllowedAny' )->willReturn( true );
+               $mock->method( 'useRCPatrol' )->willReturn( true );
                return $mock;
        }
 
@@ -193,18 +192,19 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase {
         * @return PHPUnit_Framework_MockObject_MockObject|User
         */
        private function getMockNonAnonUserWithIdAndRestrictedPermissions( $id, $notAllowedAction ) {
-               $mock = $this->getMockNonAnonUserWithId( $id );
+               $mock = $this->getMockNonAnonUserWithId( $id,
+                       [ 'isAllowed', 'isAllowedAny', 'useRCPatrol', 'useNPPatrol' ] );
 
-               $mock->expects( $this->any() )
-                       ->method( 'isAllowed' )
+               $mock->method( 'isAllowed' )
                        ->will( $this->returnCallback( function ( $action ) use ( $notAllowedAction ) {
                                return $action !== $notAllowedAction;
                        } ) );
-               $mock->expects( $this->any() )
-                       ->method( 'isAllowedAny' )
+               $mock->method( 'isAllowedAny' )
                        ->will( $this->returnCallback( function ( ...$actions ) use ( $notAllowedAction ) {
                                return !in_array( $notAllowedAction, $actions );
                        } ) );
+               $mock->method( 'useRCPatrol' )->willReturn( false );
+               $mock->method( 'useNPPatrol' )->willReturn( false );
 
                return $mock;
        }
@@ -214,7 +214,8 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase {
         * @return PHPUnit_Framework_MockObject_MockObject|User
         */
        private function getMockNonAnonUserWithIdAndNoPatrolRights( $id ) {
-               $mock = $this->getMockNonAnonUserWithId( $id );
+               $mock = $this->getMockNonAnonUserWithId( $id,
+                       [ 'isAllowed', 'isAllowedAny', 'useRCPatrol', 'useNPPatrol' ] );
 
                $mock->expects( $this->any() )
                        ->method( 'isAllowed' )
@@ -233,14 +234,6 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase {
                return $mock;
        }
 
-       private function getMockAnonUser() {
-               $mock = $this->getMockBuilder( User::class )->getMock();
-               $mock->expects( $this->any() )
-                       ->method( 'isAnon' )
-                       ->will( $this->returnValue( true ) );
-               return $mock;
-       }
-
        private function getFakeRow( array $rowValues ) {
                $fakeRow = new stdClass();
                foreach ( $rowValues as $valueName => $value ) {
@@ -1382,7 +1375,7 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase {
 
                $queryService = $this->newService( $mockDb );
                $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
-               $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
+               $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2, [ 'getOption' ] );
                $otherUser->expects( $this->once() )
                        ->method( 'getOption' )
                        ->with( 'watchlisttoken' )
@@ -1413,7 +1406,7 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase {
 
                $queryService = $this->newService( $mockDb );
                $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
-               $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
+               $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2, [ 'getOption' ] );
                $otherUser->expects( $this->once() )
                        ->method( 'getOption' )
                        ->with( 'watchlisttoken' )
@@ -1713,7 +1706,8 @@ class WatchedItemQueryServiceUnitTest extends MediaWikiTestCase {
 
                $queryService = $this->newService( $mockDb );
 
-               $items = $queryService->getWatchedItemsForUser( $this->getMockAnonUser() );
+               $items = $queryService->getWatchedItemsForUser(
+                       new UserIdentityValue( 0, 'AnonUser', 0 ) );
                $this->assertEmpty( $items );
        }
 
index 2f95688..82308de 100644 (file)
@@ -1,8 +1,10 @@
 <?php
 use MediaWiki\Linker\LinkTarget;
+use MediaWiki\Revision\RevisionLookup;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\User\UserIdentityValue;
 use Wikimedia\Rdbms\LBFactory;
 use Wikimedia\Rdbms\LoadBalancer;
-use Wikimedia\ScopedCallback;
 use Wikimedia\TestingAccessWrapper;
 
 /**
@@ -109,28 +111,42 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
        }
 
        /**
-        * @param int $id
-        * @return PHPUnit_Framework_MockObject_MockObject|User
+        * Assumes that only getSubjectPage and getTalkPage will ever be called, and everything passed
+        * to them will have namespace 0.
         */
-       private function getMockNonAnonUserWithId( $id ) {
-               $mock = $this->createMock( User::class );
-               $mock->expects( $this->any() )
-                       ->method( 'isAnon' )
-                       ->will( $this->returnValue( false ) );
-               $mock->expects( $this->any() )
-                       ->method( 'getId' )
-                       ->will( $this->returnValue( $id ) );
-               $mock->expects( $this->any() )
-                       ->method( 'getUserPage' )
-                       ->will( $this->returnValue( Title::makeTitle( NS_USER, 'MockUser' ) ) );
+       private function getMockNsInfo() : NamespaceInfo {
+               $mock = $this->createMock( NamespaceInfo::class );
+               $mock->method( 'getSubjectPage' )->will( $this->returnArgument( 0 ) );
+               $mock->method( 'getTalkPage' )->will( $this->returnCallback(
+                               function ( $target ) {
+                                       return new TitleValue( 1, $target->getDbKey() );
+                               }
+                       ) );
+               $mock->expects( $this->never() )
+                       ->method( $this->anythingBut( 'getSubjectPage', 'getTalkPage' ) );
                return $mock;
        }
 
        /**
-        * @return User
+        * No methods may be called except provided callbacks, if any.
+        *
+        * @param array $callbacks Keys are method names, values are callbacks
+        * @param array $counts Keys are method names, values are expected number of times to be called
+        *   (default is any number is okay)
         */
-       private function getAnonUser() {
-               return User::newFromName( 'Anon_User' );
+       private function getMockRevisionLookup(
+               array $callbacks = [], array $counts = []
+       ) : RevisionLookup {
+               $mock = $this->createMock( RevisionLookup::class );
+               foreach ( $callbacks as $method => $callback ) {
+                       $count = isset( $counts[$method] ) ? $this->exactly( $counts[$method] ) : $this->any();
+                       $mock->expects( $count )
+                               ->method( $method )
+                               ->will( $this->returnCallback( $callbacks[$method] ) );
+               }
+               $mock->expects( $this->never() )
+                       ->method( $this->anythingBut( ...array_keys( $callbacks ) ) );
+               return $mock;
        }
 
        private function getFakeRow( array $rowValues ) {
@@ -141,24 +157,33 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                return $fakeRow;
        }
 
-       private function newWatchedItemStore(
-               LBFactory $lbFactory,
-               JobQueueGroup $queueGroup,
-               HashBagOStuff $cache,
-               ReadOnlyMode $readOnlyMode
-       ) {
+       /**
+        * @param array $mocks Associative array providing mocks to use when constructing the
+        *   WatchedItemStore. Anything not provided will fall back to a default. Valid keys:
+        *     * lbFactory
+        *     * db
+        *     * queueGroup
+        *     * cache
+        *     * readOnlyMode
+        *     * nsInfo
+        *     * revisionLookup
+        */
+       private function newWatchedItemStore( array $mocks = [] ) : WatchedItemStore {
                return new WatchedItemStore(
-                       $lbFactory,
-                       $queueGroup,
+                       $mocks['lbFactory'] ??
+                               $this->getMockLBFactory( $mocks['db'] ?? $this->getMockDb() ),
+                       $mocks['queueGroup'] ?? $this->getMockJobQueueGroup(),
                        new HashBagOStuff(),
-                       $cache,
-                       $readOnlyMode,
-                       1000
+                       $mocks['cache'] ?? $this->getMockCache(),
+                       $mocks['readOnlyMode'] ?? $this->getMockReadOnlyMode(),
+                       1000,
+                       $mocks['nsInfo'] ?? $this->getMockNsInfo(),
+                       $mocks['revisionLookup'] ?? $this->getMockRevisionLookup()
                );
        }
 
        public function testClearWatchedItems() {
-               $user = $this->getMockNonAnonUserWithId( 7 );
+               $user = new UserIdentityValue( 7, 'MockUser', 0 );
 
                $mockDb = $this->getMockDb();
                $mockDb->expects( $this->once() )
@@ -187,12 +212,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->method( 'delete' )
                        ->with( 'RM-KEY' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
                TestingAccessWrapper::newFromObject( $store )
                        ->cacheIndex = [ 0 => [ 'F' => [ 7 => 'RM-KEY', 9 => 'KEEP-KEY' ] ] ];
 
@@ -200,7 +220,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
        }
 
        public function testClearWatchedItems_tooManyItemsWatched() {
-               $user = $this->getMockNonAnonUserWithId( 7 );
+               $user = new UserIdentityValue( 7, 'MockUser', 0 );
 
                $mockDb = $this->getMockDb();
                $mockDb->expects( $this->once() )
@@ -220,18 +240,13 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertFalse( $store->clearUserWatchedItems( $user ) );
        }
 
        public function testCountWatchedItems() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
+               $user = new UserIdentityValue( 1, 'MockUser', 0 );
 
                $mockDb = $this->getMockDb();
                $mockDb->expects( $this->exactly( 1 ) )
@@ -251,12 +266,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertEquals( 12, $store->countWatchedItems( $user ) );
        }
@@ -283,12 +293,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertEquals( 7, $store->countWatchers( $titleValue ) );
        }
@@ -336,12 +341,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $expected = [
                        0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ],
@@ -404,12 +404,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $expected = [
                        0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ],
@@ -454,12 +449,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertEquals( 7, $store->countVisitingWatchers( $titleValue, '111' ) );
        }
@@ -537,12 +527,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $expected = [
                        0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ],
@@ -643,12 +628,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $expected = [
                        0 => [
@@ -698,12 +678,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $expected = [
                        0 => [ 'SomeDbKey' => 0, 'OtherDbKey' => 0 ],
@@ -716,7 +691,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
        }
 
        public function testCountUnreadNotifications() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
+               $user = new UserIdentityValue( 1, 'MockUser', 0 );
 
                $mockDb = $this->getMockDb();
                $mockDb->expects( $this->exactly( 1 ) )
@@ -737,12 +712,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertEquals( 9, $store->countUnreadNotifications( $user ) );
        }
@@ -751,7 +721,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
         * @dataProvider provideIntWithDbUnsafeVersion
         */
        public function testCountUnreadNotifications_withUnreadLimit_overLimit( $limit ) {
-               $user = $this->getMockNonAnonUserWithId( 1 );
+               $user = new UserIdentityValue( 1, 'MockUser', 0 );
 
                $mockDb = $this->getMockDb();
                $mockDb->expects( $this->exactly( 1 ) )
@@ -773,12 +743,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertSame(
                        true,
@@ -790,7 +755,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
         * @dataProvider provideIntWithDbUnsafeVersion
         */
        public function testCountUnreadNotifications_withUnreadLimit_underLimit( $limit ) {
-               $user = $this->getMockNonAnonUserWithId( 1 );
+               $user = new UserIdentityValue( 1, 'MockUser', 0 );
 
                $mockDb = $this->getMockDb();
                $mockDb->expects( $this->exactly( 1 ) )
@@ -812,12 +777,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertEquals(
                        9,
@@ -844,16 +804,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        )
                        ->will( $this->returnValue( new FakeResultWrapper( [] ) ) );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $this->getMockCache(),
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb ] );
 
                $store->duplicateEntry(
-                       Title::newFromText( 'Old_Title' ),
-                       Title::newFromText( 'New_Title' )
+                       new TitleValue( 0, 'Old_Title' ),
+                       new TitleValue( 0, 'New_Title' )
                );
        }
 
@@ -904,16 +859,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $store->duplicateEntry(
-                       Title::newFromText( 'Old_Title' ),
-                       Title::newFromText( 'New_Title' )
+                       new TitleValue( 0, 'Old_Title' ),
+                       new TitleValue( 0, 'New_Title' )
                );
        }
 
@@ -952,22 +902,17 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $store->duplicateAllAssociatedEntries(
-                       Title::newFromText( 'Old_Title' ),
-                       Title::newFromText( 'New_Title' )
+                       new TitleValue( 0, 'Old_Title' ),
+                       new TitleValue( 0, 'New_Title' )
                );
        }
 
        public function provideLinkTargetPairs() {
                return [
-                       [ Title::newFromText( 'Old_Title' ), Title::newFromText( 'New_Title' ) ],
+                       [ new TitleValue( 0, 'Old_Title' ), new TitleValue( 0, 'New_Title' ) ],
                        [ new TitleValue( 0, 'Old_Title' ),  new TitleValue( 0, 'New_Title' ) ],
                ];
        }
@@ -1047,12 +992,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $store->duplicateAllAssociatedEntries(
                        $oldTarget,
@@ -1081,16 +1021,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->method( 'delete' )
                        ->with( '0:Some_Page:1' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $store->addWatch(
-                       $this->getMockNonAnonUserWithId( 1 ),
-                       Title::newFromText( 'Some_Page' )
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
+                       new TitleValue( 0, 'Some_Page' )
                );
        }
 
@@ -1103,30 +1038,21 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )
                        ->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $store->addWatch(
-                       $this->getAnonUser(),
-                       Title::newFromText( 'Some_Page' )
+                       new UserIdentityValue( 0, 'AnonUser', 0 ),
+                       new TitleValue( 0, 'Some_Page' )
                );
        }
 
        public function testAddWatchBatchForUser_readOnlyDBReturnsFalse() {
                $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $this->getMockDb() ),
-                       $this->getMockJobQueueGroup(),
-                       $this->getMockCache(),
-                       $this->getMockReadOnlyMode( true )
-               );
+                       [ 'readOnlyMode' => $this->getMockReadOnlyMode( true ) ] );
 
                $this->assertFalse(
                        $store->addWatchBatchForUser(
-                               $this->getMockNonAnonUserWithId( 1 ),
+                               new UserIdentityValue( 1, 'MockUser', 0 ),
                                [ new TitleValue( 0, 'Some_Page' ), new TitleValue( 1, 'Some_Page' ) ]
                        )
                );
@@ -1168,14 +1094,9 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->method( 'delete' )
                        ->with( '1:Some_Page:1' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
-               $mockUser = $this->getMockNonAnonUserWithId( 1 );
+               $mockUser = new UserIdentityValue( 1, 'MockUser', 0 );
 
                $this->assertTrue(
                        $store->addWatchBatchForUser(
@@ -1194,23 +1115,18 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )
                        ->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertFalse(
                        $store->addWatchBatchForUser(
-                               $this->getAnonUser(),
+                               new UserIdentityValue( 0, 'AnonUser', 0 ),
                                [ new TitleValue( 0, 'Other_Page' ) ]
                        )
                );
        }
 
        public function testAddWatchBatchReturnsTrue_whenGivenEmptyList() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
+               $user = new UserIdentityValue( 1, 'MockUser', 0 );
                $mockDb = $this->getMockDb();
                $mockDb->expects( $this->never() )
                        ->method( 'insert' );
@@ -1219,12 +1135,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )
                        ->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertTrue(
                        $store->addWatchBatchForUser( $user, [] )
@@ -1255,15 +1166,10 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                '0:SomeDbKey:1'
                        );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $watchedItem = $store->loadWatchedItem(
-                       $this->getMockNonAnonUserWithId( 1 ),
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
                        new TitleValue( 0, 'SomeDbKey' )
                );
                $this->assertInstanceOf( WatchedItem::class, $watchedItem );
@@ -1291,16 +1197,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertFalse(
                        $store->loadWatchedItem(
-                               $this->getMockNonAnonUserWithId( 1 ),
+                               new UserIdentityValue( 1, 'MockUser', 0 ),
                                new TitleValue( 0, 'SomeDbKey' )
                        )
                );
@@ -1315,16 +1216,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertFalse(
                        $store->loadWatchedItem(
-                               $this->getAnonUser(),
+                               new UserIdentityValue( 0, 'AnonUser', 0 ),
                                new TitleValue( 0, 'SomeDbKey' )
                        )
                );
@@ -1365,18 +1261,12 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                [ '1:SomeDbKey:1' ]
                        );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
-               $titleValue = new TitleValue( 0, 'SomeDbKey' );
                $this->assertTrue(
                        $store->removeWatch(
-                               $this->getMockNonAnonUserWithId( 1 ),
-                               Title::newFromTitleValue( $titleValue )
+                               new UserIdentityValue( 1, 'MockUser', 0 ),
+                               new TitleValue( 0, 'SomeDbKey' )
                        )
                );
        }
@@ -1417,18 +1307,12 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                [ '1:SomeDbKey:1' ]
                        );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
-               $titleValue = new TitleValue( 0, 'SomeDbKey' );
                $this->assertFalse(
                        $store->removeWatch(
-                               $this->getMockNonAnonUserWithId( 1 ),
-                               Title::newFromTitleValue( $titleValue )
+                               new UserIdentityValue( 1, 'MockUser', 0 ),
+                               new TitleValue( 0, 'SomeDbKey' )
                        )
                );
        }
@@ -1443,16 +1327,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )
                        ->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertFalse(
                        $store->removeWatch(
-                               $this->getAnonUser(),
+                               new UserIdentityValue( 0, 'AnonUser', 0 ),
                                new TitleValue( 0, 'SomeDbKey' )
                        )
                );
@@ -1489,15 +1368,10 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                '0:SomeDbKey:1'
                        );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $watchedItem = $store->getWatchedItem(
-                       $this->getMockNonAnonUserWithId( 1 ),
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
                        new TitleValue( 0, 'SomeDbKey' )
                );
                $this->assertInstanceOf( WatchedItem::class, $watchedItem );
@@ -1511,7 +1385,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockDb->expects( $this->never() )
                        ->method( 'selectRow' );
 
-               $mockUser = $this->getMockNonAnonUserWithId( 1 );
+               $mockUser = new UserIdentityValue( 1, 'MockUser', 0 );
                $linkTarget = new TitleValue( 0, 'SomeDbKey' );
                $cachedItem = new WatchedItem( $mockUser, $linkTarget, '20151212010101' );
 
@@ -1525,12 +1399,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        )
                        ->will( $this->returnValue( $cachedItem ) );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertEquals(
                        $cachedItem,
@@ -1564,16 +1433,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->with( '0:SomeDbKey:1' )
                        ->will( $this->returnValue( false ) );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertFalse(
                        $store->getWatchedItem(
-                               $this->getMockNonAnonUserWithId( 1 ),
+                               new UserIdentityValue( 1, 'MockUser', 0 ),
                                new TitleValue( 0, 'SomeDbKey' )
                        )
                );
@@ -1589,16 +1453,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertFalse(
                        $store->getWatchedItem(
-                               $this->getAnonUser(),
+                               new UserIdentityValue( 0, 'AnonUser', 0 ),
                                new TitleValue( 0, 'SomeDbKey' )
                        )
                );
@@ -1631,13 +1490,8 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'set' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-               $user = $this->getMockNonAnonUserWithId( 1 );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
+               $user = new UserIdentityValue( 1, 'MockUser', 0 );
 
                $watchedItems = $store->getWatchedItemsForUser( $user );
 
@@ -1670,7 +1524,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockDb = $this->getMockDb();
                $mockCache = $this->getMockCache();
                $mockLoadBalancer = $this->getMockLBFactory( $mockDb, $dbType );
-               $user = $this->getMockNonAnonUserWithId( 1 );
+               $user = new UserIdentityValue( 1, 'MockUser', 0 );
 
                $mockDb->expects( $this->once() )
                        ->method( 'select' )
@@ -1684,11 +1538,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->will( $this->returnValue( [] ) );
 
                $store = $this->newWatchedItemStore(
-                       $mockLoadBalancer,
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+                       [ 'lbFactory' => $mockLoadBalancer, 'cache' => $mockCache ] );
 
                $watchedItems = $store->getWatchedItemsForUser(
                        $user,
@@ -1698,16 +1548,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
        }
 
        public function testGetWatchedItemsForUser_badSortOptionThrowsException() {
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $this->getMockDb() ),
-                       $this->getMockJobQueueGroup(),
-                       $this->getMockCache(),
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore();
 
                $this->setExpectedException( InvalidArgumentException::class );
                $store->getWatchedItemsForUser(
-                       $this->getMockNonAnonUserWithId( 1 ),
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
                        [ 'sort' => 'foo' ]
                );
        }
@@ -1741,16 +1586,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                '0:SomeDbKey:1'
                        );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertTrue(
                        $store->isWatched(
-                               $this->getMockNonAnonUserWithId( 1 ),
+                               new UserIdentityValue( 1, 'MockUser', 0 ),
                                new TitleValue( 0, 'SomeDbKey' )
                        )
                );
@@ -1779,16 +1619,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->with( '0:SomeDbKey:1' )
                        ->will( $this->returnValue( false ) );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertFalse(
                        $store->isWatched(
-                               $this->getMockNonAnonUserWithId( 1 ),
+                               new UserIdentityValue( 1, 'MockUser', 0 ),
                                new TitleValue( 0, 'SomeDbKey' )
                        )
                );
@@ -1804,16 +1639,11 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertFalse(
                        $store->isWatched(
-                               $this->getAnonUser(),
+                               new UserIdentityValue( 0, 'AnonUser', 0 ),
                                new TitleValue( 0, 'SomeDbKey' )
                        )
                );
@@ -1873,19 +1703,15 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertEquals(
                        [
                                0 => [ 'SomeDbKey' => '20151212010101', ],
                                1 => [ 'AnotherDbKey' => null, ],
                        ],
-                       $store->getNotificationTimestampsBatch( $this->getMockNonAnonUserWithId( 1 ), $targets )
+                       $store->getNotificationTimestampsBatch(
+                               new UserIdentityValue( 1, 'MockUser', 0 ), $targets )
                );
        }
 
@@ -1925,18 +1751,14 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertEquals(
                        [
                                0 => [ 'OtherDbKey' => false, ],
                        ],
-                       $store->getNotificationTimestampsBatch( $this->getMockNonAnonUserWithId( 1 ), $targets )
+                       $store->getNotificationTimestampsBatch(
+                               new UserIdentityValue( 1, 'MockUser', 0 ), $targets )
                );
        }
 
@@ -1946,7 +1768,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        new TitleValue( 1, 'AnotherDbKey' ),
                ];
 
-               $user = $this->getMockNonAnonUserWithId( 1 );
+               $user = new UserIdentityValue( 1, 'MockUser', 0 );
                $cachedItem = new WatchedItem( $user, $targets[0], '20151212010101' );
 
                $mockDb = $this->getMockDb();
@@ -1988,12 +1810,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertEquals(
                        [
@@ -2010,7 +1827,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        new TitleValue( 1, 'AnotherDbKey' ),
                ];
 
-               $user = $this->getMockNonAnonUserWithId( 1 );
+               $user = new UserIdentityValue( 1, 'MockUser', 0 );
                $cachedItems = [
                        new WatchedItem( $user, $targets[0], '20151212010101' ),
                        new WatchedItem( $user, $targets[1], null ),
@@ -2030,12 +1847,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertEquals(
                        [
@@ -2058,19 +1870,15 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache = $this->getMockCache();
                $mockCache->expects( $this->never() )->method( $this->anything() );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertEquals(
                        [
                                0 => [ 'SomeDbKey' => false, ],
                                1 => [ 'AnotherDbKey' => false, ],
                        ],
-                       $store->getNotificationTimestampsBatch( $this->getAnonUser(), $targets )
+                       $store->getNotificationTimestampsBatch(
+                               new UserIdentityValue( 0, 'AnonUser', 0 ), $targets )
                );
        }
 
@@ -2084,17 +1892,12 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertFalse(
                        $store->resetNotificationTimestamp(
-                               $this->getAnonUser(),
-                               Title::newFromText( 'SomeDbKey' )
+                               new UserIdentityValue( 0, 'AnonUser', 0 ),
+                               new TitleValue( 0, 'SomeDbKey' )
                        )
                );
        }
@@ -2119,24 +1922,19 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertFalse(
                        $store->resetNotificationTimestamp(
-                               $this->getMockNonAnonUserWithId( 1 ),
-                               Title::newFromText( 'SomeDbKey' )
+                               new UserIdentityValue( 1, 'MockUser', 0 ),
+                               new TitleValue( 0, 'SomeDbKey' )
                        )
                );
        }
 
        public function testResetNotificationTimestamp_item() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
-               $title = Title::newFromText( 'SomeDbKey' );
+               $user = new UserIdentityValue( 1, 'MockUser', 0 );
+               $title = new TitleValue( 0, 'SomeDbKey' );
 
                $mockDb = $this->getMockDb();
                $mockDb->expects( $this->once() )
@@ -2173,12 +1971,22 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                // don't run
                        } );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $mockQueueGroup,
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               // We don't care if these methods actually do anything here
+               $mockRevisionLookup = $this->getMockRevisionLookup( [
+                       'getRevisionByTitle' => function () {
+                               return null;
+                       },
+                       'getTimestampFromId' => function () {
+                               return '00000000000000';
+                       },
+               ] );
+
+               $store = $this->newWatchedItemStore( [
+                       'db' => $mockDb,
+                       'queueGroup' => $mockQueueGroup,
+                       'cache' => $mockCache,
+                       'revisionLookup' => $mockRevisionLookup,
+               ] );
 
                $this->assertTrue(
                        $store->resetNotificationTimestamp(
@@ -2189,8 +1997,8 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
        }
 
        public function testResetNotificationTimestamp_noItemForced() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
-               $title = Title::newFromText( 'SomeDbKey' );
+               $user = new UserIdentityValue( 1, 'MockUser', 0 );
+               $title = new TitleValue( 0, 'SomeDbKey' );
 
                $mockDb = $this->getMockDb();
                $mockDb->expects( $this->never() )
@@ -2204,12 +2012,23 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->with( '0:SomeDbKey:1' );
 
                $mockQueueGroup = $this->getMockJobQueueGroup();
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $mockQueueGroup,
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+
+               // We don't care if these methods actually do anything here
+               $mockRevisionLookup = $this->getMockRevisionLookup( [
+                       'getRevisionByTitle' => function () {
+                               return null;
+                       },
+                       'getTimestampFromId' => function () {
+                               return '00000000000000';
+                       },
+               ] );
+
+               $store = $this->newWatchedItemStore( [
+                       'db' => $mockDb,
+                       'queueGroup' => $mockQueueGroup,
+                       'cache' => $mockCache,
+                       'revisionLookup' => $mockRevisionLookup,
+               ] );
 
                $mockQueueGroup->expects( $this->any() )
                        ->method( 'lazyPush' )
@@ -2226,26 +2045,6 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                );
        }
 
-       /**
-        * @param string $text
-        * @param int $ns
-        *
-        * @return PHPUnit_Framework_MockObject_MockObject|Title
-        */
-       private function getMockTitle( $text, $ns = 0 ) {
-               $title = $this->createMock( Title::class );
-               $title->expects( $this->any() )
-                       ->method( 'getText' )
-                       ->will( $this->returnValue( str_replace( '_', ' ', $text ) ) );
-               $title->expects( $this->any() )
-                       ->method( 'getDbKey' )
-                       ->will( $this->returnValue( str_replace( '_', ' ', $text ) ) );
-               $title->expects( $this->any() )
-                       ->method( 'getNamespace' )
-                       ->will( $this->returnValue( $ns ) );
-               return $title;
-       }
-
        private function verifyCallbackJob(
                ActivityUpdateJob $job,
                LinkTarget $expectedTitle,
@@ -2265,13 +2064,9 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
        }
 
        public function testResetNotificationTimestamp_oldidSpecifiedLatestRevisionForced() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
+               $user = new UserIdentityValue( 1, 'MockUser', 0 );
                $oldid = 22;
-               $title = $this->getMockTitle( 'SomeTitle' );
-               $title->expects( $this->once() )
-                       ->method( 'getNextRevisionID' )
-                       ->with( $oldid )
-                       ->will( $this->returnValue( false ) );
+               $title = new TitleValue( 0, 'SomeTitle' );
 
                $mockDb = $this->getMockDb();
                $mockDb->expects( $this->never() )
@@ -2285,12 +2080,35 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->with( '0:SomeTitle:1' );
 
                $mockQueueGroup = $this->getMockJobQueueGroup();
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $mockQueueGroup,
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+
+               $mockRevisionRecord = $this->createMock( RevisionRecord::class );
+               $mockRevisionRecord->expects( $this->never() )->method( $this->anything() );
+
+               $mockRevisionLookup = $this->getMockRevisionLookup( [
+                       'getTimestampFromId' => function () {
+                               return '00000000000000';
+                       },
+                       'getRevisionById' => function ( $id, $flags ) use ( $oldid, $mockRevisionRecord ) {
+                               $this->assertSame( $oldid, $id );
+                               $this->assertSame( 0, $flags );
+                               return $mockRevisionRecord;
+                       },
+                       'getNextRevision' =>
+                       function ( $oldRev, $titleArg ) use ( $mockRevisionRecord, $title ) {
+                               $this->assertSame( $mockRevisionRecord, $oldRev );
+                               $this->assertSame( $title, $titleArg );
+                               return false;
+                       },
+               ], [
+                       'getNextRevision' => 1,
+               ] );
+
+               $store = $this->newWatchedItemStore( [
+                       'db' => $mockDb,
+                       'queueGroup' => $mockQueueGroup,
+                       'cache' => $mockCache,
+                       'revisionLookup' => $mockRevisionLookup,
+               ] );
 
                $mockQueueGroup->expects( $this->any() )
                        ->method( 'lazyPush' )
@@ -2318,13 +2136,15 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
        }
 
        public function testResetNotificationTimestamp_oldidSpecifiedNotLatestRevisionForced() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
+               $user = new UserIdentityValue( 1, 'MockUser', 0 );
                $oldid = 22;
-               $title = $this->getMockTitle( 'SomeDbKey' );
-               $title->expects( $this->once() )
-                       ->method( 'getNextRevisionID' )
-                       ->with( $oldid )
-                       ->will( $this->returnValue( 33 ) );
+               $title = new TitleValue( 0, 'SomeDbKey' );
+
+               $mockRevision = $this->createMock( RevisionRecord::class );
+               $mockRevision->expects( $this->never() )->method( $this->anything() );
+
+               $mockNextRevision = $this->createMock( RevisionRecord::class );
+               $mockNextRevision->expects( $this->never() )->method( $this->anything() );
 
                $mockDb = $this->getMockDb();
                $mockDb->expects( $this->once() )
@@ -2352,12 +2172,34 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->with( '0:SomeDbKey:1' );
 
                $mockQueueGroup = $this->getMockJobQueueGroup();
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $mockQueueGroup,
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+
+               $mockRevisionLookup = $this->getMockRevisionLookup(
+                       [
+                               'getTimestampFromId' => function ( $oldidParam ) use ( $oldid ) {
+                                       $this->assertSame( $oldid, $oldidParam );
+                               },
+                               'getRevisionById' => function ( $id ) use ( $oldid, $mockRevision ) {
+                                       $this->assertSame( $oldid, $id );
+                                       return $mockRevision;
+                               },
+                               'getNextRevision' =>
+                               function ( RevisionRecord $rev ) use ( $mockRevision, $mockNextRevision ) {
+                                       $this->assertSame( $mockRevision, $rev );
+                                       return $mockNextRevision;
+                               },
+                       ],
+                       [
+                               'getTimestampFromId' => 2,
+                               'getRevisionById' => 1,
+                               'getNextRevision' => 1,
+                       ]
+               );
+               $store = $this->newWatchedItemStore( [
+                       'db' => $mockDb,
+                       'queueGroup' => $mockQueueGroup,
+                       'cache' => $mockCache,
+                       'revisionLookup' => $mockRevisionLookup,
+               ] );
 
                $mockQueueGroup->expects( $this->any() )
                        ->method( 'lazyPush' )
@@ -2374,15 +2216,6 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                }
                        ) );
 
-               $getTimestampCallCounter = 0;
-               $scopedOverrideRevision = $store->overrideRevisionGetTimestampFromIdCallback(
-                       function ( $titleParam, $oldidParam ) use ( &$getTimestampCallCounter, $title, $oldid ) {
-                               $getTimestampCallCounter++;
-                               $this->assertEquals( $title, $titleParam );
-                               $this->assertEquals( $oldid, $oldidParam );
-                       }
-               );
-
                $this->assertTrue(
                        $store->resetNotificationTimestamp(
                                $user,
@@ -2391,19 +2224,12 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                $oldid
                        )
                );
-               $this->assertEquals( 2, $getTimestampCallCounter );
-
-               ScopedCallback::consume( $scopedOverrideRevision );
        }
 
        public function testResetNotificationTimestamp_notWatchedPageForced() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
+               $user = new UserIdentityValue( 1, 'MockUser', 0 );
                $oldid = 22;
-               $title = $this->getMockTitle( 'SomeDbKey' );
-               $title->expects( $this->once() )
-                       ->method( 'getNextRevisionID' )
-                       ->with( $oldid )
-                       ->will( $this->returnValue( 33 ) );
+               $title = new TitleValue( 0, 'SomeDbKey' );
 
                $mockDb = $this->getMockDb();
                $mockDb->expects( $this->once() )
@@ -2427,13 +2253,42 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->with( '0:SomeDbKey:1' );
 
                $mockQueueGroup = $this->getMockJobQueueGroup();
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $mockQueueGroup,
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
+
+               $mockRevision = $this->createMock( RevisionRecord::class );
+               $mockRevision->expects( $this->never() )->method( $this->anything() );
+
+               $mockNextRevision = $this->createMock( RevisionRecord::class );
+               $mockNextRevision->expects( $this->never() )->method( $this->anything() );
+
+               $mockRevisionLookup = $this->getMockRevisionLookup(
+                       [
+                               'getTimestampFromId' => function ( $oldidParam ) use ( $oldid ) {
+                                       $this->assertSame( $oldid, $oldidParam );
+                               },
+                               'getRevisionById' => function ( $id ) use ( $oldid, $mockRevision ) {
+                                       $this->assertSame( $oldid, $id );
+                                       return $mockRevision;
+                               },
+                               'getNextRevision' =>
+                               function ( RevisionRecord $rev ) use ( $mockRevision, $mockNextRevision ) {
+                                       $this->assertSame( $mockRevision, $rev );
+                                       return $mockNextRevision;
+                               },
+                       ],
+                       [
+                               'getTimestampFromId' => 1,
+                               'getRevisionById' => 1,
+                               'getNextRevision' => 1,
+                       ]
                );
 
+               $store = $this->newWatchedItemStore( [
+                       'db' => $mockDb,
+                       'queueGroup' => $mockQueueGroup,
+                       'cache' => $mockCache,
+                       'revisionLookup' => $mockRevisionLookup,
+               ] );
+
                $mockQueueGroup->expects( $this->any() )
                        ->method( 'lazyPush' )
                        ->will( $this->returnCallback(
@@ -2460,13 +2315,9 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
        }
 
        public function testResetNotificationTimestamp_futureNotificationTimestampForced() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
+               $user = new UserIdentityValue( 1, 'MockUser', 0 );
                $oldid = 22;
-               $title = $this->getMockTitle( 'SomeDbKey' );
-               $title->expects( $this->once() )
-                       ->method( 'getNextRevisionID' )
-                       ->with( $oldid )
-                       ->will( $this->returnValue( 33 ) );
+               $title = new TitleValue( 0, 'SomeDbKey' );
 
                $mockDb = $this->getMockDb();
                $mockDb->expects( $this->once() )
@@ -2494,13 +2345,42 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->with( '0:SomeDbKey:1' );
 
                $mockQueueGroup = $this->getMockJobQueueGroup();
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $mockQueueGroup,
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
+
+               $mockRevision = $this->createMock( RevisionRecord::class );
+               $mockRevision->expects( $this->never() )->method( $this->anything() );
+
+               $mockNextRevision = $this->createMock( RevisionRecord::class );
+               $mockNextRevision->expects( $this->never() )->method( $this->anything() );
+
+               $mockRevisionLookup = $this->getMockRevisionLookup(
+                       [
+                               'getTimestampFromId' => function ( $oldidParam ) use ( $oldid ) {
+                                       $this->assertEquals( $oldid, $oldidParam );
+                               },
+                               'getRevisionById' => function ( $id ) use ( $oldid, $mockRevision ) {
+                                       $this->assertSame( $oldid, $id );
+                                       return $mockRevision;
+                               },
+                               'getNextRevision' =>
+                               function ( RevisionRecord $rev ) use ( $mockRevision, $mockNextRevision ) {
+                                       $this->assertSame( $mockRevision, $rev );
+                                       return $mockNextRevision;
+                               },
+                       ],
+                       [
+                               'getTimestampFromId' => 2,
+                               'getRevisionById' => 1,
+                               'getNextRevision' => 1,
+                       ]
                );
 
+               $store = $this->newWatchedItemStore( [
+                       'db' => $mockDb,
+                       'queueGroup' => $mockQueueGroup,
+                       'cache' => $mockCache,
+                       'revisionLookup' => $mockRevisionLookup,
+               ] );
+
                $mockQueueGroup->expects( $this->any() )
                        ->method( 'lazyPush' )
                        ->will( $this->returnCallback(
@@ -2516,15 +2396,6 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                }
                        ) );
 
-               $getTimestampCallCounter = 0;
-               $scopedOverrideRevision = $store->overrideRevisionGetTimestampFromIdCallback(
-                       function ( $titleParam, $oldidParam ) use ( &$getTimestampCallCounter, $title, $oldid ) {
-                               $getTimestampCallCounter++;
-                               $this->assertEquals( $title, $titleParam );
-                               $this->assertEquals( $oldid, $oldidParam );
-                       }
-               );
-
                $this->assertTrue(
                        $store->resetNotificationTimestamp(
                                $user,
@@ -2533,19 +2404,12 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                $oldid
                        )
                );
-               $this->assertEquals( 2, $getTimestampCallCounter );
-
-               ScopedCallback::consume( $scopedOverrideRevision );
        }
 
        public function testResetNotificationTimestamp_futureNotificationTimestampNotForced() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
+               $user = new UserIdentityValue( 1, 'MockUser', 0 );
                $oldid = 22;
-               $title = $this->getMockTitle( 'SomeDbKey' );
-               $title->expects( $this->once() )
-                       ->method( 'getNextRevisionID' )
-                       ->with( $oldid )
-                       ->will( $this->returnValue( 33 ) );
+               $title = new TitleValue( 0, 'SomeDbKey' );
 
                $mockDb = $this->getMockDb();
                $mockDb->expects( $this->once() )
@@ -2573,12 +2437,40 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->with( '0:SomeDbKey:1' );
 
                $mockQueueGroup = $this->getMockJobQueueGroup();
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $mockQueueGroup,
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+
+               $mockRevision = $this->createMock( RevisionRecord::class );
+               $mockRevision->expects( $this->never() )->method( $this->anything() );
+
+               $mockNextRevision = $this->createMock( RevisionRecord::class );
+               $mockNextRevision->expects( $this->never() )->method( $this->anything() );
+
+               $mockRevisionLookup = $this->getMockRevisionLookup(
+                       [
+                               'getTimestampFromId' => function ( $oldidParam ) use ( $oldid ) {
+                                       $this->assertEquals( $oldid, $oldidParam );
+                               },
+                               'getRevisionById' => function ( $id ) use ( $oldid, $mockRevision ) {
+                                       $this->assertSame( $oldid, $id );
+                                       return $mockRevision;
+                               },
+                               'getNextRevision' =>
+                               function ( RevisionRecord $rev ) use ( $mockRevision, $mockNextRevision ) {
+                                       $this->assertSame( $mockRevision, $rev );
+                                       return $mockNextRevision;
+                               },
+                       ],
+                       [
+                               'getTimestampFromId' => 2,
+                               'getRevisionById' => 1,
+                               'getNextRevision' => 1,
+                       ]
+               );
+               $store = $this->newWatchedItemStore( [
+                       'db' => $mockDb,
+                       'queueGroup' => $mockQueueGroup,
+                       'cache' => $mockCache,
+                       'revisionLookup' => $mockRevisionLookup,
+               ] );
 
                $mockQueueGroup->expects( $this->any() )
                        ->method( 'lazyPush' )
@@ -2595,15 +2487,6 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                }
                        ) );
 
-               $getTimestampCallCounter = 0;
-               $scopedOverrideRevision = $store->overrideRevisionGetTimestampFromIdCallback(
-                       function ( $titleParam, $oldidParam ) use ( &$getTimestampCallCounter, $title, $oldid ) {
-                               $getTimestampCallCounter++;
-                               $this->assertEquals( $title, $titleParam );
-                               $this->assertEquals( $oldid, $oldidParam );
-                       }
-               );
-
                $this->assertTrue(
                        $store->resetNotificationTimestamp(
                                $user,
@@ -2612,31 +2495,19 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                $oldid
                        )
                );
-               $this->assertEquals( 2, $getTimestampCallCounter );
-
-               ScopedCallback::consume( $scopedOverrideRevision );
        }
 
        public function testSetNotificationTimestampsForUser_anonUser() {
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $this->getMockDb() ),
-                       $this->getMockJobQueueGroup(),
-                       $this->getMockCache(),
-                       $this->getMockReadOnlyMode()
-               );
-               $this->assertFalse( $store->setNotificationTimestampsForUser( $this->getAnonUser(), '' ) );
+               $store = $this->newWatchedItemStore();
+               $this->assertFalse( $store->setNotificationTimestampsForUser(
+                       new UserIdentityValue( 0, 'AnonUser', 0 ), '' ) );
        }
 
        public function testSetNotificationTimestampsForUser_allRows() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
+               $user = new UserIdentityValue( 1, 'MockUser', 0 );
                $timestamp = '20100101010101';
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $this->getMockDb() ),
-                       $this->getMockJobQueueGroup(),
-                       $this->getMockCache(),
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore();
 
                // Note: This does not actually assert the job is correct
                $callableCallCounter = 0;
@@ -2653,15 +2524,10 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
        }
 
        public function testSetNotificationTimestampsForUser_nullTimestamp() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
+               $user = new UserIdentityValue( 1, 'MockUser', 0 );
                $timestamp = null;
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $this->getMockDb() ),
-                       $this->getMockJobQueueGroup(),
-                       $this->getMockCache(),
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore();
 
                // Note: This does not actually assert the job is correct
                $callableCallCounter = 0;
@@ -2677,7 +2543,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
        }
 
        public function testSetNotificationTimestampsForUser_specificTargets() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
+               $user = new UserIdentityValue( 1, 'MockUser', 0 );
                $timestamp = '20100101010101';
                $targets = [ new TitleValue( 0, 'Foo' ), new TitleValue( 0, 'Bar' ) ];
 
@@ -2699,12 +2565,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->method( 'affectedRows' )
                        ->will( $this->returnValue( 2 ) );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $this->getMockCache(),
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb ] );
 
                $this->assertTrue(
                        $store->setNotificationTimestampsForUser( $user, $timestamp, $targets )
@@ -2743,17 +2604,12 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertEquals(
                        [ 2, 3 ],
                        $store->updateNotificationTimestamp(
-                               $this->getMockNonAnonUserWithId( 1 ),
+                               new UserIdentityValue( 1, 'MockUser', 0 ),
                                new TitleValue( 0, 'SomeDbKey' ),
                                '20151212010101'
                        )
@@ -2785,15 +2641,10 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $watchers = $store->updateNotificationTimestamp(
-                       $this->getMockNonAnonUserWithId( 1 ),
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
                        new TitleValue( 0, 'SomeDbKey' ),
                        '20151212010101'
                );
@@ -2802,7 +2653,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
        }
 
        public function testUpdateNotificationTimestamp_clearsCachedItems() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
+               $user = new UserIdentityValue( 1, 'MockUser', 0 );
                $titleValue = new TitleValue( 0, 'SomeDbKey' );
 
                $mockDb = $this->getMockDb();
@@ -2830,18 +2681,13 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->method( 'delete' )
                        ->with( '0:SomeDbKey:1' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                // This will add the item to the cache
                $store->getWatchedItem( $user, $titleValue );
 
                $store->updateNotificationTimestamp(
-                       $this->getMockNonAnonUserWithId( 1 ),
+                       new UserIdentityValue( 1, 'MockUser', 0 ),
                        $titleValue,
                        '20151212010101'
                );
index 0a04993..a1bdbad 100644 (file)
@@ -32,7 +32,7 @@ class MockFileBackend extends MemoryFileBackend {
        protected function doGetLocalCopyMulti( array $params ) {
                $tmpFiles = []; // (path => MockFSFile)
                foreach ( $params['srcs'] as $src ) {
-                       $tmpFiles[$src] = new MockFSFile( wfTempDir() . '/' . wfRandomString( 32 ) );
+                       $tmpFiles[$src] = new MockFSFile( "Fake path for $src" );
                }
                return $tmpFiles;
        }
index eeaf05a..b2c51ca 100644 (file)
@@ -7,17 +7,16 @@
  * @since 1.28
  */
 class MockLocalRepo extends LocalRepo {
-       function getLocalCopy( $virtualUrl ) {
-               return new MockFSFile( wfTempDir() . '/' . wfRandomString( 32 ) );
+       public function getLocalCopy( $virtualUrl ) {
+               return new MockFSFile( "Fake path for $virtualUrl" );
        }
 
-       function getLocalReference( $virtualUrl ) {
-               return new MockFSFile( wfTempDir() . '/' . wfRandomString( 32 ) );
+       public function getLocalReference( $virtualUrl ) {
+               return new MockFSFile( "Fake path for $virtualUrl" );
        }
 
-       function getFileProps( $virtualUrl ) {
+       public function getFileProps( $virtualUrl ) {
                $fsFile = $this->getLocalReference( $virtualUrl );
-
                return $fsFile->getProps();
        }
 }
index 2453353..0d10a20 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 
+use MediaWiki\MediaWikiServices;
 use Wikimedia\TestingAccessWrapper;
 
 /**
@@ -566,7 +567,8 @@ class ApiStructureTest extends MediaWikiTestCase {
                                break;
 
                        case 'namespace':
-                               $validValues = MWNamespace::getValidNamespaces();
+                               $validValues = MediaWikiServices::getInstance()->getNamespaceInfo()->
+                                       getValidNamespaces();
                                if (
                                        isset( $config[ApiBase::PARAM_EXTRA_NAMESPACES] ) &&
                                        is_array( $config[ApiBase::PARAM_EXTRA_NAMESPACES] )
index 3b6d6f2..d340221 100644 (file)
@@ -1,5 +1,7 @@
 <?php
 
+use MediaWiki\MediaWikiServices;
+
 require_once dirname( __DIR__ ) . '/includes/upload/UploadFromUrlTest.php';
 
 class UploadFromUrlTestSuite extends PHPUnit_Framework_TestSuite {
@@ -71,7 +73,7 @@ class UploadFromUrlTestSuite extends PHPUnit_Framework_TestSuite {
                        $wgStyleDirectory = "$IP/skins";
                }
 
-               RepoGroup::destroySingleton();
+               MediaWikiServices::getInstance()->resetServiceForTesting( 'RepoGroup' );
                FileBackendGroup::destroySingleton();
        }
 
@@ -80,7 +82,7 @@ class UploadFromUrlTestSuite extends PHPUnit_Framework_TestSuite {
                        $GLOBALS[$var] = $val;
                }
                // Restore backends
-               RepoGroup::destroySingleton();
+               MediaWikiServices::getInstance()->resetServiceForTesting( 'RepoGroup' );
                FileBackendGroup::destroySingleton();
 
                parent::tearDown();
index da5e909..3f75243 100644 (file)
@@ -1,5 +1,6 @@
 const Page = require( 'wdio-mediawiki/Page' ),
-       Api = require( 'wdio-mediawiki/Api' );
+       Api = require( 'wdio-mediawiki/Api' ),
+       Util = require( 'wdio-mediawiki/Util' );
 
 class HistoryPage extends Page {
        get heading() { return browser.element( '#firstHeading' ); }
@@ -17,6 +18,16 @@ class HistoryPage extends Page {
                super.openTitle( title, { action: 'history' } );
        }
 
+       toggleRollbackConfirmationSetting( enable ) {
+               Util.waitForModuleState( 'mediawiki.api', 'ready', 5000 );
+               return browser.execute( function ( enable ) {
+                       return new mw.Api().saveOption(
+                               'showrollbackconfirmation',
+                               enable ? '1' : '0'
+                       );
+               }, enable );
+       }
+
        vandalizePage( name, content ) {
                let vandalUsername = 'Evil_' + browser.options.username;
 
index 51a1fc6..2a79467 100644 (file)
@@ -16,14 +16,7 @@ describe( 'Rollback with confirmation', function () {
                // Enable rollback confirmation for admin user
                // Requires user to log in again, handled by deleteCookie() call in beforeEach function
                UserLoginPage.loginAdmin();
-
-               UserLoginPage.waitForScriptsToBeReady();
-               browser.execute( function () {
-                       return ( new mw.Api() ).saveOption(
-                               'showrollbackconfirmation',
-                               '1'
-                       );
-               } );
+               HistoryPage.toggleRollbackConfirmationSetting( true );
        } );
 
        beforeEach( function () {
@@ -60,7 +53,7 @@ describe( 'Rollback with confirmation', function () {
                assert.strictEqual( HistoryPage.heading.getText(), 'Revision history of "' + name + '"' );
        } );
 
-       it( 'should perform rollbacks after confirming intention', function () {
+       it.skip( 'should perform rollbacks after confirming intention', function () {
                HistoryPage.rollback.click();
 
                HistoryPage.rollbackConfirmableYes.waitForVisible( 5000 );
@@ -103,14 +96,7 @@ describe( 'Rollback without confirmation', function () {
                // Disable rollback confirmation for admin user
                // Requires user to log in again, handled by deleteCookie() call in beforeEach function
                UserLoginPage.loginAdmin();
-
-               UserLoginPage.waitForScriptsToBeReady();
-               browser.execute( function () {
-                       return ( new mw.Api() ).saveOption(
-                               'showrollbackconfirmation',
-                               '0'
-                       );
-               } );
+               HistoryPage.toggleRollbackConfirmationSetting( false );
        } );
 
        beforeEach( function () {
index 60855f8..8838530 100644 (file)
@@ -1,5 +1,4 @@
-const Page = require( './Page' ),
-       Util = require( 'wdio-mediawiki/Util' );
+const Page = require( './Page' );
 
 class LoginPage extends Page {
        get username() { return browser.element( '#wpName1' ); }
@@ -21,10 +20,6 @@ class LoginPage extends Page {
        loginAdmin() {
                this.login( browser.options.username, browser.options.password );
        }
-
-       waitForScriptsToBeReady() {
-               Util.waitForModuleState( 'mediawiki.api' );
-       }
 }
 
 module.exports = new LoginPage();
index bdabdbf..5b4a9d5 100644 (file)
@@ -52,6 +52,7 @@ exports.config = {
 
        // ==================
        // Test Files
+       // FIXME: The non-core patterns to be removed once T199116 is fixed.
        // ==================
        specs: [
                relPath( './tests/selenium/wdio-mediawiki/specs/*.js' ),
@@ -63,7 +64,9 @@ exports.config = {
        ],
        // Patterns to exclude
        exclude: [
-               relPath( './extensions/CirrusSearch/tests/selenium/specs/**/*.js' )
+               relPath( './extensions/CirrusSearch/tests/selenium/specs/**/*.js' ),
+               // Disabled per T222517
+               relPath( './skins/MinervaNeue/tests/selenium/specs/**/*.js' )
        ],
 
        // ============