Merge "Consolidate duplicated unseen change logic and fix inconsistent code"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Mon, 6 May 2019 19:59:46 +0000 (19:59 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Mon, 6 May 2019 19:59:46 +0000 (19:59 +0000)
129 files changed:
RELEASE-NOTES-1.34
autoload.php
includes/DefaultSettings.php
includes/GlobalFunctions.php
includes/MediaWikiServices.php
includes/MovePage.php
includes/Permissions/PermissionManager.php
includes/Pingback.php
includes/Revision.php
includes/Revision/RevisionLookup.php
includes/Revision/RevisionStore.php
includes/ServiceWiring.php
includes/Title.php
includes/api/ApiBase.php
includes/api/ApiBlock.php
includes/api/ApiBlockInfoTrait.php [new file with mode: 0644]
includes/api/ApiQueryUserInfo.php
includes/api/ApiSetNotificationTimestamp.php
includes/api/ApiUnblock.php
includes/api/i18n/ko.json
includes/api/i18n/lb.json
includes/api/i18n/nl.json
includes/api/i18n/pl.json
includes/api/i18n/zh-hant.json
includes/cache/CacheHelper.php
includes/cache/GenderCache.php
includes/cache/LinkCache.php
includes/externalstore/ExternalStoreHttp.php
includes/filerepo/ForeignAPIRepo.php
includes/filerepo/RepoGroup.php
includes/filerepo/file/File.php
includes/filerepo/file/ForeignDBFile.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/installer/Installer.php
includes/installer/i18n/ia.json
includes/jobqueue/jobs/ActivityUpdateJob.php
includes/jobqueue/jobs/ClearUserWatchlistJob.php
includes/jobqueue/jobs/UserOptionsUpdateJob.php [new file with mode: 0644]
includes/poolcounter/PoolWorkArticleView.php
includes/preferences/DefaultPreferencesFactory.php
includes/rcfeed/UDPRCFeedEngine.php
includes/specials/SpecialMovepage.php
includes/title/NamespaceInfo.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
languages/i18n/ang.json
languages/i18n/ar.json
languages/i18n/ban.json
languages/i18n/be-tarask.json
languages/i18n/bjn.json
languages/i18n/bn.json
languages/i18n/ce.json
languages/i18n/da.json
languages/i18n/de.json
languages/i18n/diq.json
languages/i18n/eo.json
languages/i18n/hr.json
languages/i18n/hu.json
languages/i18n/hyw.json
languages/i18n/ia.json
languages/i18n/jv.json
languages/i18n/ml.json
languages/i18n/my.json
languages/i18n/nb.json
languages/i18n/nn.json
languages/i18n/nqo.json
languages/i18n/ps.json
languages/i18n/sah.json
languages/i18n/sr-ec.json
languages/i18n/sw.json
languages/i18n/tr.json
languages/i18n/yue.json
languages/i18n/zh-hant.json
maintenance/benchmarks/bench_HTTP_HTTPS.php
maintenance/findHooks.php
maintenance/importSiteScripts.php
maintenance/populateInterwiki.php
tests/integration/includes/http/MWHttpRequestTestCase.php
tests/parser/ParserTestPrinter.php
tests/parser/ParserTestRunner.php
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/includes/ContentSecurityPolicyTest.php
tests/phpunit/includes/GlobalFunctions/GlobalTest.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/auth/AuthManagerTest.php
tests/phpunit/includes/config/GlobalVarConfigTest.php
tests/phpunit/includes/db/LBFactoryTest.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/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/suites/UploadFromUrlTestSuite.php
tests/selenium/pageobjects/history.page.js
tests/selenium/specs/rollback.js
tests/selenium/wdio-mediawiki/LoginPage.js

index 5d46edd..b58c269 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 ====
 * …
 
@@ -114,7 +116,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 +128,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 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 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 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..e49398a 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();
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 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..f74ba79 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()
                );
        },
 
@@ -363,7 +364,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 +462,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 +485,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 +708,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 27baeb2..ad6c167 100644 (file)
@@ -1501,10 +1501,12 @@ class Title implements LinkTarget, IDBAccessObject {
        /**
         * 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 +1530,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 ) );
        }
 
        /**
@@ -3445,19 +3436,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 {
@@ -3730,57 +3712,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 +3742,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 +3982,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();
        }
 
        /**
index 8ab92af..19d84f7 100644 (file)
@@ -36,6 +36,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()
@@ -1811,7 +1813,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 +1836,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 +2035,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 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 615f71e..cfca2ee 100644 (file)
@@ -28,8 +28,8 @@
        "apihelp-edit-summary": "Säiten uleeën an änneren.",
        "apihelp-edit-param-sectiontitle": "Den Titel fir en neien Abschnitt.",
        "apihelp-edit-param-text": "Säiteninhalt.",
-       "apihelp-edit-param-minor": "Kleng Ännerung.",
-       "apihelp-edit-param-notminor": "Keng kleng Ännerung",
+       "apihelp-edit-param-minor": "Dës Ännerung als kleng Ännerung markéieren.",
+       "apihelp-edit-param-notminor": "Dës Ännerung net als keng kleng Ännerung markéieren esouguer wann d'Benotzerastellung \"{{int:tog-minordefault}}\" agestallt ass.",
        "apihelp-edit-param-bot": "Dës Ännerung als eng Bot-Ännerung markéieren.",
        "apihelp-edit-param-createonly": "D'Säit net ännere wann et se scho gëtt.",
        "apihelp-edit-param-watch": "D'Säit op dem aktuelle Benotzer seng Iwwerwaachungslëscht dobäisetzen.",
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]].",
index 1026e2d..e565b71 100644 (file)
        "apihelp-edit-param-text": "頁面內容。",
        "apihelp-edit-param-summary": "編輯摘要。 當未設定 $1section=new 與 $1sectiontitle 時也會當做章節標題。",
        "apihelp-edit-param-tags": "更改套用到修訂的標籤。",
-       "apihelp-edit-param-minor": "小編輯。",
-       "apihelp-edit-param-notminor": "非小編輯。",
+       "apihelp-edit-param-minor": "標記此編輯為小編輯。",
+       "apihelp-edit-param-notminor": "不要標記此編輯為小編輯,即使有設定到「{{int:tog-minordefault}}」使用者偏好設定。",
        "apihelp-edit-param-bot": "標記此編輯為機器人編輯。",
        "apihelp-edit-param-basetimestamp": "基於修訂的時間戳記,用來檢測編輯衝突。也许可以取得[[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]]認可。",
        "apihelp-edit-param-starttimestamp": "當編輯程序開始的時間戳記,用於偵測編輯衝突。當編輯程序開始時(例如:當載入要編輯的頁面內容),使用 <var>[[Special:ApiHelp/main|curtimestamp]]</var> 可以取得一個適當值。",
index ec6ce04..d798ddb 100644 (file)
@@ -288,7 +288,9 @@ class CacheHelper implements ICacheHelper {
                        throw new MWException( 'No cache key set, so cannot obtain or save the CacheHelper values.' );
                }
 
-               return wfMemcKey( ...array_values( $this->cacheKey ) );
+               return ObjectCache::getLocalClusterInstance()->makeKey(
+                       ...array_values( $this->cacheKey )
+               );
        }
 
        /**
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 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 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 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 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 e6936f6..8bf48d8 100644 (file)
        "config-license-help": "Multe wikis public pone tote le contributiones sub un [https://freedomdefined.org/Definition/Ia?uselang=ia licentia libere].\nIsto adjuta a crear un senso de proprietate communitari e incoragia le contribution in longe termino.\nIsto non es generalmente necessari pro un wiki private o de interprisa.\n\nSi tu vole poter usar texto de Wikipedia, e si tu vole que Wikipedia pote acceptar texto copiate de tu wiki, tu debe seliger <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nWikipedia usava anteriormente le Licentia GNU pro Documentation Libere (GFDL).\nIste es un licentia valide, ma es difficile a comprender.\nIl es anque difficile reusar le contento licentiate sub GFDL.",
        "config-email-settings": "Configuration de e-mail",
        "config-enable-email": "Activar le e-mail sortiente",
-       "config-enable-email-help": "Si tu vole que e-mail functiona, [Config-dbsupport-oracle/manual/en/mail.configuration.php le optiones de e-mail de PHP] debe esser configurate correctemente.\nSi tu non vole functiones de e-mail, tu pote disactivar los hic.",
+       "config-enable-email-help": "Si tu vole que e-mail functiona, [https://www.php.net/manual/en/mail.configuration.php le optiones de e-mail de PHP] debe esser configurate correctemente.\nSi tu non vole functiones de e-mail, tu pote disactivar los hic.",
        "config-email-user": "Activar le e-mail de usator a usator",
        "config-email-user-help": "Permitter a tote le usatores de inviar e-mail inter se, si illes lo ha activate in lor preferentias.",
        "config-email-usertalk": "Activar notification de cambios in paginas de discussion de usatores",
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 2c9fbc8..0abe1a5 100644 (file)
@@ -114,7 +114,9 @@ class PoolWorkArticleView extends PoolCounterWork {
                $this->revision = $revision;
                $this->audience = $audience;
                $this->cacheKey = $this->parserCache->getKey( $page, $parserOptions );
-               $keyPrefix = $this->cacheKey ?: wfMemcKey( 'articleview', 'missingcachekey' );
+               $keyPrefix = $this->cacheKey ?: ObjectCache::getLocalClusterInstance()->makeKey(
+                       'articleview', 'missingcachekey'
+               );
 
                parent::__construct( 'ArticleView', $keyPrefix . ':revid:' . $revid );
        }
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 8b5562f..39976c0 100644 (file)
@@ -591,21 +591,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;
                }
 
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 cdbbcc5..57c5130 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';
 
@@ -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 b6d7b68..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|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 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 bcabcc8..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": "احذف",
        "confirmemail_pending": "تم إرسال كود التأكيد إلى بريدك الإلكتروني مؤخراً؛\nإذا كنت قد أنشأت حسابك للتو، من الأفضل أن تنتظر بضع دقائق قبل أن تطلب كوداً آخر.",
        "confirmemail_send": "أرسل كود تأكيد",
        "confirmemail_sent": "تم إرسال رسالة التأكيد، شكرا لك.",
-       "confirmemail_oncreate": "تم إرسال كود تأكيد إلى عنوان بريدك الإلكتروني.\nالكود غير مطلوب للدخول إلى الموسوعة باسمك، ولكن يجب إدخاله قبل استخدامك أياً من خواص البريد الإلكتروني المستخدمة هنا في الويكي.",
+       "confirmemail_oncreate": "تم إرسال كود تأكيد إلى عنوان بريدك الإلكتروني.\nالكود غير مطلوب للدخول، ولكن يجب إدخاله قبل استخدامك أيًّا من خواص البريد الإلكتروني المستخدمة هنا في الويكي.",
        "confirmemail_sendfailed": "لم يتمكن {{SITENAME}} من إرسال رسالة التأكيد إليك.\nمن فضلك تأكد من عنوان بريدك الإلكتروني بحثاً عن حروف غير صحيحة.\n\nأرجع خادم البريد: $1",
        "confirmemail_invalid": "كود تأكيد غير صحيح.\nربما انتهت فترة صلاحيته.",
        "confirmemail_needlogin": "يجب عليك $1 لتأكيد بريدك الإلكتروني.",
index 2995d00..522ac69 100644 (file)
@@ -18,6 +18,7 @@
        "tog-hideminor": "engkebang suntingan ring gentosan sane pinih anyar",
        "tog-hidepatrolled": "engkebang suntingan mapatrol ring gentosan sane pinih anyar",
        "tog-newpageshidepatrolled": "engkebang lembar mapatrol saking saking kepahan lembar anyar",
+       "tog-hidecategorization": "Engkebang kacané",
        "tog-extendwatchlist": "kembangang kepahan pangiwasan antuk nampilang samian panguwahan, nenten sane anyar kewanten",
        "tog-usenewrc": "aniang suntingan ring tampilan pagentosan sane pinih anyar lan kepahan pangiwasan manutin lembar",
        "tog-numberheadings": "isinin nomor murda anggen cara otomatis",
@@ -35,9 +36,9 @@
        "tog-enotifminoredits": "taler kirimang titiang email ring panguwahan alit",
        "tog-enotifrevealaddr": "kirimang titiang alamat email ring catetan email",
        "tog-shownumberswatching": "tampilang akehnyane sane ngiwasin",
-       "tog-oldsig": "tanda tangan mangkin",
+       "tog-oldsig": "Tanda tangan mangkin",
        "tog-fancysig": "dadosang tanda tangan dados teks wiki (nenten pranala otomatis)",
-       "tog-uselivepreview": "anggen pratayang langsung(experimental)",
+       "tog-uselivepreview": "Anggen pratayang langsung ten anggen kaca sane malunan",
        "tog-forceeditsummary": "elingang titiang yening kotak ringkesan suntingan kari kosong",
        "tog-watchlisthideown": "engkebang panguwahan titiang saking kepahan pangiwasan",
        "tog-watchlisthidebots": "engkebang panguwahan bot ring kepahan pangiwasan",
@@ -48,9 +49,9 @@
        "tog-ccmeonemails": "kirimang titiang salinan email sane kirimang titiang ring anak lianan",
        "tog-diffonly": "sampunang katampilang daging lembar ring ungkur binanne suntingan",
        "tog-showhiddencats": "tampilang golongan sane kaengkebang",
-       "tog-norollbackdiff": "sampunang tampilang binanne sesampun ngewaliang",
+       "tog-norollbackdiff": "Sampunang tampilang binanne sesampun ngewaliang",
        "tog-useeditwarning": "elingang titiang yening ngalahin lembar panyuntingan sadurung nyimpen pagentosan",
-       "tog-prefershttps": "setata nganggen sambungan sane aman rikala malebu log",
+       "tog-prefershttps": "Setata nganggen sambungan sane aman rikala malebu log",
        "underline-always": "Setata",
        "underline-never": "Nénten naénin",
        "underline-default": "kulit utawi penjelajah paaban",
        "category-media-header": "lembar ring golongan \"$1\"",
        "category-empty": "\"mangkin, nenten madaging lembar utawi pekakas ring golongan puniki\"",
        "hidden-categories": "{{plural:$1|punduhan sane kaengkebang| punduhan sane kaengkebang}}",
+       "hidden-category-category": "Kategori mengkeb",
        "category-subcat-count": "{{PLURAL:$2| golongan puniki madue {{PLURAL:$1|$1 subkategori}} puniki, saking genepan $2.}}",
        "category-article-count": "{{PLURAL:$2|golongan puniki madue{{PLURAL:$1|$1 lembar}}, saking total $2.}}",
        "category-file-count": "{{PLURAL:$2|golongan puniki madue{{PLURAL:$1|$1 lembar}}, saking total $2.}}",
        "about": "Indik",
        "newwindow": "(bukak ring jendela anyar)",
        "cancel": "Buwung",
+       "mypage": "Kaca",
        "mytalk": "Wicara",
        "anontalk": "Wicara",
        "navigation": "Pengarah",
        "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}}",
        "help": "Tulung",
+       "help-mediawiki": "Pitulung MediaWiki",
        "search": "Rereh",
        "searchbutton": "Rereh",
        "searcharticle": "lanturang",
        "history": "sejarah pupulan",
        "history_short": "kawentenan sane lawas",
+       "history_small": "babad",
        "printableversion": "kawentenan lian sane macetak",
        "permalink": "Pranala ajeg",
        "view": "cingakin",
        "protect_change": "gentos",
        "newpage": "Lembar Anyar",
        "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",
        "disclaimers": "nungkas",
        "disclaimerpage": "Project:Pengelidan lumrah",
        "edithelp": "pamitulung panguwahan",
+       "helppage-top-gethelp": "Tulung",
        "mainpage": "Kaca Utama",
        "mainpage-description": "Lembar Utama",
        "portal": "Pintu nuju sekha",
        "portal-url": "Project:pamedal sekha",
        "privacy": "kawicaksanaan padewekan",
        "privacypage": "Project:kawicaksanan tanpaiket",
+       "ok": "OK",
        "retrievedfrom": "kapolihang saking \"$1\"",
        "youhavenewmessages": "{{PLURAL:$3|ida dane maduwe}} $1 ($2)",
+       "youhavenewmessagesfromusers": "{{PLURAL:$4|You have}} $1 ring {{PLURAL:$3|another user|$3 users}} ($2).",
+       "youhavenewmessagesmanyusers": "Ida dane ngelah $1 saking liyane ($2).",
        "editsection": "gentos",
        "editold": "mecikang",
        "viewsourceold": "cingak witnyane",
        "viewsourcelink": "cingak witnyane",
        "editsectionhint": "ubah kepahan$1",
        "toc": "kepahan dagingnyane",
+       "showtoc": "edengang",
+       "hidetoc": "engkebang",
+       "collapsible-expand": "buka",
+       "confirmable-confirm": "{{GENDER:$1|Ida}} dane yakin?",
+       "confirmable-yes": "Inggih",
+       "confirmable-no": "Nénten",
        "site-atom-feed": "$1 \"atom feed\"",
        "page-atom-feed": "$1 \"atom feed\"",
        "red-link-title": "$1 (kaca tan wénten)",
        "nstab-category": "golongan",
        "mainpage-nstab": "Kaca Utama",
        "nosuchspecialpage": "Ten wenten lembar spesial",
+       "error": "kaluputan",
+       "databaseerror": "Database kaluputan",
        "missing-article": "data utama nenten prasida nemu tulisan saking lembar sane sepatutne wenten, inggih punika  $1, $2\n\nindike puniki biasane keranayang olih pranala kaon nuju pabenahan sane dumun lembar sane sampun kaicalang\n\nyening nenten puniki sane ngranayang, ida dane minab sampun manggihin kaiwangang ring sajeroning piranti lunak.\nDurus sadokang indik puniki rin silih sinunggil anak \n\n[[Special:ListUsers/sysop|Pengurus]], antuk ngetik alamat URL sane katuju",
        "missingarticle-rev": "(pabenahan#:$1)",
        "badtitle": "murda sane nenten manut",
        "yourpasswordagain": "jumunin kruna sandi",
        "login": "Ngranjing log",
        "nav-login-createaccount": "malebu log / ngawe pepalihan",
+       "logout": "Medal Log",
        "userlogout": "medal saking Log",
+       "notloggedin": "Konden masuk log",
+       "userlogin-noaccount": "Durung madue akun?",
+       "userlogin-joinproject": "Indik {{SITENAME}}",
        "createaccount": "ngajuang akun anyar",
        "mailmypassword": "nyumu ngaryanin kruna sandi",
        "loginlanguagelabel": "Basa: $1",
        "pt-login": "Ngranjing log",
+       "pt-login-button": "Ngranjing log",
        "pt-createaccount": "Ngajuang akun anyar",
        "pt-userlogout": "Medal Log",
+       "botpasswords-label-create": "Ngae",
+       "botpasswords-label-cancel": "Buungan",
+       "botpasswords-label-delete": "Apus",
+       "botpasswords-label-resetpassword": "Nyumu kruna sandi",
        "passwordreset": "Nyumu kruna sandi",
        "bold_sample": "teks puniki mesurat tebel",
        "bold_tip": "teks puniki mesurat tebel",
        "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.",
        "hiddencategories": "lembar niki inggih punika krama saking {{PLURAL:$1|1 golongan sane mengkeb|$1 golongan sane mengkeb}}",
        "permissionserrorstext-withaction": "ida dané nénten madué kuasa ngranjing anggén $2, riantukan {{PLURAL:$1|alasan}} ring sor puniki:",
        "recreate-moveddeleted-warn": "\"pingetan\" ida dane ngawe malih lembar sane naenin maapus.'''\n\nmangda kayunin malih napike pantes lanturang suntingan ida dane. puniki log pengapusan lan pangisidan saking lembar puniki:",
-       "moveddeleted-notice": "lembar puniki sampun kaapus. anggen pewarah, puniki log pangapus lan pengisidan lembar puniki",
+       "moveddeleted-notice": "Lembar puniki sampun kaapus.\nAnggen pewarah, proteksi, lan pengisidan log saking lembar puniki cingakin pustaka beten.",
        "content-model-wikitext": "tulisan wiki",
        "post-expand-template-inclusion-warning": "pinget: ukuran templat sane keanggen kalangkung ageng. wenten templat sane kacampahang",
        "post-expand-template-inclusion-category": "lembar sane maukuran templat sane nglangkungin wates",
        "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 d99c4cd..63a4659 100644 (file)
        "action-editmyuserjs": "рэдагаваньне вашых уласных JavaScript-файлаў",
        "action-viewsuppressed": "прагляд вэрсіяў, схаваных ад усіх удзельнікаў",
        "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 eaddbea..478ecef 100644 (file)
        "category-subcat-count-limited": "Tumbung ini baisi {{PLURAL:$1|sub-tumbung|$1 sub-tutumbung}} barikut.",
        "category-article-count": "{{PLURAL:$2|Tumbung ni baisi asa tungkaran barikut haja.|Tutumbung ngini baisi {{PLURAL:$1|tungkaran|$1 tutungkaran}}, matan $2 sabarataan.}}",
        "category-article-count-limited": "Tumbung ini baisi {{PLURAL:$1|asa tungkaran|$1 tutungkaran}} barikut.",
-       "category-file-count": "{{PLURAL:$2|Tumbung ngini wastu baisi satu barakas barikut.|Tumbung ngini baisi {{PLURAL:$1|barakas|$1 babarakas}} barikut, matan $2 sabarataan.}}",
+       "category-file-count": "{{PLURAL:$2|Tumbung ngini baisi {{PLURAL:$1|$1 barakas}}, matan jumlah $2.}}",
        "category-file-count-limited": "Tumbung ngini baisi {{PLURAL:$1|barakas|$1 barakas}} barikut.",
        "listingcontinuesabbrev": "samb.",
        "index-category": "Tungkaran tasusun bapadalakan kata",
        "actionthrottled": "Kalakuan dikiripi",
        "actionthrottledtext": "Sawagai sabuting takaran anti-spam, Pian dibabatasi hagan balalaku kababanyakan dalam parhatan handap, wan Pian sudah limpuari batasan ngini.\nMuhun cubai pulang dalam babarapa minit.",
        "protectedpagetext": "Tungkaran ngini sudah dilindungi hagan mancagah babakan.",
-       "viewsourcetext": "Pian kawa maniringi wan manyalin asal mula tungkaran ngini:",
+       "viewsourcetext": "Pian kawa maniringi wan manyalin asal-mula tungkaran ngini.",
        "viewyourtext": "Pian kawa maniringi wan salain asalmula matan '''babakan pian''' ka tungkaran ngini:",
        "protectedinterface": "Tungkaran ini manyadiakan naskah antarmuha gasan parangkat lunak, wan dilindungi hagan mancagah tasalah puruk.",
        "editinginterface": "'''Paringatan:''' Pian mambabak sabuting tungkaran nang dipuruk hagan manyadiakan naskah antarmuha gasan parangkat lunak.\nPaubahan ka tungkaran ngini akan bapangaruh matan tampaian antarmuha gasan pamakai lain.\nGasan tarjamahan, muhun pakai [https://translatewiki.net/wiki/Main_Page?setlang=bjn translatewiki.net], rangka gawian palokalan MediaWiki.",
        "userlogin-yourname-ph": "Masukakan ngaran pamakai Pian",
        "yourpassword": "Katasunduk:",
        "userlogin-yourpassword": "Kata sandi",
+       "userlogin-yourpassword-ph": "Masukakan kata sandi",
        "createacct-yourpassword-ph": "Masukakan kata sandi",
        "yourpasswordagain": "Katik pulang katasunduk:",
        "createacct-yourpasswordagain": "Konfirmasi kata sandi",
        "createacct-yourpasswordagain-ph": "Masukakan pulang kata sandi",
+       "userlogin-remembermypassword": "Biarakan ulun tatap babuat",
        "yourdomainname": "Domain Pian:",
        "password-change-forbidden": "Pian kada kawa ma-ubah kata sunduk pada wiki ngini.",
        "externaldberror": "Ada kasalahan apakah kacucukan basis data atawa Pian kada bulih mamutakhirakan akun luar.",
        "logout": "Kaluar",
        "userlogout": "Kaluar",
        "notloggedin": "Balum babuat log",
+       "userlogin-noaccount": "Balum baisi akun?",
+       "userlogin-joinproject": "Gabung {{SITENAME}}",
        "createaccount": "Ulah akun",
+       "userlogin-resetpassword-link": "Lupa kata sandi?",
+       "userlogin-helplink2": "Patulung babuat log",
        "createacct-emailoptional": "Alamat surél/email (bagusnya diisi)",
        "createacct-email-ph": "Masukakan alamat email Pian",
        "createaccountmail": "Malalui suril",
        "loginlanguagelabel": "Basa: $1",
        "suspicious-userlogout": "Pamintaan Pian hagan kaluar log kada ditarima marga nangkaya dikirim matan panjalajah web rakai atawa tatangkap proxy.",
        "pt-login": "Babuat log",
+       "pt-login-button": "Babuat log",
        "pt-createaccount": "Ulah akun",
        "pt-userlogout": "Kaluar",
        "php-mail-error-unknown": "Kasalahan kada dipinandui dalam pungsi surat () PHP",
        "semiprotectedpagewarning": "'''Catatan:''' Tungkaran ngini sudah dilindungi nang akibatnya pamakai tadaptar haja nang kawa mambabak.\nLog masuk pauncitnya disadiakan di bawah gasan rujukan:",
        "cascadeprotectedwarning": "'''Paringatan:''' Tungkaran ngini sudah dilindungi nang akibatnya pamakai awan hak istimiwa pambakal haja nang kawa mambabak, sualnya ngini tamasuk dalam baumpat parlindungan barénténg {{PLURAL:$1|tungkaran|tutungkaran}}:",
        "titleprotectedwarning": "'''Paringatan: Tungkaran ngini sudah dilindungi nang akibatnya [[Special:ListGroupRights|hak khas]] diparluakan hagan maulah ngini.'''\nLog masuk pauncitnya disadiakan di bawah gasan rujukan:",
-       "templatesused": "{{PLURAL:$1|Citakan|Citakan}} nang digunakan di tungkaran ngini:",
+       "templatesused": "{{PLURAL:$1|Citakan|Citakan}} nang dipakai di tungkaran ngini:",
        "templatesusedpreview": "{{PLURAL:$1|Citakan|Citakan}} nang digunakan di titilikan ngini:",
        "templatesusedsection": "{{PLURAL:$1|Citakan|Cicitakan}} nang diguna'akan di hagian ini:",
        "template-protected": "(dilindungi)",
        "permissionserrorstext": "Pian kada baisi ijin gasan malakuakan itu, karana {{PLURAL:$1|alasan|alasan}} ini:",
        "permissionserrorstext-withaction": "Pian kada baisi ijin gasan $2, karana {{PLURAL:$1|alasan|alasan}} ini:",
        "recreate-moveddeleted-warn": "'''Paringatan: Pian maulah pulang sabuah tungkaran nang sabalumnya dihapus.'''\n\nPian partimbangakan dahulu sasuaikah hagan manarusakan pambabakan tungkaran ini.\nLog pahapusan wan paugahan gasan tungkaran ini disadiakan di sia:",
-       "moveddeleted-notice": "Tungkaran ini sudah dihapus.\nLog pahapusan wan paugahan gasan tungkaran ini disadiakan di bawah ini gasan rujukan.",
+       "moveddeleted-notice": "Tungkaran ini sudah dihapus.\nLog pahapusan, palindungan, wan pamindahan matan tungkaran itu tasadia di bawah ini sabagai rujukan.",
        "log-fulllog": "Tiringi samunyaan log",
        "edit-hook-aborted": "Babakan ditinggalakan ulih kakait parser.\nIni kadada panjalasan.",
        "edit-gone-missing": "Kada kawa mamutakhirakan tungkaran ini.\nIni cungul pinanya sudah tahapus.",
        "currentrev": "Ralatan pahabisannya",
        "currentrev-asof": "Ralatan pahanyarnya pada $1",
        "revisionasof": "Ralatan matan $1",
-       "revision-info": "Ralatan pada $1 ulih $2",
+       "revision-info": "Ralatan par $1 ulih {{GENDER:$6|$2}}$7",
        "previousrevision": "←Ralatan talawas",
        "nextrevision": "Ralatan salanjutnya→",
        "currentrevisionlink": "Ralatan wayahini",
        "page_first": "Panambaian",
        "page_last": "Pauncitan",
        "histlegend": "Pilihan mananding: tandai kutak-kutak radiu ralatan-ralatan nang handak ditanding wan picik enter atawa picikan di bawah.<br />Legend: '''({{int:cur}})''' =lainnya awan ralatan pahanyarnya, '''({{int:last}})''' = lainnya awan ralatan sabalumnya, '''{{int:minoreditletter}}''' = babakan sapalih.",
-       "history-fieldset-title": "Tangadahi halam",
+       "history-fieldset-title": "Ralatan nang disaring",
        "history-show-deleted": "Nang dihapus haja",
-       "histfirst": "Palawasnya",
-       "histlast": "Pahanyarnya",
+       "histfirst": "palawasnya",
+       "histlast": "pahanyarnya",
        "historysize": "($1 {{PLURAL:$1|bita|bibita}})",
        "historyempty": "(kusung)",
        "history-feed-title": "Ralatan halam",
        "mergelog": "Log panggabungan",
        "revertmerge": "Walang panggabungan",
        "mergelogpagetext": "Di bawah adalah daptar nang paling hanyar panggabungan matan sabuah tungkaran halam ka dalam nang lain.",
-       "history-title": "Ralatan halam matan ''$1''",
+       "history-title": "Sajarah ralatan matan \"$1\"",
        "difference-title": "$1: Pabidaan ralatan",
        "difference-multipage": "(Nang balain antar tungkaran-tungkaran)",
        "lineno": "Baris $1:",
        "showhideselectedversions": "Tampaiakan/sungkupakan ralatan-ralatan",
        "editundo": "walangi",
        "diff-empty": "(Kadada bida)",
+       "diff-multi-sameuser": "({{PLURAL:$1|$1 ralatan antara}} ulih pamakai nang sama kada ditampaiakan)",
        "diff-multi-manyusers": "({{PLURAL:$1|Asa ralatan tangah|$1 raralatan tangah}} ulih labih pada $2 {{PLURAL:$2|pamuruk|papamuruk}} kada ditampaiakan)",
        "searchresults": "Kulihan panggagaian",
        "searchresults-title": "Kulihan gagai gasan \"$1\"",
        "search-result-category-size": "{{PLURAL:$1|1 angguta|$1 aangguta}} ({{PLURAL:$2|1 subtumbung|$2 subtutumbung}}, {{PLURAL:$3|1 barakas|$3 babarakas}})",
        "search-redirect": "(Diugahakan matan $1)",
        "search-section": "(hagian $1)",
+       "search-file-match": "(rasuk lawan isi barakas)",
        "search-suggest": "Nginikah maksud Pian: $1",
        "search-interwiki-caption": "Dingsanak rangka gawian",
        "search-interwiki-default": "Kulihan $1",
        "recentchanges": "Paubahan pahanyarnya",
        "recentchanges-legend": "Pilihan paubahan pahanyarnya",
        "recentchanges-summary": "Jajak paubahan wiki pahanyarnya pada tungkaran ngini",
+       "recentchanges-noresult": "Kadada paubahan dalam rantang waktu ngini nang rasuk lawan syarat.",
        "recentchanges-feed-description": "Susuri paubahan pahanyarnya dalam wiki di kitihan ini",
        "recentchanges-label-newpage": "Babakan ngini maulah sabuting tungkaran hanyar",
        "recentchanges-label-minor": "Ngini sabuting babakan sapalih",
        "recentchanges-label-plusminus": "Paubahan ukuran tungkaran dalam bita",
        "recentchanges-legend-heading": "<strong>Katarangan:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (tiringi jua [[Special:NewPages|daptar tungkaran hanyar]])",
-       "rcnotefrom": "Di bawah ngini paubahan tumatan '''$2''' (ditampaiakan sampai '''$1''' paubahan)",
+       "rcnotefrom": "Di bawah ngini adalah {{PLURAL:$5|paubahan}} tumatan <strong>$3, $4</strong> (ditampaiakan sampai <strong>$1</strong> paubahan).",
        "rclistfrom": "Tampaiakan paubahan pahanyarnya matan $3 $2",
        "rcshowhideminor": "$1 pambabakan sapalih",
+       "rcshowhideminor-show": "Tampaiakan",
        "rcshowhideminor-hide": "Sungkupakan",
        "rcshowhidebots": "$1 bot",
        "rcshowhidebots-show": "Tampaiakan",
+       "rcshowhidebots-hide": "Sungkupakan",
        "rcshowhideliu": "$1 pamakai tadaptar",
+       "rcshowhideliu-show": "Tampaiakan",
        "rcshowhideliu-hide": "Sungkupakan",
        "rcshowhideanons": "$1 pamakai kada bangaran",
+       "rcshowhideanons-show": "Tampaiakan",
        "rcshowhideanons-hide": "Sungkupakan",
        "rcshowhidepatr": "$1 babakan ta'awasi",
        "rcshowhidemine": "$1 babakan ulun",
+       "rcshowhidemine-show": "Tampaiakan",
        "rcshowhidemine-hide": "Sungkupakan",
        "rclinks": "Tampaiakan $1 paubahan pahanyarnya dalam $2 hari tauncit",
        "diff": "bida",
        "querypage-disabled": "Tungkaran istimiwa ngini dikada-kawakan gasan alasan ginawi.",
        "booksources": "Buku bamula",
        "booksources-search-legend": "Gagai gasan buku asal mula",
+       "booksources-search": "Gagai",
        "booksources-text": "Di bawah adalah sabuah daptar tautan ka situs lain nang manjual bubuku hanyar wan bakas, wan jua baisi panjalasan labih pasal bubuku nang Pian ugai:",
        "booksources-invalid-isbn": "ISBN nang dibari mancungul kada sah; pariksa kalua-ai tasalah marekap matan asal-mula aslinya.",
        "specialloguserlabel": "Pamakai:",
        "delete-warning-toobig": "Tungkaran ngini baisi halam babakan ganal, labih pada $1 {{PLURAL:$1|ralatan|raralatan}}.\nMahapus ngini kawa mangaruhi databasis oparasi {{SITENAME}};\njalanakan awan ba-a-awas.",
        "rollback": "Gulung bulik babakan",
        "rollbacklink": "bulikakan",
+       "rollbacklinkcount": "bulikakan $1 {{PLURAL:$1|babakan}}",
        "rollbackfailed": "Guling-bulik luput",
        "cantrollback": "Kada kawa mambalikakan babakan;\npanyumbang tauncit adalah asa-asanya panulis tungkaran ngini.",
        "alreadyrolled": "Kada kawa malakukan pambulikan ka ralatan tauncit [[:$1]] ulih [[User:$2|$2]] ([[User talk:$2|pandir]]{{int:pipe-separator}}[[Special:Contributions/$2|{{int:contribslink}}]]);\npamuruk lain sudah mambabak atawa malakukan pambulikan lawan tungkaran ini.\n\nBabakan tauncit dilakukan ulih [[User:$3|$3]] ([[User talk:$3|pandir]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]).",
        "contributions-title": "Sumbangan pamakai gasan $1",
        "mycontris": "Sumbangan",
        "anoncontribs": "Sumbangan",
-       "contribsub2": "Gasan $1 ($2)",
+       "contribsub2": "Gasan {{GENDER:$3|$1}} ($2)",
        "nocontribs": "Kadada paubahan nang rasuk lawan syarat itu.",
-       "uctop": " atas",
+       "uctop": "wayah ini",
        "month": "Matan bulan (wan sabalumnya):",
        "year": "Matan tahun (wan sabalumnya):",
        "sp-contributions-newbies": "Tampaiakan sumbangan papamakai hanyar haja",
        "sp-contributions-search": "Gagai gasan sumbangan",
        "sp-contributions-username": "Alamat IP atawa ngaran-pamakai:",
        "sp-contributions-toponly": "Tampaiakan wastu ralatan nang paling atas (pauncitnya)",
+       "sp-contributions-newonly": "Hanya tampaiakan babakan nang barupa paulahan tungkaran",
        "sp-contributions-submit": "Gagai",
        "whatlinkshere": "Tautan apa di sia",
        "whatlinkshere-title": "Tungkaran-tungkaran nang batautan ka ''$1''",
        "revdelete-uname-unhid": "ngaran-pamuruk kada tasungkup",
        "revdelete-restricted": "Talamar pambatasan hagan pambakal-pambakal",
        "revdelete-unrestricted": "Buang pambatasan gasan pambakal-pambakal",
-       "logentry-move-move": "$1 mamindahakan tungkaran $3 ka $4",
-       "logentry-move-move-noredirect": "$1 diugah tungkaran $3 ka $4 awan-kada maninggalakan sabuah paugahan",
+       "logentry-move-move": "$1 {{GENDER:$2|mamindahakan}} tungkaran $3 ka $4",
+       "logentry-move-move-noredirect": "$1 {{GENDER:$2|mamindahakan}} tungkaran $3 ka $4 kada pakai maulah paugahan",
        "logentry-move-move_redir": "$1 diugah tungkaran $3 ka $4 lung paugahan",
        "logentry-move-move_redir-noredirect": "$1 diugah tungkaran $3 ka $4 lung sabuah paugahan awan-kada maninggalakan sabuah paugahan",
        "logentry-patrol-patrol": "$1 diciri'i ralatan $4 matan tungkaran $3 taawasi",
        "logentry-newusers-create": "$1 {{GENDER:$2|maulah}} akun pamakai",
        "logentry-newusers-create2": "$1 ma-ulah sabuting akun pamakai $3",
        "logentry-newusers-autocreate": "Akun $1 utumatis diulah",
+       "logentry-upload-upload": "$1 {{GENDER:$2|ma-unggah}} $3",
        "rightsnone": "(kadada)",
        "feedback-adding": "Manambahi kitihanbalik ka tungkaran...",
        "feedback-bugcheck": "Harat! hanyar dipariksa bahwasa ngini lainan salah asa [$1 bug nang dipinandui].",
index c48df85..725b8e9 100644 (file)
        "edit-gone-missing": "পাতাটি হালনাগাদ হয়নি।\nসম্ভবতঃ পাতাটি মুছে ফেলা হয়েছে।",
        "edit-conflict": "সম্পাদনা সংঘাত।",
        "edit-no-change": "আপনার সম্পাদনাটি উপেক্ষা করা হয়েছে, কারণ লেখাতে কোনো পরিবর্তন করা হয়নি।",
-       "edit-slots-cannot-add": "নিà¦\9aà§\87র {{PLURAL:$1|পাতাà¦\9fি|পাতাসমূহ}} এখানে সমর্থিত নয়: $2।",
+       "edit-slots-cannot-add": "নিà¦\9aà§\87র {{PLURAL:$1|সà§\8dলà¦\9fà¦\9fি|সà§\8dলà¦\9fসমূহ}} এখানে সমর্থিত নয়: $2।",
        "edit-slots-cannot-remove": "নিচের {{PLURAL:$1|স্লট|স্লটসমূহ}} প্রয়োজন এবং বাদ দেওয়া যাবে না: $2।",
        "edit-slots-missing": "নিচের {{PLURAL:$1|স্লট|স্লটসমূহ}} পাওয়া যায়নি: $2।",
        "postedit-confirmation-created": "পাতাটি তৈরি করা হয়েছে।",
        "action-changetags": "নির্দিষ্ট সংস্করণ এবং লগ ভুক্তিগুলিতে যথেচ্ছভাবে ট্যাগ সংযোজন ও অপসারণ করা",
        "action-deletechangetags": "ডাটাবেজ থেকে ট্যাগ অপসরণ করার",
        "action-purge": "এই পাতাটি শোধন করুন",
-       "action-apihighlimits": "API কোয়েরি হিসাবে আরও উচ্চ লিমিট ব্যবহার করুন",
-       "action-autoconfirmed": "à¦\86à¦\87পি-ভিতà§\8dতিà¦\95 à¦°à§\87à¦\9f à¦¸à§\80মানা à¦¦à§\8dবারা à¦ªà§\8dরভাবিত à¦¨à¦¯à¦¼à¥¤",
-       "action-bigdelete": "বিশাল à¦\87তিহাস à¦¸à¦®à§\8dবলিত à¦ªà¦¾à¦¤à¦¾ à¦®à§\81à¦\9bà§\87 à¦«à§\87লà§\8b",
-       "action-blockemail": "à¦\87-মà§\87à¦\87ল à¦ªà¦¾à¦ à¦¾à¦¤à§\87 à¦\95à§\8bনà§\8b à¦¬à§\8dযবহারà¦\95ারà§\80à¦\95à§\87 à¦¬à¦¾à¦\81ধা à¦¦à¦¾à¦\93",
-       "action-bot": "সয়à¦\82à¦\95à§\8dরিয় à¦ªà¦¦à§\8dধতি à¦¹à¦¿à¦¸à¦¾à¦¬à§\87 à¦\9aিহà§\8dনিত à¦\95রণ",
+       "action-apihighlimits": "API কোয়েরিতে আরো উচ্চতর সীমা ব্যবহার করার",
+       "action-autoconfirmed": "à¦\86à¦\87পি-ভিতà§\8dতিà¦\95 à¦°à§\87à¦\9f à¦¸à§\80মার à¦¦à§\8dবারা à¦ªà§\8dরভাবিত à¦¨à¦¾ à¦¹à¦¬à¦¾à¦°",
+       "action-bigdelete": "বিশাল à¦\87তিহাস à¦¸à¦®à§\8dবলিত à¦ªà¦¾à¦¤à¦¾ à¦\85পসারণ à¦\95রার",
+       "action-blockemail": "à¦\95à§\8bনà§\8b à¦¬à§\8dযবহারà¦\95ারà§\80à¦\95à§\87 à¦\87-মà§\87à¦\87ল à¦ªà¦¾à¦ à¦¾à¦¨à§\8b à¦¥à§\87à¦\95à§\87 à¦¬à¦¾à¦§à¦¾ à¦¦à§\87য়ার",
+       "action-bot": "সà§\8dবয়à¦\82à¦\95à§\8dরিয় à¦ªà¦¦à§\8dধতি à¦¹à¦¿à¦¸à¦¾à¦¬à§\87 à¦\9aিহà§\8dনিতà¦\95রণ à¦\95রার",
        "action-editprotected": "\"{{int:protect-level-sysop}}\" হিসেবে সুরক্ষিত পাতা সম্পাদনা করার",
        "action-editsemiprotected": "\"{{int:protect-level-autoconfirmed}}\" হিসেবে সুরক্ষিত পাতা সম্পাদনা করার",
-       "action-editinterface": "ব্যবহারকারী ইন্টারফেস সম্পাদনা",
-       "action-editusercss": "অন্য ব্যবহারকারীগণের CSS ফাইল সম্পাদনা",
-       "action-edituserjson": "অন্য ব্যবহারকারীগণের JSON ফাইল সম্পাদনা",
-       "action-edituserjs": "অন্য ব্যবহারকারীগণের জাভাস্ক্রিপ্ট ফাইল সম্পাদনা",
+       "action-editinterface": "ব্যবহারকারী ইন্টারফেস সম্পাদনা করার",
+       "action-editusercss": "অন্য ব্যবহারকারীগণের CSS ফাইল সম্পাদনা করার",
+       "action-edituserjson": "অন্য ব্যবহারকারীগণের JSON ফাইল সম্পাদনা করার",
+       "action-edituserjs": "অন্য ব্যবহারকারীগণের জাভাস্ক্রিপ্ট ফাইল সম্পাদনা করার",
        "action-editsitecss": "সাইটব্যাপী CSS সম্পাদনা করার",
        "action-editsitejson": "সাইটব্যাপী JSON সম্পাদনা করার",
        "action-editsitejs": "সাইটব্যাপী জাভাস্ক্রিপ্ট সম্পাদনা করার",
        "action-editmyusercss": "স্ব ব্যবহারকারীর CSS ফাইল সম্পাদনা করার",
-       "action-editmyuserjson": "à¦\86পনার à¦¨à¦¿à¦\9cসà§\8dব à¦¬à§\8dযবহারà¦\95ারà§\80 JSON à¦«à¦¾à¦\87ল à¦¸à¦®à§\8dপাদনা à¦\95রা",
-       "action-editmyuserjs": "à¦\86পনার à¦¨à¦¿à¦\9cসà§\8dব à¦¬à§\8dযবহারà¦\95ারà§\80 à¦\9cাভাসà§\8dà¦\95à§\8dরিপà§\8dà¦\9f à¦«à¦¾à¦\87ল à¦¸à¦®à§\8dপাদনা à¦\95রà§\81ন",
-       "action-viewsuppressed": "যà§\87à¦\95à§\8bন à¦¬à§\8dযবহারà¦\95ারà§\80র à¦\95াà¦\9b à¦¥à§\87à¦\95à§\87 à¦²à§\81à¦\95ানà§\8b à¦¸à¦\82সà§\8dà¦\95রণà¦\97à§\81লি à¦¦à§\87à¦\96à§\81ন",
-       "action-hideuser": "বà§\8dযবহারà¦\95ারà§\80à¦\95à§\87 à¦¬à¦¾à¦§à¦¾ à¦¦à¦¿à¦¨, à¦\8fবà¦\82 à¦¸à¦°à§\8dবসাধারণà§\87র à¦¦à§\83ষà§\8dà¦\9fিসà§\80মা à¦¥à§\87à¦\95à§\87 à¦¸à¦°à¦¿à¦¯à¦¼à§\87 à¦¨à¦¿à¦¨",
-       "action-ipblock-exempt": "আইপি বাধা, স্বয়ংক্রিয় বাধা ও পরিসীমার বাধা এড়ানো",
+       "action-editmyuserjson": "সà§\8dব à¦¬à§\8dযবহারà¦\95ারà§\80 JSON à¦«à¦¾à¦\87ল à¦¸à¦®à§\8dপাদনা à¦\95রার",
+       "action-editmyuserjs": "সà§\8dব à¦¬à§\8dযবহারà¦\95ারà§\80 à¦\9cাভাসà§\8dà¦\95à§\8dরিপà§\8dà¦\9f à¦«à¦¾à¦\87ল à¦¸à¦®à§\8dপাদনা à¦\95রার",
+       "action-viewsuppressed": "যà§\87à¦\95à§\8bন à¦¬à§\8dযবহারà¦\95ারà§\80র à¦\95াà¦\9b à¦¥à§\87à¦\95à§\87 à¦²à§\81à¦\95ানà§\8b à¦¸à¦\82সà§\8dà¦\95রণà¦\97à§\81লি à¦¦à§\87à¦\96ার",
+       "action-hideuser": "বà§\8dযবহারà¦\95ারà§\80à¦\95à§\87 à¦¬à¦¾à¦§à¦¾ à¦¦à§\87য়ার, à¦\8fবà¦\82 à¦¤à¦¾ à¦¸à¦°à§\8dবসাধারণà§\87র à¦¦à§\83ষà§\8dà¦\9fিসà§\80মা à¦¥à§\87à¦\95à§\87 à¦²à§\81à¦\95ানà§\8bর",
+       "action-ipblock-exempt": "আইপি বাধা, স্বয়ংক্রিয় বাধা ও পরিসীমার বাধা এড়ানো",
        "action-unblockself": "নিজেকে বাধামুক্ত করার",
-       "action-noratelimit": "রà§\87à¦\9f à¦²à¦¿à¦®à¦¿à¦\9fà§\87র à¦­à¦¿à¦¤à§\8dতিতà§\87 à¦ªà¦°à¦¿à¦¬à¦°à§\8dতন à¦¹à¦¬à§\87 à¦¨à¦¾",
-       "action-reupload-own": "নিà¦\9cà§\87র à¦¦à§\8dবারা à¦\86পলà§\8bডà¦\95à§\83ত à¦«à¦¾à¦\87ল à¦¯à¦¾ à¦\87তিমধà§\8dযà§\87à¦\87 à¦¬à¦¿à¦¦à§\8dযমান, à¦¸à§\87à¦\9fি à¦®à§\81à¦\9bà§\87 à¦ªà§\81নরায় à¦¨à¦¤à§\81ন à¦\95রà§\87 à¦\86পলà§\8bড à¦\95রা",
-       "action-nominornewtalk": "বারà§\8dতা à¦²à§\87à¦\96ার à¦®à¦¤ à¦\86লাপ à¦ªà¦¾à¦¤à¦¾à¦¯à¦¼ à¦\95à§\8bনà§\8b à¦\85নà§\81লà§\8dলà§\87à¦\96à§\8dয à¦¸à¦®à§\8dপাদনা à¦¨à§\87à¦\87",
-       "action-markbotedits": "ফà§\87রত à¦\86না à¦¸à¦®à§\8dপাদনাসমà§\82হà¦\95à§\87 à¦¬à¦\9f à¦¸à¦®à§\8dপাদনা à¦¹à¦¿à¦¸à§\87বà§\87 à¦\9aিহà§\8dনিত à¦\95রà§\87",
-       "action-patrolmarks": "সামà§\8dপà§\8dরতিà¦\95 à¦ªà¦°à¦¿à¦¬à¦°à§\8dতনà§\87র à¦ªà¦°à§\80à¦\95à§\8dষিত à¦\9aিহà§\8dন à¦¦à§\87à¦\96াà¦\93",
-       "action-override-export-depth": "লিà¦\82à¦\95সহ à¦ªà¦¾à¦¤à¦¾ à¦¯à¦¾à¦° à¦\97ভà§\80রতা à§« à¦\8fর à¦®à¦§à§\8dযà§\87 à¦¸à§\87à¦\97à§\81লà§\8b à¦°à¦ªà§\8dতানি à¦\95রà§\81ন",
-       "action-suppressredirect": "পাতা স্থানান্তরের সময় মূল পাতা থেকে পুনর্নির্দেশ তৈরী করছে না",
+       "action-noratelimit": "রà§\87à¦\9f à¦¸à§\80মার à¦¦à§\8dবারা à¦ªà§\8dরভাবিত à¦¨à¦¾ à¦¹à¦¬à¦¾à¦°",
+       "action-reupload-own": "নিà¦\9cà§\87র à¦¦à§\8dবারা à¦\86পলà§\8bডà¦\95à§\83ত à¦«à¦¾à¦\87ল à¦ªà§\81নরà§\8dলিà¦\96নà§\87র",
+       "action-nominornewtalk": "à¦\86লà§\8bà¦\9aনার à¦ªà§\83ষà§\8dঠাà¦\97à§\81লিতà§\87 à¦\85নà§\81লà§\8dলà§\87à¦\96à§\8dয à¦¸à¦®à§\8dপাদনা à¦¨à§\87à¦\87 à¦¨à¦¤à§\81ন à¦¬à¦¾à¦°à§\8dতা à¦ªà§\8dরমà§\8dপà¦\9f à¦\9fà§\8dরিà¦\97ার à¦\95রার",
+       "action-markbotedits": "ফà§\87রত à¦\86না à¦¸à¦®à§\8dপাদনাসমà§\82হà¦\95à§\87 à¦¬à¦\9f à¦¸à¦®à§\8dপাদনা à¦¹à¦¿à¦¸à§\87বà§\87 à¦\9aিহà§\8dনিত à¦\95রার",
+       "action-patrolmarks": "সামà§\8dপà§\8dরতিà¦\95 à¦ªà¦°à¦¿à¦¬à¦°à§\8dতনà§\87র à¦ªà¦°à§\80à¦\95à§\8dষণà§\87র à¦\9aিহà§\8dন à¦¦à§\87à¦\96ার",
+       "action-override-export-depth": "৫-à¦\8fর à¦\97ভà§\80রতা à¦ªà¦°à§\8dযনà§\8dত à¦¸à¦\82যà§\8bà¦\97à¦\95à§\83ত à¦ªà¦¾à¦¤à¦¾à¦¸à¦¹ à¦ªà¦¾à¦¤à¦¾à¦\97à§\81লি à¦°à¦ªà§\8dতানি à¦\95রার",
+       "action-suppressredirect": "পাতা স্থানান্তর করার সময় উৎস পাতা থেকে পুনর্নির্দেশ তৈরী করার",
        "nchanges": "$1টি {{PLURAL:$1|পরিবর্তন}}",
        "enhancedrc-since-last-visit": "{{PLURAL:$1|সর্বশেষ প্রদর্শনের পর}} $1টি",
        "enhancedrc-history": "ইতিহাস",
        "rcfilters-filter-watchlist-notwatched-description": "আপনার নজরতালিকায় থাকা পাতাগুলি ব্যতীয় সবকিছু।",
        "rcfilters-filtergroup-watchlistactivity": "নজরতালিকার কার্যক্রম",
        "rcfilters-filter-watchlistactivity-unseen-label": "অদেখা পরিবর্তন",
-       "rcfilters-filter-watchlistactivity-unseen-description": "à¦\86পনার à¦¨à¦\9cরতালিà¦\95ায় à¦¥à¦¾à¦\95া à¦ªà¦¾à¦¤à¦¾à¦\97à§\81লিতà§\87 à¦ªà¦°à¦¿à¦¬à¦°à§\8dতন à¦¯à§\87à¦\97à§\81লিতà§\87 à¦\86পনি à¦¸à¦®à§\8dপাদনা à¦\95রার à¦ªà¦° à¦\86র à¦¯à¦¾ননি।",
+       "rcfilters-filter-watchlistactivity-unseen-description": "পাতাসমà§\82হà§\87র à¦ªà¦°à¦¿à¦¬à¦°à§\8dতন à¦\98à¦\9fার à¦ªà¦° à¦¥à§\87à¦\95à§\87 à¦\86পনি à¦¯à§\87সব à¦ªà¦¾à¦¤à¦¾ à¦ªà¦°à¦¿à¦¦à¦°à§\8dশন à¦\95রà§\87ননি।",
        "rcfilters-filter-watchlistactivity-seen-label": "দেখা পরিবর্তন",
-       "rcfilters-filter-watchlistactivity-seen-description": "à¦\86পনার à¦¨à¦\9cরতালিà¦\95ায় à¦¥à¦¾à¦\95া à¦ªà¦¾à¦¤à¦¾à¦\97à§\81লিতà§\87 à¦ªà¦°à¦¿à¦¬à¦°à§\8dতন à¦¯à§\87à¦\97à§\81লিতà§\87 à¦\86পনি à¦¸à¦®à§\8dপাদনা à¦\95রার à¦ªà¦° à¦\86র à¦¯à¦¾à¦¨à¦¨à¦¿।",
+       "rcfilters-filter-watchlistactivity-seen-description": "পাতাসমà§\82হà§\87র à¦ªà¦°à¦¿à¦¬à¦°à§\8dতন à¦\98à¦\9fার à¦ªà¦° à¦¥à§\87à¦\95à§\87 à¦\86পনি à¦¯à§\87সব à¦ªà¦¾à¦¤à¦¾ à¦ªà¦°à¦¿à¦¦à¦°à§\8dশন à¦\95রà§\87à¦\9bà§\87ন।",
        "rcfilters-filtergroup-changetype": "পরিবর্তনের ধরন",
        "rcfilters-filter-pageedits-label": "পাতার সম্পাদনা",
        "rcfilters-filter-pageedits-description": "উইকি বিষয়বস্তু, আলোচনা, বিষয়শ্রেণীর বিবরণ... ইত্যাদিতে সম্পাদনা",
        "rcfilters-preference-help": "ছাঁকনিগুলি অনুসন্ধান বা আলোকপাতকরণ কার্যকারিতা ছাড়া সাম্প্রতিক পরিবর্তন লোড করে",
        "rcfilters-watchlist-preference-label": "জাভাস্ক্রিপ্টহীন ইন্টারফেস ব্যবহার করুন",
        "rcfilters-watchlist-preference-help": "ছাঁকনি অনুসন্ধান বা আলোকপাতকরণ বৈশিষ্ট্য ছাড়া নজরতালিকা লোড করে।",
-       "rcfilters-filter-showlinkedfrom-label": "লিà¦\82à¦\95 à¦\95রা à¦\8fমন à¦ªà¦¾à¦¤à¦¾à¦\97à§\81লà§\8bর পরিবর্তন দেখান",
-       "rcfilters-filter-showlinkedfrom-option-label": "নির্বাচিত পাতা থেকে <strong>পাতা লিংক করা</strong>",
-       "rcfilters-filter-showlinkedto-label": "পাতা à¦²à¦¿à¦\82à¦\95 à¦\95রা à¦\8fমন পাতাসমূহের পরিবর্তন দেখান",
-       "rcfilters-filter-showlinkedto-option-label": "নির্বাচিত পাতা থেকে <strong>পাতা লিংক করা</strong>",
+       "rcfilters-filter-showlinkedfrom-label": "à¦\8fà¦\9fি à¦¥à§\87à¦\95à§\87 à¦¸à¦\82যà§\8bà¦\97à¦\95ারà§\80 à¦ªà¦¾à¦¤à¦¾à¦¸à¦®à§\82হà§\87র পরিবর্তন দেখান",
+       "rcfilters-filter-showlinkedfrom-option-label": "নির্বাচিত পাতাটি থেকে <strong>সংযোগকারী পাতাসমূহ</strong>",
+       "rcfilters-filter-showlinkedto-label": "à¦\8fà¦\9fিতà§\87 à¦¸à¦\82যà§\8bà¦\97à¦\95ারà§\80 পাতাসমূহের পরিবর্তন দেখান",
+       "rcfilters-filter-showlinkedto-option-label": "নির্বাচিত পাতাটিতে <strong>সংযোগকারী পাতাসমূহ</strong>",
        "rcfilters-target-page-placeholder": "একটি পাতার নাম (বা বিষয়শ্রেণী) লিখুন",
        "rcnotefrom": "<strong>$2</strong>টা থেকে সংঘটিত পরিবর্তনগুলি (সর্বোচ্চ <strong>$1টি</strong> দেখানো হয়েছে)।",
        "rclistfromreset": "তারিখ নির্বাচন পুনঃস্থাপন করুন",
        "uploaded-script-svg": "আপলোডকৃত SVG ফাইলে স্ক্রিপ্টযোগ্য উপাদান \"$1\" পাওয়া গেছে।",
        "uploaded-hostile-svg": "আপলোড করা SVG ফাইলের শৈলী উপাদানে অনিরাপদ সিএসএস পাওয়া গেছে।",
        "uploaded-event-handler-on-svg": "এসভিজি ফাইলের জন্য <code>$1=\"$2\"</code> ইভেন্ট-হ্যান্ডলার বৈশিষ্ট্যটি নির্ধারণ করা অনুমোদিত নয়।",
-       "uploaded-href-attribute-svg": "এসভিজি ফাইলের href বৈশিষ্ট্যগুলির জন্য কেবলমাত্র http:// বা https:// লক্ষ্যগুলি অনুমোদিত। অন্য বিষয় যেমন, <image>, শুধুমাত্র উপাত্ত ও বৈশিষ্ঠগুলো গ্রহণযোগ্য। <code>&lt;$1 $2=\"$3\"&gt;</code> পাওয়া গেছে।",
+       "uploaded-href-attribute-svg": "<a> উপাদান শুধুমাত্র উপাত্তে সংযোগ (href) করা যাবে: (এম্বেড করা ফাইল), http:// বা https://, বা খণ্ডিত (#, একই-নথি) লক্ষ্যগুলি। অন্যান্য উপাদানের জন্য, যেমন <image>, কেবলমাত্র উপাত্ত: ও খণ্ড অনুমোদিত। আপনার এসভিজি রপ্তানি করার সময় ছবি এম্বেড করার চেষ্টা করুন। <code>&lt;$1 $2=\"$3\"&gt;</code> পাওয়া গেছে।",
        "uploaded-href-unsafe-target-svg": "অনিরাপদ উপাত্তে href পাওয়া গেছে: আপলোডকৃত SVG ফাইলে URI লক্ষ্য ছিল <code>&lt;$1 $2=\"$3\"&gt;</code>।",
        "uploaded-animate-svg": "\"animate\" ট্যাগটি পাওয়া গেছে যা আপলোডকৃত এসভিজি ফাইলের <code>&lt;$1 $2=\"$3\"&gt;</code> - এই \"from\" অ্যাট্রিবিউটটি ব্যবহার করে href পরিবর্তন করতে পারে।",
        "uploaded-setting-event-handler-svg": "ইভেন্ট-হ্যান্ডলার অ্যাট্রিবিউট নির্ধারণ করতে বাধা দেওয়া হয়েছে। আপলোডকৃত এসভিজি ফাইলে <code>&lt;$1 $2=\"$3\"&gt;</code> খুঁজে পাওয়া গেছে।",
        "blocklogpage": "বাধা দানের লগ",
        "blocklog-showlog": "এই ব্যবহারকারীকে পূর্বেও বাধা প্রদান করা হয়েছিলো।\nতথ্যসূত্র হিসেবে তাই পূর্বের বাধাদানের লগটি নিচে প্রদর্শন করা হচ্ছে:",
        "blocklog-showsuppresslog": "এই ব্যবহারকারীকে পূর্বেও বাধা প্রদান ও লুকানো হয়েছিলো।\nতথ্যসূত্র হিসেবে তাই পূর্বের অপসারণ লগটি নিচে প্রদর্শন করা হচ্ছে:",
-       "blocklogentry": "[[$1]] à¦\95à§\87 $2 à¦®à§\87য়াদের জন্য বাধাদান করেছেন $3",
-       "reblock-logentry": "[[$1]] এর ব্লক সেটিং পরিবর্তন করা হয়েছে যেটি শেষ হবে $2 $3 সময়ে",
+       "blocklogentry": "[[$1]] à¦\95à§\87 $2 à¦¸à¦®à¦¯à¦¼ের জন্য বাধাদান করেছেন $3",
+       "reblock-logentry": "[[$1]]-এর বাধাদান সেটিং পরিবর্তন করেছেন যেটি শেষ হবার মেয়াদ $2 $3",
        "blocklogtext": "এটি ব্যবহারকারীদেরকে বাধা দানের বা বাধা তুলে নেওয়ার লগ।\nস্বয়ংক্রিয়ভাবে বাধাদানকৃত আইপি ঠিকানাগুলি এখানে তালিকাবদ্ধ করা হয়নি।\nবর্তমানে সক্রিয় নিষিদ্ধকরণ ও বাধাদানের তালিকার জন্য [[Special:BlockList| বাধাদান তালিকা]] দেখুন।",
        "unblocklogentry": "$1-এর উপর বাধা তুলে নেয়া হয়েছে",
        "block-log-flags-anononly": "কেবল বেনামী ব্যবহারকারীরা",
        "ipb_expiry_old": "মেয়াদোত্তীর্ণের সময় অতীত হয়েছে।",
        "ipb_expiry_temp": "লুকানো ব্যবহারকারীনাম বাধা চিরস্থায়ী হতে হবে।",
        "ipb_hide_invalid": "এই অ্যাকাউন্ট বাধা দেয়া সম্ভব নয়; এটি {{PLURAL:$1|একের অধিক|$1টি}} সম্পাদনা করেছে।",
-       "ipb_hide_partial": "লà§\81à¦\95ায়িত à¦¬à§\8dযবহারà¦\95ারà§\80 à¦¨à¦¾à¦®à§\87র à¦¬à¦¾à¦§à¦¾à¦¦à¦¾à¦¨ à¦\85বশà§\8dযà¦\87 à¦¸à¦¾à¦\87à¦\9fà¦\93য়াà¦\87ড হতে হবে।",
+       "ipb_hide_partial": "লà§\81à¦\95ানà§\8b à¦¬à§\8dযবহারà¦\95ারà§\80 à¦¨à¦¾à¦®à§\87র à¦¬à¦¾à¦§à¦¾à¦¦à¦¾à¦¨ à¦\85বশà§\8dযà¦\87 à¦¸à¦¾à¦\87à¦\9fবà§\8dযপà§\80 হতে হবে।",
        "ipb_already_blocked": "\"$1\" ইতিমধ্যে বাধাপ্রাপ্ত।",
        "ipb-needreblock": "$1 ইতিমধ্যেই বাধাপ্রাপ্ত আছেন। আপনি কি সেটিংস পরিবর্তন করতে চান?",
        "ipb-otherblocks-header": "অন্যান্য {{PLURAL:$1|বাধা|বাধাসমূহ}}",
        "revdelete-unrestricted": "এই সীমাবদ্ধতা প্রশাসকের ক্ষেত্রে তুলে নাও",
        "logentry-block-block": "$1 {{GENDER:$4|$3}} কে $5 মেয়াদের জন্য {{GENDER:$2|বাধাদান}} করেছেন $6",
        "logentry-block-unblock": "$1 {{GENDER:$4|$3}}-এর উপর থেকে বাধা তুলে {{GENDER:$2|নিয়েছেন}}",
-       "logentry-block-reblock": "$1 {{GENDER:$4|$3}}-এর জন্য বাধাদান সেটিং $5 সময়ের জন্য {{GENDER:$2|পরিবর্তন}} করেছেন $6",
+       "logentry-block-reblock": "$1 {{GENDER:$4|$3}}-এর বাধাদান সেটিং {{GENDER:$2|পরিবর্তন করেছেন}} যেটি শেষ হবার মেয়াদ $5 $6",
+       "logentry-partialblock-block-page": "$2 {{PLURAL:$1|পাতাটি|পাতাগুলি}}",
+       "logentry-partialblock-block-ns": "$2 {{PLURAL:$1|নামস্থানটি|নামস্থানগুলি}}",
+       "logentry-partialblock-block": "$1 {{GENDER:$4|$3}} কে $7 সম্পাদনা করা থেকে $5 সময়ের জন্য {{GENDER:$2|বাধাদান করেছেন}} $6",
+       "logentry-partialblock-reblock": "$1 $7তে সম্পাদনা করা প্রতিরোধ করে {{GENDER:$4|$3}}-এর বাধাদান সেটিং {{GENDER:$2|পরিবর্তন করেছেন}} যেটি শেষ হবার মেয়াদ $5 $6",
+       "logentry-non-editing-block-block": "$1 {{GENDER:$4|$3}} কে সম্পাদনা-ছাড়া নির্দিষ্ট কর্ম করা থেকে $5 সময়ের জন্য {{GENDER:$2|বাধাদান করেছেন}} $6",
+       "logentry-non-editing-block-reblock": "$1 সম্পাদনা-ছাড়া নির্দিষ্ট কর্মের জন্য {{GENDER:$4|$3}}-এর বাধাদান সেটিং {{GENDER:$2|পরিবর্তন করেছেন}} যেটি শেষ হবার মেয়াদ $5 $6",
        "logentry-suppress-block": "$1 {{GENDER:$4|$3}} কে $5 মেয়াদের জন্য {{GENDER:$2|বাধাদান}} করেছেন $6",
-       "logentry-suppress-reblock": "$1 {{GENDER:$4|$3}}-à¦\8fর à¦\9cনà§\8dয à¦¬à¦¾à¦§à¦¾à¦¦à¦¾à¦¨ à¦¸à§\87à¦\9fিà¦\82 $5 à¦¸à¦®à¦¯à¦¼à§\87র à¦\9cনà§\8dয {{GENDER:$2|পরিবরà§\8dতন}} à¦\95রà§\87à¦\9bà§\87ন $6",
+       "logentry-suppress-reblock": "$1 {{GENDER:$4|$3}}-à¦\8fর à¦¬à¦¾à¦§à¦¾à¦¦à¦¾à¦¨ à¦¸à§\87à¦\9fিà¦\82 {{GENDER:$2|পরিবরà§\8dতন à¦\95রà§\87à¦\9bà§\87ন}} à¦¯à§\87à¦\9fি à¦¶à§\87ষ à¦¹à¦¬à¦¾à¦° à¦®à§\87য়াদ $5 $6",
        "logentry-import-upload": "$1 ফাইল আপলোড দ্বারা $3 {{GENDER:$2|আমদানি করেছেন}}",
        "logentry-import-upload-details": "$1 ফাইল আপলোড দ্বারা $3 {{GENDER:$2|আমদানি করেছেন}} ($4টি {{PLURAL:$4|সংশোধন}})",
        "logentry-import-interwiki": "$1 অন্য একটি উইকিতে থেকে $3 {{GENDER:$2|আমদানি করেছে}}",
index 55910ac..388e583 100644 (file)
        "tag-mw-removed-redirect": "дӀаяьккхина дӀасхьажорг",
        "tag-mw-changed-redirect-target": "хийцаран бахьна ду дӀасахьажорг",
        "tag-mw-blank": "цӀанъяр",
+       "tag-mw-replace": "хийцар",
        "tag-mw-rollback": "Юхаяккха",
        "tag-mw-undo": "юхаяккхар",
        "tags-title": "Билгалонаш",
index f529959..53b0054 100644 (file)
        "badretype": "De indtastede adgangskoder er ikke ens.",
        "usernameinprogress": "En oprettelse af konto for dette brugernavn er allerede i gang.\nVent venligst.",
        "userexists": "Det brugernavn, du har valgt, er allerede i brug.\nVælg venligst et andet brugernavn.",
-       "createacct-normalization": "Dit brugernavn vil blive ændret til «$2» på grund af tekniske begrænsninger.",
+       "createacct-normalization": "Dit brugernavn vil blive ændret til \"$2\" på grund af tekniske begrænsninger.",
        "loginerror": "Logon mislykket",
        "createacct-error": "Fejl ved kontooprettelse",
        "createaccounterror": "Kunne ikke oprette brugerkonto: $1",
        "content-json-empty-array": "Tomt matrix",
        "deprecated-self-close-category": "Sider, der bruger ugyldige, selvlukkende HTML-tags",
        "deprecated-self-close-category-desc": "Siden bruger ugyldige selvlukkende HTML tags, som <code>&lt;b/></code> eller <code>&lt;span/></code>. De vil snart blive ændret i overensstemmelse med HTML5-specifikationen, så de ikke kan bruges i wikitext.",
-       "duplicate-args-warning": "<strong>Advarsel</strong>: [[:$1]] kaldes [[:$2]] med flere end en værdi for \"$3\"-parameteren. Bare den sidst angitte værdien vil bruges.",
+       "duplicate-args-warning": "<strong>Advarsel</strong>: [[:$1]] kalder [[:$2]] med mere end en værdi for \"$3\"-parameteren. Kun den sidst angivne værdi vil blive brugt.",
        "duplicate-args-category": "Sider der bruger samme argument mere end en gang i en skabelon",
        "duplicate-args-category-desc": "Siden indeholder en skabelon hvor et argument er brugt mere end en gang, som <code><nowiki>{{foo|bar=1|bar=2}}</nowiki></code> eller <code><nowiki>{{foo|bar|1=baz}}</nowiki></code>.",
        "expensive-parserfunction-warning": "Advarsel: Der er for mange beregningstunge oversætter-funktionskald på denne side.\n\nDer bør være færre end {{PLURAL:$2|$2 kald}}, lige nu er der {{PLURAL:$1|$1 kald}}.",
        "post-expand-template-argument-category": "Sider med udeladte skabelonparametre",
        "parser-template-loop-warning": "Skabelonløkke fundet: [[$1]]",
        "template-loop-category": "Sider med skabelonløkker",
-       "template-loop-category-desc": "Siden indeholder en malløkke, altså en skabelon som kalder sig selv rekursivt.",
+       "template-loop-category-desc": "Siden indeholder en skabelonløkke, det vil sige en skabelon som kalder sig selv rekursivt.",
        "parser-template-recursion-depth-warning": "En skabelon er rekursivt inkluderet for mange gange ($1)",
        "language-converter-depth-warning": "Dybdegrænse for sprogkonvertering overskredet ($1)",
        "node-count-exceeded-category": "Sider hvor antal noder er overskredet",
        "page_first": "Starten",
        "page_last": "Enden",
        "histlegend": "Forklaring: (nuværende) = forskel til den nuværende\nversion, (forrige) = forskel til den forrige version, M = mindre ændring",
-       "history-fieldset-title": "Søg efter versioner",
+       "history-fieldset-title": "Filtrer versioner",
        "history-show-deleted": "Kun slettede revisioner",
        "histfirst": "ældste",
        "histlast": "nyeste",
        "action-changetags": "tilføje og fjerne vilkårlige tags for enkelte versioner og logposter",
        "action-deletechangetags": "slette tags fra databasen",
        "action-purge": "rense denne side",
+       "action-bigdelete": "slet sider med store historikker",
+       "action-blockemail": "bloker en bruger fra at sende e-mails",
        "action-bot": "blive behandlet som en automatiseret proces",
        "nchanges": "$1 {{PLURAL:$1|ændring|ændringer}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|siden sidste besøg}}",
        "rcfilters-savedqueries-add-new-title": "Gem nuværende filterindstillinger",
        "rcfilters-restore-default-filters": "Gendan standardfiltre",
        "rcfilters-clear-all-filters": "Ryd alle filtre",
-       "rcfilters-show-new-changes": "Vis seneste ændringer",
+       "rcfilters-show-new-changes": "Vis seneste ændringer siden $1",
        "rcfilters-search-placeholder": "Filtrer ændringer (brug menuen eller søg på filternavn)",
        "rcfilters-invalid-filter": "Ugyldigt filter",
        "rcfilters-empty-filter": "Ingen aktive filtre. All bidrag vises.",
        "delete-confirm": "Slet \"$1\"",
        "delete-legend": "Slet",
        "historywarning": "<strong>Advarsel:</strong> Siden du er ved at slette har en historie med $1 {{PLURAL:$1|version|versioner}}:",
-       "historyaction-submit": "Vis",
+       "historyaction-submit": "Vis revisioner",
        "confirmdeletetext": "Du er ved at slette en side sammen med hele dens tilhørende historik.\nBekræft venligst at du virkelig vil gøre dette, at du forstår konsekvenserne, og at du gør det i overensstemmelse med [[{{MediaWiki:Policy-url}}|retningslinjerne]].",
        "actioncomplete": "Gennemført",
        "actionfailed": "Handlingen mislykkedes",
index 656d41b..047c448 100644 (file)
@@ -96,7 +96,8 @@
                        "PerfektesChaos",
                        "Kurt Jansson",
                        "McDutchie",
-                       "Johanna Strodt (WMDE)"
+                       "Johanna Strodt (WMDE)",
+                       "Andi-3"
                ]
        },
        "tog-underline": "Links unterstreichen:",
        "tog-hidepatrolled": "Kontrollierte Änderungen in den „Letzten Änderungen“ ausblenden",
        "tog-newpageshidepatrolled": "Kontrollierte Seiten bei den „Neuen Seiten“ ausblenden",
        "tog-hidecategorization": "Kategorisierungen von Seiten ausblenden",
-       "tog-extendwatchlist": "Alle Änderungen in der Beobachtungsliste anzeigen, nicht nur die aktuellsten",
+       "tog-extendwatchlist": "Alle Änderungen in der Beobachtungsliste anzeigen, nicht nur die letzten",
        "tog-usenewrc": "Änderungen auf „Letzte Änderungen“ und der Beobachtungsliste nach Seite gruppieren",
        "tog-numberheadings": "Überschriften automatisch nummerieren",
        "tog-editondblclick": "Seiten mit Doppelklick bearbeiten",
        "previewnote": "'''Dies ist nur eine Vorschau.'''\nDie Seite wurde noch nicht gespeichert!",
        "continue-editing": "Zum Bearbeitungsfeld gehen",
        "previewconflict": "Diese Vorschau gibt den Inhalt des oberen Textfeldes wieder. So wird die Seite aussehen, wenn du jetzt speicherst.",
-       "session_fail_preview": "Entschuldigung! Wir konnten deine Bearbeitung nicht verarbeiten, da Sitzungsdaten verloren gegangen sind.\n\nDu wurdest eventuell abgemeldet. <strong>Bitte verifiziere, dass du noch angemeldet bist und versuche es erneut</strong>.\nFalls dies nicht funktioniert, versuche dich [[Special:UserLogout|abzumelden]] und anschließend wieder anzumelden und überprüfe, ob dein Browser Cookies von dieser Website akzeptiert.",
+       "session_fail_preview": "Entschuldigung! Wir konnten deine Bearbeitung nicht verarbeiten, da Sitzungsdaten verloren gegangen sind.\n\nDu wurdest eventuell abgemeldet. <strong>Bitte stelle sicher, dass du noch angemeldet bist, und versuche es erneut</strong>.\nFalls dies nicht funktioniert, versuche dich [[Special:UserLogout|abzumelden]] und anschließend wieder anzumelden und überprüfe, ob dein Browser Cookies von dieser Website akzeptiert.",
        "session_fail_preview_html": "Deine Bearbeitung konnte nicht gespeichert werden, da Sitzungsdaten verloren gegangen sind.\n\n<em>Da in {{SITENAME}} das Speichern von reinem HTML aktiviert ist, wurde die Vorschau ausgeblendet, um JavaScript-Attacken vorzubeugen.</em>\n\n<strong>Bitte versuche es erneut, indem du unter der folgenden Textvorschau nochmals auf „Seite speichern“ klickst.</strong>\nSollte das Problem bestehen bleiben, [[Special:UserLogout|melde dich ab]] und danach wieder an. Überprüfe, ob dein Browser Cookies von dieser Website akzeptiert.",
        "token_suffix_mismatch": "'''Deine Bearbeitung wurde zurückgewiesen, da dein Browser Zeichen im Bearbeiten-Token verstümmelt hat.\nEine Speicherung kann den Seiteninhalt zerstören. Dies geschieht bisweilen durch die Benutzung eines anonymen Proxy-Dienstes, der fehlerhaft arbeitet.'''",
        "edit_form_incomplete": "'''Der Inhalt des Bearbeitungsformulars hat den Server nicht vollständig erreicht. Bitte prüfe deine Bearbeitungen auf Vollständigkeit und versuche es erneut.'''",
index 996bd36..542c697 100644 (file)
        "ncategories": "$1 {{PLURAL:$1|Kategori|Kategoriy}}",
        "ninterwikis": "$1 {{PLURAL:$1|interwiki|interwikiy}}",
        "nlinks": "$1 {{PLURAL:$1|link|linkî}}",
-       "nmembers": "$1 {{PLURAL:$1|eza|ezayan}}",
+       "nmembers": "$1 {{PLURAL:$1|eza|ezayi}}",
        "nmemberschanged": "$1 → $2 {{PLURAL:$1|eza|ezayan}}",
        "nrevisions": "$1 {{PLURAL:$1|vurnayış|vurnayışi}}",
        "nimagelinks": "$1 {{PLURAL:$1|pele de|pelan de}} gureyeno",
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 cf1c6fc..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",
        "rcfilters-savedqueries-already-saved": "Ezek a szűrők már el lettek mentve. Módosítsd a beállításokat egy új mentett szűrő készítéséhez.",
        "rcfilters-restore-default-filters": "Alapértelmezett szűrők visszaállítása",
        "rcfilters-clear-all-filters": "Összes szűrő kikapcsolása",
-       "rcfilters-show-new-changes": "Legfrissebb változtatások megtekintése",
+       "rcfilters-show-new-changes": "$1-óta történt friss változtatások megtekintése",
        "rcfilters-search-placeholder": "Változtatások szűrése (használd a menüt vagy keress szűrőkre)",
        "rcfilters-invalid-filter": "Érvénytelen szűrő",
        "rcfilters-empty-filter": "Nincs aktív szűrő. Minden közreműködés látható.",
        "passwordpolicies-policy-passwordnotinlargeblacklist": "A jelszó nem szerepelhet a 100 000 leggyakrabban használt jelszó listáján .",
        "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."
+       "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-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 7378e8a..9e36b9f 100644 (file)
        "blocklist-editing-page": "paginas",
        "blocklist-editing-ns": "spatios de nomines",
        "ipblocklist-empty": "Le lista de blocadas es vacue.",
-       "ipblocklist-no-results": "Le adresse IP o nomine de usator que tu requestava non es blocate.",
+       "ipblocklist-no-results": "Nulle blocadas trovate que corresponde al adresse IP o nomine de usator requestate.",
        "blocklink": "blocar",
        "unblocklink": "disblocar",
        "change-blocklink": "cambiar blocada",
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 f0f16cf..e6e991b 100644 (file)
        "prefs-files": "പ്രമാണങ്ങൾ",
        "prefs-custom-css": "സ്വന്തം സി.എസ്.എസ്.",
        "prefs-custom-json": "ഐച്ഛിക ജെസൺ",
-       "prefs-custom-js": "à´¸àµ\8dവനàµ\8dà´¤à´\82 à´\9càµ\86.à´\8eà´¸àµ\8d.",
+       "prefs-custom-js": "à´¸àµ\8dവനàµ\8dà´¤à´\82 à´\9cാവാസàµ\8dà´\95àµ\8dà´°à´¿à´ªàµ\8dà´±àµ\8dà´±àµ\8d",
        "prefs-common-config": "എല്ലാ ദൃശ്യരൂപങ്ങൾക്കുമായി പങ്ക് വെയ്ക്കപ്പെട്ട സി.എസ്.എസ്./ജെസൺ/ജാവാസ്ക്രിപ്റ്റ്:",
        "prefs-reset-intro": "സൈറ്റിൽ സ്വതേയുണ്ടാവേണ്ട ക്രമീകരണങ്ങൾ പുനഃക്രമീകരിക്കാൻ താങ്കൾക്ക് ഈ താൾ ഉപയോഗിക്കാവുന്നതാണ്.\nഇത് തിരിച്ചു ചെയ്യാൻ സാദ്ധ്യമല്ല.",
        "prefs-emailconfirm-label": "ഇമെയിൽ സ്ഥിരീകരണം:",
index a1a29db..f06eb44 100644 (file)
        "allpages-hide-redirects": "ပြန်ညွှန်းများအား ဝှက်ရန်",
        "cachedspecial-viewing-cached-ttl": "သင်သည် $1 အချိန်ကြာသွားနိုင်သော ဤစာမျက်နှာ၏ cached ဗားရှင်းကို ကြည့်ရှုနေခြင်း ဖြစ်ပါသည်။",
        "cachedspecial-viewing-cached-ts": "သင်သည် ဤစာမျက်နှာ၏ အမှန်တကယ်မဟုတ်နိုင်သော cached ဗားရှင်းကို ကြည့်ရှုနေခြင်းဖြစ်သည်။",
+       "cachedspecial-refresh-now": "နောက်ဆုံးကို ကြည့်ရှုရန်။",
        "categories": "ကဏ္ဍများ",
        "categories-submit": "ပြသရန်",
        "categoriespagetext": "အောက်ပါ {{PLURAL:$1|ကဏ္ဍ|ကဏ္ဍများ}}သည် ဤဝီကီတွင် အသုံးပြု သို့မဟုတ် အသုံးမပြုထားခြင်း ဖြစ်နိုင်သည်။ [[Special:WantedCategories|အလိုရှိသော ကဏ္ဍများ]]ကိုလည်း ကြည့်ပါ။",
        "mycontris": "ဆောင်ရွက်ချက်များ",
        "anoncontribs": "ဆောင်ရွက်ချက်များ",
        "contribsub2": "{{GENDER:$3|$1}}အတွက် ($2)",
+       "contributions-subtitle": "{{GENDER:$3|$1}} အတွက်",
        "contributions-userdoesnotexist": "အသုံးပြုသူအကောင့် \"$1\" သည် မှတ်ပုံမတင်ထားပါ။",
        "nocontribs": "ဤသတ်မှတ်ချက်များနှင့် ကိုက်ညီသည့် ပြောင်းလဲမှုများ မရှိပါ။",
        "uctop": "လက်ရှိ",
        "createaccountblock": "အကောင့်ဖန်တီးခြင်းကို ပိတ်ထားသည်",
        "emailblock": "အီးမေးကို ပိတ်ပင်ထားသည်",
        "blocklist-nousertalk": "မိမိ၏ဆွေးနွေးချက်စာမျက်နှာကို တည်းဖြတ်မရနိုင်ပါ",
+       "blocklist-editing": "တည်းဖြတ်ခြင်း",
+       "blocklist-editing-page": "စာမျက်နှာများ",
+       "blocklist-editing-ns": "အမည်ညွှန်းများ",
        "ipblocklist-empty": "ပိတ်ပင်ထားမှုစာရင်းသည် ဗလာဖြစ်နေသည်။",
        "ipblocklist-no-results": "တောင်းဆိုလိုက်သော အိုင်ပီလိပ်စာ သို့မဟုတ် အသုံးပြုသူအမည်ကို မပိတ်ပင်ထားပါ။",
        "blocklink": "ပိတ်ပင်",
        "pageinfo-display-title": "ပြသခေါင်းစဉ်",
        "pageinfo-default-sort": "ပုံမှန် စာလုံးစီကီး",
        "pageinfo-length": "စာမျက်နှာ အလျား (ဘိုက်ဖြင့်)",
+       "pageinfo-namespace": "အမည်ညွှန်း",
        "pageinfo-article-id": "စာမျက်နှာ အိုင်ဒီ",
        "pageinfo-language": "စာမျက်နှာ စာကိုယ် ဘာသာစကား",
        "pageinfo-language-change": "ပြောင်းလဲရန်",
        "log-action-filter-protect-protect": "ကာကွယ်မှု",
        "log-action-filter-rights-rights": "လူဖြင့် ပြောင်းလဲမှု",
        "log-action-filter-rights-autopromote": "အလိုအလျောက် ပြောင်းလဲမှု",
+       "log-action-filter-upload-revert": "ပြန်ပြောင်းရန်",
        "authmanager-create-disabled": "အကောင့်ဖန်တီးခြင်းကို ပိတ်ထားသည်။",
        "authmanager-autocreate-noperm": "အလိုအလျာက် အကောင့်ဖန်တီးခြင်းကို ခွင့်မပြုပါ။",
        "authmanager-autocreate-exception": "ရှေ့ကအမှားများကြောင့် အလိုအလျာက် အကောင့်ဖန်တီးခြင်းကို ယာယီပိတ်ထားသည်။",
        "authmanager-realname-help": "အသုံးပြုသူ၏ အမည်ရင်း",
        "authmanager-provider-temporarypassword": "ယာယီစကားဝှက်",
        "authprovider-resetpass-skip-label": "ကျော်ရန်",
+       "specialpage-securitylevel-not-allowed-title": "ခွင့်မပြုပါ",
        "cannotauth-not-allowed-title": "ခွင့်ပြုချက် ငြင်းပယ်လိုက်သည်",
        "cannotauth-not-allowed": "သင်သည် ဤစာမျက်နှာကို အသုံးပြုခွင့်မရှိပါ",
        "userjsispublic": "ကျေးဇူးပြု၍ မှတ်သားပါ- JavaScript စာမျက်နှာခွဲများတွင် အခြားအသုံးပြုသူများ ကြည့်ရှုနိုင်သော လျို့ဝှက်အပ်သည့်အချက်အလက် မပါဝင်သင့်ပါ။",
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 176a3bb..063bae3 100644 (file)
@@ -4,7 +4,9 @@
                        "Babamamadidianee",
                        "Lancine.kounfantoh.fofana",
                        "Lanciné.kounfantoh.fofana",
-                       "Youssoufkadialy"
+                       "Youssoufkadialy",
+                       "Amire80",
+                       "Nafadji Mory Diané"
                ]
        },
        "sunday": "ߞߊ߯ߙߌߟߏ߲",
        "hidden-categories": "{{PLURAL:$1|ߦߌߟߡߊ߫ ߘߏ߲߰ߣߍ߲ |ߦߌߟߡߊ߫ ߘߏ߲߰ߣߍ߲ ߠߎ߬}}",
        "category-subcat-count": "{{PLURAL:$2|ߦߟߊߡߊߙߋ߲ ߣߌ߲߬ ߠߎ߫ ߜߊ߲߰ߛߊ߲ ߠߋ߫ ߦߋ߫ ߦߌߟߡߊ ߣߌ߲߬ ߘߐ߫.|ߦߌߟߡߊ ߣߊ߬ߕߐ ߟߎ߬ ߘߐ߫߸ {{PLURAL:$1|ߦߌߟߡߊߙߋ߲|$1 ߦߌߟߡߊߙߋ߲ ߠߎ߬}} ߟߋ߬ ߦߴߊ߬ ߘߐ߫߸ ߞߙߎߞߙߍ ߟߎ߬ ߞߐߞߊ߲߬ $2}}",
        "category-article-count": "{{PLURAL:$2|ߞߐߜߍ ߣߌ߲߬ ߘߐߙߐ߲߫ ߠߋ߬ ߦߋ߫ ߦߌߟߡߊ ߣߌ߲߬ ߘߐ߫.|ߖߡߊ߬ߦߊ߫ ߕߐ߮ ߣߊ߬ߕߊ {{PLURAL:$1|ߞߐߜߍ ߦߋ߫|$1 ߞߐߜߍ ߦߋ߫}} ߟߋ߬ ߦߋ߫ ߦߌߟߡߊ߫ ߘߌ߫߸ ߞߙߎߞߙߍ $2 ߞߐߞߊ߲߬}}",
-       "category-file-count": "{{:$2|ߞߐߕߐ߮ ߣߌ߲߬ ߜߊ߲߰ߛߊ߲ ߠߋ߫ ߦߋ߫ ߦߌߟߡߊ ߣߌ߲߬ ߘߐ߫.|ߡߍ߲ ߠߎ߬ ߦߋ߫ ߣߌ߲߬ {{PLURAL:$1|ߞߐߕߐ߮ ߦߋ߫|$1 ߞߐߕߐ߮ ߟߎ߬ ߦߋ߫}} ߦߌߟߡߊ ߣߌ߲߬ ߘߐ߫߸ ߞߙߎߞߙߍ ߣߌ߲߬ $2 ߕߴߊ߬ ߘߐ߫.}}",
+       "category-file-count": "{{PLURAL:$2|ߞߐߕߐ߮ ߣߌ߲߬ ߜߊ߲߰ߛߊ߲ ߠߋ߫ ߦߋ߫ ߦߌߟߡߊ ߣߌ߲߬ ߘߐ߫.|ߡߍ߲ ߠߎ߬ ߦߋ߫ ߣߌ߲߬ {{PLURAL:$1|ߞߐߕߐ߮ ߦߋ߫|$1 ߞߐߕߐ߮ ߟߎ߬ ߦߋ߫}} ߦߌߟߡߊ ߣߌ߲߬ ߘߐ߫߸ ߞߙߎߞߙߍ ߣߌ߲߬ $2 ߕߴߊ߬ ߘߐ߫.}}",
        "listingcontinuesabbrev": "ߖߊ߬ߕߋ߬ߘߊ",
        "index-category": "ߞߐߜߍ߫ ߓߊߕߐ߲ߛߐ߲ ߠߎ߬",
        "noindex-category": "ߞߐߜߍ߫ ߘߐߕߐ߲ߛߐ߲ߦߊߓߊߟߌ ߟߎ߬",
        "about": "ߡߊ߬ߘߎ߮",
-       "newwindow": "ߊ߬ ߟߊߞߊ߬ ߝߢߐߘߊ߫ ߞߎߘߊ߫ ߟߊ߫",
+       "newwindow": "(ߊ߬ ߟߊߞߊ߬ ߝߢߐߘߊ߫ ߞߎߘߊ߫ ߟߊ߫)",
        "cancel": "ߊ߬ ߘߐߛߊ߬",
        "moredotdotdot": "ߡߊߞߊ߬ߝߏ߬...",
        "morenotlisted": "ߛߙߍߘߍ ߣߌ߲߬ ߘߝߊߓߊߟߌ߫ ߓߍ߫ ߞߍ߫.",
@@ -95,7 +97,7 @@
        "navigation-heading": "ߛߏ߲߯ߓߊߟߌ߫ ߓߏߟߏ߲ߘߊ",
        "errorpagetitle": "ߝߎ߬ߕߎ߲߬ߕߌ",
        "returnto": "ߌ ߞߐߛߊ߬ߦߌ߲߬ ߦߊ߲߬ ߡߊ߬$1",
-       "tagline": "ߞߊ߬ ߝߘߊ߫",
+       "tagline": "ߞߊ߬ ߝߘߊ߫{{SITENAMEP}}",
        "help": "ߘߍ߬ߡߍ߲߬ߠߌ",
        "help-mediawiki": "ߘߍ߬ߡߍ߲߬ߠߌ߲ ߞߊ߬ ߓߍ߲߬ ߥߞߌ-ߟߊߛߋߢߊߥߙߍ ߡߊ߬",
        "search": "ߢߌߣߌ߲ߠߌ",
        "print": "ߜߌ߬ߙߌ߲߬ߘߌ߬ߟߌ",
        "view": "ߊ߬ ߘߐߜߍ߫",
        "view-foreign": "ߊ߬ ߦߋ߫ ߦߊ߲߬ $1",
-       "edit": "ß\8a߬ ß¡ß\8aß\9dß\8a߬ß\9fß\8b߲߬",
+       "edit": "ß\8a߬ ß¡ß\8aߦß\9fß\8d߬ߡß\8a߲߬",
        "create": "ߟߊ߬ߘߊ߲߬ߠߌ",
        "create-local": "ߕߌ߲߬ߞߎߘߎ߲ ߞߊ߲߬ߛߓߍ߬ߟߌ ߟߊߘߏ߲߬",
        "delete": "ߊ߬ ߖߐ߬ߛߌ߬",
        "undelete_short": "ߟߊ߬ߛߊ߬ߦߌ߲߬ߠߌ  {{PLURAL:$1|ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߋߟߋ߲߫|$1 ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ ߠߎ߬}}",
+       "protect": "ߊ߬ ߟߊߞߊ߲ߘߊ߫",
+       "protect_change": "ߊ߬ ߡߊߦߟߍ߬ߡߊ߲߫",
        "unprotect": "ߟߊ߬ߞߊ߲߬ߘߊ߬ߟߌ ߡߊߝߊ߬ߟߋ߲߬ߠߌ",
        "newpage": "ߘߐߜߍ߫ ߞߎߘߊ",
        "talkpagelinktext": "ߓߊ߬ߘߏ߬ߟߌ",
        "talk": "ߓߊ߬ߘߏ߬ߓߊ߬ߘߌߦߊ",
        "views": "ߦߌ߬ߘߊ߬ߟߌ",
        "toolbox": "ߖߐ߯ߙߊ߲ ߠߎ߬",
+       "tool-link-emailuser": "ߢߎߡߍߙߋ߲ߞߏ߲ߘߏ߫ ߟߊߕߊ߯ {{GENDER:$1|ߟߊߓߊ߯ߙߟߊ ߣߌ߲߬ ߡߊ߬ }}",
+       "imagepage": "ߞߐߕߐ߮ ߞߐߜߍ ߘߐߜߍ߫",
+       "mediawikipage": "ߗߋߛߓߍ ߞߐߜߍ ߘߐߜߍ߫",
+       "templatepage": "ߞߙߊߞߏ ߞߐߜߍ ߘߐߜߍ߫",
+       "viewhelppage": "ߡߊ߬ߘߍ߬ߡߍ߲߬ߠߌ߲ ߞߐߜߍ ߘߐߜߍ߫",
+       "categorypage": "ߦߌߟߡߊ ߞߐߜߍ ߘߐߜߍ߫",
+       "viewtalkpage": "ߢߊߝߐߞߣߍ ߞߐߜߍ ߘߐߜߍ߫",
        "otherlanguages": "ߞߊ߲ ߜߘߍ߫ ߟߎ߫ ߘߐ߫",
        "redirectedfrom": "(ߌ ߟߊߞߎ߲߬ߛߌ߲߬ߣߍ߲߫ ߞߊ߬ ߓߐ߫ $1)",
        "redirectto": "ߌ ߓߘߊ߫ ߟߊߞߎ߲߬ߛߌ߲߫ ߦߊ߲߬ ߠߊ߫:",
-       "lastmodifiedat": "ߞߐߜߍ ߣߌ߲߬ ߡߊߝߊߟߋ߲߫ ߟߊߓߊ߲ ߞߍ߫ ߘߊ߫ $1߸ $2",
-       "jumpto": "ߊ߬ ߕߌߙߌ߲߫",
+       "lastmodifiedat": "ߞߐߜߍ ߣߌ߲߬ ߡߊߦߟߍ߬ߡߊ߲߬ ߟߊߓߊ߲ ߞߍ߫ ߘߊ߫ $1߸ $2",
+       "protectedpage": "ߞߐߜߍ߫ ߡߊߞߊ߲ߞߊ߲ߣߍ߲",
+       "jumpto": "ߊ߬ ߕߌߙߌ߲߫:",
        "jumptonavigation": "ߛߏ߲߯ߓߊߟߌ",
        "jumptosearch": "ߊ߬ ߕߌߙߌ߲߫",
+       "pool-timeout": "ߘߊߕߎ߲߯ߠߌ߲ ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲߬ ߕߎߡߊ ߓߘߊ߫ ߕߊ߬ߡߌ߲߬",
+       "pool-errorunknown": "ߝߌ߬ߟߌ߬ ߛߎ߲߫ ߟߐ߲ߓߊߟߌ",
+       "poolcounter-usage-error": "ߟߊߓߊ߯ߙߊߟߌ߫ ߝߟߌ $1",
        "aboutsite": "ߞߊ߬ ߓߍ߲߬ {{SITENAME}}",
        "aboutpage": "Project:About",
        "copyrightpage": "{{ns:project}}: ߛߓߍߦߟߊ ߤߊߞߍ",
        "disclaimers": "ߖߊ߲߬ߘߐ߬ߓߌ߬ߟߊ߬ߟߌ ߟߎ߬",
        "disclaimerpage": "Project: ߖߊ߲߬ߘߐ߬ߓߌ߬ߟߊ߬ߟߌ ߡߎ߰ߡߍ",
        "edithelp": "ߡߊ߬ߦߟߍ߬ߢߊ߲߬ߠߌ߲ ߘߍ߬ߡߍ߲߬ߠߌ߲",
+       "helppage-top-gethelp": "ߘߍ߬ߡߍ߲߬ߠߌ",
        "mainpage": "ߓߏ߬ߟߏ߲߬ߘߊ",
        "mainpage-description": "ߓߏ߬ߟߏ߲߬ߘߊ",
+       "policy-url": "ߣߕߊ߬ߘߐ߬ߛߌ߮: ߕߐ߲ ߠߎ߬",
        "portal": "ߟߊ߬ߛߣߍ߬ߟߌ ߓߏ߬ߟߏ߲߬ߘߊ",
        "portal-url": "Project:ߟߊ߬ߛߣߍ߬ߟߌ ߓߏ߬ߟߏ߲߬ߘߊ",
        "privacy": "ߘߎ߲߬ߘߎ߬ߡߊ߬ ߤߊߞߍ",
        "privacypage": "Project:ߞߊ߬ ߓߍ߲߬ ߘߎ߲߬ߘߎ߬ߡߊ߬ ߤߊߞߍ ߡߊ߬",
+       "ok": "ߏ߬ߞߍ߫",
        "retrievedfrom": "ߊ߬ ߡߊߝߍߣߍ߲߫ ߦߊ߲߬ ߓߊ߫$1",
        "youhavenewmessages": "{{PLURAL:$3|ߌ ߓߘߊ߫ ߗߋߛߓߍ߫ ߞߎߘߊ ߛߐ߬ߘߐ߲߬$1  $2 }}",
        "editsection": "ߊ߬ ߡߊߦߟߍ߬ߡߊ߲߫",
        "viewsourceold": "ߊ߬ ߛߎ߲ ߘߐߜߍ߫",
        "editlink": "ߊ߬ ߡߊߦߟߍ߬ߡߊ߲߬",
        "viewsourcelink": "ߊ߬ ߛߎ߲ ߠߊߓߊ߯ߙߊ߫",
-       "editsectionhint": "$1:ߟߊߖߍ߲ߛߍ߲ߠߌ߫ ߦߙߐ",
+       "editsectionhint": "ߦߌߟߡߊ ߡߊߝߊ߬ߟߋ߲߬ߠߌ:$1",
        "toc": "ߞߣߐߘߐ",
+       "showtoc": "ߦߌ߬ߘߊ߬ߟߌ",
+       "hidetoc": "ߢߡߊߘߏ߲߯ߠߌ",
+       "confirmable-confirm": "ߌ ߛߍ߬ߓߍ߫ ߓߊ߬ {{GENDER:$1|}}؟",
+       "confirmable-yes": "ߐ߲߬ߤߐ߲߫",
+       "confirmable-no": "ߍ߲߬ߍ߲߫",
+       "thisisdeleted": "ߦߊ߯ߟߊ߫ ߦߴߊ߬ ߝߍ߬ ߞߵߊ߬ ߦߌ߬ߘߊ߬ ߥߟߊ߫ ߞߵߊ߬ ߟߊߛߊߦߌ߲߬ ߞߎߘߊߞߍ߫ ߓߊ߬ $1؟",
+       "viewdeleted": "ߦߌ߬ߘߊ߬ߟߌ ߓߊ߬ $1؟",
        "site-atom-feed": "$1 ߝߕߌ ߓߊߟߏ",
        "page-atom-feed": "$1 ߝߕߌ ߓߊߟߏ",
-       "red-link-title": "$1(ߞߐߜߍ ߏ߬ ߡߊ߫ ߟߊߘߊ߲߫ ߝߟߐ߫)",
+       "red-link-title": "ߞߐߜߍ߫ ߕߍ߫ ߦߋ߲߬ $1",
        "nstab-main": "ߞߐߜߍ",
        "nstab-user": "ߞߐߜߍ߫ ߟߊߓߊ߯ߙߕߊ",
        "nstab-special": "ߘߐߜߍ߫ ߓߟߏߡߊߞߊ߬ߣߍ߲",
        "nstab-category": "ߦߌߟߡߊ",
        "mainpage-nstab": "ߓߏ߬ߟߏ߲߬ߘߊ",
        "nosuchspecialpage": "ߘߐߜߍ߫ ߓߟߏߡߊߞߊ߬ߣߍ߲߬ ߛߎ߮ ߏ߬ ߝߋ߲߫ ߕߍ߫ ߦߊ߲߬",
+       "nospecialpagetext": "<strong>ߊߟߎ߫ ߓߘߊ߫ ߞߐߜߍ߫ ߓߟߏߡߊߞߊ߬ߣߍ߲ ߘߏ߫ ߢߌߣߌ߲߫ ߡߍ߲ ߕߺߴߦߋ߲߬.</strong>\nߞߐߜߍ߫ ߓߟߏߡߊߞߊ߬ߣߍ߲߫ ߓߘߍ߬ߡߊ ߟߎ߬ ߛߙߍߘߍ ߦߋ߫ ߢߌ߲߬ ߠߋ߫ ߞߊ߲߬ [[Special:SpecialPages|{{int:specialpages}}]].",
        "badtitle": "ߞߎ߲߬ߕߐ߰ ߖߎ߮",
        "viewsource": "ߊ߬ ߛߎ߲ ߘߐߜߍ߫",
        "viewsource-title": "ߣߌ߲߬ $1 ߛߎ߲ ߘߐߜߍ߫",
        "createacct-benefit-heading": "ߛߌ߲ߘߌߣߍ߲߫ ߦߴߌ ߢߐ߲߭ ߡߐ߱ ߟߎ߬ ߟߋ߬ ߓߟߏ߫",
        "createacct-benefit-body1": "{{PLURAL:$1|ߊ߬ ߡߊߦߟߍ߬ߡߊ߲߬|ߊ߬ߟߎ߬ ߡߊߦߟߍ߬ߡߊ߲߬}}",
        "createacct-benefit-body2": "$1 {{PLURAL:$1|ߘߐߜߍ|ߞߐߜߍ ߟߎ߬}}",
-       "createacct-benefit-body3": "ߕߊ߬ߡߌ߲߬ߣߍ߲߬ ߞߎߘߊ {{plural:$1|ߓߟߏߓߌߟߊߢߐ߲߮ߞߊ߲ߠߊ|ߓߟߏߓߌߟߊߢߐ߲߮ߞߊ߲ߠߊ ߟߎ߬}}",
+       "createacct-benefit-body3": "ߕߊ߬ߡߌ߲߬ߣߍ߲߬ ߞߎߘߊ {{PLURAL:$1|ߓߟߏߓߌߟߊߢߐ߲߮ߞߊ߲ߠߊ|ߓߟߏߓߌߟߊߢߐ߲߮ߞߊ߲ߠߊ ߟߎ߬}}",
        "loginlanguagelabel": "ߞߊ߲ $1",
        "pt-login": "ߌ ߜߊ߲߬ߞߎ߲߬",
        "pt-login-button": "ߌ ߜߊ߲߬ߞߎ߲߬",
        "image_tip": "ߞߐߕߐ߮ ߘߐߘߏ߲߬ߣߍ߲",
        "media_tip": "ߞߐߕߐ߮ ߛߘߌ߬ߜߋ߲",
        "sig_tip": "ߌ ߟߊ߫ ߞߟߊ߬ߣߐ ߕߎ߬ߡߊ߬ߘߊ ߓߊ߬ߘߌ߬ߟߊ߲߬ߡߊ",
-       "summary": "ߟߊ߬ߘߛߏ߬ߟߌ",
+       "summary": "ߟߊ߬ߘߛߏ߬ߟߌ:",
        "minoredit": "ߣߌ߲߬ ߦߋ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߘߋ߬ߣߍ߲ ߘߏ߫ ߟߋ߬ ߘߌ߫",
        "watchthis": "ߘߐߜߍ ߣߌ߲߬ ߘߐߜߍ߫",
        "savearticle": "ߊ߬ ߟߊߞߎ߲߬ߘߎ߬",
        "newarticletext": "ߌ ߓߘߊ߫ ߛߘߌ߬ߜߋ߲ ߘߏ߫ ߟߊߓߊ߬ߕߏ߬ ߞߐߜߍ ߘߏ߫ ߘߐ߫߸ ߡߍ߲ ߕߴߦߋ߲߬ ߡߎߣߎ߲߬.\nߣߵߌ ߦߴߊ߬ ߝߍ߫ ߞߊ߬ ߞߐߜߍ ߘߏ߫ ߟߊߘߊ߲߫߸ ߛߓߍߟߌ ߘߊߡߌ߬ߣߊ߬ ߘߎ߰ߟߊ ߘߐ߫ (ߞߊ߬ [$1 ߘߍ߬ߡߍ߲߬ߠߌ߲ ߞߐߜߍ] ߦߋ߫߸ ߖߐ߲߬ߛߊ߬ ߌ ߘߌ߫ ߞߌ߬ߓߊ߬ߙߏ߬ ߖߐ߲ߖߐ߲ ߛߐ߬ߘߐ߲߬). ߣߵߌ ߘߏ߲߬ ߞߍ߫ ߘߊ߫ ߦߊ߲߬ ߝߎ߬ߕߎ߲߬ߕߌ߬ ߓߟߏߡߊ߬߸ ߌ ߟߊ߫ ߛߏ߲߯ߓߊߟߊ߲ <strong>back</strong> ߛߐ߲߬ߞߌ߲߫.",
        "noarticletext": "ߛߓߍߟߌ߫ ߛߌ߫ ߕߍ߫ ߞߐߜߍ ߣߌ߲߭ ߞߊ߲߬ ߕߋ߲߫. ߌ ߘߌ߫ ߛߋ߫ ߞߐߜߍ ߣߌ߲߬ [[Special:Search/{{PAGENAME}}|search for this page title]] ߕߐ߮ ߢߌߣߌ߲߫ ߠߊ߫ ߞߐߜߍ ߕߐ߭ ߟߎ߬ ߘߐ߫߸ <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} search the related logs]߸ ߥߟߊ߫ [{{fullurl:{{FULLPAGENAME}}|action=edit}} create this page]</span>.",
        "noarticletext-nopermission": "ߛߓߍߟߌ߫ ߛߌ߫ ߕߍ߫ ߞߐߜߍ ߣߌ߲߭ ߞߊ߲߬ ߕߋ߲߫.\nߌ ߘߌ߫ ߛߋ߫ [[Special:Search/{{PAGENAME}}|search for this page title]] ߢߌߣߌ߲߫ ߠߊ߫ ߞߐߜߍ ߕߐ߭ ߟߎ߬ ߘߐ߫߸ ߥߟߊ߫ <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} search the related logs]</span> ߞߏ߬ߣߌ߲߬ ߘߌ߬ߢߍ߬ ߞߍߣߍ߲߫ ߕߴߌ ߡߊ߬ ߞߐߜߍ߫ ߣߌ߲߬ ߠߊߞߊ߭ ߘߐ߫.",
-       "userpage-userdoesnotexist-view": "ߟߊ߬ߓߊ߰ߙߊ߬ ߕߐ߮ \"$1\" ߛߙߍߘߍߦߊߣߍ߲߫ ߕߍ߫",
+       "userpage-userdoesnotexist-view": "ߟߊ߬ߓߊ߰ߙߊ߬ ߕߐ߮ \"$1\" ߛߙߍߘߍߦߊߣߍ߲߫ ߕߍ߫.",
        "previewnote": "<strong>ߌ ߖߊ߲߬ߓߌ߬ߟߊ߬ ߞߏ߫ ߣߌ߲߬ ߦߋ߫ ߢߍߝߟߍߟߌ ߘߐߙߐ߲߫ ߠߋ߬ ߘߌ߫. </strong> ߌ ߟߊ߫ ߡߝߊ߬ߟߋ߲߬ߠߌ ߟߎ߫ ߡߊ߫ ߟߊߞߎ߲߬ߘߎ߬ ߝߟߐ߫ ߘߋ߬ ߹",
        "continue-editing": "ߥߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߬ ߞߣߍ ߞߊ߲߬",
        "editing": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߦߋ߫ ߛߋ߲߬ߠߊ߫ $1",
        "creating": "$1 ߛߌ߲ߘߟߌ ߦߋ߫ ߛߋ߲߬ߠߊ߫",
        "editingsection": "(ߛߌ߰ߘߊ߬)$1 ߡߊߦߟߍ߬ߡߊ߲ ߦߋ߫ ߛߋ߲߬ߠߊ߫",
        "templatesused": "{{PLURAL:$1|ߞߙߊߞߏ|ߞߙߊߞߏ ߟߎ߫}} ߟߎ߫ ߟߊߓߊ߯ߙߊ߫ ߘߊ߫ ߞߐߜߍ ߣߌ߲߬ ߘߐ߫",
-       "template-protected": "ߊ߬ ߟߊߞߊ߲ߘߊߣߍ߲ ߠߋ߬",
+       "template-protected": "(ߊ߬ ߟߊߞߊ߲ߘߊߣߍ߲ ߠߋ߬)",
        "template-semiprotected": "(ߟߊ߬ߞߊ߲߬ߘߊ߬ߟߌ-ߝߊ߲߬ߞߋ߬ߟߋ߲߬ߡߊ)",
        "hiddencategories": "ߞߐߜߍ ߣߌ߲߬ ߦߋ߫ ߢߌ߲߬ ߠߎ߫ ߛߌ߲߬ߝߏ߲ ߠߋ߬ ߘߌ߫{{PLURAL:$1|}}",
        "permissionserrors": "ߝߌ߬ߟߌ߫ ߘߌ߬ߢߍ߬ߒߧߋ",
        "content-model-wikitext": "ߥߞߌ߫ ߞߟߏߜߍ",
        "viewpagelogs": "ߞߐߜߍ ߣߌ߲߬ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߠߎ߬ ߦߋ߫",
        "currentrev-asof": "$1 ߟߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߕߊ߬ߡߌ߲߬ߣߍ߲",
-       "revisionasof": "ߊ߬ ߡߊߛߊ߬ߦߌ߲ ߦߊ߲߬ ߓߊ߫",
-       "revision-info": "{{ߞߊ߬ߘߌ߬ߛߊ߬:$6|$2}} ߟߊ߫ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ $2",
-       "previousrevision": "ߡߊ߬ߛߊ߬ߦߌ߲߬ߠߌ߲ ߞߘߐ߬ߡߊ߲",
+       "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": "ߢߌߣߌ߲ߠߌ߲ ߞߐߝߟߌ ߟߎ߬",
        "searchprofile-images-tooltip": "ߞߐߕߐ߮ ߟߎ߬ ߢߌߣߌ߲߫",
        "searchprofile-everything-tooltip": "ߊ߬ ߞߣߐߘߐ ߓߍ߯ ߢߌߣߌ߲߫ (ߤߊߟߌ߬ ߞߎߡߊߢߐ߲߯ߦߊ߫ ߞߐߜߍ ߟߎ߬)",
        "searchprofile-advanced-tooltip": "ߊ߬ ߢߌߣߌ߲߫ ߛߊ߲߬ߠߌ߲߬ߢߐ߲߮ ߠߎ߬ ߕߐ߮ ߞߣߍ ߘߐ߫",
-       "search-result-size": "$1 ({{PLURAL:$2|1 ߞߎߡߊߘߋ߲ |$2 ߞߎߡߊߘߋ߲ ߠߎ߬ }})",
-       "search-redirect": "ߌ ߟߊߞߎ߲߬ߛߌ߲߬ߣߍ߲߫ ߞߊ߬ ߓߐ߫ ߦߊ߲߬ $1",
+       "search-result-size": "$1 ({{PLURAL:$2|1 ߞߎߡߊߘߋ߲|$2 ߞߎߡߊߘߋ߲ ߠߎ߬}})",
+       "search-redirect": "(ߌ ߟߊߞߎ߲߬ߛߌ߲߬ߣߍ߲߫ ߞߊ߬ ߓߐ߫ ߦߊ߲߬ $1)",
        "search-section": "(ߕߍߕߍ߮ $1)",
        "search-suggest": "ߌ ߞߊ߲߫ ߦߋ߫ ߣߌ߲߬ ߠߋ߬ ߡߊ߬ $1",
        "searchall": "ߊ߬ ߓߍ߯",
-       "search-nonefound": "ߖߋ߬ߓߟߌ߬ ߛߌ߫ ߕߍ߫ ߢߌ߬ߣߌ߲߬ߞߊ߬ߟߌ ߣߌ߲߫ ߞߊ߲߬",
+       "search-nonefound": "ߖߋ߬ߓߟߌ߬ ߛߌ߫ ߕߍ߫ ߢߌ߬ߣߌ߲߬ߞߊ߬ߟߌ ߣߌ߲߫ ߞߊ߲߬.",
        "mypreferences": "ߟߊ߬ߝߌ߬ߛߦߊ߬ߟߌ",
+       "group-sysop": "ߡߙߊ߬ߟߌ߬ߟߊ",
        "right-writeapi": "ߛߓߍߟߌ API ߟߊߓߊ߯ߙߊ߫",
        "newuserlogpage": "ߖߊ߬ߕߋ߬ߘߊ߬ ߓߘߊ߫ ߟߊߞߊ߬ ߌ ߜߊ߲߬ߞߎ߲߬",
        "action-edit": "ߞߐߜߍ ߣߌ߲߬ ߡߊߦߟߍ߬ߡߊ߲߬",
        "enhancedrc-history": "ߕߊ߬ߡߌ߲߬ߣߍ߲",
        "recentchanges": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߫ ߞߎߘߊ",
        "recentchanges-legend": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߎߘߊ ߟߎ߫ ߟߊ߬ߓߍ߲߬ߢߐ߰ߡߦߊ߬ߘߊ",
+       "recentchanges-summary": "ߥߞߌ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߎ߲ߓߊ ߡߍ߲ ߠߎ߬ ߞߍߣߍ߲߫ ߞߐߜߍ ߣߌ߲߬ ߞߊ߲߬߸ ߏ߬ ߟߎ߫ ߣߐ߬ߣߐ߬.",
+       "recentchanges-noresult": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߬ ߛߌ߫ ߓߍ߲߬ߢߐ߲߰ߦߊ߬ߣߍ߲߬ ߕߍ߫ ߛߎߡߊ߲ߡߕߊ ߢߌ߲߬ ߠߎ߫ ߡߊ߬ ߕߎ߬ߡߊ߬ ߟߊߕߍ߰ߣߍ߲ ߦߌ߬ߘߊ ߘߐ߫.",
        "recentchanges-label-newpage": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߣߌ߲߬ ߓߘߊ߫ ߘߐߜߍ߫ ߞߎߘߊ ߟߊߘߊ߲߫",
        "recentchanges-label-minor": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߘߋ߬ߣߍ߲ ߠߋ߫ ߦߋ߫",
        "recentchanges-label-bot": "ߡߐ߰ߡߐ߮ ߟߋ߫ ߣߐ߬ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ ߣߌ߲߬ ߞߍ߫ ߟߊ߫",
        "recentchangeslinked-toolbox": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߫ ߓߌ߬ߟߊ߬ߢߐ߲߰ߡߊ",
        "recentchangeslinked-title": "ߊ߬ ߟߌ߬ߤߟߊ ߡߊߦߟߍ߬ߡߊ߲߫ ߦߊ߲߬$1",
        "recentchangeslinked-summary": "ߞߐߜߍ ߕߐ߮ ߟߊߘߏ߲߬߸ ߦߟߍ߬ߡߊ߲ ߡߍ߲ ߠߎ߬ ߘߏ߲߬ߣߍ߲߬ ߦߋ߫ ߞߐߜߍ ߟߎ߬ ߘߐ߫߸ ߥߟߊ߫ ߞߐߜߍ ߣߌ߲߬ ߘߐ߫߸ ߞߵߏ߬ ߦߋ߫. (ߖߐ߲߬ߛߊ߬ ߌ ߘߌ߫ ߦߌߟߡߊ ߛߌ߲߬ߝߏ߲ ߠߎ߬ ߦߋ߫߸  {{ns:category}}: ߦߌߟߡߊ ߕߐ߮ ߟߊߘߏ߲߬).ߞߵߊ߬ ߦߟߍ߬ߡߊ߲߬ ߞߐߜߍ ߣߌ߲߬ [[Special:Watchlist|your Watchlist]] ߘߌ߫߸ ߏ߬ ߦߋ߫ <strong>ߛߓߍߘߋ߲߫ ߞߎ߲ߓߊ</strong>",
-       "recentchangeslinked-page": "ߘߐߜߍ ߕߐ߮",
+       "recentchangeslinked-page": "ߘߐߜߍ ߕߐ߮:",
        "upload": "ߞߐߕߐ߮ ߟߊߦߟߍ߬",
        "filedesc": "ߟߊߘߛߏߣߍ߲",
+       "license-header": "ߟߊ߬ߘߌ߬ߢߍ߬ߟߌ ߦߴߌ ߘߐ߫",
        "imgfile": "ߞߐߕߐ߮",
        "listfiles": "ߞߐߕߐ߮ ߛߙߍߘߍ",
        "file-anchor-link": "ߞߐߕߐ߮",
        "filehist": "ߞߐߕߐ߮ ߟߊ߫ ߘߐ߬ߝߐ",
-       "filehist-help": "ߕߎ߬ߡߊ߬ߘߊ/ߕߎ߬ߡߊ ߛߐ߲߬ߞߌ߲߬ ߓߊ߫߸ ߞߊ߬ ߕߎ߬ߡߊ߬ߘߊ ߞߐߕߐ߮ ߟߎ߬ ߦߋ߫",
+       "filehist-help": "ߕߎ߬ߡߊ߬ߘߊ/ߕߎ߬ߡߊ ߛߐ߲߬ߞߌ߲߬ ߓߊ߫߸ ߞߊ߬ ߕߎ߬ߡߊ߬ߘߊ ߞߐߕߐ߮ ߟߎ߬ ߦߋ߫.",
        "filehist-current": "ߞߍߛߊ߲ߞߏ",
        "filehist-datetime": "ߕߎ߬ߡߊ߬ߘߊ/ߕߎ߬ߡߊ߬ߟߊ߲",
        "filehist-thumb": "ߞߝߊ߬ߟߋ߲ߛߋ߲",
        "filehist-dimensions": "ߛߎߡߊ߲ߘߐ",
        "filehist-comment": "ߞߊ߲߬ߝߐߟߌ",
        "imagelinks": "ߞߐߕߐ߮ ߟߊߓߊ߯ߙߊ",
-       "linkstoimage": "ߞߐߕߐ߮ ߣߌ߲߬ {{plural:$1|ߞߐߜߍ ߟߎ߬|$1 ߞߐߜߍ ߟߎ߬}}",
+       "linkstoimage": "ߞߐߕߐ߮ ߣߌ߲߬ {{PLURAL:$1|ߞߐߜߍ ߟߎ߬|$1 ߞߐߜߍ ߟߎ߬}}:",
        "nolinkstoimage": " ߞߐߜߍ߫ ߛߌ߫ ߡߊ߫ ߞߐߕߐ߮ ߣߌ߲߬ ߠߊߓߊ߯ߙߊ߫ ߡߎߣߎ߲߬",
        "sharedupload-desc-here": "ߘߐ߬ߛߙߋ ߣߌ߲߬ ߦߋ߫ ߦߊ߲߬ ߠߋ߫ $1 ߖߊ߬ߕߋ߬ߘߐ߬ߛߌ߮ ߕߐ߭ ߟߎ߬ ߞߏ߬ߣߌ߲ ߘߌ߫ ߛߴߊ߬ ߟߊߓߊ߯ߙߊ߫ ߟߊ߫. ߊ߬ ߕߐ߯ ߛߓߍߟߌ ߦߙߐ $2 ߟߋ߬ ߦߋ߫ ߘߎ߰ߟߊ ߘߐ߫ ߣߌ߲߬.",
        "filepage-nofile": "ߕߐ߮ ߣߌ߲߬ ߞߐߕߐ߯ ߛߎ߯ ߕߍ߫ ߦߋ߲߬",
-       "upload-disallowed-here": "ߌ ߕߍߣߊ߬ ߞߐߜߍ ߣߌ߲߬ ߞߊ߲߬ߛߓߍ߫ ߟߊ߫",
+       "upload-disallowed-here": "ߌ ߕߍߣߊ߬ ߞߐߜߍ ߣߌ߲߬ ߞߊ߲߬ߛߓߍ߫ ߟߊ߫.",
        "randompage": "ߓߍ߲߬ߛߋ߲߬ߡߊ߬ ߞߐߜߍ",
        "statistics": "ߖߊ߬ߕߋ߬ߛߎ߬ߓߐ ߟߎ߬",
        "nbytes": "$1 {{PLURAL:$1|byte|bytes}}",
+       "nmembers": "$1 {{PLURAL:$1|ߛߌ߲߬ߝߏ߲ |members}}",
        "prefixindex": "ߞߐߜߍ߫ ߡߍ߲ ߠߎ߬ ߓߍ߯ ߟߊߝߟߐߣߍ߲߫...",
        "listusers": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߛߙߍߘߍ",
        "newpages": "ߘߐߜߍ߫ ߞߎߘߊ",
        "booksources-search": "ߢߌߣߌ߲ߠߌ߲",
        "specialloguserlabel": "ߞߍߓߊ߮ :",
        "log": "ߘߏ߲߬",
+       "logempty": "ߦߙߍߞߍߟߌ߫ ߛߌ߫ ߓߍ߲߬ߢߐ߲߰ߦߊ߬ߣߍ߲߬ ߕߍ߫ ߝߐ߰ߓߍ ߟߎ߬ ߘߐ߫",
        "allpages": "ߞߐߜߍ ߟߎ߬ ߓߍ߯",
        "allarticles": "ߞߐߜߍ ߟߎ߬ ߓߍ߯",
        "allpagessubmit": "ߥߊ߫",
        "namespace": "ߕߐ߯ ߛߓߍ ߞߣߍ",
        "tooltip-invert": "ߞߏ߲߬ߘߏ ߣߌ߲߬ ߘߐߜߍ߫߸ ߞߊ߬ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߠߎ߬ ߢߡߊߘߏ߲߰ ߞߐߜߍ ߟߎ߬ ߕߐ߯ ߞߣߍ߫ ߓߊߓߌ߬ߟߊ߬ߣߍ߲ ߘߐ߫ (ߊ߬ ߣߌ߫ ߕߐ߯ ߞߣߍ߫ ߓߟߏߘߏ߲߬ߣߍ߲ ߘߐߜߍߣߍ߲ ߠߎ߬)",
        "namespace_association": "ߕߐ߯ ߓߟߏߘߏ߲߬ߣߍ߲߫ ߢߐ߲߰ߓߟߏ",
-       "blanknamespace": "ߓߊߖߎߟߞߊ",
-       "contributions": "{{ߟߊߓߊ߯ߙߟߊ:$1|ߞߊ߬ߘߌ߬ߛߊ߬}} ߓߟߏߡߊߜߍ߲",
+       "blanknamespace": "ߓߊߖߎ",
+       "contributions": "{{GENDER:$1|ߞߊ߬ߘߌ߬ߛߊ߬}} ߓߟߏߡߊߜߍ߲",
        "contributions-title": "ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߟߊ߫ ߓߟߏߓߌߟߊߢߐ߲߯ߞߊ߲ ߡߍ߲ ߦߋ߫$1",
        "mycontris": "ߓߟߏߓߌߟߊߢߐ߲߯ߞߊ߲",
        "anoncontribs": "ߓߟߏߓߌߟߊߢߐ߲߯ߞߊ߲ ߠߎ߬",
-       "contribsub2": " {{ߞߊ߬ߘߌ߬ߛߊ߬:$3|$1}} ߕߊ ($2)",
+       "contribsub2": "{{GENDER:$3|$1}} ߕߊ ($2)",
        "month": "ߞߵߊ߬ ߕߊ߬ ߞߊߙߏ ߡߊ߬ (ߊ߬ ߣߌ߫ ߞߊߙߏ ߞߎ߲߬ߝߟߐ ߘߐ߫)",
        "year": "ߞߵߊ߬ ߕߊ߬ ߞߊߙߏ ߡߊ߬ (ߊ߬ ߣߌ߫ ߞߊߙߏ ߞߎ߲߬ߝߟߐ ߡߊ߬)",
        "sp-contributions-newbies": "ߖߊ߬ߕߋ߬ߘߊ߬ ߞߎߘߊ ߟߎ߫ ߘߐߙߐ߲߫ ߠߊ߫ ߓߟߏߓߌߟߊߢߐ߲߯ߞߊ߲ ߦߌ߬ߘߊ߫ ߕߋ߲߬",
        "sp-contributions-uploads": "ߟߊ߬ߦߟߍ߬ߟߌ ߟߎ߬",
        "sp-contributions-talk": "ߞߎߡߊߢߐ߲߯ߦߊ",
        "sp-contributions-search": "ߓߟߏߓߌߟߊߢߐ߲߯ߞߊ߲ ߘߏ߫ ߢߌߣߌ߲߫",
-       "sp-contributions-username": "IP ߛߊ߲߬ߓߊ߬ߕߐ߮:ߥߟߊ߫ ߟߊ߬ߓߊ߰ߙߊ߬ߕߐ߮",
+       "sp-contributions-username": "IP ߛߊ߲߬ߓߊ߬ߕߐ߮ ߥߟߊ߫ ߟߊ߬ߓߊ߰ߙߊ߬ߕߐ߮:",
        "sp-contributions-newonly": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߡߍ߲ ߣߊ߬ ߞߐߜߍ߫ ߟߊߘߊ߲ ߘߌ߫߸ ߏ߬ ߟߎ߫ ߘߐߙߐ߲߫ ߦߌ߬ߘߊ߬",
        "sp-contributions-submit": "ߢߌߣߌ߲ߠߌ߲",
        "whatlinkshere": "ߛߘߌ߬ߜߋ߲ ߢߎ߬ߡߊ߲߬ ߦߋ߫ ߦߊ߲߬",
        "whatlinkshere-title": "ߞߐߜߍ ߡߍ߲ ߠߎ߫ ߛߘߌ߬ߣߍ߲߫ ߝߊ߲߭ ߣߌ߲߬ $1 ߡߊ߬",
        "whatlinkshere-page": "ߘߐߜߍ:",
        "linkshere": "ߞߐߜߍ ߟߎ߬ ߛߘߌ߬ߜߋ߲ ߡߍ߲ ߠߎ߬ ߦߋ߫ ߦߊ߲߬ <strong>$2</strong>:",
-       "nolinkshere": "ߞߐߜߍ߫ ߛߌ߫ ߟߎ߫ ߛߘߌ߬ߜߋ߲߬ ߕߍ߫ ߦߋ߲߬ <strong>$2</strong>",
+       "nolinkshere": "ߞߐߜߍ߫ ߛߌ߫ ߟߎ߫ ߛߘߌ߬ߜߋ߲߬ ߕߍ߫ ߦߋ߲߬ <strong>$2</strong>.",
        "isredirect": "ߞߎ߲߬ߕߋ߬ߟߋ߲߬ ߞߎߘߊ ߞߐߜߍ",
        "isimage": "ߞߐߕߐ߮ ߛߘߌ߬ߜߋ߲",
        "whatlinkshere-prev": "{{PLURAL:$1|ߢߝߍߕߊ ߟߎ߬|ߢߝߍߕߊ ߟߎ߬ $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|ߢߍߕߊ|ߢߍߕߊ $1}}",
-       "whatlinkshere-links": "ߛߘߌ߬ߜߋ߲",
+       "whatlinkshere-links": "→ ߛߘߌ߬ߜߋ߲",
        "whatlinkshere-hideredirs": "ߟߊ߬ߞߎ߲߬ߛߌ߲߬ߠߌ߲ ߠߎ߬ $1",
+       "whatlinkshere-hidetrans": "ߟߊ߬ߘߏ߲߬ߘߐ߬ߟߌ ߓߊ߲ߓߊ߲ߣߍ߲",
        "whatlinkshere-hidelinks": "ߛߘߌ߬ߜߋ߲$1",
        "whatlinkshere-hideimages": "ߞߐߕߐ߮ ߛߘߌ߬ߜߋ߲$1",
        "whatlinkshere-filters": "ߢߡߊߘߏ߲߰ߣߍ߲",
        "export": "ߞߐߜߍ ߟߎ߬ ߟߊߝߏ߬ߦߌ߬",
        "thumbnail-more": "ߊ߬ ߟߊߞߎ߲߬ߓߦߊ߬",
        "tooltip-pt-userpage": "{{GENDER:|ߌ ߟߊ߫ ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ߬}} ߞߐߜߍ",
-       "tooltip-pt-mytalk": "{{ߖߊ߲߬ߕߌ߮:|ߟߊ߫}} ߞߎߡߊ߫ ߞߐߜߍ",
-       "tooltip-pt-preferences": "{{ߞߊ߬ߘߌ߬ߛߊ߬:|ߌ}} ߤߣߍߕߊ ߟߎ߬",
+       "tooltip-pt-mytalk": "{{GENDER:|ߟߊ߫}} ߞߎߡߊ߫ ߞߐߜߍ",
+       "tooltip-pt-preferences": "{{GENDER:|ߌ}} ߤߣߍߕߊ ߟߎ߬",
        "tooltip-pt-watchlist": "ߌ ߟߊ߫ ߞߐߜߍ߫ ߡߊߦߟߍ߬ߡߊ߲߬ߕߊ ߜߋ߬ߟߎ߲߬ߣߍ߲ ߠߎ߬ ߛߙߍߘߍ",
-       "tooltip-pt-mycontris": "{{ߖߊ߲߬ߕߌ߮:| ߟߊ߫}} ߓߟߏߡߊߜߍ߲ ߠߎ߬",
+       "tooltip-pt-mycontris": "{{GENDER:|ߟߊ߫}} ߓߟߏߡߊߜߍ߲ ߠߎ߬",
        "tooltip-pt-login": "ߌ ߡߊߘߌߦߊߣߍ߲߫ ߦߴߌ ߜߊ߲߬ߞߎ߲߫ ߸ ߘߌߦߊߜߏߦߊ߫ ߞߏ߬ߣߌ߲߬ ߕߍ߫",
        "tooltip-pt-logout": "ߌ ߜߊ߲߬ߞߎ߲߬ ߓߐ߫",
        "tooltip-pt-createaccount": "ߌ ߡߊߘߌߦߊߣߍ߲߫ ߦߋ߫ ߖߊ߬ߕߋ߬ߘߊ߫ ߟߊߞߊ߬ ߞߵߌ ߜߊ߲߬ߞߎ߲߫ ߸ ߓߊ߬ߙߌ߬ ߌ ߘߌߦߊߜߏߦߊߣߍ߲߫ ߕߍ߫",
        "tooltip-ca-talk": "ߘߐ߬ߞߕߌ߬ߟߌ ߞߊ߬ ߓߍ߲߬ ߞߐߜߍ ߞߣߐߘߐ ߡߊ߬",
-       "tooltip-ca-edit": "ß\9eß\90ß\9cß\8d ß£ß\8c߲߬ ß¡ß\8aß\9dß\8a߬ß\9fß\8b߲߬",
+       "tooltip-ca-edit": "ß\9eß\90ß\9cß\8d ß£ß\8c߲߬ ß¡ß\8aߦß\9fß\8d߬ߡß\8a߲߬",
        "tooltip-ca-addsection": "ߛߌ߰ߘߊ߬ ߞߎߘߊ߫ ߘߊߡߌ߬ߣߊ߬",
        "tooltip-ca-viewsource": "ߞߐߜߍ ߣߌ߲߬ ߠߊߞߊ߲ߘߊߣߍ߲߫ ߠߋ߬.\nߌ ߘߌ߫ ߛߴߊ߬ ߛߎ߲ ߘߐߜߍ߫ ߟߊ߫",
        "tooltip-ca-history": "ߞߐߜߍ ߣߌ߲߬ ߛߊߞߍߟߌ߫ ߕߊ߬ߡߌ߲߬ߣߍ߲ ߠߎ߫ ߘߐߜߍ߫",
        "tooltip-ca-delete": "ߞߐߜߍ ߣߌ߲߬ ߖߏ߰ߛߌ߫",
        "tooltip-ca-move": "ߘߐߜߍ ߣߌ߲߬ ߛߋ߲߬ߓߐ߫",
        "tooltip-ca-watch": "ߞߐߜߍ ߣߌ߲߬ ߝߙߊ߬ ߌ ߟߊ߫ ߟߊߞߙߐ߬ߛߌ߬ߕߊ߬ ߛߙߍߘߍ ߟߎ߫ ߞߊ߲߬",
-       "tooltip-search": " {{ߞߍߦߙߐ ߕߐ߮}} ߊ߬ ߢߌߣߌ߲߫",
+       "tooltip-search": "ߊ߬ ߢߌߣߌ߲߫ {{SITENAME}} ߘߐ߫",
        "tooltip-search-go": "ߕߐ߮ ߣߌ߲߬ ߢߌߣߌ߲߫ ߞߐߜߍ߫ ߞߣߐ߫ ߣߴߊ߬ ߞߍ߫ ߘߊ߫ ߦߋ߲߬",
        "tooltip-search-fulltext": "ߞߎߡߊߘߋ߲߫  ߣߌ߲߬ ߞߐߜߍ߫ ߟߎ߫ ߢߌߣߌ߲߫",
        "tooltip-p-logo": "ߞߐߜߍ߫ ߓߏߟߏ߲ߘߊ ߡߊߝߍߣߍ߲߫",
        "tooltip-t-whatlinkshere": "ߥߞߌ߫ ߞߐߜߍ ߓߍ߯ ߛߘߌ߬ߜߋ߲ ߠߋ߬ ߦߋ߫ ߦߊ߲߬",
        "tooltip-t-recentchangeslinked": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߬ ߞߎߘߊ ߟߎ߬ ߞߐߜߍ߫ ߘߐ߫ ߡߍ߲ ߣߌ߫ ߞߐߜߍ ߣߌ߲߬ ߕߎ߲߰ߣߍ߲߫",
        "tooltip-feed-atom": "ߞߐߜߍ ߣߌ߲߬ ߝߕߌ߫ ߓߊߟߏ",
-       "tooltip-t-contributions": "{{ߞߊ߬ߘߌ߬ߛߊ߬:$1|ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ}} ߟߊ߫ ߓߟߏߓߌߟߊߢߐ߲߮ߞߊ߲ ߛߙߍߘߍ",
+       "tooltip-t-contributions": "{{GENDER:$1|ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ}} ߟߊ߫ ߓߟߏߓߌߟߊߢߐ߲߮ߞߊ߲ ߛߙߍߘߍ",
+       "tooltip-t-emailuser": " ߢߎߡߍߙߋ߲ߞߏ߲ߘߏ ߟߊߕߊ߯ ߟߊߓߊ߯ߙߟߊ ߣߌ߲߬ ߡߊ߬{{GENDER:$1|ߟߊߓߊ߯ߙߟߊ(ߡߏ߬ߛߏ) }}",
        "tooltip-t-upload": "ߞߐߕߐ߮ ߟߎ߫ ߟߊߦߟߍ߬",
        "tooltip-t-specialpages": "ߘߎ߲߬ߘߎ߬ߡߊ߬ ߞߐߜߍ߫ ߟߎ߫ ߛߙߍߘߍ",
        "tooltip-t-print": " ߞߐߜߍ ߣߌ߲߬  ߜߌ߬ߙߌ߲߬ߘߌ߬ߕߊ߬ߡߊ ߛߎ߮",
        "tooltip-t-permalink": "ߞߐߜߍ ߣߌ߲߬ ߡߛߊ߬ߦߌ߲߬ߠߌ߲߬ ߛߘߌ߬ߜߋ߲߬ ߓߟߏߕߍ߰ߓߊߟߌ",
        "tooltip-ca-nstab-main": "ߞߐߜߍ ߞߣߐߘߐ ߘߐߜߍ߫",
        "tooltip-ca-nstab-user": "ߞߐߜߍ߫ ߟߊߓߊ߯ߙߕߊ ߘߐߜߍ߫",
-       "tooltip-ca-nstab-special": "ߣߌ߲߬ ߦߋ߫ ߘߐߜߍ߫ ߓߟߏߡߊߞߊ߬ߣߍ߲ ߠߋ߬ ߘߌ߫߸ ߊ߬ ߕߍ߫ ߛߋ߫ ߡߊߦߟߍ߬ߡߊ߲߬ ߠߊ߫.",
+       "tooltip-ca-nstab-special": "ߣߌ߲߬ ߦߋ߫ ߘߐߜߍ߫ ߓߟߏߡߊߞߊ߬ߣߍ߲ ߠߋ߬ ߘߌ߫߸ ߊ߬ ߕߍ߫ ߛߋ߫ ߡߊߦߟߍ߬ߡߊ߲߬ ߠߊ߫",
        "tooltip-ca-nstab-project": "ߖߊ߬ߕߋ߬ߘߐ߬ߛߌ߰ ߞߐߜߍ ߘߐߜߍ߫",
        "tooltip-ca-nstab-image": "ߞߐߕߐ߮ ߞߐߜߍ ߟߎ߫ ߘߐߜߍ߫",
        "tooltip-ca-nstab-mediawiki": "ߞߊ߲ߞߋ ߗߋߛߓߍ ߘߐߜߍ߫",
        "file-nohires": "ߢߊߓߐߣߍ߲ ߛߊ߲ߘߐߕߊ߫ ߜߘߍ߫ ߕߍ߫ ߦߋ߲߬",
        "show-big-image": "ߞߐߕߐ߮ ߓߊߛߎ߲",
        "show-big-image-preview": "ߊ߬ ߢߍߦߋߟߌ ߢߊ߲ߞߊ߲$1",
-       "show-big-image-other": "{{PLURAL:$2|ߢߊߓߐߟߌ|ߢߊߓߐߟߌ ߟߎ߬}} ߕߐ߬ߡߊ $1",
+       "show-big-image-other": "{{PLURAL:$2|ߢߊߓߐߟߌ|ߢߊߓߐߟߌ ߟߎ߬}} ߕߐ߬ߡߊ $1.",
        "show-big-image-size": "$1 × $2 ߖߌ߬ߦߊ߬ߘߊ߲ߕߊ",
        "metadata": "ߡߋߕߊߘߊ߯ߕߊ߫",
        "metadata-fields": "Image metadata fields listed in this message will be included on image page display when the metadata table is collapsed.\nOthers will be hidden by default.\n* make\n* model\n* datetimeoriginal\n* exposuretime\n* fnumber\n* isospeedratings\n* focallength\n* artist\n* copyright\n* imagedescription\n* gpslatitude\n* gpslongitude\n* gpsaltitude",
-       "namespacesall": "ߓߍ߯",
+       "namespacesall": "ß\8a߬ ß\93ß\8d߯",
        "monthsall": "ߡߎ߰ߡߍ",
        "imgmultipagenext": "ߞߐߜߍ ߢߍߕߊ",
        "imgmultigo": "ߥߊ߫",
        "imgmultigoto": "ߥߊ߫ ߞߐߜߍ ߣߌ߲߬ ߞߊ߲߬$1",
+       "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|talk]])",
        "redirect-submit": "ߕߊ߯",
        "redirect-lookup": "ߊ߬ ߘߐߜߍ߫",
        "redirect-value": "ߡߐ߬ߟߐ߲",
        "redirect-revision": "ߞߐߜߍ ߣߐ߬ߡߊ߬ߛߊ߬ߦߌ߬ ߝߙߍߕߍ",
        "redirect-file": "ߞߐߕߐ߯ ߕߐ߮",
        "specialpages": "ߘߎ߲߬ߘߎ߬ߡߊ߬ ߘߐߜߍ",
-       "tag-filter": "[[Special:Tags|Tag]] ߢߡߊߘߏ߲߰ߣߍ߲",
-       "tag-list-wrapper": "[[ߛߐ߲߬ߞߌ߲߬ߠߌ߲߬: ߓߟߏߡߊߞߊ߬ߣߍ߲|{{PLURAL:$1|ߛߐ߲߬ߞߌ߲߬ߠߌ߲|ߛߐ߲߬ߞߌ߲߬ߠߌ߲ ߠߎ߬}}]]: $2",
+       "tag-filter": "[[Special:Tags|Tag]] ߢߡߊߘߏ߲߰ߣߍ߲:",
+       "tag-list-wrapper": "[[Special:Tags|{{PLURAL:$1|Tag|Tags}}]]: $2",
        "tags-active-yes": "ߐ߲߬ߐ߲߬ߐ߲߫",
        "tags-active-no": "ߍ߲߬ߍ߲ߍ߲߬",
-       "tags-hitcount": "$1{{PLURAL:$1|ߦߟߍ߬ߡߊ߲߬ߠߌ|ߦߟߍ߬ߡߊ߲߬ߠߌ ߠߎ߬ }}",
-       "logentry-delete-delete": "$1 {{ߞߊ߬ߘߌ߬ߛߊ߫:$2|ߖߏ߰ߛߌ߬ߣߍ߲}} ߞߐߜߍ$3",
+       "tags-hitcount": "$1 {{PLURAL:$1|ߦߟߍ߬ߡߊ߲߬ߠߌ|ߦߟߍ߬ߡߊ߲߬ߠߌ ߠߎ߬}}",
+       "logentry-delete-delete": "$1 {{GENDER:$2|ߖߏ߰ߛߌ߬ߣߍ߲}} ߞߐߜߍ$3",
        "revdelete-content-hid": "ߞߣߐߘߐ ߘߐ߲߰ߣߍ߲߫ ߠߋ߬",
-       "logentry-move-move": "$1 {{ߞߊ߬ߘߌ߬ߛߊ߬:$2|ߓߘߊ߫ ߞߐߜߍ}} ߓߐ߫ ߦߊ߲߬ $3 ߞߴߊ߬ ߟߊߕߊ߯ $4",
-       "logentry-move-move-noredirect": "$1 {{ߞߊ߬ߘߌ߬ߛߊ߬:$1|ߓߘߴߊ߬ ߓߐ߫ ߦߋ߲߬}} ߞߐߜߍ ߣߌ߲߬ $3 ߞߊ߬ ߥߴߊ߬ ߘߌ߫ $4 ߞߵߊ߬ ߕߘߍ߬ ߊ߬ ߡߴߊ߬ ߟߊߞߎ߲߬ߛߌ߲߫",
-       "logentry-newusers-create": "ߖߊ߬ߕߋ߬ߘߊ߬ ߟߊߓߊ߯ߙߕߊ $1 ߕߘߍ߬ ߦߋ߫ {{ߞߊ߬ߘߌ߬ߛߊ߬:$2|ߕߊ ߟߋ߬ ߘߌ߫}}",
+       "logentry-move-move": "$1 {{GENDER:$2|ߓߘߊ߫ ߞߐߜߍ}} ߓߐ߫ ߦߊ߲߬ $3 ߞߴߊ߬ ߟߊߕߊ߯ $4",
+       "logentry-move-move-noredirect": "$1 {{GENDER:$1|ߓߘߴߊ߬ ߓߐ߫ ߦߋ߲߬}} ߞߐߜߍ ߣߌ߲߬ $3 ߞߊ߬ ߥߴߊ߬ ߘߌ߫ $4 ߞߵߊ߬ ߕߘߍ߬ ߊ߬ ߡߴߊ߬ ߟߊߞߎ߲߬ߛߌ߲߫",
+       "logentry-newusers-create": "ߖߊ߬ߕߋ߬ߘߊ߬ ߟߊߓߊ߯ߙߕߊ $1 ߕߘߍ߬ ߦߋ߫ {{GENDER:$2|ߕߊ ߟߋ߬ ߘߌ߫}}",
        "logentry-newusers-autocreate": "ߟߊߓߊ߯ߙߊߟߊ ߟߊ߫ ߖߊ߬ߕߋ߬ߘߊ $1{{GENDER:$2|ߟߊߘߊ߲߫ ߘߊ߫ }} ߞߍߒߖߘߍߦߋ߫ ߓߟߏߡߊ߬",
-       "logentry-upload-upload": "$1 {{ߞߊ߬ߘߌ߬ߛߊ߫:$2|ߟߊ߬ߦߟߍ߬ߟߌ߬ߣߐ ߟߋ߬}} $3",
-       "searchsuggest-search": " {{SITENAME}} ߊ߬ ߢߌߣߌ߲߫",
+       "logentry-upload-upload": "$1 {{GENDER:$2|ߟߊ߬ߦߟߍ߬ߟߌ߬ߣߐ ߟߋ߬}} $3",
+       "searchsuggest-search": "{{SITENAME}} ߊ߬ ߢߌߣߌ߲߫",
        "duration-days": "$1 {{PLURAL:$1|ߟߏ߲|ߟߏ߲ ߠߎ߬}}"
 }
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 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 995667c..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-opt-sitewide": "На новоу сајта",
+       "blocklist-type": "Тип:",
+       "blocklist-type-opt-all": "Све",
+       "blocklist-type-opt-sitewide": "На нивоу сајта",
        "blocklist-type-opt-partial": "Делимично",
        "blocklist-rangeblocks": "Сакриј блокаде опсега",
        "blocklist-timestamp": "Временска ознака",
        "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 7857def..8f8dfd9 100644 (file)
@@ -21,7 +21,8 @@
                        "Muddyb",
                        "Fitoschido",
                        "Rance",
-                       "Vlad5250"
+                       "Vlad5250",
+                       "Yasen igra"
                ]
        },
        "tog-underline": "Wekea mstari viungo:",
@@ -46,7 +47,7 @@
        "tog-enotifminoredits": "Pia nitumie barua pale mabadiliko ya ukurasa yanapokuwa madogo tu.",
        "tog-enotifrevealaddr": "Onyesha anwani ya barua pepe yangu katika barua pepe za taarifa",
        "tog-shownumberswatching": "Onyesha idadi ya watumiaji waangalizi",
-       "tog-oldsig": "Sahihi iliyopo:",
+       "tog-oldsig": "Sahihi iliyopo yenu:",
        "tog-fancysig": "Weka sahihi tu (bila kujiweka kiungo yenyewe)",
        "tog-uselivepreview": "Tumia kihakikio cha papohapo",
        "tog-forceeditsummary": "Nishtue pale ninapoingiza muhtasari mtupu wa kuhariri",
        "nstab-template": "Kigezo",
        "nstab-help": "Msaada",
        "nstab-category": "Jamii",
+       "mainpage-nstab": "Mwanzo",
        "nosuchaction": "Kitendo hiki hakipo",
        "nosuchactiontext": "Haiwezikani kutenda kitendo kilichoandikwa kwenye KISARA.\nLabda ulikosea kuandika KISARA, au kiungo ulichofuata ina kasoro.\nAu labda kuna hitilafu kwenye programu inayotumika na {{SITENAME}}.",
        "nosuchspecialpage": "Ukurasa maalum huu hakuna",
        "minoredit": "Haya ni mabadiliko madogo",
        "watchthis": "Fuatilia ukurasa huu",
        "savearticle": "Hifadhi ukurasa",
+       "savechanges": "Hifadhi mabadiliko",
        "preview": "Hakiki",
        "showpreview": "Onyesha hakikisho la mabadiliko",
        "showdiff": "Onyesha mabadiliko",
        "recentchanges-label-plusminus": "Ukubwa ukurasa kubadilishwa na hii idadi ya baiti",
        "recentchanges-legend-heading": "<strong>Simulizi:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (pia tazama [[Special:NewPages|orodha ya kurasa mpya]])",
+       "rcfilters-filter-editsbyself-description": "Michango yenu.",
        "rcnotefrom": "Hapo chini {{PLURAL:$5|is the change|yaonekana mabadiliko}} tangu <strong>$3,$4</strong> (hadi <strong>$1</strong>tunaonyesha).",
        "rclistfrom": "Onyesha mabadiliko mapya kuanzia $3 $2",
        "rcshowhideminor": "$1 mabadiliko madogo",
        "contributions": "Michango ya {{GENDER:$1|mtumiaji}}",
        "contributions-title": "Michango ya mtumiaji $1",
        "mycontris": "Michango",
+       "anoncontribs": "Michango",
        "contribsub2": "Kwa {{GENDER:$3|$1}} ($2)",
        "nocontribs": "Mabadiliko yanayolingana na vigezo vilivyoulizwa hayakupatikana.",
        "uctop": "ya kisasa",
        "whatlinkshere-hidelinks": "$1 viungo",
        "whatlinkshere-hideimages": "Viungo vya faili $1",
        "whatlinkshere-filters": "Machujio",
+       "whatlinkshere-submit": "Nenda",
        "block": "Kumzuia mtumiaji",
        "unblock": "Kuacha kumzuia mtumiaji",
        "blockip": "Zuia mtumiaji",
        "imgmultipagenext": "ukurasa ujao →",
        "imgmultigo": "Nenda!",
        "imgmultigoto": "Uende kwenye ukurasa wa $1",
+       "img-lang-go": "Enda",
        "ascending_abbrev": "pand",
        "descending_abbrev": "shuk",
        "table_pager_next": "Ukurasa ujao",
        "version-software-version": "Toleo",
        "version-entrypoints-header-url": "KISARA Kioneshi Sanifu Raslimali",
        "redirect-submit": "Nenda",
+       "redirect-file": "Jina la faili",
        "fileduplicatesearch": "Tafuta mafaili ya nakili",
        "fileduplicatesearch-summary": "Kutafuta mafaili ya nakili kwa kuzingatia thamani za reli.",
        "fileduplicatesearch-filename": "Jina la faili:",
        "tag-filter-submit": "Chuja",
        "tags-title": "Tagi",
        "tags-description-header": "Maelezo kamili ya maana",
+       "tags-active-yes": "Ndiyo",
+       "tags-active-no": "Siyo",
        "tags-edit": "hariri",
        "tags-hitcount": "{{PLURAL:$1|badiliko|mabadiliko}} $1",
        "comparepages": "Linganisha kurasa",
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 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 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 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 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..df897d9 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' );
        }
 
        /**
index ebc3b79..ec61c23 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;
        }
@@ -2408,4 +2419,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 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 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..96e2766 100644 (file)
@@ -625,6 +625,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 +668,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..e02e8a4 100644 (file)
@@ -1332,7 +1332,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 +1390,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 209ed55..5cf93c9 100644 (file)
@@ -2670,7 +2670,7 @@ class AuthManagerTest extends \MediaWikiTestCase {
 
                // Test backoff
                $cache = \ObjectCache::getLocalClusterInstance();
-               $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
+               $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
                $cache->set( $backoffKey, true );
                $session->clear();
                $user = \User::newFromName( $username );
@@ -2709,7 +2709,7 @@ class AuthManagerTest extends \MediaWikiTestCase {
 
                // Test addToDatabase throws an exception
                $cache = \ObjectCache::getLocalClusterInstance();
-               $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
+               $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
                $this->assertFalse( $cache->get( $backoffKey ), 'sanity check' );
                $session->clear();
                $user = $this->getMockBuilder( \User::class )
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 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 91ee276..e90577c 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' );
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..474decf 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 ) {
+               }
        }
 
        /**
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 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..383b372 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 () {
@@ -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();