Merge "Add string length limits"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 21 Nov 2017 15:21:26 +0000 (15:21 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 21 Nov 2017 15:21:26 +0000 (15:21 +0000)
227 files changed:
.mailmap
CREDITS
RELEASE-NOTES-1.30
RELEASE-NOTES-1.31
api.php
autoload.php
composer.json
includes/DefaultSettings.php
includes/EditPage.php
includes/Feed.php
includes/GlobalFunctions.php
includes/Html.php
includes/MediaWikiServices.php
includes/Message.php
includes/Preferences.php
includes/Sanitizer.php [deleted file]
includes/ServiceWiring.php
includes/Setup.php
includes/SiteConfiguration.php
includes/WatchedItem.php [deleted file]
includes/WatchedItemQueryService.php [deleted file]
includes/WatchedItemQueryServiceExtension.php [deleted file]
includes/WatchedItemStore.php [deleted file]
includes/api/ApiBase.php
includes/api/ApiFormatBase.php
includes/api/ApiFormatRaw.php
includes/api/ApiHelp.php
includes/api/ApiLogin.php
includes/api/ApiQuery.php
includes/api/ApiStashEdit.php
includes/api/i18n/cs.json
includes/api/i18n/ko.json
includes/api/i18n/nb.json
includes/auth/LocalPasswordPrimaryAuthenticationProvider.php
includes/changes/ChangesList.php
includes/composer/ComposerVendorHtaccessCreator.php [new file with mode: 0644]
includes/diff/DifferenceEngine.php
includes/exception/MWException.php
includes/exception/MWExceptionRenderer.php
includes/externalstore/ExternalStore.php
includes/externalstore/ExternalStoreFactory.php [new file with mode: 0644]
includes/filerepo/file/File.php
includes/htmlform/OOUIHTMLForm.php
includes/htmlform/fields/HTMLMultiSelectField.php
includes/htmlform/fields/HTMLRadioField.php
includes/installer/DatabaseUpdater.php
includes/installer/PostgresUpdater.php
includes/installer/i18n/es.json
includes/installer/i18n/eu.json
includes/installer/i18n/fr.json
includes/installer/i18n/pl.json
includes/libs/HashRing.php
includes/libs/filebackend/SwiftFileBackend.php
includes/libs/http/HttpAcceptNegotiator.php
includes/libs/objectcache/BagOStuff.php
includes/libs/objectcache/CachedBagOStuff.php
includes/libs/objectcache/MemcachedBagOStuff.php
includes/libs/objectcache/MultiWriteBagOStuff.php
includes/libs/objectcache/WANObjectCache.php
includes/linker/LinkTarget.php
includes/mail/MailAddress.php
includes/media/SVG.php
includes/media/TransformationalImageHandler.php
includes/page/Article.php
includes/page/ImagePage.php
includes/parser/RemexStripTagHandler.php [new file with mode: 0644]
includes/parser/Sanitizer.php [new file with mode: 0644]
includes/profiler/ProfilerFunctions.php [deleted file]
includes/skins/SkinFallbackTemplate.php
includes/specials/SpecialEditTags.php
includes/specials/SpecialMovepage.php
includes/specials/SpecialPreferences.php
includes/specials/SpecialRecentchangeslinked.php
includes/specials/SpecialResetTokens.php
includes/specials/SpecialRevisiondelete.php
includes/specials/SpecialSearch.php
includes/specials/SpecialUserrights.php
includes/specials/forms/PreferencesForm.php
includes/tidy/RemexCompatMunger.php
includes/tidy/RemexMungerData.php
includes/title/TitleValue.php
includes/user/BotPassword.php
includes/user/User.php
includes/user/UserIdentity.php [new file with mode: 0644]
includes/watcheditem/WatchedItem.php [new file with mode: 0644]
includes/watcheditem/WatchedItemQueryService.php [new file with mode: 0644]
includes/watcheditem/WatchedItemQueryServiceExtension.php [new file with mode: 0644]
includes/watcheditem/WatchedItemStore.php [new file with mode: 0644]
includes/watcheditem/WatchedItemStoreInterface.php [new file with mode: 0644]
languages/LanguageConverter.php
languages/classes/LanguageCrh.php [new file with mode: 0644]
languages/data/CrhExceptions.php [new file with mode: 0644]
languages/i18n/ais.json
languages/i18n/ar.json
languages/i18n/azb.json
languages/i18n/be-tarask.json
languages/i18n/bn.json
languages/i18n/ca.json
languages/i18n/cdo.json
languages/i18n/ce.json
languages/i18n/ckb.json
languages/i18n/crh-cyrl.json
languages/i18n/crh-latn.json
languages/i18n/cs.json
languages/i18n/de.json
languages/i18n/diq.json
languages/i18n/egl.json
languages/i18n/el.json
languages/i18n/en.json
languages/i18n/eo.json
languages/i18n/es.json
languages/i18n/et.json
languages/i18n/eu.json
languages/i18n/fa.json
languages/i18n/fi.json
languages/i18n/fr.json
languages/i18n/ga.json
languages/i18n/gl.json
languages/i18n/gom-latn.json
languages/i18n/gu.json
languages/i18n/he.json
languages/i18n/hr.json
languages/i18n/hu.json
languages/i18n/hy.json
languages/i18n/id.json
languages/i18n/it.json
languages/i18n/ja.json
languages/i18n/jv.json
languages/i18n/ko.json
languages/i18n/lad.json
languages/i18n/lb.json
languages/i18n/lki.json
languages/i18n/lv.json
languages/i18n/map-bms.json
languages/i18n/mr.json
languages/i18n/nl.json
languages/i18n/pl.json
languages/i18n/ps.json
languages/i18n/pt-br.json
languages/i18n/qqq.json
languages/i18n/roa-tara.json
languages/i18n/ru.json
languages/i18n/sat.json
languages/i18n/shn.json
languages/i18n/skr-arab.json
languages/i18n/sl.json
languages/i18n/sr-ec.json
languages/i18n/sr-el.json
languages/i18n/su.json
languages/i18n/sv.json
languages/i18n/ta.json
languages/i18n/tl.json
languages/i18n/tyv.json
languages/i18n/ur.json
languages/i18n/zh-hans.json
languages/i18n/zh-hant.json
languages/messages/MessagesCrh.php
languages/messages/MessagesCrh_cyrl.php
languages/messages/MessagesCrh_latn.php
languages/messages/MessagesMwl.php
maintenance/checkComposerLockUpToDate.php
maintenance/update.php
maintenance/userOptions.inc [deleted file]
maintenance/userOptions.php
maintenance/wrapOldPasswords.php
resources/Resources.php
resources/src/mediawiki.action/mediawiki.action.history.styles.css
resources/src/mediawiki.legacy/commonPrint.css
resources/src/mediawiki.legacy/oldshared.css
resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ItemMenuOptionWidget.js
resources/src/mediawiki.skinning/content.externallinks.css
resources/src/mediawiki.skinning/elements.css
resources/src/mediawiki.skinning/interface.css
resources/src/mediawiki.special/mediawiki.special.css
resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js
resources/src/mediawiki.special/mediawiki.special.preferences.editfont.js [new file with mode: 0644]
resources/src/mediawiki.special/mediawiki.special.preferences.styles.css
resources/src/mediawiki.special/mediawiki.special.preferences.tabs.js
resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js
resources/src/mediawiki.special/mediawiki.special.userrights.css
resources/src/mediawiki.special/mediawiki.special.userrights.js
resources/src/mediawiki/htmlform/ooui.styles.less
resources/src/mediawiki/mediawiki.diff.styles.css
resources/src/mediawiki/mediawiki.notification.css
tests/common/TestsAutoLoader.php
tests/parser/parserTests.txt
tests/phpunit/autoload.ide.php
tests/phpunit/includes/GlobalFunctions/wfStringToBoolTest.php [new file with mode: 0644]
tests/phpunit/includes/PreferencesTest.php
tests/phpunit/includes/RevisionContentHandlerDbTest.php [new file with mode: 0644]
tests/phpunit/includes/RevisionDbTestBase.php [new file with mode: 0644]
tests/phpunit/includes/RevisionIntegrationTest.php [deleted file]
tests/phpunit/includes/RevisionNoContentHandlerDbTest.php [new file with mode: 0644]
tests/phpunit/includes/RevisionTest.php [new file with mode: 0644]
tests/phpunit/includes/RevisionUnitTest.php [deleted file]
tests/phpunit/includes/SanitizerTest.php [deleted file]
tests/phpunit/includes/WatchedItemIntegrationTest.php [deleted file]
tests/phpunit/includes/WatchedItemQueryServiceUnitTest.php [deleted file]
tests/phpunit/includes/WatchedItemStoreIntegrationTest.php [deleted file]
tests/phpunit/includes/WatchedItemStoreUnitTest.php [deleted file]
tests/phpunit/includes/WatchedItemUnitTest.php [deleted file]
tests/phpunit/includes/actions/ActionTest.php
tests/phpunit/includes/api/ApiOptionsTest.php
tests/phpunit/includes/auth/AuthPluginPrimaryAuthenticationProviderTest.php
tests/phpunit/includes/auth/LocalPasswordPrimaryAuthenticationProviderTest.php
tests/phpunit/includes/externalstore/ExternalStoreFactoryTest.php [new file with mode: 0644]
tests/phpunit/includes/externalstore/ExternalStoreForTesting.php [new file with mode: 0644]
tests/phpunit/includes/externalstore/ExternalStoreTest.php
tests/phpunit/includes/jobqueue/JobQueueMemoryTest.php
tests/phpunit/includes/media/SVGTest.php
tests/phpunit/includes/objectcache/MemcachedBagOStuffTest.php
tests/phpunit/includes/parser/SanitizerTest.php [new file with mode: 0644]
tests/phpunit/includes/specials/SpecialPageTestBase.php
tests/phpunit/includes/tidy/RemexDriverTest.php
tests/phpunit/includes/title/TitleValueTest.php
tests/phpunit/includes/watcheditem/WatchedItemIntegrationTest.php [new file with mode: 0644]
tests/phpunit/includes/watcheditem/WatchedItemQueryServiceUnitTest.php [new file with mode: 0644]
tests/phpunit/includes/watcheditem/WatchedItemStoreIntegrationTest.php [new file with mode: 0644]
tests/phpunit/includes/watcheditem/WatchedItemStoreUnitTest.php [new file with mode: 0644]
tests/phpunit/includes/watcheditem/WatchedItemUnitTest.php [new file with mode: 0644]
tests/phpunit/languages/LanguageCodeTest.php
tests/phpunit/languages/LanguageConverterTest.php
tests/phpunit/languages/classes/LanguageCrhTest.php [new file with mode: 0644]
tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js
tests/selenium/pageobjects/preferences.page.js

index 5a76fb9..c4f8604 100644 (file)
--- a/.mailmap
+++ b/.mailmap
@@ -413,8 +413,9 @@ Sumit Asthana <asthana.sumit23@gmail.com>
 TerraCodes <terracodes@tools.wmflabs.org>
 Thalia Chan <thalia@cantorion.org>
 Thalia Chan <thalia@cantorion.org> <thalia.e.chan@googlemail.com>
-Thiemo Mättig <thiemo.maettig@wikimedia.de>
-Thiemo Mättig <thiemo.maettig@wikimedia.de> <mr.heat@gmx.de>
+Thiemo Kreuz <thiemo.kreuz@wikimedia.de>
+Thiemo Kreuz <thiemo.kreuz@wikimedia.de> <thiemo.maettig@wikimedia.de>
+Thiemo Kreuz <thiemo.kreuz@wikimedia.de> <mr.heat@gmx.de>
 This, that and the other <at.light@live.com.au>
 tholam <t.lam@lamsinfosystem.com>
 Thomas Bleher <ThomasBleher@gmx.de> <tbleher@users.mediawiki.org>
diff --git a/CREDITS b/CREDITS
index c38c3fc..6ab4ad3 100644 (file)
--- a/CREDITS
+++ b/CREDITS
@@ -625,7 +625,7 @@ The following list can be found parsed under Special:Version/Credits -->
 * The Discoverer
 * The Evil IP address
 * theopolisme
-* Thiemo Mättig
+* Thiemo Kreuz
 * This, that and the other
 * tholam
 * Thomas Arrow
index f79ae83..1449dab 100644 (file)
@@ -80,12 +80,19 @@ section).
 === External library changes in 1.30 ===
 
 ==== Upgraded external libraries ====
-* mediawiki/mediawiki-codesniffer updated to 0.8.1.
-* wikimedia/composer-merge-plugin updated to 1.4.1.
+* Updated justinrainbow/json-schema from v3.0 to v5.2.
+* Updated mediawiki/mediawiki-codesniffer from v0.7.2 to v0.12.0.
+* Updated wikimedia/composer-merge-plugin from v1.4.0 to v1.4.1.
+* Updated wikimedia/relpath from v1.0.3 to v2.0.0.
+* Updated OOjs from v2.0.0 to v2.1.0.
+* Updated OOUI from v0.21.1 to v0.23.0.
+* Updated QUnit from v1.23.1 to v2.4.0.
+* Updated phpunit/phpunit from v4.8.35 to v4.8.36.
 
 ==== New external libraries ====
 * The class \TestingAccessWrapper has been moved to the external library
   wikimedia/testing-access-wrapper and renamed \Wikimedia\TestingAccessWrapper.
+* Purtle, a fast, lightweight RDF generator.
 
 ==== Removed and replaced external libraries ====
 * …
index 9a4c74c..f1fd9d3 100644 (file)
@@ -15,11 +15,13 @@ production.
   possible for fallback images such as png.
 * (T44246) $wgFilterLogTypes will no longer ignore 'patrol' when user does
   not have the right to mark things patrolled.
-* …
 
 === New features in 1.31 ===
 * Wikimedia\Rdbms\IDatabase->select() and similar methods now support
   joins with parentheses for grouping.
+* As a first pass in standardizing dialog boxes across the MediaWiki product,
+  Html class now provides helper methods for messageBox, successBox, errorBox and
+  warningBox generation.
 
 === External library changes in 1.31 ===
 
@@ -38,11 +40,9 @@ production.
   'mediawiki.viewport' module instead.
 * The deprecated 'mediawiki.widgets.CategorySelector' module alias was removed.
   Use the 'mediawiki.widgets.CategoryMultiselectWidget' module directly instead.
-* …
 
 === Bug fixes in 1.31 ===
 * (T90902) Non-breaking space in header ID breaks anchor
-* …
 
 === Action API changes in 1.31 ===
 * …
@@ -55,7 +55,7 @@ MediaWiki supports over 350 languages. Many localisations are updated
 regularly. Below only new and removed languages are listed, as well as
 changes to languages because of Phabricator reports.
 
-* 
+* (T180052) Mirandese (mwl) now supports gendered NS_USER/NS_USER_TALK namespaces.
 
 === Other changes in 1.31 ===
 * MessageBlobStore::insertMessageBlob() (deprecated in 1.27) was removed.
@@ -93,9 +93,15 @@ changes to languages because of Phabricator reports.
   * Revision::selectArchiveFields() → Revision::getArchiveQueryInfo()
   * User::selectFields() → User::getQueryInfo()
   * WikiPage::selectFields() → WikiPage::getQueryInfo()
-  * Due to significant refactoring, method ContribsPager::getUserCond() that had
-    no access restriction has been removed.
-  * Revision::setUserIdAndName() was deprecated.
+* Due to significant refactoring, method ContribsPager::getUserCond() that had
+  no access restriction has been removed.
+* Revision::setUserIdAndName() was deprecated.
+* Access to TitleValue class properties was deprecated, the relevant getters
+  should be used instead.
+* DifferenceEngine::getDiffBodyCacheKey() is deprecated. Subclasses should
+  override DifferenceEngine::getDiffBodyCacheKeyParams() instead.
+* The deprecated MW_DIFF_VERSION constant was removed.
+  DifferenceEngine::MW_DIFF_VERSION should be used instead.
 
 == Compatibility ==
 MediaWiki 1.31 requires PHP 5.5.9 or later. There is experimental support for
diff --git a/api.php b/api.php
index a6ce3b2..d9a69db 100644 (file)
--- a/api.php
+++ b/api.php
@@ -44,6 +44,17 @@ if ( !$wgRequest->checkUrlExtension() ) {
        return;
 }
 
+// Pathinfo can be used for stupid things. We don't support it for api.php at
+// all, so error out if it's present.
+if ( isset( $_SERVER['PATH_INFO'] ) && $_SERVER['PATH_INFO'] != '' ) {
+       $correctUrl = wfAppendQuery( wfScript( 'api' ), $wgRequest->getQueryValues() );
+       $correctUrl = wfExpandUrl( $correctUrl, PROTO_CANONICAL );
+       header( "Location: $correctUrl", true, 301 );
+       echo 'This endpoint does not support "path info", i.e. extra text between "api.php"'
+               . 'and the "?". Remove any such text and try again.';
+       die( 1 );
+}
+
 // Verify that the API has not been disabled
 if ( !$wgEnableAPI ) {
        header( $_SERVER['SERVER_PROTOCOL'] . ' 500 MediaWiki configuration Error', true, 500 );
index 39ec4b0..5a2156a 100644 (file)
@@ -284,6 +284,7 @@ $wgAutoloadLocalClasses = [
        'ComposerJson' => __DIR__ . '/includes/libs/composer/ComposerJson.php',
        'ComposerLock' => __DIR__ . '/includes/libs/composer/ComposerLock.php',
        'ComposerPackageModifier' => __DIR__ . '/includes/composer/ComposerPackageModifier.php',
+       'ComposerVendorHtaccessCreator' => __DIR__ . '/includes/composer/ComposerVendorHtaccessCreator.php',
        'ComposerVersionNormalizer' => __DIR__ . '/includes/composer/ComposerVersionNormalizer.php',
        'CompressOld' => __DIR__ . '/maintenance/storage/compressOld.php',
        'ConcatenatedGzipHistoryBlob' => __DIR__ . '/includes/HistoryBlob.php',
@@ -311,6 +312,7 @@ $wgAutoloadLocalClasses = [
        'CreateAndPromote' => __DIR__ . '/maintenance/createAndPromote.php',
        'CreateFileOp' => __DIR__ . '/includes/libs/filebackend/fileop/CreateFileOp.php',
        'CreditsAction' => __DIR__ . '/includes/actions/CreditsAction.php',
+       'CrhConverter' => __DIR__ . '/languages/classes/LanguageCrh.php',
        'CryptHKDF' => __DIR__ . '/includes/libs/CryptHKDF.php',
        'CryptRand' => __DIR__ . '/includes/libs/CryptRand.php',
        'CssContent' => __DIR__ . '/includes/content/CssContent.php',
@@ -454,6 +456,7 @@ $wgAutoloadLocalClasses = [
        'ExtensionRegistry' => __DIR__ . '/includes/registration/ExtensionRegistry.php',
        'ExternalStore' => __DIR__ . '/includes/externalstore/ExternalStore.php',
        'ExternalStoreDB' => __DIR__ . '/includes/externalstore/ExternalStoreDB.php',
+       'ExternalStoreFactory' => __DIR__ . '/includes/externalstore/ExternalStoreFactory.php',
        'ExternalStoreHttp' => __DIR__ . '/includes/externalstore/ExternalStoreHttp.php',
        'ExternalStoreMedium' => __DIR__ . '/includes/externalstore/ExternalStoreMedium.php',
        'ExternalStoreMwstore' => __DIR__ . '/includes/externalstore/ExternalStoreMwstore.php',
@@ -704,6 +707,7 @@ $wgAutoloadLocalClasses = [
        'LanguageBs' => __DIR__ . '/languages/classes/LanguageBs.php',
        'LanguageCode' => __DIR__ . '/languages/LanguageCode.php',
        'LanguageConverter' => __DIR__ . '/languages/LanguageConverter.php',
+       'LanguageCrh' => __DIR__ . '/languages/classes/LanguageCrh.php',
        'LanguageCu' => __DIR__ . '/languages/classes/LanguageCu.php',
        'LanguageDsb' => __DIR__ . '/languages/classes/LanguageDsb.php',
        'LanguageEn' => __DIR__ . '/languages/classes/LanguageEn.php',
@@ -883,6 +887,7 @@ $wgAutoloadLocalClasses = [
        'MediaWiki\\Interwiki\\ClassicInterwikiLookup' => __DIR__ . '/includes/interwiki/ClassicInterwikiLookup.php',
        'MediaWiki\\Interwiki\\InterwikiLookup' => __DIR__ . '/includes/interwiki/InterwikiLookup.php',
        'MediaWiki\\Interwiki\\InterwikiLookupAdapter' => __DIR__ . '/includes/interwiki/InterwikiLookupAdapter.php',
+       'MediaWiki\\Languages\\Data\\CrhExceptions' => __DIR__ . '/languages/data/CrhExceptions.php',
        'MediaWiki\\Languages\\Data\\Names' => __DIR__ . '/languages/data/Names.php',
        'MediaWiki\\Languages\\Data\\ZhConversion' => __DIR__ . '/languages/data/ZhConversion.php',
        'MediaWiki\\Linker\\LinkRenderer' => __DIR__ . '/includes/linker/LinkRenderer.php',
@@ -955,6 +960,7 @@ $wgAutoloadLocalClasses = [
        'MediaWiki\\Tidy\\RemexDriver' => __DIR__ . '/includes/tidy/RemexDriver.php',
        'MediaWiki\\Tidy\\RemexMungerData' => __DIR__ . '/includes/tidy/RemexMungerData.php',
        'MediaWiki\\Tidy\\TidyDriverBase' => __DIR__ . '/includes/tidy/TidyDriverBase.php',
+       'MediaWiki\\User\\UserIdentity' => __DIR__ . '/includes/user/UserIdentity.php',
        'MediaWiki\\Widget\\ComplexNamespaceInputWidget' => __DIR__ . '/includes/widget/ComplexNamespaceInputWidget.php',
        'MediaWiki\\Widget\\ComplexTitleInputWidget' => __DIR__ . '/includes/widget/ComplexTitleInputWidget.php',
        'MediaWiki\\Widget\\DateInputWidget' => __DIR__ . '/includes/widget/DateInputWidget.php',
@@ -1217,6 +1223,7 @@ $wgAutoloadLocalClasses = [
        'RefreshLinks' => __DIR__ . '/maintenance/refreshLinks.php',
        'RefreshLinksJob' => __DIR__ . '/includes/jobqueue/jobs/RefreshLinksJob.php',
        'RegexlikeReplacer' => __DIR__ . '/includes/libs/replacers/RegexlikeReplacer.php',
+       'RemexStripTagHandler' => __DIR__ . '/includes/parser/RemexStripTagHandler.php',
        'RemoveInvalidEmails' => __DIR__ . '/maintenance/removeInvalidEmails.php',
        'RemoveUnusedAccounts' => __DIR__ . '/maintenance/removeUnusedAccounts.php',
        'RenameDbPrefix' => __DIR__ . '/maintenance/renameDbPrefix.php',
@@ -1295,7 +1302,7 @@ $wgAutoloadLocalClasses = [
        'SVGMetadataExtractor' => __DIR__ . '/includes/media/SVGMetadataExtractor.php',
        'SVGReader' => __DIR__ . '/includes/media/SVGMetadataExtractor.php',
        'SamplingStatsdClient' => __DIR__ . '/includes/libs/stats/SamplingStatsdClient.php',
-       'Sanitizer' => __DIR__ . '/includes/Sanitizer.php',
+       'Sanitizer' => __DIR__ . '/includes/parser/Sanitizer.php',
        'ScopedCallback' => __DIR__ . '/includes/compat/ScopedCallback.php',
        'ScopedLock' => __DIR__ . '/includes/libs/lockmanager/ScopedLock.php',
        'SearchApi' => __DIR__ . '/includes/api/SearchApi.php',
@@ -1568,7 +1575,7 @@ $wgAutoloadLocalClasses = [
        'UserMailer' => __DIR__ . '/includes/mail/UserMailer.php',
        'UserNamePrefixSearch' => __DIR__ . '/includes/user/UserNamePrefixSearch.php',
        'UserNotLoggedIn' => __DIR__ . '/includes/exception/UserNotLoggedIn.php',
-       'UserOptions' => __DIR__ . '/maintenance/userOptions.inc',
+       'UserOptionsMaintenance' => __DIR__ . '/maintenance/userOptions.php',
        'UserPasswordPolicy' => __DIR__ . '/includes/password/UserPasswordPolicy.php',
        'UserRightsProxy' => __DIR__ . '/includes/user/UserRightsProxy.php',
        'UserrightsPage' => __DIR__ . '/includes/specials/SpecialUserrights.php',
@@ -1591,10 +1598,11 @@ $wgAutoloadLocalClasses = [
        'WantedQueryPage' => __DIR__ . '/includes/specialpage/WantedQueryPage.php',
        'WantedTemplatesPage' => __DIR__ . '/includes/specials/SpecialWantedtemplates.php',
        'WatchAction' => __DIR__ . '/includes/actions/WatchAction.php',
-       'WatchedItem' => __DIR__ . '/includes/WatchedItem.php',
-       'WatchedItemQueryService' => __DIR__ . '/includes/WatchedItemQueryService.php',
-       'WatchedItemQueryServiceExtension' => __DIR__ . '/includes/WatchedItemQueryServiceExtension.php',
-       'WatchedItemStore' => __DIR__ . '/includes/WatchedItemStore.php',
+       'WatchedItem' => __DIR__ . '/includes/watcheditem/WatchedItem.php',
+       'WatchedItemQueryService' => __DIR__ . '/includes/watcheditem/WatchedItemQueryService.php',
+       'WatchedItemQueryServiceExtension' => __DIR__ . '/includes/watcheditem/WatchedItemQueryServiceExtension.php',
+       'WatchedItemStore' => __DIR__ . '/includes/watcheditem/WatchedItemStore.php',
+       'WatchedItemStoreInterface' => __DIR__ . '/includes/watcheditem/WatchedItemStoreInterface.php',
        'WatchlistCleanup' => __DIR__ . '/maintenance/cleanupWatchlist.php',
        'WebInstaller' => __DIR__ . '/includes/installer/WebInstaller.php',
        'WebInstallerComplete' => __DIR__ . '/includes/installer/WebInstallerComplete.php',
index 7031cad..a5501d0 100644 (file)
@@ -58,7 +58,7 @@
                "monolog/monolog": "~1.22.1",
                "nikic/php-parser": "2.1.0",
                "nmred/kafka-php": "0.1.5",
-               "phpunit/phpunit": "4.8.35",
+               "phpunit/phpunit": "4.8.36",
                "psy/psysh": "0.8.11",
                "wikimedia/avro": "1.7.7",
                "wikimedia/testing-access-wrapper": "~1.0",
@@ -79,7 +79,8 @@
        },
        "autoload": {
                "psr-0": {
-                       "ComposerHookHandler": "includes/composer"
+                       "ComposerHookHandler": "includes/composer",
+                       "ComposerVendorHtaccessCreator": "includes/composer"
                },
                "files": [
                        "includes/compat/Timestamp.php"
@@ -97,6 +98,8 @@
                "fix": "phpcbf",
                "pre-install-cmd": "ComposerHookHandler::onPreInstall",
                "pre-update-cmd": "ComposerHookHandler::onPreUpdate",
+               "post-install-cmd": "ComposerVendorHtaccessCreator::onEvent",
+               "post-update-cmd": "ComposerVendorHtaccessCreator::onEvent",
                "test": [
                        "composer lint",
                        "composer phpcs"
index d9f032c..3cd7ef1 100644 (file)
@@ -5785,7 +5785,7 @@ $wgPasswordAttemptThrottle = [
 ];
 
 /**
- * @var Array Map of (grant => right => boolean)
+ * @var array Map of (grant => right => boolean)
  * Users authorize consumers (like Apps) to act on their behalf but only with
  * a subset of the user's normal account rights (signed off on by the user).
  * The possible rights to grant to a consumer are bundled into groups called
@@ -5887,7 +5887,7 @@ $wgGrantPermissions['createaccount']['createaccount'] = true;
 $wgGrantPermissions['privateinfo']['viewmyprivateinfo'] = true;
 
 /**
- * @var Array Map of grants to their UI grouping
+ * @var array Map of grants to their UI grouping
  * @since 1.27
  */
 $wgGrantPermissionGroups = [
index 4260c99..ff224c5 100644 (file)
@@ -3288,7 +3288,7 @@ class EditPage {
 
        protected function showFormBeforeText() {
                $out = $this->context->getOutput();
-               $out->addHTML( Html::hidden( 'wpSection', htmlspecialchars( $this->section ) ) );
+               $out->addHTML( Html::hidden( 'wpSection', $this->section ) );
                $out->addHTML( Html::hidden( 'wpStarttime', $this->starttime ) );
                $out->addHTML( Html::hidden( 'wpEdittime', $this->edittime ) );
                $out->addHTML( Html::hidden( 'editRevId', $this->editRevId ) );
index bc7747f..fd223e6 100644 (file)
@@ -230,6 +230,12 @@ abstract class ChannelFeed extends FeedItem {
                $wgOut->disable();
                $mimetype = $this->contentType();
                header( "Content-type: $mimetype; charset=UTF-8" );
+
+               // Set a sane filename
+               $exts = MimeMagic::singleton()->getExtensionsForType( $mimetype );
+               $ext = $exts ? strtok( $exts, ' ' ) : 'xml';
+               header( "Content-Disposition: inline; filename=\"feed.{$ext}\"" );
+
                if ( $wgVaryOnXFP ) {
                        $wgOut->addVaryHeader( 'X-Forwarded-Proto' );
                }
index 1cff881..bb1951d 100644 (file)
@@ -2225,7 +2225,23 @@ function wfPercent( $nr, $acc = 2, $round = true ) {
  * @return bool
  */
 function wfIniGetBool( $setting ) {
-       $val = strtolower( ini_get( $setting ) );
+       return wfStringToBool( ini_get( $setting ) );
+}
+
+/**
+ * Convert string value to boolean, when the following are interpreted as true:
+ * - on
+ * - true
+ * - yes
+ * - Any number, except 0
+ * All other strings are interpreted as false.
+ *
+ * @param string $val
+ * @return bool
+ * @since 1.31
+ */
+function wfStringToBool( $val ) {
+       $val = strtolower( $val );
        // 'on' and 'true' can't have whitespace around them, but '1' can.
        return $val == 'on'
                || $val == 'true'
@@ -3487,3 +3503,37 @@ function wfArrayPlus2d( array $baseArray, array $newValues ) {
 
        return $baseArray;
 }
+
+/**
+ * Get system resource usage of current request context.
+ * Invokes the getrusage(2) system call, requesting RUSAGE_SELF if on PHP5
+ * or RUSAGE_THREAD if on HHVM. Returns false if getrusage is not available.
+ *
+ * @since 1.24
+ * @return array|bool Resource usage data or false if no data available.
+ */
+function wfGetRusage() {
+       if ( !function_exists( 'getrusage' ) ) {
+               return false;
+       } elseif ( defined( 'HHVM_VERSION' ) && PHP_OS === 'Linux' ) {
+               return getrusage( 2 /* RUSAGE_THREAD */ );
+       } else {
+               return getrusage( 0 /* RUSAGE_SELF */ );
+       }
+}
+
+/**
+ * Begin profiling of a function
+ * @param string $functionname Name of the function we will profile
+ * @deprecated since 1.25
+ */
+function wfProfileIn( $functionname ) {
+}
+
+/**
+ * Stop profiling of a function
+ * @param string $functionname Name of the function we have profiled
+ * @deprecated since 1.25
+ */
+function wfProfileOut( $functionname = 'missing' ) {
+}
index 0988b05..524fdcd 100644 (file)
@@ -675,6 +675,52 @@ class Html {
                return self::input( $name, $value, 'checkbox', $attribs );
        }
 
+       /**
+        * Return the HTML for a message box.
+        * @since 1.31
+        * @param string $html of contents of box
+        * @param string $className corresponding to box
+        * @param string $heading (optional)
+        * @return string of HTML representing a box.
+        */
+       public static function messageBox( $html, $className, $heading = '' ) {
+               if ( $heading ) {
+                       $html = self::element( 'h2', [], $heading ) . $html;
+               }
+               return self::rawElement( 'div', [ 'class' => $className ], $html );
+       }
+
+       /**
+        * Return a warning box.
+        * @since 1.31
+        * @param string $html of contents of box
+        * @return string of HTML representing a warning box.
+        */
+       public static function warningBox( $html ) {
+               return self::messageBox( $html, 'warningbox' );
+       }
+
+       /**
+        * Return an error box.
+        * @since 1.31
+        * @param string $html of contents of error box
+        * @param string $heading (optional)
+        * @return string of HTML representing an error box.
+        */
+       public static function errorBox( $html, $heading = '' ) {
+               return self::messageBox( $html, 'errorbox', $heading );
+       }
+
+       /**
+        * Return a success box.
+        * @since 1.31
+        * @param string $html of contents of box
+        * @return string of HTML representing a success box.
+        */
+       public static function successBox( $html ) {
+               return self::messageBox( $html, 'successbox' );
+       }
+
        /**
         * Convenience function to produce a radio button (input element with type=radio)
         *
index 0d010b4..19b71f1 100644 (file)
@@ -31,7 +31,7 @@ use SearchEngineConfig;
 use SearchEngineFactory;
 use SiteLookup;
 use SiteStore;
-use WatchedItemStore;
+use WatchedItemStoreInterface;
 use WatchedItemQueryService;
 use SkinFactory;
 use TitleFormatter;
@@ -513,7 +513,7 @@ class MediaWikiServices extends ServiceContainer {
 
        /**
         * @since 1.28
-        * @return WatchedItemStore
+        * @return WatchedItemStoreInterface
         */
        public function getWatchedItemStore() {
                return $this->getService( 'WatchedItemStore' );
@@ -690,6 +690,14 @@ class MediaWikiServices extends ServiceContainer {
                return $this->getService( 'ShellCommandFactory' );
        }
 
+       /**
+        * @since 1.31
+        * @return \ExternalStoreFactory
+        */
+       public function getExternalStoreFactory() {
+               return $this->getService( 'ExternalStoreFactory' );
+       }
+
        ///////////////////////////////////////////////////////////////////////////
        // NOTE: When adding a service getter here, don't forget to add a test
        // case for it in MediaWikiServicesTest::provideGetters() and in
index 2a55d0e..3b2f3cc 100644 (file)
@@ -1123,11 +1123,29 @@ class Message implements MessageSpecifier, Serializable {
         * @return string
         */
        protected function replaceParameters( $message, $type = 'before', $format ) {
+               // A temporary marker for $1 parameters that is only valid
+               // in non-attribute contexts. However if the entire message is escaped
+               // then we don't want to use it because it will be mangled in all contexts
+               // and its unnessary as ->escaped() messages aren't html.
+               $marker = $format === self::FORMAT_ESCAPED ? '$' : '$\'"';
                $replacementKeys = [];
                foreach ( $this->parameters as $n => $param ) {
                        list( $paramType, $value ) = $this->extractParam( $param, $format );
-                       if ( $type === $paramType ) {
-                               $replacementKeys['$' . ( $n + 1 )] = $value;
+                       if ( $type === 'before' ) {
+                               if ( $paramType === 'before' ) {
+                                       $replacementKeys['$' . ( $n + 1 )] = $value;
+                               } else /* $paramType === 'after' */ {
+                                       // To protect against XSS from replacing parameters
+                                       // inside html attributes, we convert $1 to $'"1.
+                                       // In the event that one of the parameters ends up
+                                       // in an attribute, either the ' or the " will be
+                                       // escaped, breaking the replacement and avoiding XSS.
+                                       $replacementKeys['$' . ( $n + 1 )] = $marker . ( $n + 1 );
+                               }
+                       } else {
+                               if ( $paramType === 'after' ) {
+                                       $replacementKeys[$marker . ( $n + 1 )] = $value;
+                               }
                        }
                }
                $message = strtr( $message, $replacementKeys );
index 94854fa..e383f03 100644 (file)
@@ -47,9 +47,6 @@ use MediaWiki\MediaWikiServices;
  * over to the tryUISubmit static method of this class.
  */
 class Preferences {
-       /** @var array */
-       protected static $defaultPreferences = null;
-
        /** @var array */
        protected static $saveFilters = [
                'timecorrection' => [ 'Preferences', 'filterTimezoneInput' ],
@@ -78,9 +75,10 @@ class Preferences {
         * @return array|null
         */
        static function getPreferences( $user, IContextSource $context ) {
-               if ( self::$defaultPreferences ) {
-                       return self::$defaultPreferences;
-               }
+               OutputPage::setupOOUI(
+                       strtolower( $context->getSkin()->getSkinName() ),
+                       $context->getLanguage()->getDir()
+               );
 
                $defaultPreferences = [];
 
@@ -98,7 +96,6 @@ class Preferences {
                Hooks::run( 'GetPreferences', [ $user, &$defaultPreferences ] );
 
                self::loadPreferenceValues( $user, $context, $defaultPreferences );
-               self::$defaultPreferences = $defaultPreferences;
                return $defaultPreferences;
        }
 
@@ -320,14 +317,17 @@ class Preferences {
                if ( $canEditPrivateInfo && $authManager->allowsAuthenticationDataChange(
                        new PasswordAuthenticationRequest(), false )->isGood()
                ) {
-                       $link = $linkRenderer->makeLink( SpecialPage::getTitleFor( 'ChangePassword' ),
-                               $context->msg( 'prefs-resetpass' )->text(), [],
-                               [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
+                       $link = new OOUI\ButtonWidget( [
+                               'href' => SpecialPage::getTitleFor( 'ChangePassword' )->getLinkURL( [
+                                       'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
+                               ] ),
+                               'label' => $context->msg( 'prefs-resetpass' )->text(),
+                       ] );
 
                        $defaultPreferences['password'] = [
                                'type' => 'info',
                                'raw' => true,
-                               'default' => $link,
+                               'default' => (string)$link,
                                'label-message' => 'yourpassword',
                                'section' => 'personal/info',
                        ];
@@ -471,16 +471,15 @@ class Preferences {
 
                                $emailAddress = $user->getEmail() ? htmlspecialchars( $user->getEmail() ) : '';
                                if ( $canEditPrivateInfo && $authManager->allowsPropertyChange( 'emailaddress' ) ) {
-                                       $link = $linkRenderer->makeLink(
-                                               SpecialPage::getTitleFor( 'ChangeEmail' ),
-                                               $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->text(),
-                                               [],
-                                               [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
-
-                                       $emailAddress .= $emailAddress == '' ? $link : (
-                                               $context->msg( 'word-separator' )->escaped()
-                                               . $context->msg( 'parentheses' )->rawParams( $link )->escaped()
-                                       );
+                                       $link = new OOUI\ButtonWidget( [
+                                               'href' => SpecialPage::getTitleFor( 'ChangeEmail' )->getLinkURL( [
+                                                       'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
+                                               ] ),
+                                               'label' =>
+                                                       $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->text(),
+                                       ] );
+
+                                       $emailAddress .= $emailAddress == '' ? $link : ( '<br />' . $link );
                                }
 
                                $defaultPreferences['emailaddress'] = [
@@ -515,10 +514,10 @@ class Preferences {
                                        } else {
                                                $disableEmailPrefs = true;
                                                $emailauthenticated = $context->msg( 'emailnotauthenticated' )->parse() . '<br />' .
-                                                       $linkRenderer->makeKnownLink(
-                                                               SpecialPage::getTitleFor( 'Confirmemail' ),
-                                                               $context->msg( 'emailconfirmlink' )->text()
-                                                       ) . '<br />';
+                                                       new OOUI\ButtonWidget( [
+                                                               'href' => SpecialPage::getTitleFor( 'Confirmemail' )->getLinkURL(),
+                                                               'label' => $context->msg( 'emailconfirmlink' )->text(),
+                                                       ] );
                                                $emailauthenticationclass = "mw-email-not-authenticated";
                                        }
                                } else {
@@ -755,6 +754,7 @@ class Preferences {
                        'default' => $tzSetting,
                        'size' => 20,
                        'section' => 'rendering/timeoffset',
+                       'id' => 'wpTimeCorrection',
                ];
        }
 
@@ -997,7 +997,7 @@ class Preferences {
 
                # # Watchlist #####################################
                if ( $user->isAllowed( 'editmywatchlist' ) ) {
-                       $editWatchlistLinks = [];
+                       $editWatchlistLinks = '';
                        $editWatchlistModes = [
                                'edit' => [ 'EditWatchlist', false ],
                                'raw' => [ 'EditWatchlist', 'raw' ],
@@ -1006,16 +1006,19 @@ class Preferences {
                        $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
                        foreach ( $editWatchlistModes as $editWatchlistMode => $mode ) {
                                // Messages: prefs-editwatchlist-edit, prefs-editwatchlist-raw, prefs-editwatchlist-clear
-                               $editWatchlistLinks[] = $linkRenderer->makeKnownLink(
-                                       SpecialPage::getTitleFor( $mode[0], $mode[1] ),
-                                       new HtmlArmor( $context->msg( "prefs-editwatchlist-{$editWatchlistMode}" )->parse() )
-                               );
+                               $editWatchlistLinks .=
+                                       new OOUI\ButtonWidget( [
+                                               'href' => SpecialPage::getTitleFor( $mode[0], $mode[1] )->getLinkURL(),
+                                               'label' => new OOUI\HtmlSnippet(
+                                                       $context->msg( "prefs-editwatchlist-{$editWatchlistMode}" )->parse()
+                                               ),
+                                       ] );
                        }
 
                        $defaultPreferences['editwatchlist'] = [
                                'type' => 'info',
                                'raw' => true,
-                               'default' => $context->getLanguage()->pipeList( $editWatchlistLinks ),
+                               'default' => $editWatchlistLinks,
                                'label-message' => 'prefs-editwatchlist-label',
                                'section' => 'watchlist/editwatchlist',
                        ];
@@ -1138,6 +1141,12 @@ class Preferences {
                                'default' => $user->getTokenFromOption( 'watchlisttoken' ),
                                'help-message' => 'prefs-help-watchlist-token2',
                        ];
+                       $defaultPreferences['watchlisttoken-info2'] = [
+                               'type' => 'info',
+                               'section' => 'watchlist/tokenwatchlist',
+                               'raw' => true,
+                               'default' => $context->msg( 'prefs-help-watchlist-token2' )->parse(),
+                       ];
                }
        }
 
@@ -1358,6 +1367,9 @@ class Preferences {
                $formClass = 'PreferencesForm',
                array $remove = []
        ) {
+               // We use ButtonWidgets in some of the getPreferences() functions
+               $context->getOutput()->enableOOUI();
+
                $formDescriptor = self::getPreferences( $user, $context );
                if ( count( $remove ) ) {
                        $removeKeys = array_flip( $remove );
diff --git a/includes/Sanitizer.php b/includes/Sanitizer.php
deleted file mode 100644 (file)
index 4c99677..0000000
+++ /dev/null
@@ -1,2115 +0,0 @@
-<?php
-/**
- * HTML sanitizer for %MediaWiki.
- *
- * Copyright © 2002-2005 Brion Vibber <brion@pobox.com> et al
- * https://www.mediawiki.org/
- *
- * 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 Parser
- */
-
-/**
- * HTML sanitizer for MediaWiki
- * @ingroup Parser
- */
-class Sanitizer {
-       /**
-        * Regular expression to match various types of character references in
-        * Sanitizer::normalizeCharReferences and Sanitizer::decodeCharReferences
-        */
-       const CHAR_REFS_REGEX =
-               '/&([A-Za-z0-9\x80-\xff]+);
-                |&\#([0-9]+);
-                |&\#[xX]([0-9A-Fa-f]+);
-                |(&)/x';
-
-       /**
-        * Acceptable tag name charset from HTML5 parsing spec
-        * https://www.w3.org/TR/html5/syntax.html#tag-open-state
-        */
-       const ELEMENT_BITS_REGEX = '!^(/?)([A-Za-z][^\t\n\v />\0]*+)([^>]*?)(/?>)([^<]*)$!';
-
-       /**
-        * Blacklist for evil uris like javascript:
-        * WARNING: DO NOT use this in any place that actually requires blacklisting
-        * for security reasons. There are NUMEROUS[1] ways to bypass blacklisting, the
-        * only way to be secure from javascript: uri based xss vectors is to whitelist
-        * things that you know are safe and deny everything else.
-        * [1]: http://ha.ckers.org/xss.html
-        */
-       const EVIL_URI_PATTERN = '!(^|\s|\*/\s*)(javascript|vbscript)([^\w]|$)!i';
-       const XMLNS_ATTRIBUTE_PATTERN = "/^xmlns:[:A-Z_a-z-.0-9]+$/";
-
-       /**
-        * Tells escapeUrlForHtml() to encode the ID using the wiki's primary encoding.
-        *
-        * @since 1.30
-        */
-       const ID_PRIMARY = 0;
-
-       /**
-        * Tells escapeUrlForHtml() to encode the ID using the fallback encoding, or return false
-        * if no fallback is configured.
-        *
-        * @since 1.30
-        */
-       const ID_FALLBACK = 1;
-
-       /**
-        * List of all named character entities defined in HTML 4.01
-        * https://www.w3.org/TR/html4/sgml/entities.html
-        * As well as &apos; which is only defined starting in XHTML1.
-        */
-       private static $htmlEntities = [
-               'Aacute'   => 193,
-               'aacute'   => 225,
-               'Acirc'    => 194,
-               'acirc'    => 226,
-               'acute'    => 180,
-               'AElig'    => 198,
-               'aelig'    => 230,
-               'Agrave'   => 192,
-               'agrave'   => 224,
-               'alefsym'  => 8501,
-               'Alpha'    => 913,
-               'alpha'    => 945,
-               'amp'      => 38,
-               'and'      => 8743,
-               'ang'      => 8736,
-               'apos'     => 39, // New in XHTML & HTML 5; avoid in output for compatibility with IE.
-               'Aring'    => 197,
-               'aring'    => 229,
-               'asymp'    => 8776,
-               'Atilde'   => 195,
-               'atilde'   => 227,
-               'Auml'     => 196,
-               'auml'     => 228,
-               'bdquo'    => 8222,
-               'Beta'     => 914,
-               'beta'     => 946,
-               'brvbar'   => 166,
-               'bull'     => 8226,
-               'cap'      => 8745,
-               'Ccedil'   => 199,
-               'ccedil'   => 231,
-               'cedil'    => 184,
-               'cent'     => 162,
-               'Chi'      => 935,
-               'chi'      => 967,
-               'circ'     => 710,
-               'clubs'    => 9827,
-               'cong'     => 8773,
-               'copy'     => 169,
-               'crarr'    => 8629,
-               'cup'      => 8746,
-               'curren'   => 164,
-               'dagger'   => 8224,
-               'Dagger'   => 8225,
-               'darr'     => 8595,
-               'dArr'     => 8659,
-               'deg'      => 176,
-               'Delta'    => 916,
-               'delta'    => 948,
-               'diams'    => 9830,
-               'divide'   => 247,
-               'Eacute'   => 201,
-               'eacute'   => 233,
-               'Ecirc'    => 202,
-               'ecirc'    => 234,
-               'Egrave'   => 200,
-               'egrave'   => 232,
-               'empty'    => 8709,
-               'emsp'     => 8195,
-               'ensp'     => 8194,
-               'Epsilon'  => 917,
-               'epsilon'  => 949,
-               'equiv'    => 8801,
-               'Eta'      => 919,
-               'eta'      => 951,
-               'ETH'      => 208,
-               'eth'      => 240,
-               'Euml'     => 203,
-               'euml'     => 235,
-               'euro'     => 8364,
-               'exist'    => 8707,
-               'fnof'     => 402,
-               'forall'   => 8704,
-               'frac12'   => 189,
-               'frac14'   => 188,
-               'frac34'   => 190,
-               'frasl'    => 8260,
-               'Gamma'    => 915,
-               'gamma'    => 947,
-               'ge'       => 8805,
-               'gt'       => 62,
-               'harr'     => 8596,
-               'hArr'     => 8660,
-               'hearts'   => 9829,
-               'hellip'   => 8230,
-               'Iacute'   => 205,
-               'iacute'   => 237,
-               'Icirc'    => 206,
-               'icirc'    => 238,
-               'iexcl'    => 161,
-               'Igrave'   => 204,
-               'igrave'   => 236,
-               'image'    => 8465,
-               'infin'    => 8734,
-               'int'      => 8747,
-               'Iota'     => 921,
-               'iota'     => 953,
-               'iquest'   => 191,
-               'isin'     => 8712,
-               'Iuml'     => 207,
-               'iuml'     => 239,
-               'Kappa'    => 922,
-               'kappa'    => 954,
-               'Lambda'   => 923,
-               'lambda'   => 955,
-               'lang'     => 9001,
-               'laquo'    => 171,
-               'larr'     => 8592,
-               'lArr'     => 8656,
-               'lceil'    => 8968,
-               'ldquo'    => 8220,
-               'le'       => 8804,
-               'lfloor'   => 8970,
-               'lowast'   => 8727,
-               'loz'      => 9674,
-               'lrm'      => 8206,
-               'lsaquo'   => 8249,
-               'lsquo'    => 8216,
-               'lt'       => 60,
-               'macr'     => 175,
-               'mdash'    => 8212,
-               'micro'    => 181,
-               'middot'   => 183,
-               'minus'    => 8722,
-               'Mu'       => 924,
-               'mu'       => 956,
-               'nabla'    => 8711,
-               'nbsp'     => 160,
-               'ndash'    => 8211,
-               'ne'       => 8800,
-               'ni'       => 8715,
-               'not'      => 172,
-               'notin'    => 8713,
-               'nsub'     => 8836,
-               'Ntilde'   => 209,
-               'ntilde'   => 241,
-               'Nu'       => 925,
-               'nu'       => 957,
-               'Oacute'   => 211,
-               'oacute'   => 243,
-               'Ocirc'    => 212,
-               'ocirc'    => 244,
-               'OElig'    => 338,
-               'oelig'    => 339,
-               'Ograve'   => 210,
-               'ograve'   => 242,
-               'oline'    => 8254,
-               'Omega'    => 937,
-               'omega'    => 969,
-               'Omicron'  => 927,
-               'omicron'  => 959,
-               'oplus'    => 8853,
-               'or'       => 8744,
-               'ordf'     => 170,
-               'ordm'     => 186,
-               'Oslash'   => 216,
-               'oslash'   => 248,
-               'Otilde'   => 213,
-               'otilde'   => 245,
-               'otimes'   => 8855,
-               'Ouml'     => 214,
-               'ouml'     => 246,
-               'para'     => 182,
-               'part'     => 8706,
-               'permil'   => 8240,
-               'perp'     => 8869,
-               'Phi'      => 934,
-               'phi'      => 966,
-               'Pi'       => 928,
-               'pi'       => 960,
-               'piv'      => 982,
-               'plusmn'   => 177,
-               'pound'    => 163,
-               'prime'    => 8242,
-               'Prime'    => 8243,
-               'prod'     => 8719,
-               'prop'     => 8733,
-               'Psi'      => 936,
-               'psi'      => 968,
-               'quot'     => 34,
-               'radic'    => 8730,
-               'rang'     => 9002,
-               'raquo'    => 187,
-               'rarr'     => 8594,
-               'rArr'     => 8658,
-               'rceil'    => 8969,
-               'rdquo'    => 8221,
-               'real'     => 8476,
-               'reg'      => 174,
-               'rfloor'   => 8971,
-               'Rho'      => 929,
-               'rho'      => 961,
-               'rlm'      => 8207,
-               'rsaquo'   => 8250,
-               'rsquo'    => 8217,
-               'sbquo'    => 8218,
-               'Scaron'   => 352,
-               'scaron'   => 353,
-               'sdot'     => 8901,
-               'sect'     => 167,
-               'shy'      => 173,
-               'Sigma'    => 931,
-               'sigma'    => 963,
-               'sigmaf'   => 962,
-               'sim'      => 8764,
-               'spades'   => 9824,
-               'sub'      => 8834,
-               'sube'     => 8838,
-               'sum'      => 8721,
-               'sup'      => 8835,
-               'sup1'     => 185,
-               'sup2'     => 178,
-               'sup3'     => 179,
-               'supe'     => 8839,
-               'szlig'    => 223,
-               'Tau'      => 932,
-               'tau'      => 964,
-               'there4'   => 8756,
-               'Theta'    => 920,
-               'theta'    => 952,
-               'thetasym' => 977,
-               'thinsp'   => 8201,
-               'THORN'    => 222,
-               'thorn'    => 254,
-               'tilde'    => 732,
-               'times'    => 215,
-               'trade'    => 8482,
-               'Uacute'   => 218,
-               'uacute'   => 250,
-               'uarr'     => 8593,
-               'uArr'     => 8657,
-               'Ucirc'    => 219,
-               'ucirc'    => 251,
-               'Ugrave'   => 217,
-               'ugrave'   => 249,
-               'uml'      => 168,
-               'upsih'    => 978,
-               'Upsilon'  => 933,
-               'upsilon'  => 965,
-               'Uuml'     => 220,
-               'uuml'     => 252,
-               'weierp'   => 8472,
-               'Xi'       => 926,
-               'xi'       => 958,
-               'Yacute'   => 221,
-               'yacute'   => 253,
-               'yen'      => 165,
-               'Yuml'     => 376,
-               'yuml'     => 255,
-               'Zeta'     => 918,
-               'zeta'     => 950,
-               'zwj'      => 8205,
-               'zwnj'     => 8204
-       ];
-
-       /**
-        * Character entity aliases accepted by MediaWiki
-        */
-       private static $htmlEntityAliases = [
-               'רלמ' => 'rlm',
-               'رلم' => 'rlm',
-       ];
-
-       /**
-        * Lazy-initialised attributes regex, see getAttribsRegex()
-        */
-       private static $attribsRegex;
-
-       /**
-        * Regular expression to match HTML/XML attribute pairs within a tag.
-        * Allows some... latitude. Based on,
-        * https://www.w3.org/TR/html5/syntax.html#before-attribute-value-state
-        * Used in Sanitizer::fixTagAttributes and Sanitizer::decodeTagAttributes
-        * @return string
-        */
-       static function getAttribsRegex() {
-               if ( self::$attribsRegex === null ) {
-                       $attribFirst = "[:_\p{L}\p{N}]";
-                       $attrib = "[:_\.\-\p{L}\p{N}]";
-                       $space = '[\x09\x0a\x0c\x0d\x20]';
-                       self::$attribsRegex =
-                               "/(?:^|$space)({$attribFirst}{$attrib}*)
-                                       ($space*=$space*
-                                       (?:
-                                               # The attribute value: quoted or alone
-                                               \"([^\"]*)(?:\"|\$)
-                                               | '([^']*)(?:'|\$)
-                                               | (((?!$space|>).)*)
-                                       )
-                               )?(?=$space|\$)/sxu";
-               }
-               return self::$attribsRegex;
-       }
-
-       /**
-        * Return the various lists of recognized tags
-        * @param array $extratags For any extra tags to include
-        * @param array $removetags For any tags (default or extra) to exclude
-        * @return array
-        */
-       public static function getRecognizedTagData( $extratags = [], $removetags = [] ) {
-               global $wgAllowImageTag;
-
-               static $htmlpairsStatic, $htmlsingle, $htmlsingleonly, $htmlnest, $tabletags,
-                       $htmllist, $listtags, $htmlsingleallowed, $htmlelementsStatic, $staticInitialised;
-
-               // Base our staticInitialised variable off of the global config state so that if the globals
-               // are changed (like in the screwed up test system) we will re-initialise the settings.
-               $globalContext = $wgAllowImageTag;
-               if ( !$staticInitialised || $staticInitialised != $globalContext ) {
-                       $htmlpairsStatic = [ # Tags that must be closed
-                               'b', 'bdi', 'del', 'i', 'ins', 'u', 'font', 'big', 'small', 'sub', 'sup', 'h1',
-                               'h2', 'h3', 'h4', 'h5', 'h6', 'cite', 'code', 'em', 's',
-                               'strike', 'strong', 'tt', 'var', 'div', 'center',
-                               'blockquote', 'ol', 'ul', 'dl', 'table', 'caption', 'pre',
-                               'ruby', 'rb', 'rp', 'rt', 'rtc', 'p', 'span', 'abbr', 'dfn',
-                               'kbd', 'samp', 'data', 'time', 'mark'
-                       ];
-                       $htmlsingle = [
-                               'br', 'wbr', 'hr', 'li', 'dt', 'dd', 'meta', 'link'
-                       ];
-
-                       # Elements that cannot have close tags. This is (not coincidentally)
-                       # also the list of tags for which the HTML 5 parsing algorithm
-                       # requires you to "acknowledge the token's self-closing flag", i.e.
-                       # a self-closing tag like <br/> is not an HTML 5 parse error only
-                       # for this list.
-                       $htmlsingleonly = [
-                               'br', 'wbr', 'hr', 'meta', 'link'
-                       ];
-
-                       $htmlnest = [ # Tags that can be nested--??
-                               'table', 'tr', 'td', 'th', 'div', 'blockquote', 'ol', 'ul',
-                               'li', 'dl', 'dt', 'dd', 'font', 'big', 'small', 'sub', 'sup', 'span',
-                               'var', 'kbd', 'samp', 'em', 'strong', 'q', 'ruby', 'bdo'
-                       ];
-                       $tabletags = [ # Can only appear inside table, we will close them
-                               'td', 'th', 'tr',
-                       ];
-                       $htmllist = [ # Tags used by list
-                               'ul', 'ol',
-                       ];
-                       $listtags = [ # Tags that can appear in a list
-                               'li',
-                       ];
-
-                       if ( $wgAllowImageTag ) {
-                               $htmlsingle[] = 'img';
-                               $htmlsingleonly[] = 'img';
-                       }
-
-                       $htmlsingleallowed = array_unique( array_merge( $htmlsingle, $tabletags ) );
-                       $htmlelementsStatic = array_unique( array_merge( $htmlsingle, $htmlpairsStatic, $htmlnest ) );
-
-                       # Convert them all to hashtables for faster lookup
-                       $vars = [ 'htmlpairsStatic', 'htmlsingle', 'htmlsingleonly', 'htmlnest', 'tabletags',
-                               'htmllist', 'listtags', 'htmlsingleallowed', 'htmlelementsStatic' ];
-                       foreach ( $vars as $var ) {
-                               $$var = array_flip( $$var );
-                       }
-                       $staticInitialised = $globalContext;
-               }
-
-               # Populate $htmlpairs and $htmlelements with the $extratags and $removetags arrays
-               $extratags = array_flip( $extratags );
-               $removetags = array_flip( $removetags );
-               $htmlpairs = array_merge( $extratags, $htmlpairsStatic );
-               $htmlelements = array_diff_key( array_merge( $extratags, $htmlelementsStatic ), $removetags );
-
-               return [
-                       'htmlpairs' => $htmlpairs,
-                       'htmlsingle' => $htmlsingle,
-                       'htmlsingleonly' => $htmlsingleonly,
-                       'htmlnest' => $htmlnest,
-                       'tabletags' => $tabletags,
-                       'htmllist' => $htmllist,
-                       'listtags' => $listtags,
-                       'htmlsingleallowed' => $htmlsingleallowed,
-                       'htmlelements' => $htmlelements,
-               ];
-       }
-
-       /**
-        * Cleans up HTML, removes dangerous tags and attributes, and
-        * removes HTML comments
-        * @param string $text
-        * @param callable $processCallback Callback to do any variable or parameter
-        *   replacements in HTML attribute values
-        * @param array|bool $args Arguments for the processing callback
-        * @param array $extratags For any extra tags to include
-        * @param array $removetags For any tags (default or extra) to exclude
-        * @param callable $warnCallback (Deprecated) Callback allowing the
-        *   addition of a tracking category when bad input is encountered.
-        *   DO NOT ADD NEW PARAMETERS AFTER $warnCallback, since it will be
-        *   removed shortly.
-        * @return string
-        */
-       public static function removeHTMLtags( $text, $processCallback = null,
-               $args = [], $extratags = [], $removetags = [], $warnCallback = null
-       ) {
-               extract( self::getRecognizedTagData( $extratags, $removetags ) );
-
-               # Remove HTML comments
-               $text = self::removeHTMLcomments( $text );
-               $bits = explode( '<', $text );
-               $text = str_replace( '>', '&gt;', array_shift( $bits ) );
-               if ( !MWTidy::isEnabled() ) {
-                       $tagstack = $tablestack = [];
-                       foreach ( $bits as $x ) {
-                               $regs = [];
-                               # $slash: Does the current element start with a '/'?
-                               # $t: Current element name
-                               # $params: String between element name and >
-                               # $brace: Ending '>' or '/>'
-                               # $rest: Everything until the next element of $bits
-                               if ( preg_match( self::ELEMENT_BITS_REGEX, $x, $regs ) ) {
-                                       list( /* $qbar */, $slash, $t, $params, $brace, $rest ) = $regs;
-                               } else {
-                                       $slash = $t = $params = $brace = $rest = null;
-                               }
-
-                               $badtag = false;
-                               $t = strtolower( $t );
-                               if ( isset( $htmlelements[$t] ) ) {
-                                       # Check our stack
-                                       if ( $slash && isset( $htmlsingleonly[$t] ) ) {
-                                               $badtag = true;
-                                       } elseif ( $slash ) {
-                                               # Closing a tag... is it the one we just opened?
-                                               MediaWiki\suppressWarnings();
-                                               $ot = array_pop( $tagstack );
-                                               MediaWiki\restoreWarnings();
-
-                                               if ( $ot != $t ) {
-                                                       if ( isset( $htmlsingleallowed[$ot] ) ) {
-                                                               # Pop all elements with an optional close tag
-                                                               # and see if we find a match below them
-                                                               $optstack = [];
-                                                               array_push( $optstack, $ot );
-                                                               MediaWiki\suppressWarnings();
-                                                               $ot = array_pop( $tagstack );
-                                                               MediaWiki\restoreWarnings();
-                                                               while ( $ot != $t && isset( $htmlsingleallowed[$ot] ) ) {
-                                                                       array_push( $optstack, $ot );
-                                                                       MediaWiki\suppressWarnings();
-                                                                       $ot = array_pop( $tagstack );
-                                                                       MediaWiki\restoreWarnings();
-                                                               }
-                                                               if ( $t != $ot ) {
-                                                                       # No match. Push the optional elements back again
-                                                                       $badtag = true;
-                                                                       MediaWiki\suppressWarnings();
-                                                                       $ot = array_pop( $optstack );
-                                                                       MediaWiki\restoreWarnings();
-                                                                       while ( $ot ) {
-                                                                               array_push( $tagstack, $ot );
-                                                                               MediaWiki\suppressWarnings();
-                                                                               $ot = array_pop( $optstack );
-                                                                               MediaWiki\restoreWarnings();
-                                                                       }
-                                                               }
-                                                       } else {
-                                                               MediaWiki\suppressWarnings();
-                                                               array_push( $tagstack, $ot );
-                                                               MediaWiki\restoreWarnings();
-
-                                                               # <li> can be nested in <ul> or <ol>, skip those cases:
-                                                               if ( !isset( $htmllist[$ot] ) || !isset( $listtags[$t] ) ) {
-                                                                       $badtag = true;
-                                                               }
-                                                       }
-                                               } else {
-                                                       if ( $t == 'table' ) {
-                                                               $tagstack = array_pop( $tablestack );
-                                                       }
-                                               }
-                                               $newparams = '';
-                                       } else {
-                                               # Keep track for later
-                                               if ( isset( $tabletags[$t] ) && !in_array( 'table', $tagstack ) ) {
-                                                       $badtag = true;
-                                               } elseif ( in_array( $t, $tagstack ) && !isset( $htmlnest[$t] ) ) {
-                                                       $badtag = true;
-                                               #  Is it a self closed htmlpair ? (T7487)
-                                               } elseif ( $brace == '/>' && isset( $htmlpairs[$t] ) ) {
-                                                       // Eventually we'll just remove the self-closing
-                                                       // slash, in order to be consistent with HTML5
-                                                       // semantics.
-                                                       // $brace = '>';
-                                                       // For now, let's just warn authors to clean up.
-                                                       if ( is_callable( $warnCallback ) ) {
-                                                               call_user_func_array( $warnCallback, [ 'deprecated-self-close-category' ] );
-                                                       }
-                                                       $badtag = true;
-                                               } elseif ( isset( $htmlsingleonly[$t] ) ) {
-                                                       # Hack to force empty tag for unclosable elements
-                                                       $brace = '/>';
-                                               } elseif ( isset( $htmlsingle[$t] ) ) {
-                                                       # Hack to not close $htmlsingle tags
-                                                       $brace = null;
-                                                       # Still need to push this optionally-closed tag to
-                                                       # the tag stack so that we can match end tags
-                                                       # instead of marking them as bad.
-                                                       array_push( $tagstack, $t );
-                                               } elseif ( isset( $tabletags[$t] ) && in_array( $t, $tagstack ) ) {
-                                                       // New table tag but forgot to close the previous one
-                                                       $text .= "</$t>";
-                                               } else {
-                                                       if ( $t == 'table' ) {
-                                                               array_push( $tablestack, $tagstack );
-                                                               $tagstack = [];
-                                                       }
-                                                       array_push( $tagstack, $t );
-                                               }
-
-                                               # Replace any variables or template parameters with
-                                               # plaintext results.
-                                               if ( is_callable( $processCallback ) ) {
-                                                       call_user_func_array( $processCallback, [ &$params, $args ] );
-                                               }
-
-                                               if ( !self::validateTag( $params, $t ) ) {
-                                                       $badtag = true;
-                                               }
-
-                                               # Strip non-approved attributes from the tag
-                                               $newparams = self::fixTagAttributes( $params, $t );
-                                       }
-                                       if ( !$badtag ) {
-                                               $rest = str_replace( '>', '&gt;', $rest );
-                                               $close = ( $brace == '/>' && !$slash ) ? ' /' : '';
-                                               $text .= "<$slash$t$newparams$close>$rest";
-                                               continue;
-                                       }
-                               }
-                               $text .= '&lt;' . str_replace( '>', '&gt;', $x );
-                       }
-                       # Close off any remaining tags
-                       while ( is_array( $tagstack ) && ( $t = array_pop( $tagstack ) ) ) {
-                               $text .= "</$t>\n";
-                               if ( $t == 'table' ) {
-                                       $tagstack = array_pop( $tablestack );
-                               }
-                       }
-               } else {
-                       # this might be possible using tidy itself
-                       foreach ( $bits as $x ) {
-                               if ( preg_match( self::ELEMENT_BITS_REGEX, $x, $regs ) ) {
-                                       list( /* $qbar */, $slash, $t, $params, $brace, $rest ) = $regs;
-
-                                       $badtag = false;
-                                       $t = strtolower( $t );
-                                       if ( isset( $htmlelements[$t] ) ) {
-                                               if ( is_callable( $processCallback ) ) {
-                                                       call_user_func_array( $processCallback, [ &$params, $args ] );
-                                               }
-
-                                               if ( $brace == '/>' && !( isset( $htmlsingle[$t] ) || isset( $htmlsingleonly[$t] ) ) ) {
-                                                       // Eventually we'll just remove the self-closing
-                                                       // slash, in order to be consistent with HTML5
-                                                       // semantics.
-                                                       // $brace = '>';
-                                                       // For now, let's just warn authors to clean up.
-                                                       if ( is_callable( $warnCallback ) ) {
-                                                               call_user_func_array( $warnCallback, [ 'deprecated-self-close-category' ] );
-                                                       }
-                                               }
-                                               if ( !self::validateTag( $params, $t ) ) {
-                                                       $badtag = true;
-                                               }
-
-                                               $newparams = self::fixTagAttributes( $params, $t );
-                                               if ( !$badtag ) {
-                                                       if ( $brace === '/>' && !isset( $htmlsingleonly[$t] ) ) {
-                                                               # Interpret self-closing tags as empty tags even when
-                                                               # HTML 5 would interpret them as start tags. Such input
-                                                               # is commonly seen on Wikimedia wikis with this intention.
-                                                               $brace = "></$t>";
-                                                       }
-
-                                                       $rest = str_replace( '>', '&gt;', $rest );
-                                                       $text .= "<$slash$t$newparams$brace$rest";
-                                                       continue;
-                                               }
-                                       }
-                               }
-                               $text .= '&lt;' . str_replace( '>', '&gt;', $x );
-                       }
-               }
-               return $text;
-       }
-
-       /**
-        * Remove '<!--', '-->', and everything between.
-        * To avoid leaving blank lines, when a comment is both preceded
-        * and followed by a newline (ignoring spaces), trim leading and
-        * trailing spaces and one of the newlines.
-        *
-        * @param string $text
-        * @return string
-        */
-       public static function removeHTMLcomments( $text ) {
-               while ( ( $start = strpos( $text, '<!--' ) ) !== false ) {
-                       $end = strpos( $text, '-->', $start + 4 );
-                       if ( $end === false ) {
-                               # Unterminated comment; bail out
-                               break;
-                       }
-
-                       $end += 3;
-
-                       # Trim space and newline if the comment is both
-                       # preceded and followed by a newline
-                       $spaceStart = max( $start - 1, 0 );
-                       $spaceLen = $end - $spaceStart;
-                       while ( substr( $text, $spaceStart, 1 ) === ' ' && $spaceStart > 0 ) {
-                               $spaceStart--;
-                               $spaceLen++;
-                       }
-                       while ( substr( $text, $spaceStart + $spaceLen, 1 ) === ' ' ) {
-                               $spaceLen++;
-                       }
-                       if ( substr( $text, $spaceStart, 1 ) === "\n"
-                               && substr( $text, $spaceStart + $spaceLen, 1 ) === "\n" ) {
-                               # Remove the comment, leading and trailing
-                               # spaces, and leave only one newline.
-                               $text = substr_replace( $text, "\n", $spaceStart, $spaceLen + 1 );
-                       } else {
-                               # Remove just the comment.
-                               $text = substr_replace( $text, '', $start, $end - $start );
-                       }
-               }
-               return $text;
-       }
-
-       /**
-        * Takes attribute names and values for a tag and the tag name and
-        * validates that the tag is allowed to be present.
-        * This DOES NOT validate the attributes, nor does it validate the
-        * tags themselves. This method only handles the special circumstances
-        * where we may want to allow a tag within content but ONLY when it has
-        * specific attributes set.
-        *
-        * @param string $params
-        * @param string $element
-        * @return bool
-        */
-       static function validateTag( $params, $element ) {
-               $params = self::decodeTagAttributes( $params );
-
-               if ( $element == 'meta' || $element == 'link' ) {
-                       if ( !isset( $params['itemprop'] ) ) {
-                               // <meta> and <link> must have an itemprop="" otherwise they are not valid or safe in content
-                               return false;
-                       }
-                       if ( $element == 'meta' && !isset( $params['content'] ) ) {
-                               // <meta> must have a content="" for the itemprop
-                               return false;
-                       }
-                       if ( $element == 'link' && !isset( $params['href'] ) ) {
-                               // <link> must have an associated href=""
-                               return false;
-                       }
-               }
-
-               return true;
-       }
-
-       /**
-        * Take an array of attribute names and values and normalize or discard
-        * illegal values for the given element type.
-        *
-        * - Discards attributes not on a whitelist for the given element
-        * - Unsafe style attributes are discarded
-        * - Invalid id attributes are re-encoded
-        *
-        * @param array $attribs
-        * @param string $element
-        * @return array
-        *
-        * @todo Check for legal values where the DTD limits things.
-        * @todo Check for unique id attribute :P
-        */
-       static function validateTagAttributes( $attribs, $element ) {
-               return self::validateAttributes( $attribs,
-                       self::attributeWhitelist( $element ) );
-       }
-
-       /**
-        * Take an array of attribute names and values and normalize or discard
-        * illegal values for the given whitelist.
-        *
-        * - Discards attributes not on the given whitelist
-        * - Unsafe style attributes are discarded
-        * - Invalid id attributes are re-encoded
-        *
-        * @param array $attribs
-        * @param array $whitelist List of allowed attribute names
-        * @return array
-        *
-        * @todo Check for legal values where the DTD limits things.
-        * @todo Check for unique id attribute :P
-        */
-       static function validateAttributes( $attribs, $whitelist ) {
-               $whitelist = array_flip( $whitelist );
-               $hrefExp = '/^(' . wfUrlProtocols() . ')[^\s]+$/';
-
-               $out = [];
-               foreach ( $attribs as $attribute => $value ) {
-                       # Allow XML namespace declaration to allow RDFa
-                       if ( preg_match( self::XMLNS_ATTRIBUTE_PATTERN, $attribute ) ) {
-                               if ( !preg_match( self::EVIL_URI_PATTERN, $value ) ) {
-                                       $out[$attribute] = $value;
-                               }
-
-                               continue;
-                       }
-
-                       # Allow any attribute beginning with "data-"
-                       # However:
-                       # * Disallow data attributes used by MediaWiki code
-                       # * Ensure that the attribute is not namespaced by banning
-                       #   colons.
-                       if ( !preg_match( '/^data-[^:]*$/i', $attribute )
-                               && !isset( $whitelist[$attribute] )
-                               || self::isReservedDataAttribute( $attribute )
-                       ) {
-                               continue;
-                       }
-
-                       # Strip javascript "expression" from stylesheets.
-                       # https://msdn.microsoft.com/en-us/library/ms537634.aspx
-                       if ( $attribute == 'style' ) {
-                               $value = self::checkCss( $value );
-                       }
-
-                       # Escape HTML id attributes
-                       if ( $attribute === 'id' ) {
-                               $value = self::escapeIdForAttribute( $value, self::ID_PRIMARY );
-                       }
-
-                       # Escape HTML id reference lists
-                       if ( $attribute === 'aria-describedby'
-                               || $attribute === 'aria-flowto'
-                               || $attribute === 'aria-labelledby'
-                               || $attribute === 'aria-owns'
-                       ) {
-                               $value = self::escapeIdReferenceList( $value );
-                       }
-
-                       // RDFa and microdata properties allow URLs, URIs and/or CURIs.
-                       // Check them for sanity.
-                       if ( $attribute === 'rel' || $attribute === 'rev'
-                               # RDFa
-                               || $attribute === 'about' || $attribute === 'property'
-                               || $attribute === 'resource' || $attribute === 'datatype'
-                               || $attribute === 'typeof'
-                               # HTML5 microdata
-                               || $attribute === 'itemid' || $attribute === 'itemprop'
-                               || $attribute === 'itemref' || $attribute === 'itemscope'
-                               || $attribute === 'itemtype'
-                       ) {
-                               // Paranoia. Allow "simple" values but suppress javascript
-                               if ( preg_match( self::EVIL_URI_PATTERN, $value ) ) {
-                                       continue;
-                               }
-                       }
-
-                       # NOTE: even though elements using href/src are not allowed directly, supply
-                       #       validation code that can be used by tag hook handlers, etc
-                       if ( $attribute === 'href' || $attribute === 'src' || $attribute === 'poster' ) {
-                               if ( !preg_match( $hrefExp, $value ) ) {
-                                       continue; // drop any href or src attributes not using an allowed protocol.
-                                       // NOTE: this also drops all relative URLs
-                               }
-                       }
-
-                       // If this attribute was previously set, override it.
-                       // Output should only have one attribute of each name.
-                       $out[$attribute] = $value;
-               }
-
-               # itemtype, itemid, itemref don't make sense without itemscope
-               if ( !array_key_exists( 'itemscope', $out ) ) {
-                       unset( $out['itemtype'] );
-                       unset( $out['itemid'] );
-                       unset( $out['itemref'] );
-               }
-               # TODO: Strip itemprop if we aren't descendants of an itemscope or pointed to by an itemref.
-
-               return $out;
-       }
-
-       /**
-        * Given an attribute name, checks whether it is a reserved data attribute
-        * (such as data-mw-foo) which is unavailable to user-generated HTML so MediaWiki
-        * core and extension code can safely use it to communicate with frontend code.
-        * @param string $attr Attribute name.
-        * @return bool
-        */
-       public static function isReservedDataAttribute( $attr ) {
-               // data-ooui is reserved for ooui.
-               // data-mw and data-parsoid are reserved for parsoid.
-               // data-mw-<name here> is reserved for extensions (or core) if
-               // they need to communicate some data to the client and want to be
-               // sure that it isn't coming from an untrusted user.
-               // We ignore the possibility of namespaces since user-generated HTML
-               // can't use them anymore.
-               return (bool)preg_match( '/^data-(ooui|mw|parsoid)/i', $attr );
-       }
-
-       /**
-        * Merge two sets of HTML attributes.  Conflicting items in the second set
-        * will override those in the first, except for 'class' attributes which
-        * will be combined (if they're both strings).
-        *
-        * @todo implement merging for other attributes such as style
-        * @param array $a
-        * @param array $b
-        * @return array
-        */
-       static function mergeAttributes( $a, $b ) {
-               $out = array_merge( $a, $b );
-               if ( isset( $a['class'] ) && isset( $b['class'] )
-                       && is_string( $a['class'] ) && is_string( $b['class'] )
-                       && $a['class'] !== $b['class']
-               ) {
-                       $classes = preg_split( '/\s+/', "{$a['class']} {$b['class']}",
-                               -1, PREG_SPLIT_NO_EMPTY );
-                       $out['class'] = implode( ' ', array_unique( $classes ) );
-               }
-               return $out;
-       }
-
-       /**
-        * Normalize CSS into a format we can easily search for hostile input
-        *  - decode character references
-        *  - decode escape sequences
-        *  - convert characters that IE6 interprets into ascii
-        *  - remove comments, unless the entire value is one single comment
-        * @param string $value the css string
-        * @return string normalized css
-        */
-       public static function normalizeCss( $value ) {
-               // Decode character references like &#123;
-               $value = self::decodeCharReferences( $value );
-
-               // Decode escape sequences and line continuation
-               // See the grammar in the CSS 2 spec, appendix D.
-               // This has to be done AFTER decoding character references.
-               // This means it isn't possible for this function to return
-               // unsanitized escape sequences. It is possible to manufacture
-               // input that contains character references that decode to
-               // escape sequences that decode to character references, but
-               // it's OK for the return value to contain character references
-               // because the caller is supposed to escape those anyway.
-               static $decodeRegex;
-               if ( !$decodeRegex ) {
-                       $space = '[\\x20\\t\\r\\n\\f]';
-                       $nl = '(?:\\n|\\r\\n|\\r|\\f)';
-                       $backslash = '\\\\';
-                       $decodeRegex = "/ $backslash
-                               (?:
-                                       ($nl) |  # 1. Line continuation
-                                       ([0-9A-Fa-f]{1,6})$space? |  # 2. character number
-                                       (.) | # 3. backslash cancelling special meaning
-                                       () | # 4. backslash at end of string
-                               )/xu";
-               }
-               $value = preg_replace_callback( $decodeRegex,
-                       [ __CLASS__, 'cssDecodeCallback' ], $value );
-
-               // Normalize Halfwidth and Fullwidth Unicode block that IE6 might treat as ascii
-               $value = preg_replace_callback(
-                       '/[!-[]-z]/u', // U+FF01 to U+FF5A, excluding U+FF3C (T60088)
-                       function ( $matches ) {
-                               $cp = UtfNormal\Utils::utf8ToCodepoint( $matches[0] );
-                               if ( $cp === false ) {
-                                       return '';
-                               }
-                               return chr( $cp - 65248 ); // ASCII range \x21-\x7A
-                       },
-                       $value
-               );
-
-               // Convert more characters IE6 might treat as ascii
-               // U+0280, U+0274, U+207F, U+029F, U+026A, U+207D, U+208D
-               $value = str_replace(
-                       [ 'ʀ', 'ɴ', 'ⁿ', 'ʟ', 'ɪ', '⁽', '₍' ],
-                       [ 'r', 'n', 'n', 'l', 'i', '(', '(' ],
-                       $value
-               );
-
-               // Let the value through if it's nothing but a single comment, to
-               // allow other functions which may reject it to pass some error
-               // message through.
-               if ( !preg_match( '! ^ \s* /\* [^*\\/]* \*/ \s* $ !x', $value ) ) {
-                       // Remove any comments; IE gets token splitting wrong
-                       // This must be done AFTER decoding character references and
-                       // escape sequences, because those steps can introduce comments
-                       // This step cannot introduce character references or escape
-                       // sequences, because it replaces comments with spaces rather
-                       // than removing them completely.
-                       $value = StringUtils::delimiterReplace( '/*', '*/', ' ', $value );
-
-                       // Remove anything after a comment-start token, to guard against
-                       // incorrect client implementations.
-                       $commentPos = strpos( $value, '/*' );
-                       if ( $commentPos !== false ) {
-                               $value = substr( $value, 0, $commentPos );
-                       }
-               }
-
-               // S followed by repeat, iteration, or prolonged sound marks,
-               // which IE will treat as "ss"
-               $value = preg_replace(
-                       '/s(?:
-                               \xE3\x80\xB1 | # U+3031
-                               \xE3\x82\x9D | # U+309D
-                               \xE3\x83\xBC | # U+30FC
-                               \xE3\x83\xBD | # U+30FD
-                               \xEF\xB9\xBC | # U+FE7C
-                               \xEF\xB9\xBD | # U+FE7D
-                               \xEF\xBD\xB0   # U+FF70
-                       )/ix',
-                       'ss',
-                       $value
-               );
-
-               return $value;
-       }
-
-       /**
-        * Pick apart some CSS and check it for forbidden or unsafe structures.
-        * Returns a sanitized string. This sanitized string will have
-        * character references and escape sequences decoded and comments
-        * stripped (unless it is itself one valid comment, in which case the value
-        * will be passed through). If the input is just too evil, only a comment
-        * complaining about evilness will be returned.
-        *
-        * Currently URL references, 'expression', 'tps' are forbidden.
-        *
-        * NOTE: Despite the fact that character references are decoded, the
-        * returned string may contain character references given certain
-        * clever input strings. These character references must
-        * be escaped before the return value is embedded in HTML.
-        *
-        * @param string $value
-        * @return string
-        */
-       static function checkCss( $value ) {
-               $value = self::normalizeCss( $value );
-
-               // Reject problematic keywords and control characters
-               if ( preg_match( '/[\000-\010\013\016-\037\177]/', $value ) ||
-                       strpos( $value, UtfNormal\Constants::UTF8_REPLACEMENT ) !== false ) {
-                       return '/* invalid control char */';
-               } elseif ( preg_match(
-                       '! expression
-                               | filter\s*:
-                               | accelerator\s*:
-                               | -o-link\s*:
-                               | -o-link-source\s*:
-                               | -o-replace\s*:
-                               | url\s*\(
-                               | image\s*\(
-                               | image-set\s*\(
-                               | attr\s*\([^)]+[\s,]+url
-                       !ix', $value ) ) {
-                       return '/* insecure input */';
-               }
-               return $value;
-       }
-
-       /**
-        * @param array $matches
-        * @return string
-        */
-       static function cssDecodeCallback( $matches ) {
-               if ( $matches[1] !== '' ) {
-                       // Line continuation
-                       return '';
-               } elseif ( $matches[2] !== '' ) {
-                       $char = UtfNormal\Utils::codepointToUtf8( hexdec( $matches[2] ) );
-               } elseif ( $matches[3] !== '' ) {
-                       $char = $matches[3];
-               } else {
-                       $char = '\\';
-               }
-               if ( $char == "\n" || $char == '"' || $char == "'" || $char == '\\' ) {
-                       // These characters need to be escaped in strings
-                       // Clean up the escape sequence to avoid parsing errors by clients
-                       return '\\' . dechex( ord( $char ) ) . ' ';
-               } else {
-                       // Decode unnecessary escape
-                       return $char;
-               }
-       }
-
-       /**
-        * Take a tag soup fragment listing an HTML element's attributes
-        * and normalize it to well-formed XML, discarding unwanted attributes.
-        * Output is safe for further wikitext processing, with escaping of
-        * values that could trigger problems.
-        *
-        * - Normalizes attribute names to lowercase
-        * - Discards attributes not on a whitelist for the given element
-        * - Turns broken or invalid entities into plaintext
-        * - Double-quotes all attribute values
-        * - Attributes without values are given the name as attribute
-        * - Double attributes are discarded
-        * - Unsafe style attributes are discarded
-        * - Prepends space if there are attributes.
-        * - (Optionally) Sorts attributes by name.
-        *
-        * @param string $text
-        * @param string $element
-        * @param bool $sorted Whether to sort the attributes (default: false)
-        * @return string
-        */
-       static function fixTagAttributes( $text, $element, $sorted = false ) {
-               if ( trim( $text ) == '' ) {
-                       return '';
-               }
-
-               $decoded = self::decodeTagAttributes( $text );
-               $stripped = self::validateTagAttributes( $decoded, $element );
-
-               if ( $sorted ) {
-                       ksort( $stripped );
-               }
-
-               return self::safeEncodeTagAttributes( $stripped );
-       }
-
-       /**
-        * Encode an attribute value for HTML output.
-        * @param string $text
-        * @return string HTML-encoded text fragment
-        */
-       static function encodeAttribute( $text ) {
-               $encValue = htmlspecialchars( $text, ENT_QUOTES );
-
-               // Whitespace is normalized during attribute decoding,
-               // so if we've been passed non-spaces we must encode them
-               // ahead of time or they won't be preserved.
-               $encValue = strtr( $encValue, [
-                       "\n" => '&#10;',
-                       "\r" => '&#13;',
-                       "\t" => '&#9;',
-               ] );
-
-               return $encValue;
-       }
-
-       /**
-        * Encode an attribute value for HTML tags, with extra armoring
-        * against further wiki processing.
-        * @param string $text
-        * @return string HTML-encoded text fragment
-        */
-       static function safeEncodeAttribute( $text ) {
-               $encValue = self::encodeAttribute( $text );
-
-               # Templates and links may be expanded in later parsing,
-               # creating invalid or dangerous output. Suppress this.
-               $encValue = strtr( $encValue, [
-                       '<'    => '&lt;',   // This should never happen,
-                       '>'    => '&gt;',   // we've received invalid input
-                       '"'    => '&quot;', // which should have been escaped.
-                       '{'    => '&#123;',
-                       '}'    => '&#125;', // prevent unpaired language conversion syntax
-                       '['    => '&#91;',
-                       "''"   => '&#39;&#39;',
-                       'ISBN' => '&#73;SBN',
-                       'RFC'  => '&#82;FC',
-                       'PMID' => '&#80;MID',
-                       '|'    => '&#124;',
-                       '__'   => '&#95;_',
-               ] );
-
-               # Stupid hack
-               $encValue = preg_replace_callback(
-                       '/((?i)' . wfUrlProtocols() . ')/',
-                       [ 'Sanitizer', 'armorLinksCallback' ],
-                       $encValue );
-               return $encValue;
-       }
-
-       /**
-        * Given a value, escape it so that it can be used in an id attribute and
-        * return it.  This will use HTML5 validation if $wgExperimentalHtmlIds is
-        * true, allowing anything but ASCII whitespace.  Otherwise it will use
-        * HTML 4 rules, which means a narrow subset of ASCII, with bad characters
-        * escaped with lots of dots.
-        *
-        * To ensure we don't have to bother escaping anything, we also strip ', ",
-        * & even if $wgExperimentalIds is true.  TODO: Is this the best tactic?
-        * We also strip # because it upsets IE, and % because it could be
-        * ambiguous if it's part of something that looks like a percent escape
-        * (which don't work reliably in fragments cross-browser).
-        *
-        * @deprecated since 1.30, use one of this class' escapeIdFor*() functions
-        *
-        * @see https://www.w3.org/TR/html401/types.html#type-name Valid characters
-        *   in the id and name attributes
-        * @see https://www.w3.org/TR/html401/struct/links.html#h-12.2.3 Anchors with
-        *   the id attribute
-        * @see https://www.w3.org/TR/html5/dom.html#the-id-attribute
-        *   HTML5 definition of id attribute
-        *
-        * @param string $id Id to escape
-        * @param string|array $options String or array of strings (default is array()):
-        *   'noninitial': This is a non-initial fragment of an id, not a full id,
-        *       so don't pay attention if the first character isn't valid at the
-        *       beginning of an id.  Only matters if $wgExperimentalHtmlIds is
-        *       false.
-        *   'legacy': Behave the way the old HTML 4-based ID escaping worked even
-        *       if $wgExperimentalHtmlIds is used, so we can generate extra
-        *       anchors and links won't break.
-        * @return string
-        */
-       static function escapeId( $id, $options = [] ) {
-               global $wgExperimentalHtmlIds;
-               $options = (array)$options;
-
-               if ( $wgExperimentalHtmlIds && !in_array( 'legacy', $options ) ) {
-                       $id = preg_replace( '/[ \t\n\r\f_\'"&#%]+/', '_', $id );
-                       $id = trim( $id, '_' );
-                       if ( $id === '' ) {
-                               // Must have been all whitespace to start with.
-                               return '_';
-                       } else {
-                               return $id;
-                       }
-               }
-
-               // HTML4-style escaping
-               static $replace = [
-                       '%3A' => ':',
-                       '%' => '.'
-               ];
-
-               $id = urlencode( strtr( $id, ' ', '_' ) );
-               $id = strtr( $id, $replace );
-
-               if ( !preg_match( '/^[a-zA-Z]/', $id ) && !in_array( 'noninitial', $options ) ) {
-                       // Initial character must be a letter!
-                       $id = "x$id";
-               }
-               return $id;
-       }
-
-       /**
-        * Given a section name or other user-generated or otherwise unsafe string, escapes it to be
-        * a valid HTML id attribute.
-        *
-        * WARNING: unlike escapeId(), the output of this function is not guaranteed to be HTML safe,
-        * be sure to use proper escaping.
-        *
-        * @param string $id String to escape
-        * @param int $mode One of ID_* constants, specifying whether the primary or fallback encoding
-        *     should be used.
-        * @return string|bool Escaped ID or false if fallback encoding is requested but it's not
-        *     configured.
-        *
-        * @since 1.30
-        */
-       public static function escapeIdForAttribute( $id, $mode = self::ID_PRIMARY ) {
-               global $wgFragmentMode;
-
-               if ( !isset( $wgFragmentMode[$mode] ) ) {
-                       if ( $mode === self::ID_PRIMARY ) {
-                               throw new UnexpectedValueException( '$wgFragmentMode is configured with no primary mode' );
-                       }
-                       return false;
-               }
-
-               $internalMode = $wgFragmentMode[$mode];
-
-               return self::escapeIdInternal( $id, $internalMode );
-       }
-
-       /**
-        * Given a section name or other user-generated or otherwise unsafe string, escapes it to be
-        * a valid URL fragment.
-        *
-        * WARNING: unlike escapeId(), the output of this function is not guaranteed to be HTML safe,
-        * be sure to use proper escaping.
-        *
-        * @param string $id String to escape
-        * @return string Escaped ID
-        *
-        * @since 1.30
-        */
-       public static function escapeIdForLink( $id ) {
-               global $wgFragmentMode;
-
-               if ( !isset( $wgFragmentMode[self::ID_PRIMARY] ) ) {
-                       throw new UnexpectedValueException( '$wgFragmentMode is configured with no primary mode' );
-               }
-
-               $mode = $wgFragmentMode[self::ID_PRIMARY];
-
-               $id = self::escapeIdInternal( $id, $mode );
-
-               return $id;
-       }
-
-       /**
-        * Given a section name or other user-generated or otherwise unsafe string, escapes it to be
-        * a valid URL fragment for external interwikis.
-        *
-        * @param string $id String to escape
-        * @return string Escaped ID
-        *
-        * @since 1.30
-        */
-       public static function escapeIdForExternalInterwiki( $id ) {
-               global $wgExternalInterwikiFragmentMode;
-
-               $id = self::escapeIdInternal( $id, $wgExternalInterwikiFragmentMode );
-
-               return $id;
-       }
-
-       /**
-        * Helper for escapeIdFor*() functions. Performs most of the actual escaping.
-        *
-        * @param string $id String to escape
-        * @param string $mode One of modes from $wgFragmentMode
-        * @return string
-        */
-       private static function escapeIdInternal( $id, $mode ) {
-               switch ( $mode ) {
-                       case 'html5':
-                               $id = str_replace( ' ', '_', $id );
-                               break;
-                       case 'legacy':
-                               // This corresponds to 'noninitial' mode of the old escapeId()
-                               static $replace = [
-                                       '%3A' => ':',
-                                       '%' => '.'
-                               ];
-
-                               $id = urlencode( str_replace( ' ', '_', $id ) );
-                               $id = strtr( $id, $replace );
-                               break;
-                       case 'html5-legacy':
-                               $id = preg_replace( '/[ \t\n\r\f_\'"&#%]+/', '_', $id );
-                               $id = trim( $id, '_' );
-                               if ( $id === '' ) {
-                                       // Must have been all whitespace to start with.
-                                       $id = '_';
-                               }
-                               break;
-                       default:
-                               throw new InvalidArgumentException( "Invalid mode '$mode' passed to '" . __METHOD__ );
-               }
-
-               return $id;
-       }
-
-       /**
-        * Given a string containing a space delimited list of ids, escape each id
-        * to match ids escaped by the escapeId() function.
-        *
-        * @todo remove $options completely in 1.32
-        *
-        * @since 1.27
-        *
-        * @param string $referenceString Space delimited list of ids
-        * @param string|array $options Deprecated and does nothing.
-        * @return string
-        */
-       static function escapeIdReferenceList( $referenceString, $options = [] ) {
-               if ( $options ) {
-                       wfDeprecated( __METHOD__ . ' with $options', '1.31' );
-               }
-               # Explode the space delimited list string into an array of tokens
-               $references = preg_split( '/\s+/', "{$referenceString}", -1, PREG_SPLIT_NO_EMPTY );
-
-               # Escape each token as an id
-               foreach ( $references as &$ref ) {
-                       $ref = self::escapeIdForAttribute( $ref );
-               }
-
-               # Merge the array back to a space delimited list string
-               # If the array is empty, the result will be an empty string ('')
-               $referenceString = implode( ' ', $references );
-
-               return $referenceString;
-       }
-
-       /**
-        * Given a value, escape it so that it can be used as a CSS class and
-        * return it.
-        *
-        * @todo For extra validity, input should be validated UTF-8.
-        *
-        * @see https://www.w3.org/TR/CSS21/syndata.html Valid characters/format
-        *
-        * @param string $class
-        * @return string
-        */
-       static function escapeClass( $class ) {
-               // Convert ugly stuff to underscores and kill underscores in ugly places
-               return rtrim( preg_replace(
-                       [ '/(^[0-9\\-])|[\\x00-\\x20!"#$%&\'()*+,.\\/:;<=>?@[\\]^`{|}~]|\\xC2\\xA0/', '/_+/' ],
-                       '_',
-                       $class ), '_' );
-       }
-
-       /**
-        * Given HTML input, escape with htmlspecialchars but un-escape entities.
-        * This allows (generally harmless) entities like &#160; to survive.
-        *
-        * @param string $html HTML to escape
-        * @return string Escaped input
-        */
-       static function escapeHtmlAllowEntities( $html ) {
-               $html = self::decodeCharReferences( $html );
-               # It seems wise to escape ' as well as ", as a matter of course.  Can't
-               # hurt. Use ENT_SUBSTITUTE so that incorrectly truncated multibyte characters
-               # don't cause the entire string to disappear.
-               $html = htmlspecialchars( $html, ENT_QUOTES | ENT_SUBSTITUTE );
-               return $html;
-       }
-
-       /**
-        * Regex replace callback for armoring links against further processing.
-        * @param array $matches
-        * @return string
-        */
-       private static function armorLinksCallback( $matches ) {
-               return str_replace( ':', '&#58;', $matches[1] );
-       }
-
-       /**
-        * Return an associative array of attribute names and values from
-        * a partial tag string. Attribute names are forced to lowercase,
-        * character references are decoded to UTF-8 text.
-        *
-        * @param string $text
-        * @return array
-        */
-       public static function decodeTagAttributes( $text ) {
-               if ( trim( $text ) == '' ) {
-                       return [];
-               }
-
-               $attribs = [];
-               $pairs = [];
-               if ( !preg_match_all(
-                       self::getAttribsRegex(),
-                       $text,
-                       $pairs,
-                       PREG_SET_ORDER ) ) {
-                       return $attribs;
-               }
-
-               foreach ( $pairs as $set ) {
-                       $attribute = strtolower( $set[1] );
-                       $value = self::getTagAttributeCallback( $set );
-
-                       // Normalize whitespace
-                       $value = preg_replace( '/[\t\r\n ]+/', ' ', $value );
-                       $value = trim( $value );
-
-                       // Decode character references
-                       $attribs[$attribute] = self::decodeCharReferences( $value );
-               }
-               return $attribs;
-       }
-
-       /**
-        * Build a partial tag string from an associative array of attribute
-        * names and values as returned by decodeTagAttributes.
-        *
-        * @param array $assoc_array
-        * @return string
-        */
-       public static function safeEncodeTagAttributes( $assoc_array ) {
-               $attribs = [];
-               foreach ( $assoc_array as $attribute => $value ) {
-                       $encAttribute = htmlspecialchars( $attribute );
-                       $encValue = self::safeEncodeAttribute( $value );
-
-                       $attribs[] = "$encAttribute=\"$encValue\"";
-               }
-               return count( $attribs ) ? ' ' . implode( ' ', $attribs ) : '';
-       }
-
-       /**
-        * Pick the appropriate attribute value from a match set from the
-        * attribs regex matches.
-        *
-        * @param array $set
-        * @throws MWException When tag conditions are not met.
-        * @return string
-        */
-       private static function getTagAttributeCallback( $set ) {
-               if ( isset( $set[5] ) ) {
-                       # No quotes.
-                       return $set[5];
-               } elseif ( isset( $set[4] ) ) {
-                       # Single-quoted
-                       return $set[4];
-               } elseif ( isset( $set[3] ) ) {
-                       # Double-quoted
-                       return $set[3];
-               } elseif ( !isset( $set[2] ) ) {
-                       # In XHTML, attributes must have a value so return an empty string.
-                       # See "Empty attribute syntax",
-                       # https://www.w3.org/TR/html5/syntax.html#syntax-attribute-name
-                       return "";
-               } else {
-                       throw new MWException( "Tag conditions not met. This should never happen and is a bug." );
-               }
-       }
-
-       /**
-        * @param string $text
-        * @return string
-        */
-       private static function normalizeWhitespace( $text ) {
-               return preg_replace(
-                       '/\r\n|[\x20\x0d\x0a\x09]/',
-                       ' ',
-                       $text );
-       }
-
-       /**
-        * Normalizes whitespace in a section name, such as might be returned
-        * by Parser::stripSectionName(), for use in the id's that are used for
-        * section links.
-        *
-        * @param string $section
-        * @return string
-        */
-       static function normalizeSectionNameWhitespace( $section ) {
-               return trim( preg_replace( '/[ _]+/', ' ', $section ) );
-       }
-
-       /**
-        * Ensure that any entities and character references are legal
-        * for XML and XHTML specifically. Any stray bits will be
-        * &amp;-escaped to result in a valid text fragment.
-        *
-        * a. named char refs can only be &lt; &gt; &amp; &quot;, others are
-        *   numericized (this way we're well-formed even without a DTD)
-        * b. any numeric char refs must be legal chars, not invalid or forbidden
-        * c. use lower cased "&#x", not "&#X"
-        * d. fix or reject non-valid attributes
-        *
-        * @param string $text
-        * @return string
-        * @private
-        */
-       static function normalizeCharReferences( $text ) {
-               return preg_replace_callback(
-                       self::CHAR_REFS_REGEX,
-                       [ 'Sanitizer', 'normalizeCharReferencesCallback' ],
-                       $text );
-       }
-
-       /**
-        * @param string $matches
-        * @return string
-        */
-       static function normalizeCharReferencesCallback( $matches ) {
-               $ret = null;
-               if ( $matches[1] != '' ) {
-                       $ret = self::normalizeEntity( $matches[1] );
-               } elseif ( $matches[2] != '' ) {
-                       $ret = self::decCharReference( $matches[2] );
-               } elseif ( $matches[3] != '' ) {
-                       $ret = self::hexCharReference( $matches[3] );
-               }
-               if ( is_null( $ret ) ) {
-                       return htmlspecialchars( $matches[0] );
-               } else {
-                       return $ret;
-               }
-       }
-
-       /**
-        * If the named entity is defined in the HTML 4.0/XHTML 1.0 DTD,
-        * return the equivalent numeric entity reference (except for the core &lt;
-        * &gt; &amp; &quot;). If the entity is a MediaWiki-specific alias, returns
-        * the HTML equivalent. Otherwise, returns HTML-escaped text of
-        * pseudo-entity source (eg &amp;foo;)
-        *
-        * @param string $name
-        * @return string
-        */
-       static function normalizeEntity( $name ) {
-               if ( isset( self::$htmlEntityAliases[$name] ) ) {
-                       return '&' . self::$htmlEntityAliases[$name] . ';';
-               } elseif ( in_array( $name, [ 'lt', 'gt', 'amp', 'quot' ] ) ) {
-                       return "&$name;";
-               } elseif ( isset( self::$htmlEntities[$name] ) ) {
-                       return '&#' . self::$htmlEntities[$name] . ';';
-               } else {
-                       return "&amp;$name;";
-               }
-       }
-
-       /**
-        * @param int $codepoint
-        * @return null|string
-        */
-       static function decCharReference( $codepoint ) {
-               $point = intval( $codepoint );
-               if ( self::validateCodepoint( $point ) ) {
-                       return sprintf( '&#%d;', $point );
-               } else {
-                       return null;
-               }
-       }
-
-       /**
-        * @param int $codepoint
-        * @return null|string
-        */
-       static function hexCharReference( $codepoint ) {
-               $point = hexdec( $codepoint );
-               if ( self::validateCodepoint( $point ) ) {
-                       return sprintf( '&#x%x;', $point );
-               } else {
-                       return null;
-               }
-       }
-
-       /**
-        * Returns true if a given Unicode codepoint is a valid character in
-        * both HTML5 and XML.
-        * @param int $codepoint
-        * @return bool
-        */
-       private static function validateCodepoint( $codepoint ) {
-               # U+000C is valid in HTML5 but not allowed in XML.
-               # U+000D is valid in XML but not allowed in HTML5.
-               # U+007F - U+009F are disallowed in HTML5 (control characters).
-               return $codepoint == 0x09
-                       || $codepoint == 0x0a
-                       || ( $codepoint >= 0x20 && $codepoint <= 0x7e )
-                       || ( $codepoint >= 0xa0 && $codepoint <= 0xd7ff )
-                       || ( $codepoint >= 0xe000 && $codepoint <= 0xfffd )
-                       || ( $codepoint >= 0x10000 && $codepoint <= 0x10ffff );
-       }
-
-       /**
-        * Decode any character references, numeric or named entities,
-        * in the text and return a UTF-8 string.
-        *
-        * @param string $text
-        * @return string
-        */
-       public static function decodeCharReferences( $text ) {
-               return preg_replace_callback(
-                       self::CHAR_REFS_REGEX,
-                       [ 'Sanitizer', 'decodeCharReferencesCallback' ],
-                       $text );
-       }
-
-       /**
-        * Decode any character references, numeric or named entities,
-        * in the next and normalize the resulting string. (T16952)
-        *
-        * This is useful for page titles, not for text to be displayed,
-        * MediaWiki allows HTML entities to escape normalization as a feature.
-        *
-        * @param string $text Already normalized, containing entities
-        * @return string Still normalized, without entities
-        */
-       public static function decodeCharReferencesAndNormalize( $text ) {
-               global $wgContLang;
-               $text = preg_replace_callback(
-                       self::CHAR_REFS_REGEX,
-                       [ 'Sanitizer', 'decodeCharReferencesCallback' ],
-                       $text,
-                       -1, //limit
-                       $count
-               );
-
-               if ( $count ) {
-                       return $wgContLang->normalize( $text );
-               } else {
-                       return $text;
-               }
-       }
-
-       /**
-        * @param string $matches
-        * @return string
-        */
-       static function decodeCharReferencesCallback( $matches ) {
-               if ( $matches[1] != '' ) {
-                       return self::decodeEntity( $matches[1] );
-               } elseif ( $matches[2] != '' ) {
-                       return self::decodeChar( intval( $matches[2] ) );
-               } elseif ( $matches[3] != '' ) {
-                       return self::decodeChar( hexdec( $matches[3] ) );
-               }
-               # Last case should be an ampersand by itself
-               return $matches[0];
-       }
-
-       /**
-        * Return UTF-8 string for a codepoint if that is a valid
-        * character reference, otherwise U+FFFD REPLACEMENT CHARACTER.
-        * @param int $codepoint
-        * @return string
-        * @private
-        */
-       static function decodeChar( $codepoint ) {
-               if ( self::validateCodepoint( $codepoint ) ) {
-                       return UtfNormal\Utils::codepointToUtf8( $codepoint );
-               } else {
-                       return UtfNormal\Constants::UTF8_REPLACEMENT;
-               }
-       }
-
-       /**
-        * If the named entity is defined in the HTML 4.0/XHTML 1.0 DTD,
-        * return the UTF-8 encoding of that character. Otherwise, returns
-        * pseudo-entity source (eg "&foo;")
-        *
-        * @param string $name
-        * @return string
-        */
-       static function decodeEntity( $name ) {
-               if ( isset( self::$htmlEntityAliases[$name] ) ) {
-                       $name = self::$htmlEntityAliases[$name];
-               }
-               if ( isset( self::$htmlEntities[$name] ) ) {
-                       return UtfNormal\Utils::codepointToUtf8( self::$htmlEntities[$name] );
-               } else {
-                       return "&$name;";
-               }
-       }
-
-       /**
-        * Fetch the whitelist of acceptable attributes for a given element name.
-        *
-        * @param string $element
-        * @return array
-        */
-       static function attributeWhitelist( $element ) {
-               $list = self::setupAttributeWhitelist();
-               return isset( $list[$element] )
-                       ? $list[$element]
-                       : [];
-       }
-
-       /**
-        * Foreach array key (an allowed HTML element), return an array
-        * of allowed attributes
-        * @return array
-        */
-       static function setupAttributeWhitelist() {
-               static $whitelist;
-
-               if ( $whitelist !== null ) {
-                       return $whitelist;
-               }
-
-               $common = [
-                       # HTML
-                       'id',
-                       'class',
-                       'style',
-                       'lang',
-                       'dir',
-                       'title',
-
-                       # WAI-ARIA
-                       'aria-describedby',
-                       'aria-flowto',
-                       'aria-label',
-                       'aria-labelledby',
-                       'aria-owns',
-                       'role',
-
-                       # RDFa
-                       # These attributes are specified in section 9 of
-                       # https://www.w3.org/TR/2008/REC-rdfa-syntax-20081014
-                       'about',
-                       'property',
-                       'resource',
-                       'datatype',
-                       'typeof',
-
-                       # Microdata. These are specified by
-                       # https://html.spec.whatwg.org/multipage/microdata.html#the-microdata-model
-                       'itemid',
-                       'itemprop',
-                       'itemref',
-                       'itemscope',
-                       'itemtype',
-               ];
-
-               $block = array_merge( $common, [ 'align' ] );
-               $tablealign = [ 'align', 'valign' ];
-               $tablecell = [
-                       'abbr',
-                       'axis',
-                       'headers',
-                       'scope',
-                       'rowspan',
-                       'colspan',
-                       'nowrap', # deprecated
-                       'width', # deprecated
-                       'height', # deprecated
-                       'bgcolor', # deprecated
-               ];
-
-               # Numbers refer to sections in HTML 4.01 standard describing the element.
-               # See: https://www.w3.org/TR/html4/
-               $whitelist = [
-                       # 7.5.4
-                       'div'        => $block,
-                       'center'     => $common, # deprecated
-                       'span'       => $common,
-
-                       # 7.5.5
-                       'h1'         => $block,
-                       'h2'         => $block,
-                       'h3'         => $block,
-                       'h4'         => $block,
-                       'h5'         => $block,
-                       'h6'         => $block,
-
-                       # 7.5.6
-                       # address
-
-                       # 8.2.4
-                       'bdo'        => $common,
-
-                       # 9.2.1
-                       'em'         => $common,
-                       'strong'     => $common,
-                       'cite'       => $common,
-                       'dfn'        => $common,
-                       'code'       => $common,
-                       'samp'       => $common,
-                       'kbd'        => $common,
-                       'var'        => $common,
-                       'abbr'       => $common,
-                       # acronym
-
-                       # 9.2.2
-                       'blockquote' => array_merge( $common, [ 'cite' ] ),
-                       'q'          => array_merge( $common, [ 'cite' ] ),
-
-                       # 9.2.3
-                       'sub'        => $common,
-                       'sup'        => $common,
-
-                       # 9.3.1
-                       'p'          => $block,
-
-                       # 9.3.2
-                       'br'         => array_merge( $common, [ 'clear' ] ),
-
-                       # https://www.w3.org/TR/html5/text-level-semantics.html#the-wbr-element
-                       'wbr'        => $common,
-
-                       # 9.3.4
-                       'pre'        => array_merge( $common, [ 'width' ] ),
-
-                       # 9.4
-                       'ins'        => array_merge( $common, [ 'cite', 'datetime' ] ),
-                       'del'        => array_merge( $common, [ 'cite', 'datetime' ] ),
-
-                       # 10.2
-                       'ul'         => array_merge( $common, [ 'type' ] ),
-                       'ol'         => array_merge( $common, [ 'type', 'start', 'reversed' ] ),
-                       'li'         => array_merge( $common, [ 'type', 'value' ] ),
-
-                       # 10.3
-                       'dl'         => $common,
-                       'dd'         => $common,
-                       'dt'         => $common,
-
-                       # 11.2.1
-                       'table'      => array_merge( $common,
-                                                               [ 'summary', 'width', 'border', 'frame',
-                                                                               'rules', 'cellspacing', 'cellpadding',
-                                                                               'align', 'bgcolor',
-                                                               ] ),
-
-                       # 11.2.2
-                       'caption'    => $block,
-
-                       # 11.2.3
-                       'thead'      => $common,
-                       'tfoot'      => $common,
-                       'tbody'      => $common,
-
-                       # 11.2.4
-                       'colgroup'   => array_merge( $common, [ 'span' ] ),
-                       'col'        => array_merge( $common, [ 'span' ] ),
-
-                       # 11.2.5
-                       'tr'         => array_merge( $common, [ 'bgcolor' ], $tablealign ),
-
-                       # 11.2.6
-                       'td'         => array_merge( $common, $tablecell, $tablealign ),
-                       'th'         => array_merge( $common, $tablecell, $tablealign ),
-
-                       # 12.2
-                       # NOTE: <a> is not allowed directly, but the attrib
-                       # whitelist is used from the Parser object
-                       'a'          => array_merge( $common, [ 'href', 'rel', 'rev' ] ), # rel/rev esp. for RDFa
-
-                       # 13.2
-                       # Not usually allowed, but may be used for extension-style hooks
-                       # such as <math> when it is rasterized, or if $wgAllowImageTag is
-                       # true
-                       'img'        => array_merge( $common, [ 'alt', 'src', 'width', 'height', 'srcset' ] ),
-
-                       'video'      => array_merge( $common, [ 'poster', 'controls', 'preload', 'width', 'height' ] ),
-                       'source'     => array_merge( $common, [ 'type', 'src' ] ),
-                       'track'      => array_merge( $common, [ 'type', 'src', 'srclang', 'kind', 'label' ] ),
-
-                       # 15.2.1
-                       'tt'         => $common,
-                       'b'          => $common,
-                       'i'          => $common,
-                       'big'        => $common,
-                       'small'      => $common,
-                       'strike'     => $common,
-                       's'          => $common,
-                       'u'          => $common,
-
-                       # 15.2.2
-                       'font'       => array_merge( $common, [ 'size', 'color', 'face' ] ),
-                       # basefont
-
-                       # 15.3
-                       'hr'         => array_merge( $common, [ 'width' ] ),
-
-                       # HTML Ruby annotation text module, simple ruby only.
-                       # https://www.w3.org/TR/html5/text-level-semantics.html#the-ruby-element
-                       'ruby'       => $common,
-                       # rbc
-                       'rb'         => $common,
-                       'rp'         => $common,
-                       'rt'         => $common, # array_merge( $common, array( 'rbspan' ) ),
-                       'rtc'        => $common,
-
-                       # MathML root element, where used for extensions
-                       # 'title' may not be 100% valid here; it's XHTML
-                       # https://www.w3.org/TR/REC-MathML/
-                       'math'       => [ 'class', 'style', 'id', 'title' ],
-
-                       // HTML 5 section 4.5
-                       'figure'     => $common,
-                       'figcaption' => $common,
-
-                       # HTML 5 section 4.6
-                       'bdi' => $common,
-
-                       # HTML5 elements, defined by:
-                       # https://html.spec.whatwg.org/multipage/semantics.html#the-data-element
-                       'data' => array_merge( $common, [ 'value' ] ),
-                       'time' => array_merge( $common, [ 'datetime' ] ),
-                       'mark' => $common,
-
-                       // meta and link are only permitted by removeHTMLtags when Microdata
-                       // is enabled so we don't bother adding a conditional to hide these
-                       // Also meta and link are only valid in WikiText as Microdata elements
-                       // (ie: validateTag rejects tags missing the attributes needed for Microdata)
-                       // So we don't bother including $common attributes that have no purpose.
-                       'meta' => [ 'itemprop', 'content' ],
-                       'link' => [ 'itemprop', 'href', 'title' ],
-               ];
-
-               return $whitelist;
-       }
-
-       /**
-        * Take a fragment of (potentially invalid) HTML and return
-        * a version with any tags removed, encoded as plain text.
-        *
-        * Warning: this return value must be further escaped for literal
-        * inclusion in HTML output as of 1.10!
-        *
-        * @param string $text HTML fragment
-        * @return string
-        */
-       static function stripAllTags( $text ) {
-               # Actual <tags>
-               $text = StringUtils::delimiterReplace( '<', '>', '', $text );
-
-               # Normalize &entities and whitespace
-               $text = self::decodeCharReferences( $text );
-               $text = self::normalizeWhitespace( $text );
-
-               return $text;
-       }
-
-       /**
-        * Hack up a private DOCTYPE with HTML's standard entity declarations.
-        * PHP 4 seemed to know these if you gave it an HTML doctype, but
-        * PHP 5.1 doesn't.
-        *
-        * Use for passing XHTML fragments to PHP's XML parsing functions
-        *
-        * @return string
-        */
-       static function hackDocType() {
-               $out = "<!DOCTYPE html [\n";
-               foreach ( self::$htmlEntities as $entity => $codepoint ) {
-                       $out .= "<!ENTITY $entity \"&#$codepoint;\">";
-               }
-               $out .= "]>\n";
-               return $out;
-       }
-
-       /**
-        * @param string $url
-        * @return mixed|string
-        */
-       static function cleanUrl( $url ) {
-               # Normalize any HTML entities in input. They will be
-               # re-escaped by makeExternalLink().
-               $url = self::decodeCharReferences( $url );
-
-               # Escape any control characters introduced by the above step
-               $url = preg_replace_callback( '/[\][<>"\\x00-\\x20\\x7F\|]/',
-                       [ __CLASS__, 'cleanUrlCallback' ], $url );
-
-               # Validate hostname portion
-               $matches = [];
-               if ( preg_match( '!^([^:]+:)(//[^/]+)?(.*)$!iD', $url, $matches ) ) {
-                       list( /* $whole */, $protocol, $host, $rest ) = $matches;
-
-                       // Characters that will be ignored in IDNs.
-                       // https://tools.ietf.org/html/rfc3454#section-3.1
-                       // Strip them before further processing so blacklists and such work.
-                       $strip = "/
-                               \\s|          # general whitespace
-                               \xc2\xad|     # 00ad SOFT HYPHEN
-                               \xe1\xa0\x86| # 1806 MONGOLIAN TODO SOFT HYPHEN
-                               \xe2\x80\x8b| # 200b ZERO WIDTH SPACE
-                               \xe2\x81\xa0| # 2060 WORD JOINER
-                               \xef\xbb\xbf| # feff ZERO WIDTH NO-BREAK SPACE
-                               \xcd\x8f|     # 034f COMBINING GRAPHEME JOINER
-                               \xe1\xa0\x8b| # 180b MONGOLIAN FREE VARIATION SELECTOR ONE
-                               \xe1\xa0\x8c| # 180c MONGOLIAN FREE VARIATION SELECTOR TWO
-                               \xe1\xa0\x8d| # 180d MONGOLIAN FREE VARIATION SELECTOR THREE
-                               \xe2\x80\x8c| # 200c ZERO WIDTH NON-JOINER
-                               \xe2\x80\x8d| # 200d ZERO WIDTH JOINER
-                               [\xef\xb8\x80-\xef\xb8\x8f] # fe00-fe0f VARIATION SELECTOR-1-16
-                               /xuD";
-
-                       $host = preg_replace( $strip, '', $host );
-
-                       // IPv6 host names are bracketed with [].  Url-decode these.
-                       if ( substr_compare( "//%5B", $host, 0, 5 ) === 0 &&
-                               preg_match( '!^//%5B([0-9A-Fa-f:.]+)%5D((:\d+)?)$!', $host, $matches )
-                       ) {
-                               $host = '//[' . $matches[1] . ']' . $matches[2];
-                       }
-
-                       // @todo FIXME: Validate hostnames here
-
-                       return $protocol . $host . $rest;
-               } else {
-                       return $url;
-               }
-       }
-
-       /**
-        * @param array $matches
-        * @return string
-        */
-       static function cleanUrlCallback( $matches ) {
-               return urlencode( $matches[0] );
-       }
-
-       /**
-        * Does a string look like an e-mail address?
-        *
-        * This validates an email address using an HTML5 specification found at:
-        * http://www.whatwg.org/html/states-of-the-type-attribute.html#valid-e-mail-address
-        * Which as of 2011-01-24 says:
-        *
-        *   A valid e-mail address is a string that matches the ABNF production
-        *   1*( atext / "." ) "@" ldh-str *( "." ldh-str ) where atext is defined
-        *   in RFC 5322 section 3.2.3, and ldh-str is defined in RFC 1034 section
-        *   3.5.
-        *
-        * This function is an implementation of the specification as requested in
-        * T24449.
-        *
-        * Client-side forms will use the same standard validation rules via JS or
-        * HTML 5 validation; additional restrictions can be enforced server-side
-        * by extensions via the 'isValidEmailAddr' hook.
-        *
-        * Note that this validation doesn't 100% match RFC 2822, but is believed
-        * to be liberal enough for wide use. Some invalid addresses will still
-        * pass validation here.
-        *
-        * @since 1.18
-        *
-        * @param string $addr E-mail address
-        * @return bool
-        */
-       public static function validateEmail( $addr ) {
-               $result = null;
-               if ( !Hooks::run( 'isValidEmailAddr', [ $addr, &$result ] ) ) {
-                       return $result;
-               }
-
-               // Please note strings below are enclosed in brackets [], this make the
-               // hyphen "-" a range indicator. Hence it is double backslashed below.
-               // See T28948
-               $rfc5322_atext = "a-z0-9!#$%&'*+\\-\/=?^_`{|}~";
-               $rfc1034_ldh_str = "a-z0-9\\-";
-
-               $html5_email_regexp = "/
-               ^                      # start of string
-               [$rfc5322_atext\\.]+    # user part which is liberal :p
-               @                      # 'apostrophe'
-               [$rfc1034_ldh_str]+       # First domain part
-               (\\.[$rfc1034_ldh_str]+)*  # Following part prefixed with a dot
-               $                      # End of string
-               /ix"; // case Insensitive, eXtended
-
-               return (bool)preg_match( $html5_email_regexp, $addr );
-       }
-}
index 0496b67..ae88d37 100644 (file)
@@ -447,6 +447,14 @@ return [
                return $factory;
        },
 
+       'ExternalStoreFactory' => function ( MediaWikiServices $services ) {
+               $config = $services->getMainConfig();
+
+               return new ExternalStoreFactory(
+                       $config->get( 'ExternalStores' )
+               );
+       },
+
        ///////////////////////////////////////////////////////////////////////////
        // NOTE: When adding a service here, don't forget to add a getter function
        // in the MediaWikiServices class. The convenience getter should just call
index e4396ba..4c281b1 100644 (file)
@@ -37,8 +37,11 @@ if ( !defined( 'MEDIAWIKI' ) ) {
  * Pre-config setup: Before loading LocalSettings.php
  */
 
-// Grab profiling functions
-require_once "$IP/includes/profiler/ProfilerFunctions.php";
+// Get profiler configuraton
+$wgProfiler = [];
+if ( file_exists( "$IP/StartProfiler.php" ) ) {
+       require "$IP/StartProfiler.php";
+}
 
 // Start the autoloader, so that extensions can derive classes from core files
 require_once "$IP/includes/AutoLoader.php";
@@ -46,12 +49,6 @@ require_once "$IP/includes/AutoLoader.php";
 // Load up some global defines
 require_once "$IP/includes/Defines.php";
 
-// Start the profiler
-$wgProfiler = [];
-if ( file_exists( "$IP/StartProfiler.php" ) ) {
-       require "$IP/StartProfiler.php";
-}
-
 // Load default settings
 require_once "$IP/includes/DefaultSettings.php";
 
index 7a01a65..2d1d961 100644 (file)
@@ -556,7 +556,7 @@ class SiteConfiguration {
                                ]
                        );
                        // ulimit5.sh breaks this call
-                       $data = trim( wfShellExec( $cmd, $retVal, [], [ 'memory' => 0 ] ) );
+                       $data = trim( wfShellExec( $cmd, $retVal, [], [ 'memory' => 0, 'filesize' => 0 ] ) );
                        if ( $retVal != 0 || !strlen( $data ) ) {
                                throw new MWException( "Failed to run getConfiguration.php." );
                        }
diff --git a/includes/WatchedItem.php b/includes/WatchedItem.php
deleted file mode 100644 (file)
index bfd1d61..0000000
+++ /dev/null
@@ -1,200 +0,0 @@
-<?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 Watchlist
- */
-use MediaWiki\MediaWikiServices;
-use MediaWiki\Linker\LinkTarget;
-
-/**
- * Representation of a pair of user and title for watchlist entries.
- *
- * @author Tim Starling
- * @author Addshore
- *
- * @ingroup Watchlist
- */
-class WatchedItem {
-
-       /**
-        * @deprecated since 1.27, see User::IGNORE_USER_RIGHTS
-        */
-       const IGNORE_USER_RIGHTS = User::IGNORE_USER_RIGHTS;
-
-       /**
-        * @deprecated since 1.27, see User::CHECK_USER_RIGHTS
-        */
-       const CHECK_USER_RIGHTS = User::CHECK_USER_RIGHTS;
-
-       /**
-        * @deprecated Internal class use only
-        */
-       const DEPRECATED_USAGE_TIMESTAMP = -100;
-
-       /**
-        * @var bool
-        * @deprecated Internal class use only
-        */
-       public $checkRights = User::CHECK_USER_RIGHTS;
-
-       /**
-        * @var Title
-        * @deprecated Internal class use only
-        */
-       private $title;
-
-       /**
-        * @var LinkTarget
-        */
-       private $linkTarget;
-
-       /**
-        * @var User
-        */
-       private $user;
-
-       /**
-        * @var null|string the value of the wl_notificationtimestamp field
-        */
-       private $notificationTimestamp;
-
-       /**
-        * @param User $user
-        * @param LinkTarget $linkTarget
-        * @param null|string $notificationTimestamp the value of the wl_notificationtimestamp field
-        * @param bool|null $checkRights DO NOT USE - used internally for backward compatibility
-        */
-       public function __construct(
-               User $user,
-               LinkTarget $linkTarget,
-               $notificationTimestamp,
-               $checkRights = null
-       ) {
-               $this->user = $user;
-               $this->linkTarget = $linkTarget;
-               $this->notificationTimestamp = $notificationTimestamp;
-               if ( $checkRights !== null ) {
-                       $this->checkRights = $checkRights;
-               }
-       }
-
-       /**
-        * @return User
-        */
-       public function getUser() {
-               return $this->user;
-       }
-
-       /**
-        * @return LinkTarget
-        */
-       public function getLinkTarget() {
-               return $this->linkTarget;
-       }
-
-       /**
-        * Get the notification timestamp of this entry.
-        *
-        * @return bool|null|string
-        */
-       public function getNotificationTimestamp() {
-               // Back compat for objects constructed using self::fromUserTitle
-               if ( $this->notificationTimestamp === self::DEPRECATED_USAGE_TIMESTAMP ) {
-                       // wfDeprecated( __METHOD__, '1.27' );
-                       if ( $this->checkRights && !$this->user->isAllowed( 'viewmywatchlist' ) ) {
-                               return false;
-                       }
-                       $item = MediaWikiServices::getInstance()->getWatchedItemStore()
-                               ->loadWatchedItem( $this->user, $this->linkTarget );
-                       if ( $item ) {
-                               $this->notificationTimestamp = $item->getNotificationTimestamp();
-                       } else {
-                               $this->notificationTimestamp = false;
-                       }
-               }
-               return $this->notificationTimestamp;
-       }
-
-       /**
-        * Back compat pre 1.27 with the WatchedItemStore introduction
-        * @todo remove in 1.28/9
-        * -------------------------------------------------
-        */
-
-       /**
-        * @return Title
-        * @deprecated Internal class use only
-        */
-       public function getTitle() {
-               if ( !$this->title ) {
-                       $this->title = Title::newFromLinkTarget( $this->linkTarget );
-               }
-               return $this->title;
-       }
-
-       /**
-        * @deprecated since 1.27 Use the constructor, WatchedItemStore::getWatchedItem()
-        *             or WatchedItemStore::loadWatchedItem()
-        */
-       public static function fromUserTitle( $user, $title, $checkRights = User::CHECK_USER_RIGHTS ) {
-               wfDeprecated( __METHOD__, '1.27' );
-               return new self( $user, $title, self::DEPRECATED_USAGE_TIMESTAMP, (bool)$checkRights );
-       }
-
-       /**
-        * @deprecated since 1.27 Use User::addWatch()
-        * @return bool
-        */
-       public function addWatch() {
-               wfDeprecated( __METHOD__, '1.27' );
-               $this->user->addWatch( $this->getTitle(), $this->checkRights );
-               return true;
-       }
-
-       /**
-        * @deprecated since 1.27 Use User::removeWatch()
-        * @return bool
-        */
-       public function removeWatch() {
-               wfDeprecated( __METHOD__, '1.27' );
-               if ( $this->checkRights && !$this->user->isAllowed( 'editmywatchlist' ) ) {
-                       return false;
-               }
-               $this->user->removeWatch( $this->getTitle(), $this->checkRights );
-               return true;
-       }
-
-       /**
-        * @deprecated since 1.27 Use User::isWatched()
-        * @return bool
-        */
-       public function isWatched() {
-               wfDeprecated( __METHOD__, '1.27' );
-               return $this->user->isWatched( $this->getTitle(), $this->checkRights );
-       }
-
-       /**
-        * @deprecated since 1.27 Use WatchedItemStore::duplicateAllAssociatedEntries()
-        */
-       public static function duplicateEntries( Title $oldTitle, Title $newTitle ) {
-               wfDeprecated( __METHOD__, '1.27' );
-               $store = MediaWikiServices::getInstance()->getWatchedItemStore();
-               $store->duplicateAllAssociatedEntries( $oldTitle, $newTitle );
-       }
-
-}
diff --git a/includes/WatchedItemQueryService.php b/includes/WatchedItemQueryService.php
deleted file mode 100644 (file)
index d0f45be..0000000
+++ /dev/null
@@ -1,684 +0,0 @@
-<?php
-
-use Wikimedia\Rdbms\IDatabase;
-use MediaWiki\Linker\LinkTarget;
-use Wikimedia\Assert\Assert;
-use Wikimedia\Rdbms\LoadBalancer;
-
-/**
- * Class performing complex database queries related to WatchedItems.
- *
- * @since 1.28
- *
- * @file
- * @ingroup Watchlist
- *
- * @license GNU GPL v2+
- */
-class WatchedItemQueryService {
-
-       const DIR_OLDER = 'older';
-       const DIR_NEWER = 'newer';
-
-       const INCLUDE_FLAGS = 'flags';
-       const INCLUDE_USER = 'user';
-       const INCLUDE_USER_ID = 'userid';
-       const INCLUDE_COMMENT = 'comment';
-       const INCLUDE_PATROL_INFO = 'patrol';
-       const INCLUDE_SIZES = 'sizes';
-       const INCLUDE_LOG_INFO = 'loginfo';
-
-       // FILTER_* constants are part of public API (are used in ApiQueryWatchlist and
-       // ApiQueryWatchlistRaw classes) and should not be changed.
-       // Changing values of those constants will result in a breaking change in the API
-       const FILTER_MINOR = 'minor';
-       const FILTER_NOT_MINOR = '!minor';
-       const FILTER_BOT = 'bot';
-       const FILTER_NOT_BOT = '!bot';
-       const FILTER_ANON = 'anon';
-       const FILTER_NOT_ANON = '!anon';
-       const FILTER_PATROLLED = 'patrolled';
-       const FILTER_NOT_PATROLLED = '!patrolled';
-       const FILTER_UNREAD = 'unread';
-       const FILTER_NOT_UNREAD = '!unread';
-       const FILTER_CHANGED = 'changed';
-       const FILTER_NOT_CHANGED = '!changed';
-
-       const SORT_ASC = 'ASC';
-       const SORT_DESC = 'DESC';
-
-       /**
-        * @var LoadBalancer
-        */
-       private $loadBalancer;
-
-       /** @var WatchedItemQueryServiceExtension[]|null */
-       private $extensions = null;
-
-       /**
-        * @var CommentStore|null */
-       private $commentStore = null;
-
-       public function __construct( LoadBalancer $loadBalancer ) {
-               $this->loadBalancer = $loadBalancer;
-       }
-
-       /**
-        * @return WatchedItemQueryServiceExtension[]
-        */
-       private function getExtensions() {
-               if ( $this->extensions === null ) {
-                       $this->extensions = [];
-                       Hooks::run( 'WatchedItemQueryServiceExtensions', [ &$this->extensions, $this ] );
-               }
-               return $this->extensions;
-       }
-
-       /**
-        * @return IDatabase
-        * @throws MWException
-        */
-       private function getConnection() {
-               return $this->loadBalancer->getConnectionRef( DB_REPLICA, [ 'watchlist' ] );
-       }
-
-       private function getCommentStore() {
-               if ( !$this->commentStore ) {
-                       $this->commentStore = new CommentStore( 'rc_comment' );
-               }
-               return $this->commentStore;
-       }
-
-       /**
-        * @param User $user
-        * @param array $options Allowed keys:
-        *        'includeFields'       => string[] RecentChange fields to be included in the result,
-        *                                 self::INCLUDE_* constants should be used
-        *        'filters'             => string[] optional filters to narrow down resulted items
-        *        'namespaceIds'        => int[] optional namespace IDs to filter by
-        *                                 (defaults to all namespaces)
-        *        'allRevisions'        => bool return multiple revisions of the same page if true,
-        *                                 only the most recent if false (default)
-        *        'rcTypes'             => int[] which types of RecentChanges to include
-        *                                 (defaults to all types), allowed values: RC_EDIT, RC_NEW,
-        *                                 RC_LOG, RC_EXTERNAL, RC_CATEGORIZE
-        *        'onlyByUser'          => string only list changes by a specified user
-        *        'notByUser'           => string do not incluide changes by a specified user
-        *        'dir'                 => string in which direction to enumerate, accepted values:
-        *                                 - DIR_OLDER list newest first
-        *                                 - DIR_NEWER list oldest first
-        *        'start'               => string (format accepted by wfTimestamp) requires 'dir' option,
-        *                                 timestamp to start enumerating from
-        *        '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
-        *        'watchlistOwnerToken' => string a watchlist token used to access another user's
-        *                                 watchlist, used with 'watchlistOwnerToken' option
-        *        'limit'               => int maximum numbers of items to return
-        *        'usedInGenerator'     => bool include only RecentChange id field required by the
-        *                                 generator ('rc_cur_id' or 'rc_this_oldid') if true, or all
-        *                                 id fields ('rc_cur_id', 'rc_this_oldid', 'rc_last_oldid')
-        *                                 if false (default)
-        * @param array|null &$startFrom Continuation value: [ string $rcTimestamp, int $rcId ]
-        * @return array of pairs ( WatchedItem $watchedItem, string[] $recentChangeInfo ),
-        *         where $recentChangeInfo contains the following keys:
-        *         - 'rc_id',
-        *         - 'rc_namespace',
-        *         - 'rc_title',
-        *         - 'rc_timestamp',
-        *         - 'rc_type',
-        *         - 'rc_deleted',
-        *         Additional keys could be added by specifying the 'includeFields' option
-        */
-       public function getWatchedItemsWithRecentChangeInfo(
-               User $user, array $options = [], &$startFrom = null
-       ) {
-               $options += [
-                       'includeFields' => [],
-                       'namespaceIds' => [],
-                       'filters' => [],
-                       'allRevisions' => false,
-                       'usedInGenerator' => false
-               ];
-
-               Assert::parameter(
-                       !isset( $options['rcTypes'] )
-                               || !array_diff( $options['rcTypes'], [ RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL, RC_CATEGORIZE ] ),
-                       '$options[\'rcTypes\']',
-                       'must be an array containing only: RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL and/or RC_CATEGORIZE'
-               );
-               Assert::parameter(
-                       !isset( $options['dir'] ) || in_array( $options['dir'], [ self::DIR_OLDER, self::DIR_NEWER ] ),
-                       '$options[\'dir\']',
-                       'must be DIR_OLDER or DIR_NEWER'
-               );
-               Assert::parameter(
-                       !isset( $options['start'] ) && !isset( $options['end'] ) && $startFrom === null
-                               || isset( $options['dir'] ),
-                       '$options[\'dir\']',
-                       'must be provided when providing the "start" or "end" options or the $startFrom parameter'
-               );
-               Assert::parameter(
-                       !isset( $options['startFrom'] ),
-                       '$options[\'startFrom\']',
-                       'must not be provided, use $startFrom instead'
-               );
-               Assert::parameter(
-                       !isset( $startFrom ) || ( is_array( $startFrom ) && count( $startFrom ) === 2 ),
-                       '$startFrom',
-                       'must be a two-element array'
-               );
-               if ( array_key_exists( 'watchlistOwner', $options ) ) {
-                       Assert::parameterType(
-                               User::class,
-                               $options['watchlistOwner'],
-                               '$options[\'watchlistOwner\']'
-                       );
-                       Assert::parameter(
-                               isset( $options['watchlistOwnerToken'] ),
-                               '$options[\'watchlistOwnerToken\']',
-                               'must be provided when providing watchlistOwner option'
-                       );
-               }
-
-               $db = $this->getConnection();
-
-               $tables = $this->getWatchedItemsWithRCInfoQueryTables( $options );
-               $fields = $this->getWatchedItemsWithRCInfoQueryFields( $options );
-               $conds = $this->getWatchedItemsWithRCInfoQueryConds( $db, $user, $options );
-               $dbOptions = $this->getWatchedItemsWithRCInfoQueryDbOptions( $options );
-               $joinConds = $this->getWatchedItemsWithRCInfoQueryJoinConds( $options );
-
-               if ( $startFrom !== null ) {
-                       $conds[] = $this->getStartFromConds( $db, $options, $startFrom );
-               }
-
-               foreach ( $this->getExtensions() as $extension ) {
-                       $extension->modifyWatchedItemsWithRCInfoQuery(
-                               $user, $options, $db,
-                               $tables,
-                               $fields,
-                               $conds,
-                               $dbOptions,
-                               $joinConds
-                       );
-               }
-
-               $res = $db->select(
-                       $tables,
-                       $fields,
-                       $conds,
-                       __METHOD__,
-                       $dbOptions,
-                       $joinConds
-               );
-
-               $limit = isset( $dbOptions['LIMIT'] ) ? $dbOptions['LIMIT'] : INF;
-               $items = [];
-               $startFrom = null;
-               foreach ( $res as $row ) {
-                       if ( --$limit <= 0 ) {
-                               $startFrom = [ $row->rc_timestamp, $row->rc_id ];
-                               break;
-                       }
-
-                       $items[] = [
-                               new WatchedItem(
-                                       $user,
-                                       new TitleValue( (int)$row->rc_namespace, $row->rc_title ),
-                                       $row->wl_notificationtimestamp
-                               ),
-                               $this->getRecentChangeFieldsFromRow( $row )
-                       ];
-               }
-
-               foreach ( $this->getExtensions() as $extension ) {
-                       $extension->modifyWatchedItemsWithRCInfo( $user, $options, $db, $items, $res, $startFrom );
-               }
-
-               return $items;
-       }
-
-       /**
-        * For simple listing of user's watchlist items, see WatchedItemStore::getWatchedItemsForUser
-        *
-        * @param User $user
-        * @param array $options Allowed keys:
-        *        'sort'         => string optional sorting by namespace ID and title
-        *                          one of the self::SORT_* constants
-        *        'namespaceIds' => int[] optional namespace IDs to filter by (defaults to all namespaces)
-        *        'limit'        => int maximum number of items to return
-        *        'filter'       => string optional filter, one of the self::FILTER_* contants
-        *        'from'         => LinkTarget requires 'sort' key, only return items starting from
-        *                          those related to the link target
-        *        'until'        => LinkTarget requires 'sort' key, only return items until
-        *                          those related to the link target
-        *        'startFrom'    => LinkTarget requires 'sort' key, only return items starting from
-        *                          those related to the link target, allows to skip some link targets
-        *                          specified using the form option
-        * @return WatchedItem[]
-        */
-       public function getWatchedItemsForUser( User $user, array $options = [] ) {
-               if ( $user->isAnon() ) {
-                       // TODO: should this just return an empty array or rather complain loud at this point
-                       // as e.g. ApiBase::getWatchlistUser does?
-                       return [];
-               }
-
-               $options += [ 'namespaceIds' => [] ];
-
-               Assert::parameter(
-                       !isset( $options['sort'] ) || in_array( $options['sort'], [ self::SORT_ASC, self::SORT_DESC ] ),
-                       '$options[\'sort\']',
-                       'must be SORT_ASC or SORT_DESC'
-               );
-               Assert::parameter(
-                       !isset( $options['filter'] ) || in_array(
-                               $options['filter'], [ self::FILTER_CHANGED, self::FILTER_NOT_CHANGED ]
-                       ),
-                       '$options[\'filter\']',
-                       'must be FILTER_CHANGED or FILTER_NOT_CHANGED'
-               );
-               Assert::parameter(
-                       !isset( $options['from'] ) && !isset( $options['until'] ) && !isset( $options['startFrom'] )
-                       || isset( $options['sort'] ),
-                       '$options[\'sort\']',
-                       'must be provided if any of "from", "until", "startFrom" options is provided'
-               );
-
-               $db = $this->getConnection();
-
-               $conds = $this->getWatchedItemsForUserQueryConds( $db, $user, $options );
-               $dbOptions = $this->getWatchedItemsForUserQueryDbOptions( $options );
-
-               $res = $db->select(
-                       'watchlist',
-                       [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
-                       $conds,
-                       __METHOD__,
-                       $dbOptions
-               );
-
-               $watchedItems = [];
-               foreach ( $res as $row ) {
-                       // todo these could all be cached at some point?
-                       $watchedItems[] = new WatchedItem(
-                               $user,
-                               new TitleValue( (int)$row->wl_namespace, $row->wl_title ),
-                               $row->wl_notificationtimestamp
-                       );
-               }
-
-               return $watchedItems;
-       }
-
-       private function getRecentChangeFieldsFromRow( stdClass $row ) {
-               // This can be simplified to single array_filter call filtering by key value,
-               // once we stop supporting PHP 5.5
-               $allFields = get_object_vars( $row );
-               $rcKeys = array_filter(
-                       array_keys( $allFields ),
-                       function ( $key ) {
-                               return substr( $key, 0, 3 ) === 'rc_';
-                       }
-               );
-               return array_intersect_key( $allFields, array_flip( $rcKeys ) );
-       }
-
-       private function getWatchedItemsWithRCInfoQueryTables( array $options ) {
-               $tables = [ 'recentchanges', 'watchlist' ];
-               if ( !$options['allRevisions'] ) {
-                       $tables[] = 'page';
-               }
-               if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
-                       $tables += $this->getCommentStore()->getJoin()['tables'];
-               }
-               return $tables;
-       }
-
-       private function getWatchedItemsWithRCInfoQueryFields( array $options ) {
-               $fields = [
-                       'rc_id',
-                       'rc_namespace',
-                       'rc_title',
-                       'rc_timestamp',
-                       'rc_type',
-                       'rc_deleted',
-                       'wl_notificationtimestamp'
-               ];
-
-               $rcIdFields = [
-                       'rc_cur_id',
-                       'rc_this_oldid',
-                       'rc_last_oldid',
-               ];
-               if ( $options['usedInGenerator'] ) {
-                       if ( $options['allRevisions'] ) {
-                               $rcIdFields = [ 'rc_this_oldid' ];
-                       } else {
-                               $rcIdFields = [ 'rc_cur_id' ];
-                       }
-               }
-               $fields = array_merge( $fields, $rcIdFields );
-
-               if ( in_array( self::INCLUDE_FLAGS, $options['includeFields'] ) ) {
-                       $fields = array_merge( $fields, [ 'rc_type', 'rc_minor', 'rc_bot' ] );
-               }
-               if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ) {
-                       $fields[] = 'rc_user_text';
-               }
-               if ( in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ) {
-                       $fields[] = 'rc_user';
-               }
-               if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
-                       $fields += $this->getCommentStore()->getJoin()['fields'];
-               }
-               if ( in_array( self::INCLUDE_PATROL_INFO, $options['includeFields'] ) ) {
-                       $fields = array_merge( $fields, [ 'rc_patrolled', 'rc_log_type' ] );
-               }
-               if ( in_array( self::INCLUDE_SIZES, $options['includeFields'] ) ) {
-                       $fields = array_merge( $fields, [ 'rc_old_len', 'rc_new_len' ] );
-               }
-               if ( in_array( self::INCLUDE_LOG_INFO, $options['includeFields'] ) ) {
-                       $fields = array_merge( $fields, [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ] );
-               }
-
-               return $fields;
-       }
-
-       private function getWatchedItemsWithRCInfoQueryConds(
-               IDatabase $db,
-               User $user,
-               array $options
-       ) {
-               $watchlistOwnerId = $this->getWatchlistOwnerId( $user, $options );
-               $conds = [ 'wl_user' => $watchlistOwnerId ];
-
-               if ( !$options['allRevisions'] ) {
-                       $conds[] = $db->makeList(
-                               [ 'rc_this_oldid=page_latest', 'rc_type=' . RC_LOG ],
-                               LIST_OR
-                       );
-               }
-
-               if ( $options['namespaceIds'] ) {
-                       $conds['wl_namespace'] = array_map( 'intval', $options['namespaceIds'] );
-               }
-
-               if ( array_key_exists( 'rcTypes', $options ) ) {
-                       $conds['rc_type'] = array_map( 'intval', $options['rcTypes'] );
-               }
-
-               $conds = array_merge(
-                       $conds,
-                       $this->getWatchedItemsWithRCInfoQueryFilterConds( $user, $options )
-               );
-
-               $conds = array_merge( $conds, $this->getStartEndConds( $db, $options ) );
-
-               if ( !isset( $options['start'] ) && !isset( $options['end'] ) ) {
-                       if ( $db->getType() === 'mysql' ) {
-                               // This is an index optimization for mysql
-                               $conds[] = 'rc_timestamp > ' . $db->addQuotes( '' );
-                       }
-               }
-
-               $conds = array_merge( $conds, $this->getUserRelatedConds( $db, $user, $options ) );
-
-               $deletedPageLogCond = $this->getExtraDeletedPageLogEntryRelatedCond( $db, $user );
-               if ( $deletedPageLogCond ) {
-                       $conds[] = $deletedPageLogCond;
-               }
-
-               return $conds;
-       }
-
-       private function getWatchlistOwnerId( User $user, array $options ) {
-               if ( array_key_exists( 'watchlistOwner', $options ) ) {
-                       /** @var User $watchlistOwner */
-                       $watchlistOwner = $options['watchlistOwner'];
-                       $ownersToken = $watchlistOwner->getOption( 'watchlisttoken' );
-                       $token = $options['watchlistOwnerToken'];
-                       if ( $ownersToken == '' || !hash_equals( $ownersToken, $token ) ) {
-                               throw ApiUsageException::newWithMessage( null, 'apierror-bad-watchlist-token', 'bad_wltoken' );
-                       }
-                       return $watchlistOwner->getId();
-               }
-               return $user->getId();
-       }
-
-       private function getWatchedItemsWithRCInfoQueryFilterConds( User $user, array $options ) {
-               $conds = [];
-
-               if ( in_array( self::FILTER_MINOR, $options['filters'] ) ) {
-                       $conds[] = 'rc_minor != 0';
-               } elseif ( in_array( self::FILTER_NOT_MINOR, $options['filters'] ) ) {
-                       $conds[] = 'rc_minor = 0';
-               }
-
-               if ( in_array( self::FILTER_BOT, $options['filters'] ) ) {
-                       $conds[] = 'rc_bot != 0';
-               } elseif ( in_array( self::FILTER_NOT_BOT, $options['filters'] ) ) {
-                       $conds[] = 'rc_bot = 0';
-               }
-
-               if ( in_array( self::FILTER_ANON, $options['filters'] ) ) {
-                       $conds[] = 'rc_user = 0';
-               } elseif ( in_array( self::FILTER_NOT_ANON, $options['filters'] ) ) {
-                       $conds[] = 'rc_user != 0';
-               }
-
-               if ( $user->useRCPatrol() || $user->useNPPatrol() ) {
-                       // TODO: not sure if this should simply ignore patrolled filters if user does not have the patrol
-                       // right, or maybe rather fail loud at this point, same as e.g. ApiQueryWatchlist does?
-                       if ( in_array( self::FILTER_PATROLLED, $options['filters'] ) ) {
-                               $conds[] = 'rc_patrolled != 0';
-                       } elseif ( in_array( self::FILTER_NOT_PATROLLED, $options['filters'] ) ) {
-                               $conds[] = 'rc_patrolled = 0';
-                       }
-               }
-
-               if ( in_array( self::FILTER_UNREAD, $options['filters'] ) ) {
-                       $conds[] = 'rc_timestamp >= wl_notificationtimestamp';
-               } elseif ( in_array( self::FILTER_NOT_UNREAD, $options['filters'] ) ) {
-                       // TODO: should this be changed to use Database::makeList?
-                       $conds[] = 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp';
-               }
-
-               return $conds;
-       }
-
-       private function getStartEndConds( IDatabase $db, array $options ) {
-               if ( !isset( $options['start'] ) && !isset( $options['end'] ) ) {
-                       return [];
-               }
-
-               $conds = [];
-
-               if ( isset( $options['start'] ) ) {
-                       $after = $options['dir'] === self::DIR_OLDER ? '<=' : '>=';
-                       $conds[] = 'rc_timestamp ' . $after . ' ' .
-                               $db->addQuotes( $db->timestamp( $options['start'] ) );
-               }
-               if ( isset( $options['end'] ) ) {
-                       $before = $options['dir'] === self::DIR_OLDER ? '>=' : '<=';
-                       $conds[] = 'rc_timestamp ' . $before . ' ' .
-                               $db->addQuotes( $db->timestamp( $options['end'] ) );
-               }
-
-               return $conds;
-       }
-
-       private function getUserRelatedConds( IDatabase $db, User $user, array $options ) {
-               if ( !array_key_exists( 'onlyByUser', $options ) && !array_key_exists( 'notByUser', $options ) ) {
-                       return [];
-               }
-
-               $conds = [];
-
-               if ( array_key_exists( 'onlyByUser', $options ) ) {
-                       $conds['rc_user_text'] = $options['onlyByUser'];
-               } elseif ( array_key_exists( 'notByUser', $options ) ) {
-                       $conds[] = 'rc_user_text != ' . $db->addQuotes( $options['notByUser'] );
-               }
-
-               // Avoid brute force searches (T19342)
-               $bitmask = 0;
-               if ( !$user->isAllowed( 'deletedhistory' ) ) {
-                       $bitmask = Revision::DELETED_USER;
-               } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
-                       $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED;
-               }
-               if ( $bitmask ) {
-                       $conds[] = $db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask";
-               }
-
-               return $conds;
-       }
-
-       private function getExtraDeletedPageLogEntryRelatedCond( IDatabase $db, User $user ) {
-               // LogPage::DELETED_ACTION hides the affected page, too. So hide those
-               // entirely from the watchlist, or someone could guess the title.
-               $bitmask = 0;
-               if ( !$user->isAllowed( 'deletedhistory' ) ) {
-                       $bitmask = LogPage::DELETED_ACTION;
-               } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
-                       $bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED;
-               }
-               if ( $bitmask ) {
-                       return $db->makeList( [
-                               'rc_type != ' . RC_LOG,
-                               $db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask",
-                       ], LIST_OR );
-               }
-               return '';
-       }
-
-       private function getStartFromConds( IDatabase $db, array $options, array $startFrom ) {
-               $op = $options['dir'] === self::DIR_OLDER ? '<' : '>';
-               list( $rcTimestamp, $rcId ) = $startFrom;
-               $rcTimestamp = $db->addQuotes( $db->timestamp( $rcTimestamp ) );
-               $rcId = (int)$rcId;
-               return $db->makeList(
-                       [
-                               "rc_timestamp $op $rcTimestamp",
-                               $db->makeList(
-                                       [
-                                               "rc_timestamp = $rcTimestamp",
-                                               "rc_id $op= $rcId"
-                                       ],
-                                       LIST_AND
-                               )
-                       ],
-                       LIST_OR
-               );
-       }
-
-       private function getWatchedItemsForUserQueryConds( IDatabase $db, User $user, array $options ) {
-               $conds = [ 'wl_user' => $user->getId() ];
-               if ( $options['namespaceIds'] ) {
-                       $conds['wl_namespace'] = array_map( 'intval', $options['namespaceIds'] );
-               }
-               if ( isset( $options['filter'] ) ) {
-                       $filter = $options['filter'];
-                       if ( $filter === self::FILTER_CHANGED ) {
-                               $conds[] = 'wl_notificationtimestamp IS NOT NULL';
-                       } else {
-                               $conds[] = 'wl_notificationtimestamp IS NULL';
-                       }
-               }
-
-               if ( isset( $options['from'] ) ) {
-                       $op = $options['sort'] === self::SORT_ASC ? '>' : '<';
-                       $conds[] = $this->getFromUntilTargetConds( $db, $options['from'], $op );
-               }
-               if ( isset( $options['until'] ) ) {
-                       $op = $options['sort'] === self::SORT_ASC ? '<' : '>';
-                       $conds[] = $this->getFromUntilTargetConds( $db, $options['until'], $op );
-               }
-               if ( isset( $options['startFrom'] ) ) {
-                       $op = $options['sort'] === self::SORT_ASC ? '>' : '<';
-                       $conds[] = $this->getFromUntilTargetConds( $db, $options['startFrom'], $op );
-               }
-
-               return $conds;
-       }
-
-       /**
-        * Creates a query condition part for getting only items before or after the given link target
-        * (while ordering using $sort mode)
-        *
-        * @param IDatabase $db
-        * @param LinkTarget $target
-        * @param string $op comparison operator to use in the conditions
-        * @return string
-        */
-       private function getFromUntilTargetConds( IDatabase $db, LinkTarget $target, $op ) {
-               return $db->makeList(
-                       [
-                               "wl_namespace $op " . $target->getNamespace(),
-                               $db->makeList(
-                                       [
-                                               'wl_namespace = ' . $target->getNamespace(),
-                                               "wl_title $op= " . $db->addQuotes( $target->getDBkey() )
-                                       ],
-                                       LIST_AND
-                               )
-                       ],
-                       LIST_OR
-               );
-       }
-
-       private function getWatchedItemsWithRCInfoQueryDbOptions( array $options ) {
-               $dbOptions = [];
-
-               if ( array_key_exists( 'dir', $options ) ) {
-                       $sort = $options['dir'] === self::DIR_OLDER ? ' DESC' : '';
-                       $dbOptions['ORDER BY'] = [ 'rc_timestamp' . $sort, 'rc_id' . $sort ];
-               }
-
-               if ( array_key_exists( 'limit', $options ) ) {
-                       $dbOptions['LIMIT'] = (int)$options['limit'] + 1;
-               }
-
-               return $dbOptions;
-       }
-
-       private function getWatchedItemsForUserQueryDbOptions( array $options ) {
-               $dbOptions = [];
-               if ( array_key_exists( 'sort', $options ) ) {
-                       $dbOptions['ORDER BY'] = [
-                               "wl_namespace {$options['sort']}",
-                               "wl_title {$options['sort']}"
-                       ];
-                       if ( count( $options['namespaceIds'] ) === 1 ) {
-                               $dbOptions['ORDER BY'] = "wl_title {$options['sort']}";
-                       }
-               }
-               if ( array_key_exists( 'limit', $options ) ) {
-                       $dbOptions['LIMIT'] = (int)$options['limit'];
-               }
-               return $dbOptions;
-       }
-
-       private function getWatchedItemsWithRCInfoQueryJoinConds( array $options ) {
-               $joinConds = [
-                       'watchlist' => [ 'INNER JOIN',
-                               [
-                                       'wl_namespace=rc_namespace',
-                                       'wl_title=rc_title'
-                               ]
-                       ]
-               ];
-               if ( !$options['allRevisions'] ) {
-                       $joinConds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
-               }
-               if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
-                       $joinConds += $this->getCommentStore()->getJoin()['joins'];
-               }
-               return $joinConds;
-       }
-
-}
diff --git a/includes/WatchedItemQueryServiceExtension.php b/includes/WatchedItemQueryServiceExtension.php
deleted file mode 100644 (file)
index 93d5033..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php
-
-use Wikimedia\Rdbms\ResultWrapper;
-use Wikimedia\Rdbms\IDatabase;
-
-/**
- * Extension mechanism for WatchedItemQueryService
- *
- * @since 1.29
- *
- * @file
- * @ingroup Watchlist
- *
- * @license GNU GPL v2+
- */
-interface WatchedItemQueryServiceExtension {
-
-       /**
-        * Modify the WatchedItemQueryService::getWatchedItemsWithRecentChangeInfo()
-        * query before it's made.
-        *
-        * @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 array $options Options from
-        *  WatchedItemQueryService::getWatchedItemsWithRecentChangeInfo()
-        * @param IDatabase $db Database connection being used for the query
-        * @param array &$tables Tables for Database::select()
-        * @param array &$fields Fields for Database::select()
-        * @param array &$conds Conditions for Database::select()
-        * @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
-       );
-
-       /**
-        * Modify the results from WatchedItemQueryService::getWatchedItemsWithRecentChangeInfo()
-        * before they're returned.
-        *
-        * @param User $user
-        * @param array $options Options from
-        *  WatchedItemQueryService::getWatchedItemsWithRecentChangeInfo()
-        * @param IDatabase $db Database connection being used for the query
-        * @param array &$items array of pairs ( WatchedItem $watchedItem, string[] $recentChangeInfo ).
-        *  May be truncated if necessary, in which case $startFrom must be updated.
-        * @param ResultWrapper|bool $res Database query result
-        * @param array|null &$startFrom Continuation value. If you truncate $items, set this to
-        *  [ $recentChangeInfo['rc_timestamp'], $recentChangeInfo['rc_id'] ] from the first item
-        *  removed.
-        */
-       public function modifyWatchedItemsWithRCInfo( User $user, array $options, IDatabase $db,
-               array &$items, $res, &$startFrom
-       );
-
-}
diff --git a/includes/WatchedItemStore.php b/includes/WatchedItemStore.php
deleted file mode 100644 (file)
index 60d8b76..0000000
+++ /dev/null
@@ -1,986 +0,0 @@
-<?php
-
-use Wikimedia\Rdbms\IDatabase;
-use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
-use MediaWiki\Linker\LinkTarget;
-use MediaWiki\MediaWikiServices;
-use Wikimedia\Assert\Assert;
-use Wikimedia\ScopedCallback;
-use Wikimedia\Rdbms\LoadBalancer;
-use Wikimedia\Rdbms\DBUnexpectedError;
-
-/**
- * Storage layer class for WatchedItems.
- * Database interaction.
- *
- * Uses database because this uses User::isAnon
- *
- * @group Database
- *
- * @author Addshore
- * @since 1.27
- */
-class WatchedItemStore implements StatsdAwareInterface {
-
-       const SORT_DESC = 'DESC';
-       const SORT_ASC = 'ASC';
-
-       /**
-        * @var LoadBalancer
-        */
-       private $loadBalancer;
-
-       /**
-        * @var ReadOnlyMode
-        */
-       private $readOnlyMode;
-
-       /**
-        * @var HashBagOStuff
-        */
-       private $cache;
-
-       /**
-        * @var array[] Looks like $cacheIndex[Namespace ID][Target DB Key][User Id] => 'key'
-        * The index is needed so that on mass changes all relevant items can be un-cached.
-        * For example: Clearing a users watchlist of all items or updating notification timestamps
-        *              for all users watching a single target.
-        */
-       private $cacheIndex = [];
-
-       /**
-        * @var callable|null
-        */
-       private $deferredUpdatesAddCallableUpdateCallback;
-
-       /**
-        * @var callable|null
-        */
-       private $revisionGetTimestampFromIdCallback;
-
-       /**
-        * @var StatsdDataFactoryInterface
-        */
-       private $stats;
-
-       /**
-        * @param LoadBalancer $loadBalancer
-        * @param HashBagOStuff $cache
-        * @param ReadOnlyMode $readOnlyMode
-        */
-       public function __construct(
-               LoadBalancer $loadBalancer,
-               HashBagOStuff $cache,
-               ReadOnlyMode $readOnlyMode
-       ) {
-               $this->loadBalancer = $loadBalancer;
-               $this->cache = $cache;
-               $this->readOnlyMode = $readOnlyMode;
-               $this->stats = new NullStatsdDataFactory();
-               $this->deferredUpdatesAddCallableUpdateCallback = [ 'DeferredUpdates', 'addCallableUpdate' ];
-               $this->revisionGetTimestampFromIdCallback = [ 'Revision', 'getTimestampFromId' ];
-       }
-
-       public function setStatsdDataFactory( StatsdDataFactoryInterface $stats ) {
-               $this->stats = $stats;
-       }
-
-       /**
-        * Overrides the DeferredUpdates::addCallableUpdate callback
-        * This is intended for use while testing and will fail if MW_PHPUNIT_TEST is not defined.
-        *
-        * @param callable $callback
-        *
-        * @see DeferredUpdates::addCallableUpdate for callback signiture
-        *
-        * @return ScopedCallback to reset the overridden value
-        * @throws MWException
-        */
-       public function overrideDeferredUpdatesAddCallableUpdateCallback( callable $callback ) {
-               if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
-                       throw new MWException(
-                               'Cannot override DeferredUpdates::addCallableUpdate callback in operation.'
-                       );
-               }
-               $previousValue = $this->deferredUpdatesAddCallableUpdateCallback;
-               $this->deferredUpdatesAddCallableUpdateCallback = $callback;
-               return new ScopedCallback( function () use ( $previousValue ) {
-                       $this->deferredUpdatesAddCallableUpdateCallback = $previousValue;
-               } );
-       }
-
-       /**
-        * 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 ) {
-               return $this->cache->makeKey(
-                       (string)$target->getNamespace(),
-                       $target->getDBkey(),
-                       (string)$user->getId()
-               );
-       }
-
-       private function cache( WatchedItem $item ) {
-               $user = $item->getUser();
-               $target = $item->getLinkTarget();
-               $key = $this->getCacheKey( $user, $target );
-               $this->cache->set( $key, $item );
-               $this->cacheIndex[$target->getNamespace()][$target->getDBkey()][$user->getId()] = $key;
-               $this->stats->increment( 'WatchedItemStore.cache' );
-       }
-
-       private function uncache( User $user, LinkTarget $target ) {
-               $this->cache->delete( $this->getCacheKey( $user, $target ) );
-               unset( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()][$user->getId()] );
-               $this->stats->increment( 'WatchedItemStore.uncache' );
-       }
-
-       private function uncacheLinkTarget( LinkTarget $target ) {
-               $this->stats->increment( 'WatchedItemStore.uncacheLinkTarget' );
-               if ( !isset( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()] ) ) {
-                       return;
-               }
-               foreach ( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()] as $key ) {
-                       $this->stats->increment( 'WatchedItemStore.uncacheLinkTarget.items' );
-                       $this->cache->delete( $key );
-               }
-       }
-
-       private function uncacheUser( User $user ) {
-               $this->stats->increment( 'WatchedItemStore.uncacheUser' );
-               foreach ( $this->cacheIndex as $ns => $dbKeyArray ) {
-                       foreach ( $dbKeyArray as $dbKey => $userArray ) {
-                               if ( isset( $userArray[$user->getId()] ) ) {
-                                       $this->stats->increment( 'WatchedItemStore.uncacheUser.items' );
-                                       $this->cache->delete( $userArray[$user->getId()] );
-                               }
-                       }
-               }
-       }
-
-       /**
-        * @param User $user
-        * @param LinkTarget $target
-        *
-        * @return WatchedItem|false
-        */
-       private function getCached( User $user, LinkTarget $target ) {
-               return $this->cache->get( $this->getCacheKey( $user, $target ) );
-       }
-
-       /**
-        * Return an array of conditions to select or update the appropriate database
-        * row.
-        *
-        * @param User $user
-        * @param LinkTarget $target
-        *
-        * @return array
-        */
-       private function dbCond( User $user, LinkTarget $target ) {
-               return [
-                       'wl_user' => $user->getId(),
-                       'wl_namespace' => $target->getNamespace(),
-                       'wl_title' => $target->getDBkey(),
-               ];
-       }
-
-       /**
-        * @param int $dbIndex DB_MASTER or DB_REPLICA
-        *
-        * @return IDatabase
-        * @throws MWException
-        */
-       private function getConnectionRef( $dbIndex ) {
-               return $this->loadBalancer->getConnectionRef( $dbIndex, [ 'watchlist' ] );
-       }
-
-       /**
-        * Count the number of individual items that are watched by the user.
-        * If a subject and corresponding talk page are watched this will return 2.
-        *
-        * @param User $user
-        *
-        * @return int
-        */
-       public function countWatchedItems( User $user ) {
-               $dbr = $this->getConnectionRef( DB_REPLICA );
-               $return = (int)$dbr->selectField(
-                       'watchlist',
-                       'COUNT(*)',
-                       [
-                               'wl_user' => $user->getId()
-                       ],
-                       __METHOD__
-               );
-
-               return $return;
-       }
-
-       /**
-        * @param LinkTarget $target
-        *
-        * @return int
-        */
-       public function countWatchers( LinkTarget $target ) {
-               $dbr = $this->getConnectionRef( DB_REPLICA );
-               $return = (int)$dbr->selectField(
-                       'watchlist',
-                       'COUNT(*)',
-                       [
-                               'wl_namespace' => $target->getNamespace(),
-                               'wl_title' => $target->getDBkey(),
-                       ],
-                       __METHOD__
-               );
-
-               return $return;
-       }
-
-       /**
-        * Number of page watchers who also visited a "recent" edit
-        *
-        * @param LinkTarget $target
-        * @param mixed $threshold timestamp accepted by wfTimestamp
-        *
-        * @return int
-        * @throws DBUnexpectedError
-        * @throws MWException
-        */
-       public function countVisitingWatchers( LinkTarget $target, $threshold ) {
-               $dbr = $this->getConnectionRef( DB_REPLICA );
-               $visitingWatchers = (int)$dbr->selectField(
-                       'watchlist',
-                       'COUNT(*)',
-                       [
-                               'wl_namespace' => $target->getNamespace(),
-                               'wl_title' => $target->getDBkey(),
-                               'wl_notificationtimestamp >= ' .
-                               $dbr->addQuotes( $dbr->timestamp( $threshold ) ) .
-                               ' OR wl_notificationtimestamp IS NULL'
-                       ],
-                       __METHOD__
-               );
-
-               return $visitingWatchers;
-       }
-
-       /**
-        * @param LinkTarget[] $targets
-        * @param array $options Allowed keys:
-        *        'minimumWatchers' => int
-        *
-        * @return array multi dimensional like $return[$namespaceId][$titleString] = int $watchers
-        *         All targets will be present in the result. 0 either means no watchers or the number
-        *         of watchers was below the minimumWatchers option if passed.
-        */
-       public function countWatchersMultiple( array $targets, array $options = [] ) {
-               $dbOptions = [ 'GROUP BY' => [ 'wl_namespace', 'wl_title' ] ];
-
-               $dbr = $this->getConnectionRef( DB_REPLICA );
-
-               if ( array_key_exists( 'minimumWatchers', $options ) ) {
-                       $dbOptions['HAVING'] = 'COUNT(*) >= ' . (int)$options['minimumWatchers'];
-               }
-
-               $lb = new LinkBatch( $targets );
-               $res = $dbr->select(
-                       'watchlist',
-                       [ 'wl_title', 'wl_namespace', 'watchers' => 'COUNT(*)' ],
-                       [ $lb->constructSet( 'wl', $dbr ) ],
-                       __METHOD__,
-                       $dbOptions
-               );
-
-               $watchCounts = [];
-               foreach ( $targets as $linkTarget ) {
-                       $watchCounts[$linkTarget->getNamespace()][$linkTarget->getDBkey()] = 0;
-               }
-
-               foreach ( $res as $row ) {
-                       $watchCounts[$row->wl_namespace][$row->wl_title] = (int)$row->watchers;
-               }
-
-               return $watchCounts;
-       }
-
-       /**
-        * Number of watchers of each page who have visited recent edits to that page
-        *
-        * @param array $targetsWithVisitThresholds array of pairs (LinkTarget $target, mixed $threshold),
-        *        $threshold is:
-        *        - a timestamp of the recent edit if $target exists (format accepted by wfTimestamp)
-        *        - null if $target doesn't exist
-        * @param int|null $minimumWatchers
-        * @return array multi-dimensional like $return[$namespaceId][$titleString] = $watchers,
-        *         where $watchers is an int:
-        *         - if the page exists, number of users watching who have visited the page recently
-        *         - if the page doesn't exist, number of users that have the page on their watchlist
-        *         - 0 means there are no visiting watchers or their number is below the minimumWatchers
-        *         option (if passed).
-        */
-       public function countVisitingWatchersMultiple(
-               array $targetsWithVisitThresholds,
-               $minimumWatchers = null
-       ) {
-               $dbr = $this->getConnectionRef( DB_REPLICA );
-
-               $conds = $this->getVisitingWatchersCondition( $dbr, $targetsWithVisitThresholds );
-
-               $dbOptions = [ 'GROUP BY' => [ 'wl_namespace', 'wl_title' ] ];
-               if ( $minimumWatchers !== null ) {
-                       $dbOptions['HAVING'] = 'COUNT(*) >= ' . (int)$minimumWatchers;
-               }
-               $res = $dbr->select(
-                       'watchlist',
-                       [ 'wl_namespace', 'wl_title', 'watchers' => 'COUNT(*)' ],
-                       $conds,
-                       __METHOD__,
-                       $dbOptions
-               );
-
-               $watcherCounts = [];
-               foreach ( $targetsWithVisitThresholds as list( $target ) ) {
-                       /* @var LinkTarget $target */
-                       $watcherCounts[$target->getNamespace()][$target->getDBkey()] = 0;
-               }
-
-               foreach ( $res as $row ) {
-                       $watcherCounts[$row->wl_namespace][$row->wl_title] = (int)$row->watchers;
-               }
-
-               return $watcherCounts;
-       }
-
-       /**
-        * Generates condition for the query used in a batch count visiting watchers.
-        *
-        * @param IDatabase $db
-        * @param array $targetsWithVisitThresholds array of pairs (LinkTarget, last visit threshold)
-        * @return string
-        */
-       private function getVisitingWatchersCondition(
-               IDatabase $db,
-               array $targetsWithVisitThresholds
-       ) {
-               $missingTargets = [];
-               $namespaceConds = [];
-               foreach ( $targetsWithVisitThresholds as list( $target, $threshold ) ) {
-                       if ( $threshold === null ) {
-                               $missingTargets[] = $target;
-                               continue;
-                       }
-                       /* @var LinkTarget $target */
-                       $namespaceConds[$target->getNamespace()][] = $db->makeList( [
-                               'wl_title = ' . $db->addQuotes( $target->getDBkey() ),
-                               $db->makeList( [
-                                       'wl_notificationtimestamp >= ' . $db->addQuotes( $db->timestamp( $threshold ) ),
-                                       'wl_notificationtimestamp IS NULL'
-                               ], LIST_OR )
-                       ], LIST_AND );
-               }
-
-               $conds = [];
-               foreach ( $namespaceConds as $namespace => $pageConds ) {
-                       $conds[] = $db->makeList( [
-                               'wl_namespace = ' . $namespace,
-                               '(' . $db->makeList( $pageConds, LIST_OR ) . ')'
-                       ], LIST_AND );
-               }
-
-               if ( $missingTargets ) {
-                       $lb = new LinkBatch( $missingTargets );
-                       $conds[] = $lb->constructSet( 'wl', $db );
-               }
-
-               return $db->makeList( $conds, LIST_OR );
-       }
-
-       /**
-        * Get an item (may be cached)
-        *
-        * @param User $user
-        * @param LinkTarget $target
-        *
-        * @return WatchedItem|false
-        */
-       public function getWatchedItem( User $user, LinkTarget $target ) {
-               if ( $user->isAnon() ) {
-                       return false;
-               }
-
-               $cached = $this->getCached( $user, $target );
-               if ( $cached ) {
-                       $this->stats->increment( 'WatchedItemStore.getWatchedItem.cached' );
-                       return $cached;
-               }
-               $this->stats->increment( 'WatchedItemStore.getWatchedItem.load' );
-               return $this->loadWatchedItem( $user, $target );
-       }
-
-       /**
-        * Loads an item from the db
-        *
-        * @param User $user
-        * @param LinkTarget $target
-        *
-        * @return WatchedItem|false
-        */
-       public function loadWatchedItem( User $user, LinkTarget $target ) {
-               // Only loggedin user can have a watchlist
-               if ( $user->isAnon() ) {
-                       return false;
-               }
-
-               $dbr = $this->getConnectionRef( DB_REPLICA );
-               $row = $dbr->selectRow(
-                       'watchlist',
-                       'wl_notificationtimestamp',
-                       $this->dbCond( $user, $target ),
-                       __METHOD__
-               );
-
-               if ( !$row ) {
-                       return false;
-               }
-
-               $item = new WatchedItem(
-                       $user,
-                       $target,
-                       wfTimestampOrNull( TS_MW, $row->wl_notificationtimestamp )
-               );
-               $this->cache( $item );
-
-               return $item;
-       }
-
-       /**
-        * @param User $user
-        * @param array $options Allowed keys:
-        *        'forWrite' => bool defaults to false
-        *        'sort' => string optional sorting by namespace ID and title
-        *                     one of the self::SORT_* constants
-        *
-        * @return WatchedItem[]
-        */
-       public function getWatchedItemsForUser( User $user, array $options = [] ) {
-               $options += [ 'forWrite' => false ];
-
-               $dbOptions = [];
-               if ( array_key_exists( 'sort', $options ) ) {
-                       Assert::parameter(
-                               ( in_array( $options['sort'], [ self::SORT_ASC, self::SORT_DESC ] ) ),
-                               '$options[\'sort\']',
-                               'must be SORT_ASC or SORT_DESC'
-                       );
-                       $dbOptions['ORDER BY'] = [
-                               "wl_namespace {$options['sort']}",
-                               "wl_title {$options['sort']}"
-                       ];
-               }
-               $db = $this->getConnectionRef( $options['forWrite'] ? DB_MASTER : DB_REPLICA );
-
-               $res = $db->select(
-                       'watchlist',
-                       [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
-                       [ 'wl_user' => $user->getId() ],
-                       __METHOD__,
-                       $dbOptions
-               );
-
-               $watchedItems = [];
-               foreach ( $res as $row ) {
-                       // @todo: Should we add these to the process cache?
-                       $watchedItems[] = new WatchedItem(
-                               $user,
-                               new TitleValue( (int)$row->wl_namespace, $row->wl_title ),
-                               $row->wl_notificationtimestamp
-                       );
-               }
-
-               return $watchedItems;
-       }
-
-       /**
-        * Must be called separately for Subject & Talk namespaces
-        *
-        * @param User $user
-        * @param LinkTarget $target
-        *
-        * @return bool
-        */
-       public function isWatched( User $user, LinkTarget $target ) {
-               return (bool)$this->getWatchedItem( $user, $target );
-       }
-
-       /**
-        * @param User $user
-        * @param LinkTarget[] $targets
-        *
-        * @return array multi-dimensional like $return[$namespaceId][$titleString] = $timestamp,
-        *         where $timestamp is:
-        *         - string|null value of wl_notificationtimestamp,
-        *         - false if $target is not watched by $user.
-        */
-       public function getNotificationTimestampsBatch( User $user, array $targets ) {
-               $timestamps = [];
-               foreach ( $targets as $target ) {
-                       $timestamps[$target->getNamespace()][$target->getDBkey()] = false;
-               }
-
-               if ( $user->isAnon() ) {
-                       return $timestamps;
-               }
-
-               $targetsToLoad = [];
-               foreach ( $targets as $target ) {
-                       $cachedItem = $this->getCached( $user, $target );
-                       if ( $cachedItem ) {
-                               $timestamps[$target->getNamespace()][$target->getDBkey()] =
-                                       $cachedItem->getNotificationTimestamp();
-                       } else {
-                               $targetsToLoad[] = $target;
-                       }
-               }
-
-               if ( !$targetsToLoad ) {
-                       return $timestamps;
-               }
-
-               $dbr = $this->getConnectionRef( DB_REPLICA );
-
-               $lb = new LinkBatch( $targetsToLoad );
-               $res = $dbr->select(
-                       'watchlist',
-                       [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
-                       [
-                               $lb->constructSet( 'wl', $dbr ),
-                               'wl_user' => $user->getId(),
-                       ],
-                       __METHOD__
-               );
-
-               foreach ( $res as $row ) {
-                       $timestamps[$row->wl_namespace][$row->wl_title] =
-                               wfTimestampOrNull( TS_MW, $row->wl_notificationtimestamp );
-               }
-
-               return $timestamps;
-       }
-
-       /**
-        * Must be called separately for Subject & Talk namespaces
-        *
-        * @param User $user
-        * @param LinkTarget $target
-        */
-       public function addWatch( User $user, LinkTarget $target ) {
-               $this->addWatchBatchForUser( $user, [ $target ] );
-       }
-
-       /**
-        * @param User $user
-        * @param LinkTarget[] $targets
-        *
-        * @return bool success
-        */
-       public function addWatchBatchForUser( User $user, array $targets ) {
-               if ( $this->readOnlyMode->isReadOnly() ) {
-                       return false;
-               }
-               // Only loggedin user can have a watchlist
-               if ( $user->isAnon() ) {
-                       return false;
-               }
-
-               if ( !$targets ) {
-                       return true;
-               }
-
-               $rows = [];
-               $items = [];
-               foreach ( $targets as $target ) {
-                       $rows[] = [
-                               'wl_user' => $user->getId(),
-                               'wl_namespace' => $target->getNamespace(),
-                               'wl_title' => $target->getDBkey(),
-                               'wl_notificationtimestamp' => null,
-                       ];
-                       $items[] = new WatchedItem(
-                               $user,
-                               $target,
-                               null
-                       );
-                       $this->uncache( $user, $target );
-               }
-
-               $dbw = $this->getConnectionRef( DB_MASTER );
-               foreach ( array_chunk( $rows, 100 ) as $toInsert ) {
-                       // Use INSERT IGNORE to avoid overwriting the notification timestamp
-                       // if there's already an entry for this page
-                       $dbw->insert( 'watchlist', $toInsert, __METHOD__, 'IGNORE' );
-               }
-               // Update process cache to ensure skin doesn't claim that the current
-               // page is unwatched in the response of action=watch itself (T28292).
-               // This would otherwise be re-queried from a slave by isWatched().
-               foreach ( $items as $item ) {
-                       $this->cache( $item );
-               }
-
-               return true;
-       }
-
-       /**
-        * Removes the an entry for the User watching the LinkTarget
-        * Must be called separately for Subject & Talk namespaces
-        *
-        * @param User $user
-        * @param LinkTarget $target
-        *
-        * @return bool success
-        * @throws DBUnexpectedError
-        * @throws MWException
-        */
-       public function removeWatch( User $user, LinkTarget $target ) {
-               // Only logged in user can have a watchlist
-               if ( $this->readOnlyMode->isReadOnly() || $user->isAnon() ) {
-                       return false;
-               }
-
-               $this->uncache( $user, $target );
-
-               $dbw = $this->getConnectionRef( DB_MASTER );
-               $dbw->delete( 'watchlist',
-                       [
-                               'wl_user' => $user->getId(),
-                               'wl_namespace' => $target->getNamespace(),
-                               'wl_title' => $target->getDBkey(),
-                       ], __METHOD__
-               );
-               $success = (bool)$dbw->affectedRows();
-
-               return $success;
-       }
-
-       /**
-        * @param User $user The user to set the timestamp 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, $timestamp, array $targets = [] ) {
-               // Only loggedin user can have a watchlist
-               if ( $user->isAnon() ) {
-                       return false;
-               }
-
-               $dbw = $this->getConnectionRef( DB_MASTER );
-
-               $conds = [ 'wl_user' => $user->getId() ];
-               if ( $targets ) {
-                       $batch = new LinkBatch( $targets );
-                       $conds[] = $batch->constructSet( 'wl', $dbw );
-               }
-
-               if ( $timestamp !== null ) {
-                       $timestamp = $dbw->timestamp( $timestamp );
-               }
-
-               $success = $dbw->update(
-                       'watchlist',
-                       [ 'wl_notificationtimestamp' => $timestamp ],
-                       $conds,
-                       __METHOD__
-               );
-
-               $this->uncacheUser( $user );
-
-               return $success;
-       }
-
-       /**
-        * @param User $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 ) {
-               $dbw = $this->getConnectionRef( DB_MASTER );
-               $uids = $dbw->selectFieldValues(
-                       'watchlist',
-                       'wl_user',
-                       [
-                               'wl_user != ' . intval( $editor->getId() ),
-                               'wl_namespace' => $target->getNamespace(),
-                               'wl_title' => $target->getDBkey(),
-                               'wl_notificationtimestamp IS NULL',
-                       ],
-                       __METHOD__
-               );
-
-               $watchers = array_map( 'intval', $uids );
-               if ( $watchers ) {
-                       // Update wl_notificationtimestamp for all watching users except the editor
-                       $fname = __METHOD__;
-                       DeferredUpdates::addCallableUpdate(
-                               function () use ( $timestamp, $watchers, $target, $fname ) {
-                                       global $wgUpdateRowsPerQuery;
-
-                                       $dbw = $this->getConnectionRef( DB_MASTER );
-                                       $factory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
-                                       $ticket = $factory->getEmptyTransactionTicket( __METHOD__ );
-
-                                       $watchersChunks = array_chunk( $watchers, $wgUpdateRowsPerQuery );
-                                       foreach ( $watchersChunks as $watchersChunk ) {
-                                               $dbw->update( 'watchlist',
-                                                       [ /* SET */
-                                                               'wl_notificationtimestamp' => $dbw->timestamp( $timestamp )
-                                                       ], [ /* WHERE - TODO Use wl_id T130067 */
-                                                               'wl_user' => $watchersChunk,
-                                                               'wl_namespace' => $target->getNamespace(),
-                                                               'wl_title' => $target->getDBkey(),
-                                                       ], $fname
-                                               );
-                                               if ( count( $watchersChunks ) > 1 ) {
-                                                       $factory->commitAndWaitForReplication(
-                                                               __METHOD__, $ticket, [ 'domain' => $dbw->getDomainID() ]
-                                                       );
-                                               }
-                                       }
-                                       $this->uncacheLinkTarget( $target );
-                               },
-                               DeferredUpdates::POSTSEND,
-                               $dbw
-                       );
-               }
-
-               return $watchers;
-       }
-
-       /**
-        * Reset the notification timestamp of this entry
-        *
-        * @param User $user
-        * @param Title $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
-        * @param int $oldid The revision id being viewed. If not given or 0, latest revision is assumed.
-        *
-        * @return bool success
-        */
-       public function resetNotificationTimestamp( User $user, Title $title, $force = '', $oldid = 0 ) {
-               // Only loggedin user can have a watchlist
-               if ( $this->readOnlyMode->isReadOnly() || $user->isAnon() ) {
-                       return false;
-               }
-
-               $item = null;
-               if ( $force != 'force' ) {
-                       $item = $this->loadWatchedItem( $user, $title );
-                       if ( !$item || $item->getNotificationTimestamp() === null ) {
-                               return false;
-                       }
-               }
-
-               // If the page is watched by the user (or may be watched), update the timestamp
-               $job = new ActivityUpdateJob(
-                       $title,
-                       [
-                               'type'      => 'updateWatchlistNotification',
-                               'userid'    => $user->getId(),
-                               'notifTime' => $this->getNotificationTimestamp( $user, $title, $item, $force, $oldid ),
-                               'curTime'   => time()
-                       ]
-               );
-
-               // Try to run this post-send
-               // Calls DeferredUpdates::addCallableUpdate in normal operation
-               call_user_func(
-                       $this->deferredUpdatesAddCallableUpdateCallback,
-                       function () use ( $job ) {
-                               $job->run();
-                       }
-               );
-
-               $this->uncache( $user, $title );
-
-               return true;
-       }
-
-       private function getNotificationTimestamp( User $user, Title $title, $item, $force, $oldid ) {
-               if ( !$oldid ) {
-                       // No oldid given, assuming latest revision; clear the timestamp.
-                       return null;
-               }
-
-               if ( !$title->getNextRevisionID( $oldid ) ) {
-                       // Oldid given and is the latest revision for this title; clear the timestamp.
-                       return null;
-               }
-
-               if ( $item === null ) {
-                       $item = $this->loadWatchedItem( $user, $title );
-               }
-
-               if ( !$item ) {
-                       // This can only happen if $force is enabled.
-                       return null;
-               }
-
-               // 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
-               );
-
-               // We need to go one second to the future because of various strict comparisons
-               // throughout the codebase
-               $ts = new MWTimestamp( $notificationTimestamp );
-               $ts->timestamp->add( new DateInterval( 'PT1S' ) );
-               $notificationTimestamp = $ts->getTimestamp( TS_MW );
-
-               if ( $notificationTimestamp < $item->getNotificationTimestamp() ) {
-                       if ( $force != 'force' ) {
-                               return false;
-                       } else {
-                               // This is a little silly…
-                               return $item->getNotificationTimestamp();
-                       }
-               }
-
-               return $notificationTimestamp;
-       }
-
-       /**
-        * @param User $user
-        * @param int $unreadLimit
-        *
-        * @return int|bool The number of unread notifications
-        *                  true if greater than or equal to $unreadLimit
-        */
-       public function countUnreadNotifications( User $user, $unreadLimit = null ) {
-               $queryOptions = [];
-               if ( $unreadLimit !== null ) {
-                       $unreadLimit = (int)$unreadLimit;
-                       $queryOptions['LIMIT'] = $unreadLimit;
-               }
-
-               $dbr = $this->getConnectionRef( DB_REPLICA );
-               $rowCount = $dbr->selectRowCount(
-                       'watchlist',
-                       '1',
-                       [
-                               'wl_user' => $user->getId(),
-                               'wl_notificationtimestamp IS NOT NULL',
-                       ],
-                       __METHOD__,
-                       $queryOptions
-               );
-
-               if ( !isset( $unreadLimit ) ) {
-                       return $rowCount;
-               }
-
-               if ( $rowCount >= $unreadLimit ) {
-                       return true;
-               }
-
-               return $rowCount;
-       }
-
-       /**
-        * Check if the given title already is watched by the user, and if so
-        * add a watch for the new title.
-        *
-        * To be used for page renames and such.
-        *
-        * @param LinkTarget $oldTarget
-        * @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() );
-       }
-
-       /**
-        * Check if the given title already is watched by the user, and if so
-        * add a watch for the new title.
-        *
-        * To be used for page renames and such.
-        * This must be called separately for Subject and Talk pages
-        *
-        * @param LinkTarget $oldTarget
-        * @param LinkTarget $newTarget
-        */
-       public function duplicateEntry( LinkTarget $oldTarget, LinkTarget $newTarget ) {
-               $dbw = $this->getConnectionRef( DB_MASTER );
-
-               $result = $dbw->select(
-                       'watchlist',
-                       [ 'wl_user', 'wl_notificationtimestamp' ],
-                       [
-                               'wl_namespace' => $oldTarget->getNamespace(),
-                               'wl_title' => $oldTarget->getDBkey(),
-                       ],
-                       __METHOD__,
-                       [ 'FOR UPDATE' ]
-               );
-
-               $newNamespace = $newTarget->getNamespace();
-               $newDBkey = $newTarget->getDBkey();
-
-               # Construct array to replace into the watchlist
-               $values = [];
-               foreach ( $result as $row ) {
-                       $values[] = [
-                               'wl_user' => $row->wl_user,
-                               'wl_namespace' => $newNamespace,
-                               'wl_title' => $newDBkey,
-                               'wl_notificationtimestamp' => $row->wl_notificationtimestamp,
-                       ];
-               }
-
-               if ( !empty( $values ) ) {
-                       # Perform replace
-                       # Note that multi-row replace is very efficient for MySQL but may be inefficient for
-                       # some other DBMSes, mostly due to poor simulation by us
-                       $dbw->replace(
-                               'watchlist',
-                               [ [ 'wl_user', 'wl_namespace', 'wl_title' ] ],
-                               $values,
-                               __METHOD__
-                       );
-               }
-       }
-
-}
index cb3c2f6..83d2ae9 100644 (file)
@@ -1081,10 +1081,10 @@ abstract class ApiBase extends ContextSource {
                        } else {
                                $type = 'NULL'; // allow everything
                        }
+               }
 
-                       if ( $type == 'password' || !empty( $paramSettings[self::PARAM_SENSITIVE] ) ) {
-                               $this->getMain()->markParamsSensitive( $encParamName );
-                       }
+               if ( $type == 'password' || !empty( $paramSettings[self::PARAM_SENSITIVE] ) ) {
+                       $this->getMain()->markParamsSensitive( $encParamName );
                }
 
                if ( $type == 'boolean' ) {
index 06eaa19..c5f2fcf 100644 (file)
@@ -64,6 +64,26 @@ abstract class ApiFormatBase extends ApiBase {
         */
        abstract public function getMimeType();
 
+       /**
+        * Return a filename for this module's output.
+        * @note If $this->getIsWrappedHtml() || $this->getIsHtml(), you'll very
+        *  likely want to fall back to this class's version.
+        * @since 1.27
+        * @return string Generally this should be "api-result.$ext", and must be
+        *  encoded for inclusion in a Content-Disposition header's filename parameter.
+        */
+       public function getFilename() {
+               if ( $this->getIsWrappedHtml() ) {
+                       return 'api-result-wrapped.json';
+               } elseif ( $this->getIsHtml() ) {
+                       return 'api-result.html';
+               } else {
+                       $exts = MimeMagic::singleton()->getExtensionsForType( $this->getMimeType() );
+                       $ext = $exts ? strtok( $exts, ' ' ) : strtolower( $this->mFormat );
+                       return "api-result.$ext";
+               }
+       }
+
        /**
         * Get the internal format name
         * @return string
@@ -192,6 +212,13 @@ abstract class ApiFormatBase extends ApiBase {
                if ( $apiFrameOptions ) {
                        $this->getMain()->getRequest()->response()->header( "X-Frame-Options: $apiFrameOptions" );
                }
+
+               // Set a Content-Disposition header so something downloading an API
+               // response uses a halfway-sensible filename (T128209).
+               $filename = $this->getFilename();
+               $this->getMain()->getRequest()->response()->header(
+                       "Content-Disposition: inline; filename=\"{$filename}\""
+               );
        }
 
        /**
index 228b47e..ebaeb2c 100644 (file)
@@ -60,6 +60,17 @@ class ApiFormatRaw extends ApiFormatBase {
                return $data['mime'];
        }
 
+       public function getFilename() {
+               $data = $this->getResult()->getResultData();
+               if ( isset( $data['error'] ) ) {
+                       return $this->errorFallback->getFilename();
+               } elseif ( !isset( $data['filename'] ) || $this->getIsWrappedHtml() || $this->getIsHtml() ) {
+                       return parent::getFilename();
+               } else {
+                       return $data['filename'];
+               }
+       }
+
        public function initPrinter( $unused = false ) {
                $data = $this->getResult()->getResultData();
                if ( isset( $data['error'] ) || isset( $data['errors'] ) ) {
index 02404c4..529b32c 100644 (file)
@@ -62,6 +62,7 @@ class ApiHelp extends ApiBase {
                if ( $params['wrap'] ) {
                        $data = [
                                'mime' => 'text/html',
+                               'filename' => 'api-help.html',
                                'help' => $html,
                        ];
                        ApiResult::setSubelementsList( $data, 'help' );
@@ -70,6 +71,7 @@ class ApiHelp extends ApiBase {
                        $result->reset();
                        $result->addValue( null, 'text', $html, ApiResult::NO_SIZE_CHECK );
                        $result->addValue( null, 'mime', 'text/html', ApiResult::NO_SIZE_CHECK );
+                       $result->addValue( null, 'filename', 'api-help.html', ApiResult::NO_SIZE_CHECK );
                }
        }
 
index aa7e25e..9636789 100644 (file)
@@ -134,7 +134,7 @@ class ApiLogin extends ApiBase {
                                $session = $status->getValue();
                                $authRes = 'Success';
                                $loginType = 'BotPassword';
-                       } elseif ( !$botLoginData[2] ) {
+                       } elseif ( !$botLoginData[2] || $status->hasMessage( 'login-throttled' ) ) {
                                $authRes = 'Failed';
                                $message = $status->getMessage();
                                LoggerFactory::getInstance( 'authentication' )->info(
index 44a46b8..31bcc7a 100644 (file)
@@ -459,6 +459,7 @@ class ApiQuery extends ApiBase {
                        // Raw formatter will handle this
                        $result->addValue( null, 'text', $sink, ApiResult::NO_SIZE_CHECK );
                        $result->addValue( null, 'mime', 'text/xml', ApiResult::NO_SIZE_CHECK );
+                       $result->addValue( null, 'filename', 'export.xml', ApiResult::NO_SIZE_CHECK );
                } else {
                        $result->addValue( 'query', 'export', $sink, ApiResult::NO_SIZE_CHECK );
                        $result->addValue( 'query', ApiResult::META_BC_SUBELEMENTS, [ 'export' ] );
index 8a9de06..4bd6a3f 100644 (file)
@@ -209,8 +209,10 @@ class ApiStashEdit extends ApiBase {
                        Hooks::run( 'ParserOutputStashForEdit',
                                [ $page, $content, $editInfo->output, $summary, $user ] );
 
+                       $titleStr = (string)$title;
                        if ( $alreadyCached ) {
-                               $logger->debug( "Already cached parser output for key '$key' ('$title')." );
+                               $logger->debug( "Already cached parser output for key '{cachekey}' ('{title}').",
+                                       [ 'cachekey' => $key, 'title' => $titleStr ] );
                                return self::ERROR_NONE;
                        }
 
@@ -224,14 +226,17 @@ class ApiStashEdit extends ApiBase {
                        if ( $stashInfo ) {
                                $ok = $cache->set( $key, $stashInfo, $ttl );
                                if ( $ok ) {
-                                       $logger->debug( "Cached parser output for key '$key' ('$title')." );
+                                       $logger->debug( "Cached parser output for key '{cachekey}' ('{title}').",
+                                               [ 'cachekey' => $key, 'title' => $titleStr ] );
                                        return self::ERROR_NONE;
                                } else {
-                                       $logger->error( "Failed to cache parser output for key '$key' ('$title')." );
+                                       $logger->error( "Failed to cache parser output for key '{cachekey}' ('{title}').",
+                                               [ 'cachekey' => $key, 'title' => $titleStr ] );
                                        return self::ERROR_CACHE;
                                }
                        } else {
-                               $logger->info( "Uncacheable parser output for key '$key' ('$title') [$code]." );
+                               $logger->info( "Uncacheable parser output for key '{cachekey}' ('{title}') [{code}].",
+                                       [ 'cachekey' => $key, 'title' => $titleStr, 'code' => $code ] );
                                return self::ERROR_UNCACHEABLE;
                        }
                }
index fcb4af4..ea42e24 100644 (file)
        "api-help-permissions-granted-to": "Uděleno {{PLURAL:$1|skupině|skupinám}}: $2",
        "api-help-right-apihighlimits": "Používání vyšších limitů v API dotazech (pomalé dotazy: $1, rychlé dotazy: $2). Limity pro pomalé dotazy se vztahují i na vícehodnotové parametry.",
        "api-help-open-in-apisandbox": "<small>[otevřít v pískovišti]</small>",
+       "apierror-mustbeloggedin": "Abyste mohli $1, musíte být přihlášeni.",
        "apierror-nosuchsection-what": "$2 neobsahuje sekci $1.",
        "apierror-sectionsnotsupported-what": "$1 nepodporuje sekce.",
        "apierror-timeout": "Server neodpověděl v očekávaném čase.",
index 0410ddf..a2dc344 100644 (file)
        "apihelp-import-param-xml": "업로드한 XML 파일.",
        "apihelp-linkaccount-summary": "서드파티 제공자의 계정을 현재 사용자와 연결합니다.",
        "apihelp-login-summary": "로그인한 다음 인증 쿠키를 가져옵니다.",
+       "apihelp-login-extended-description": "이 동작은 [[Special:BotPasswords|특수:BotPasswords]]와 함께 사용해야만 합니다. 주 계정 로그인을 위해 사용하는 것은 권장되지 않으며 경고 없이 실패할 수 있습니다. 주 계정에 안전하게 로그인하려면 <kbd>[[Special:ApiHelp/clientlogin|action=clientlogin]]</kbd>을 사용하십시오.",
        "apihelp-login-param-name": "사용자 이름.",
        "apihelp-login-param-password": "비밀번호.",
        "apihelp-login-param-domain": "도메인 (선택).",
index 1c8877c..2e89ea3 100644 (file)
        "apihelp-options-example-complex": "Tilbakestill alle innstillinger, og sett så <kbd>skin</kbd> og <kbd>nickname</kbd>.",
        "apihelp-paraminfo-summary": "Hent informasjon om API-moduler.",
        "apihelp-paraminfo-param-helpformat": "Format for hjelpestrenger.",
+       "apihelp-parse-param-prop": "Hvilke informasjonsdeler som skal hentes:",
+       "apihelp-parse-paramvalue-prop-categorieshtml": "Gir HTML-versjonen av kategoriene.",
+       "apihelp-parse-paramvalue-prop-headitems": "Gir elementer som skal puttes i <code>&lt;head&gt;</code>-taggen til siden.",
+       "apihelp-patrol-summary": "Patruljer en side eller revisjon.",
        "apihelp-query+allfileusages-paramvalue-prop-title": "Legger til filens tittel.",
        "apihelp-query+allfileusages-param-limit": "Hvor mange elementer som skal returneres totalt.",
        "apihelp-query+allfileusages-param-dir": "Retningen det skal listes opp i.",
index 7f93c12..86a6aae 100644 (file)
@@ -96,7 +96,10 @@ class LocalPasswordPrimaryAuthenticationProvider
                        __METHOD__
                );
                if ( !$row ) {
-                       return AuthenticationResponse::newAbstain();
+                       // Do not reveal whether its bad username or
+                       // bad password to prevent username enumeration
+                       // on private wikis. (T134100)
+                       return $this->failResponse( $req );
                }
 
                $oldRow = clone $row;
index bc50096..5b8559e 100644 (file)
@@ -576,7 +576,9 @@ class ChangesList extends ContextSource {
                        return '';
                }
                $cache = $this->watchMsgCache;
-               return $cache->getWithSetCallback( $count, $cache::TTL_INDEFINITE,
+               return $cache->getWithSetCallback(
+                       $cache->makeKey( 'watching-users-msg', $count ),
+                       $cache::TTL_INDEFINITE,
                        function () use ( $count ) {
                                return $this->msg( 'number_of_watching_users_RCview' )
                                        ->numParams( $count )->escaped();
diff --git a/includes/composer/ComposerVendorHtaccessCreator.php b/includes/composer/ComposerVendorHtaccessCreator.php
new file mode 100644 (file)
index 0000000..1e5efdf
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+/**
+ * Copyright (C) 2017 Kunal Mehta <legoktm@member.fsf.org>
+ *
+ * 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.
+ *
+ */
+
+/**
+ * Creates a .htaccess in the vendor/ directory
+ * to prevent web access.
+ *
+ * This class runs *outside* of the normal MediaWiki
+ * environment and cannot depend upon any MediaWiki
+ * code.
+ */
+class ComposerVendorHtaccessCreator {
+
+       /**
+        * Handle post-install-cmd and post-update-cmd hooks
+        */
+       public static function onEvent() {
+               $fname = dirname( dirname( __DIR__ ) ) . "/vendor/.htaccess";
+               if ( file_exists( $fname ) ) {
+                       // Already exists
+                       return;
+               }
+
+               file_put_contents( $fname, "Deny from all\n" );
+       }
+}
index 813ee08..03f7212 100644 (file)
@@ -23,9 +23,6 @@
 use MediaWiki\MediaWikiServices;
 use MediaWiki\Shell\Shell;
 
-/** @deprecated use class constant instead */
-define( 'MW_DIFF_VERSION', '1.11a' );
-
 /**
  * @todo document
  * @ingroup DifferenceEngine
@@ -37,7 +34,7 @@ class DifferenceEngine extends ContextSource {
         * fixes important bugs or such to force cached diff views to
         * clear.
         */
-       const DIFF_VERSION = MW_DIFF_VERSION;
+       const DIFF_VERSION = '1.12';
 
        /** @var int */
        public $mOldid;
@@ -753,14 +750,22 @@ class DifferenceEngine extends ContextSource {
                $key = false;
                $cache = ObjectCache::getMainWANInstance();
                if ( $this->mOldid && $this->mNewid ) {
+                       // Check if subclass is still using the old way
+                       // for backwards-compatibility
                        $key = $this->getDiffBodyCacheKey();
+                       if ( $key === null ) {
+                               $key = call_user_func_array(
+                                       [ $cache, 'makeKey' ],
+                                       $this->getDiffBodyCacheKeyParams()
+                               );
+                       }
 
                        // Try cache
                        if ( !$this->mRefreshCache ) {
                                $difftext = $cache->get( $key );
                                if ( $difftext ) {
                                        wfIncrStats( 'diff_cache.hit' );
-                                       $difftext = $this->localiseLineNumbers( $difftext );
+                                       $difftext = $this->localiseDiff( $difftext );
                                        $difftext .= "\n<!-- diff cache key $key -->\n";
 
                                        return $difftext;
@@ -788,9 +793,9 @@ class DifferenceEngine extends ContextSource {
                } else {
                        wfIncrStats( 'diff_cache.uncacheable' );
                }
-               // Replace line numbers with the text in the user's language
+               // localise line numbers and title attribute text
                if ( $difftext !== false ) {
-                       $difftext = $this->localiseLineNumbers( $difftext );
+                       $difftext = $this->localiseDiff( $difftext );
                }
 
                return $difftext;
@@ -799,18 +804,49 @@ class DifferenceEngine extends ContextSource {
        /**
         * Returns the cache key for diff body text or content.
         *
+        * @deprecated since 1.31, use getDiffBodyCacheKeyParams() instead
         * @since 1.23
         *
         * @throws MWException
-        * @return string
+        * @return string|null
         */
        protected function getDiffBodyCacheKey() {
+               return null;
+       }
+
+       /**
+        * Get the cache key parameters
+        *
+        * Subclasses can replace the first element in the array to something
+        * more specific to the type of diff (e.g. "inline-diff"), or append
+        * if the cache should vary on more things. Overriding entirely should
+        * be avoided.
+        *
+        * @since 1.31
+        *
+        * @return array
+        * @throws MWException
+        */
+       protected function getDiffBodyCacheKeyParams() {
                if ( !$this->mOldid || !$this->mNewid ) {
                        throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' );
                }
 
-               return wfMemcKey( 'diff', 'version', self::DIFF_VERSION,
-                       'oldid', $this->mOldid, 'newid', $this->mNewid );
+               $engine = $this->getEngine();
+               $params = [
+                       'diff',
+                       $engine,
+                       self::DIFF_VERSION,
+                       "old-{$this->mOldid}",
+                       "rev-{$this->mNewid}"
+               ];
+
+               if ( $engine === 'wikidiff2' ) {
+                       $params[] = phpversion( 'wikidiff2' );
+                       $params[] = $this->getConfig()->get( 'WikiDiff2MovedParagraphDetectionCutoff' );
+               }
+
+               return $params;
        }
 
        /**
@@ -897,19 +933,14 @@ class DifferenceEngine extends ContextSource {
        }
 
        /**
-        * Generates diff, to be wrapped internally in a logging/instrumentation
+        * Process $wgExternalDiffEngine and get a sane, usable engine
         *
-        * @param string $otext Old text, must be already segmented
-        * @param string $ntext New text, must be already segmented
-        * @return bool|string
-        * @throws Exception
+        * @return bool|string 'wikidiff2', path to an executable, or false
         */
-       protected function textDiff( $otext, $ntext ) {
-               global $wgExternalDiffEngine, $wgContLang;
-
-               $otext = str_replace( "\r\n", "\n", $otext );
-               $ntext = str_replace( "\r\n", "\n", $ntext );
-
+       private function getEngine() {
+               global $wgExternalDiffEngine;
+               // We use the global here instead of Config because we write to the value,
+               // and Config is not mutable.
                if ( $wgExternalDiffEngine == 'wikidiff' || $wgExternalDiffEngine == 'wikidiff3' ) {
                        wfDeprecated( "\$wgExternalDiffEngine = '{$wgExternalDiffEngine}'", '1.27' );
                        $wgExternalDiffEngine = false;
@@ -922,9 +953,34 @@ class DifferenceEngine extends ContextSource {
                        $wgExternalDiffEngine = false;
                }
 
+               if ( is_string( $wgExternalDiffEngine ) && is_executable( $wgExternalDiffEngine ) ) {
+                       return $wgExternalDiffEngine;
+               } elseif ( $wgExternalDiffEngine === false && function_exists( 'wikidiff2_do_diff' ) ) {
+                       return 'wikidiff2';
+               } else {
+                       // Native PHP
+                       return false;
+               }
+       }
+
+       /**
+        * Generates diff, to be wrapped internally in a logging/instrumentation
+        *
+        * @param string $otext Old text, must be already segmented
+        * @param string $ntext New text, must be already segmented
+        * @return bool|string
+        */
+       protected function textDiff( $otext, $ntext ) {
+               global $wgContLang;
+
+               $otext = str_replace( "\r\n", "\n", $otext );
+               $ntext = str_replace( "\r\n", "\n", $ntext );
+
+               $engine = $this->getEngine();
+
                // Better external diff engine, the 2 may some day be dropped
                // This one does the escaping and segmenting itself
-               if ( function_exists( 'wikidiff2_do_diff' ) && $wgExternalDiffEngine === false ) {
+               if ( $engine === 'wikidiff2' ) {
                        $wikidiff2Version = phpversion( 'wikidiff2' );
                        if (
                                $wikidiff2Version !== false &&
@@ -954,7 +1010,7 @@ class DifferenceEngine extends ContextSource {
                        $text .= $this->debug( 'wikidiff2' );
 
                        return $text;
-               } elseif ( $wgExternalDiffEngine !== false && is_executable( $wgExternalDiffEngine ) ) {
+               } elseif ( $engine !== false ) {
                        # Diff via the shell
                        $tmpDir = wfTempDir();
                        $tempName1 = tempnam( $tmpDir, 'diff_' );
@@ -972,7 +1028,7 @@ class DifferenceEngine extends ContextSource {
                        fwrite( $tempFile2, $ntext );
                        fclose( $tempFile1 );
                        fclose( $tempFile2 );
-                       $cmd = [ $wgExternalDiffEngine, $tempName1, $tempName2 ];
+                       $cmd = [ $engine, $tempName1, $tempName2 ];
                        $result = Shell::command( $cmd )
                                ->execute();
                        $exitCode = $result->getExitCode();
@@ -982,7 +1038,7 @@ class DifferenceEngine extends ContextSource {
                                );
                        }
                        $difftext = $result->getStdout();
-                       $difftext .= $this->debug( "external $wgExternalDiffEngine" );
+                       $difftext .= $this->debug( "external $engine" );
                        unlink( $tempName1 );
                        unlink( $tempName2 );
 
@@ -1024,6 +1080,22 @@ class DifferenceEngine extends ContextSource {
                        " -->\n";
        }
 
+       /**
+        * Localise diff output
+        *
+        * @param string $text
+        * @return string
+        */
+       private function localiseDiff( $text ) {
+               $text = $this->localiseLineNumbers( $text );
+               if ( $this->getEngine() === 'wikidiff2' &&
+                       version_compare( phpversion( 'wikidiff2' ), '1.5.1', '>=' )
+               ) {
+                       $text = $this->addLocalisedTitleTooltips( $text );
+               }
+               return $text;
+       }
+
        /**
         * Replace line numbers with the text in the user's language
         *
@@ -1047,6 +1119,31 @@ class DifferenceEngine extends ContextSource {
                return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
        }
 
+       /**
+        * Add title attributes for tooltips on moved paragraph indicators
+        *
+        * @param string $text
+        * @return string
+        */
+       private function addLocalisedTitleTooltips( $text ) {
+               return preg_replace_callback(
+                       '/class="mw-diff-movedpara-(left|old)"/',
+                       [ $this, 'addLocalisedTitleTooltipsCb' ],
+                       $text
+               );
+       }
+
+       /**
+        * @param array $matches
+        * @return string
+        */
+       private function addLocalisedTitleTooltipsCb( array $matches ) {
+               $key = $matches[1] === 'right' ?
+                       'diff-paragraph-moved-toold' :
+                       'diff-paragraph-moved-tonew';
+               return $matches[0] . ' title="' . $this->msg( $key )->escaped() . '"';
+       }
+
        /**
         * If there are revisions between the ones being compared, return a note saying so.
         *
index 8c1f8dc..6d95919 100644 (file)
@@ -102,15 +102,17 @@ class MWException extends Exception {
                } else {
                        $logId = WebRequest::getRequestId();
                        $type = static::class;
-                       return "<div class=\"errorbox\">" .
-                       '[' . $logId . '] ' .
-                       gmdate( 'Y-m-d H:i:s' ) . ": " .
-                       $this->msg( "internalerror-fatal-exception",
-                               "Fatal exception of type $1",
-                               $type,
-                               $logId,
-                               MWExceptionHandler::getURL( $this )
-                       ) . "</div>\n" .
+                       return Html::errorBox(
+                       htmlspecialchars(
+                               '[' . $logId . '] ' .
+                               gmdate( 'Y-m-d H:i:s' ) . ": " .
+                               $this->msg( "internalerror-fatal-exception",
+                                       "Fatal exception of type $1",
+                                       $type,
+                                       $logId,
+                                       MWExceptionHandler::getURL( $this )
+                               )
+                       ) ) .
                        "<!-- Set \$wgShowExceptionDetails = true; " .
                        "at the bottom of LocalSettings.php to show detailed " .
                        "debugging information. -->";
index bb5e4f4..b22e87b 100644 (file)
@@ -128,7 +128,7 @@ class MWExceptionRenderer {
 
                        // Show any custom GUI message before the details
                        if ( $e instanceof MessageSpecifier ) {
-                               $wgOut->addHTML( Message::newFromSpecifier( $e )->escaped() );
+                               $wgOut->addHTML( Html::element( 'p', [], Message::newFromSpecifier( $e )->text() ) );
                        }
                        $wgOut->addHTML( self::getHTML( $e ) );
 
@@ -169,14 +169,15 @@ class MWExceptionRenderer {
                } else {
                        $logId = WebRequest::getRequestId();
                        $html = "<div class=\"errorbox mw-content-ltr\">" .
-                               '[' . $logId . '] ' .
-                               gmdate( 'Y-m-d H:i:s' ) . ": " .
-                               self::msg( "internalerror-fatal-exception",
-                                       "Fatal exception of type $1",
-                                       get_class( $e ),
-                                       $logId,
-                                       MWExceptionHandler::getURL()
-                               ) . "</div>\n" .
+                               htmlspecialchars(
+                                       '[' . $logId . '] ' .
+                                       gmdate( 'Y-m-d H:i:s' ) . ": " .
+                                       self::msg( "internalerror-fatal-exception",
+                                               "Fatal exception of type $1",
+                                               get_class( $e ),
+                                               $logId,
+                                               MWExceptionHandler::getURL()
+                               ) ) . "</div>\n" .
                                "<!-- " . wordwrap( self::getShowBacktraceError( $e ), 50 ) . " -->";
                }
 
index 1563baf..3beab29 100644 (file)
@@ -3,6 +3,8 @@
  * @defgroup ExternalStorage ExternalStorage
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * Interface for data storage in external repositories.
  *
@@ -52,16 +54,9 @@ class ExternalStore {
         * @return ExternalStoreMedium|bool The store class or false on error
         */
        public static function getStoreObject( $proto, array $params = [] ) {
-               global $wgExternalStores;
-
-               if ( !$wgExternalStores || !in_array( $proto, $wgExternalStores ) ) {
-                       return false; // protocol not enabled
-               }
-
-               $class = 'ExternalStore' . ucfirst( $proto );
-
-               // Any custom modules should be added to $wgAutoLoadClasses for on-demand loading
-               return class_exists( $class ) ? new $class( $params ) : false;
+               return MediaWikiServices::getInstance()
+                       ->getExternalStoreFactory()
+                       ->getStoreObject( $proto, $params );
        }
 
        /**
diff --git a/includes/externalstore/ExternalStoreFactory.php b/includes/externalstore/ExternalStoreFactory.php
new file mode 100644 (file)
index 0000000..940fb2e
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+/**
+ * @defgroup ExternalStorage ExternalStorage
+ */
+
+/**
+ * @ingroup ExternalStorage
+ */
+class ExternalStoreFactory {
+
+       /**
+        * @var array
+        */
+       private $externalStores;
+
+       /**
+        * @param array $externalStores See $wgExternalStores
+        */
+       public function __construct( array $externalStores ) {
+               $this->externalStores = array_map( 'strtolower', $externalStores );
+       }
+
+       /**
+        * Get an external store object of the given type, with the given parameters
+        *
+        * @param string $proto Type of external storage, should be a value in $wgExternalStores
+        * @param array $params Associative array of ExternalStoreMedium parameters
+        * @return ExternalStoreMedium|bool The store class or false on error
+        */
+       public function getStoreObject( $proto, array $params = [] ) {
+               if ( !$this->externalStores || !in_array( strtolower( $proto ), $this->externalStores ) ) {
+                       // Protocol not enabled
+                       return false;
+               }
+
+               $class = 'ExternalStore' . ucfirst( $proto );
+
+               // Any custom modules should be added to $wgAutoLoadClasses for on-demand loading
+               return class_exists( $class ) ? new $class( $params ) : false;
+       }
+
+}
index 54bd0a5..827f4ca 100644 (file)
@@ -581,6 +581,25 @@ abstract class File implements IDBAccessObject {
                }
        }
 
+       /**
+        * Get the language code from the available languages for this file that matches the language
+        * requested by the user
+        *
+        * @param string $userPreferredLanguage
+        * @return string|null
+        */
+       public function getMatchedLanguage( $userPreferredLanguage ) {
+               $handler = $this->getHandler();
+               if ( $handler && method_exists( $handler, 'getMatchedLanguage' ) ) {
+                       return $handler->getMatchedLanguage(
+                               $userPreferredLanguage,
+                               $handler->getAvailableLanguages( $this )
+                       );
+               } else {
+                       return null;
+               }
+       }
+
        /**
         * In files that support multiple language, what is the default language
         * to use if none specified.
index 1755e98..ba36888 100644 (file)
@@ -182,16 +182,16 @@ class OOUIHTMLForm extends HTMLForm {
                        return '';
                }
 
-               $config = [
-                       'items' => $fieldsHtml,
-               ];
+               $html = implode( '', $fieldsHtml );
+
                if ( $sectionName ) {
-                       $config['id'] = Sanitizer::escapeIdForAttribute( $sectionName );
-               }
-               if ( is_string( $this->mWrapperLegend ) ) {
-                       $config['label'] = $this->mWrapperLegend;
+                       $html = Html::rawElement(
+                               'div',
+                               [ 'id' => Sanitizer::escapeIdForAttribute( $sectionName ) ],
+                               $html
+                       );
                }
-               return new OOUI\FieldsetLayout( $config );
+               return $html;
        }
 
        /**
@@ -249,9 +249,8 @@ class OOUIHTMLForm extends HTMLForm {
        }
 
        public function getBody() {
-               $fieldset = parent::getBody();
-               // FIXME This only works for forms with no subsections
-               if ( $fieldset instanceof OOUI\FieldsetLayout ) {
+               $html = parent::getBody();
+               if ( $this->mHeader || $this->oouiErrors || $this->oouiWarnings ) {
                        $classes = [ 'mw-htmlform-ooui-header' ];
                        if ( $this->oouiErrors ) {
                                $classes[] = 'mw-htmlform-ooui-header-errors';
@@ -259,33 +258,42 @@ class OOUIHTMLForm extends HTMLForm {
                        if ( $this->oouiWarnings ) {
                                $classes[] = 'mw-htmlform-ooui-header-warnings';
                        }
-                       if ( $this->mHeader || $this->oouiErrors || $this->oouiWarnings ) {
-                               // if there's no header, don't create an (empty) LabelWidget, simply use a placeholder
-                               if ( $this->mHeader ) {
-                                       $element = new OOUI\LabelWidget( [ 'label' => new OOUI\HtmlSnippet( $this->mHeader ) ] );
-                               } else {
-                                       $element = new OOUI\Widget( [] );
-                               }
-                               $fieldset->addItems( [
-                                       new OOUI\FieldLayout(
-                                               $element,
-                                               [
-                                                       'align' => 'top',
-                                                       'errors' => $this->oouiErrors,
-                                                       'notices' => $this->oouiWarnings,
-                                                       'classes' => $classes,
-                                               ]
-                                       )
-                               ], 0 );
+                       // if there's no header, don't create an (empty) LabelWidget, simply use a placeholder
+                       if ( $this->mHeader ) {
+                               $element = new OOUI\LabelWidget( [ 'label' => new OOUI\HtmlSnippet( $this->mHeader ) ] );
+                       } else {
+                               $element = new OOUI\Widget( [] );
                        }
+                       $html = new OOUI\FieldLayout(
+                               $element,
+                               [
+                                       'align' => 'top',
+                                       'errors' => $this->oouiErrors,
+                                       'notices' => $this->oouiWarnings,
+                                       'classes' => $classes,
+                               ]
+                       ) . $html;
                }
-               return $fieldset;
+               return $html;
        }
 
        public function wrapForm( $html ) {
+               if ( is_string( $this->mWrapperLegend ) ) {
+                       $content = new OOUI\FieldsetLayout( [
+                               'label' => $this->mWrapperLegend,
+                               'items' => [
+                                       new OOUI\Widget( [
+                                               'content' => new OOUI\HtmlSnippet( $html )
+                                       ] ),
+                               ],
+                       ] );
+               } else {
+                       $content = new OOUI\HtmlSnippet( $html );
+               }
+
                $form = new OOUI\FormLayout( $this->getFormAttributes() + [
                        'classes' => [ 'mw-htmlform', 'mw-htmlform-ooui' ],
-                       'content' => new OOUI\HtmlSnippet( $html ),
+                       'content' => $content,
                ] );
 
                // Include a wrapper for style, if requested.
index 515166c..238b2b4 100644 (file)
@@ -132,13 +132,17 @@ class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable
        /**
         * Get the OOUI version of this field.
         *
+        * Returns OOUI\CheckboxMultiselectInputWidget for fields that only have one section,
+        * string otherwise.
+        *
         * @since 1.28
         * @param string[] $value
-        * @return OOUI\CheckboxMultiselectInputWidget
+        * @return string|OOUI\CheckboxMultiselectInputWidget
         */
        public function getInputOOUI( $value ) {
                $this->mParent->getOutput()->addModules( 'oojs-ui-widgets' );
 
+               $hasSections = false;
                $optionsOouiSections = [];
                $options = $this->getOptions();
                // If the options are supposed to be split into sections, each section becomes a separate
@@ -147,6 +151,7 @@ class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable
                        if ( is_array( $section ) ) {
                                $optionsOouiSections[ $label ] = Xml::listDropDownOptionsOoui( $section );
                                unset( $options[$label] );
+                               $hasSections = true;
                        }
                }
                // If anything remains in the array, they are sectionless options. Put them in a separate widget
@@ -158,7 +163,7 @@ class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable
                        );
                }
 
-               $out = '';
+               $out = [];
                foreach ( $optionsOouiSections as $sectionLabel => $optionsOoui ) {
                        $attr = [];
                        $attr['name'] = "{$this->mName}[]";
@@ -185,16 +190,22 @@ class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable
 
                        $widget = new OOUI\CheckboxMultiselectInputWidget( $attr );
                        if ( $sectionLabel ) {
-                               $out .= new OOUI\FieldsetLayout( [
+                               $out[] = new OOUI\FieldsetLayout( [
                                        'items' => [ $widget ],
                                        'label' => $sectionLabel,
                                ] );
                        } else {
-                               $out .= $widget;
+                               $out[] = $widget;
                        }
                }
 
-               return $out;
+               if ( !$hasSections ) {
+                       // Directly return the only OOUI\CheckboxMultiselectInputWidget.
+                       // This allows it to be made infusable and later tweaked by JS code.
+                       return $out[ 0 ];
+               }
+
+               return implode( '', $out );
        }
 
        /**
index 77ea7cd..c98e1ec 100644 (file)
@@ -72,10 +72,6 @@ class HTMLRadioField extends HTMLFormField {
                ) );
        }
 
-       protected function shouldInfuseOOUI() {
-               return true;
-       }
-
        public function formatOptions( $options, $value ) {
                global $wgUseMediaWikiUIEverywhere;
 
index a317822..54ff712 100644 (file)
@@ -340,12 +340,22 @@ abstract class DatabaseUpdater {
         *
         * @param string $tableName The table name
         * @param string $fieldName The field to be modified
-        * @param string $sqlPath The path to the SQL change path
+        * @param string $sqlPath The path to the SQL patch
         */
        public function modifyExtensionField( $tableName, $fieldName, $sqlPath ) {
                $this->extensionUpdates[] = [ 'modifyField', $tableName, $fieldName, $sqlPath, true ];
        }
 
+       /**
+        * @since 1.31
+        *
+        * @param string $tableName The table name
+        * @param string $sqlPath The path to the SQL patch
+        */
+       public function modifyExtensionTable( $tableName, $sqlPath ) {
+               $this->extensionUpdates[] = [ 'modifyTable', $tableName, $sqlPath, true ];
+       }
+
        /**
         *
         * @since 1.20
index 1f17fec..91f569f 100644 (file)
@@ -481,7 +481,7 @@ class PostgresUpdater extends DatabaseUpdater {
                        [ 'changeNullableField', 'protected_titles', 'pt_reason', 'NOT NULL', true ],
                        [ 'addPgField', 'protected_titles', 'pt_reason_id', 'INTEGER NOT NULL DEFAULT 0' ],
                        [ 'addTable', 'comment', 'patch-comment-table.sql' ],
-                       [ 'addIndex', 'site_stats', 'PRIMARY', 'patch-site_stats-pk.sql' ],
+                       [ 'addIndex', 'site_stats', 'site_stats_pkey', 'patch-site_stats-pk.sql' ],
                ];
        }
 
index 7d7e47b..0eecddb 100644 (file)
        "config-email-settings": "Configuración de correo electrónico",
        "config-enable-email": "Activar el envío de correos electrónicos",
        "config-enable-email-help": "Si quieres que el correo electrónico funcione, la [http://www.php.net/manual/en/mail.configuration.php configuración PHP de correo electrónico] debe ser la correcta.\nSi no quieres la funcionalidad de correo electrónico, puedes desactivarla aquí.",
-       "config-email-user": "Habilitar correo electrónico entre usuarios",
+       "config-email-user": "Activar correo electrónico entre usuarios",
        "config-email-user-help": "Permitir que todos los usuarios intercambien correos electrónicos si lo han activado en sus preferencias.",
        "config-email-usertalk": "Activar notificaciones de páginas de discusión de usuarios",
        "config-email-usertalk-help": "Permitir a los usuarios recibir notificaciones de cambios en la página de discusión de usuario, si lo han activado en sus preferencias.",
index 4d6095c..ae97154 100644 (file)
        "config-missing-db-host": "\"{{int:config-db-host}}\"-rentzako balioa sartu behar duzu.",
        "config-missing-db-server-oracle": "\"{{int:config-db-host-oracle}}\"-rentzako balioa sartu behar duzu.",
        "config-invalid-db-server-oracle": "\"$1\" TNS datu basea baliogabea.\nErabili \"TNS izena\" edo \"Konektagarritasun erraza\" katea ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle Naming Methods]).",
+       "config-invalid-db-name": "Datu-basearen izen okerra \"$1\"\nErabil ezazu ASCII letrak bakarrik (a-z, A-Z), zenbakiak (09), behe-gidoiak (_) eta gidoiak (-)",
+       "config-invalid-db-prefix": "Datu-basearen aurrizki okerra \"$1\"\nErabil ezazu ASCII letrak bakarrik (a-z, A-Z) behe-gidoiak (_) eta gidoiak (-)",
        "config-connection-error": "$1\n\nHost-a, erabiltzaile izena eta pasahitza egiaztatu eta saiatu berriro.",
+       "config-invalid-schema": "MediaWikiko eskema okerra \"$1\"\nErabil ezazu ASCII letrak bakarrik (a-z, A-Z) behe-gidoiak (_).",
        "config-db-sys-create-oracle": "Instalatzaileak bakarrik jasaten du SYSBDA kontu bat erabiltzaile kontu berri bat sortzeko.",
        "config-db-sys-user-exists-oracle": "$1 erabiltzaile kontua dagoeneko existitzen da. SYSDBA kontu berri bat sortzeko erabili daiteke soilik!",
        "config-postgres-old": "PostgreSQL $1 edo berriagoa behar da. Zuk $2 badaukazu.",
        "config-upgrade-done-no-regenerate": "Eguneratze prozesua amaitu egin da.\n\nHasi ahal zara [ $1 wikia arabiltzen]",
        "config-regenerate": "Birsortu LocalSettings.php →",
        "config-show-table-status": "<code>SHOW TABLE STATUS</code> kontsulta huts egin du!",
+       "config-unknown-collation": "<strong>Abisua:</strong> Datu-baseak kolazio ezezaguna ari da erabiltzen.",
        "config-db-web-account": "Datu-basearen kontua web sarbiderako.",
        "config-db-web-help": "Aukeratu erabiliko den erabiltzaile izena eta pasahitza web serbidorea eta datu-basearen serbidorea konektatzeko, wikiren operazio normalaren bitartean.",
        "config-db-web-account-same": "Instalazioan erabili duzun kontu berdina erabili.",
        "config-mysql-myisam": "MyISAM",
        "config-mysql-myisam-dep": "<strong>Oharra:</strong> MyISAM MySQL biltegiratze-motor gisa aukeratu duzu, MediaWikirekin erabiltzeko gomendagarria ez dena honengatik:\n*taula blokeoak direla-eta gauza gutxi onartu ohi du\n*beste motore batzuek baino ustelkeria gehiago izateko aukerak ditu\n*MediaWiki-ren kode baseak ez du beti kudeatzen MyISAM behar bezala\n\nZure MySQL instalazioa InnoDB onartzen badu, hori aukeratzeko gomendatzen da.\nZure MySQL instalazioa InnoDB ez badu onartzen, baliteke bertsioa berritzeko ordua izatea.",
        "config-mysql-only-myisam-dep": "<strong> Oharra: </strong> MyISAM makinaren MySQL biltegiratze motarako bakarra da, eta hau ez da MediaWiki-rekin erabiltzeko gomendatzen, honengatik:\n* maiztasunez taula blokeoek konkurrentzia ez dute onartzen \n* Beste motore batzuek baino ustelkeria gehiago izaten dute\n* MediaWiki-ren kodekak ez du beti kudeatzen MyISAM behar bezala\n\nZure MySQL instalazioak ez du InnoDB onartzen, agian bertsio berritzeko ordua da.",
+       "config-mysql-charset": "Datu-basearen karaktere multzoa:",
        "config-mysql-binary": "Bitarra",
        "config-mysql-utf8": "UTF-8",
        "config-mssql-auth": "Autentifikazio mota:",
        "config-install-mainpage-exists": "Orri nagusia dagoeneko existitzen da, hurrengora saltatzen",
        "config-install-extension-tables": "Taulak sortzen aktibatutako luzapenentzako.",
        "config-install-mainpage-failed": "Orri nagusia ezin izan da txertatu: $1",
-       "config-install-done": "<strong>Zorionak!</strong>\nMediaWiki instalatu duzu.\n\nInstalatzaileak sortu egin du <code>LocalSettings.php</code>\nZure konfigurazio guztia dauka.\n\nDeskargatu egin behar duzu eta jarri <code>$4</code> -ean . Deskarga automakikoki hasiko da.\n\nEz badizu deskargatzeko aukerarik eman, edo kantzelatu egin baduzu, hurrengo linkean klikatu berrabiatzeko:\n\n$3\n\n<strong>Oharra:</strong> Instalazio prozesuatik ateratzen bazara konfigurazio artxikoa deskargatu barik, gero ez da egongo eskuragarri.\n\nBehin hori eginda, <strong>[$2 enter your wiki]</strong> ahal duzu.",
+       "config-install-done": "<strong>Zorionak!</strong>\nMediaWiki instalatu duzu.\n\nInstalatzaileak <code>LocalSettings.php</code> fitxategia sortu egin du. \nZure konfigurazio guztia darama.\n\nDeskargatu egin beharko duzu eta zure wiki instalazio oinarrian jarri (index.php-rako direktorio berean). Deskarga automakikoki hasi behar izan da.\n\nDeskargatzeko aukerarik ez bazaizu eskaini, edo kantzelatu egin baduzu, hurrengo linkean klikatu berrabiarazteko deskarga:\n\n$3\n\n<strong>Oharra:</strong> Orain ez baduzu egiten, sortutako konfigurazio fitxategi hau ez da erabilgarri egongo geroago instalazioa bertan behera uzten baduzu deskargatu gabe.\n\nBehin hori eginda, <strong>[$2 zure wikia sartu]</strong> ahal duzu.",
        "config-install-done-path": "<strong>Zorionak!</strong>\nMediaWiki instalatu duzu.\n\nInstalatzaileak sortu egin du <code>LocalSettings.php</code>\nZure konfigurazio guztia dauka.\n\nDeskargatu egin behar duzu eta jarri <code>$4</code> -ean . Deskarga automakikoki hasiko da.\n\nEz badizu deskargatzeko aukerarik eman, edo kantzalatu egin baduzu, hurrengo linkean klikatu berrabiatzeko:\n\n$3\n\n<strong>Oharra:</strong> Instalazio prozesuatik ateratzen bazara konfigurazio artxikoa deskargatu barik, gero ez da egongo eskuragarri.\n\nBehin hori eginda, <strong>[$2 enter your wiki]</strong> ahal duzu.",
        "config-download-localsettings": "Jaitsi <code>LocalSettings.php</code>",
        "config-help": "Laguntza",
        "config-skins-screenshots": "$1 (Pantaila-irudia: $2)",
        "config-screenshot": "Pantaila-irudia",
        "mainpagetext": "<strong>MediaWiki instalatu da.</strong>",
-       "mainpagedocfooter": "Ikus [https://meta.wikimedia.org/wiki/Help:Contents Erabiltzaile Gida] wiki softwarea erabiltzen hasteko informazio gehiagorako.\n\n== Nola hasi ==\n\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Konfigurazio balioen zerrenda]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ (Maiz egindako galderak)]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWikiren argitalpenen posta zerrenda]"
+       "mainpagedocfooter": "Ikusi [https://meta.wikimedia.org/wiki/Help:Contents Erabiltzailearen Gida] wiki softwarea erabiltzen hasteko informazio gehiagorako.\n\n== Nola hasi ==\n\n*\n [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Konfigurazio balioen zerrenda]\n*\n [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ (MediaWikin Maiz egindako galderak)]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWikiren argitalpenen posta zerrenda]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Aurkitu MediaWiki zure hizkuntzan]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Zure wikian spam-a nola borrokatzen ikasi]"
 }
index 50521f6..2bfe36f 100644 (file)
@@ -68,7 +68,7 @@
        "config-help-restart": "Voulez-vous effacer toutes les données enregistrées que vous avez entrées et relancer le processus d'installation ?",
        "config-restart": "Oui, le relancer",
        "config-welcome": "=== Vérifications liées à l’environnement ===\nDes vérifications de base vont maintenant être effectuées pour voir si cet environnement est adapté à l’installation de MediaWiki.\nRappelez-vous d’inclure ces informations si vous recherchez de l’aide sur la manière de terminer l’installation.",
-       "config-copyright": "=== Droit d'auteur et conditions ===\n\n$1\n\nCe programme est un logiciel libre : vous pouvez le redistribuer et/ou le modifier selon les termes de la Licence Publique Générale GNU telle que publiée par la Free Software Foundation (version 2 de la Licence, ou, à votre choix, toute version ultérieure).\n\nCe programme est distribué dans l’espoir qu’il sera utile, mais '''sans aucune garantie''' : sans même les garanties implicites de '''commerciabilité''' ou d’'''adéquation à un usage particulier'''.\nVoir la Licence Publique Générale GNU pour plus de détails.\n\nVous devriez avoir reçu <doclink href=Copying>une copie de la Licence Publique Générale GNU</doclink> avec ce programme ; dans le cas contraire, écrivez à la Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ou [http://www.gnu.org/copyleft/gpl.html lisez-la en ligne].",
+       "config-copyright": "=== Droit d’auteur et conditions ===\n\n$1\n\nCe programme est un logiciel gratuit : vous pouvez le redistribuer ou le modifier selon les termes de la Licence Publique Générale GNU telle que publiée par la Free Software Foundation (version 2 de la Licence, ou, à votre choix, toute version ultérieure).\n\nCe programme est distribué dans l’espoir qu’il sera utile, mais '''sans aucune garantie''' : sans même les garanties implicites de '''commercialisabilité''' ou d’'''adéquation à un usage particulier'''.\nVoir la Licence Publique Générale GNU pour plus de détails.\n\nVous devriez avoir reçu <doclink href=Copying>une copie de la Licence Publique Générale GNU</doclink> avec ce programme ; dans le cas contraire, écrivez à la Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ou [http://www.gnu.org/copyleft/gpl.html lisez-la en ligne].",
        "config-sidebar": "* [https://www.mediawiki.org Accueil MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Guide de l’utilisateur]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Guide de l’administrateur]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ]\n----\n* <doclink href=Readme>Lisez-moi</doclink>\n* <doclink href=ReleaseNotes>Notes de publication</doclink>\n* <doclink href=Copying>Copie</doclink>\n* <doclink href=UpgradeDoc>Mise à jour</doclink>",
        "config-env-good": "L’environnement a été vérifié.\nVous pouvez installer MediaWiki.",
        "config-env-bad": "L’environnement a été vérifié.\nVous ne pouvez pas installer MediaWiki.",
index d712afe..a19dbc3 100644 (file)
        "config-help-tooltip": "kliknij, aby rozwinąć",
        "config-nofile": "Nie udało się odnaleźć pliku \"$1\". Czy nie został usunięty?",
        "config-extension-link": "Czy wiesz, że twoja wiki obsługuje [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions rozszerzenia]?\n\nMożesz przejrzeć [https://www.mediawiki.org/wiki/Category:Extensions_by_category rozszerzenia według kategorii] lub [https://www.mediawiki.org/wiki/Extension_Matrix Extension Matrix], aby zobaczyć pełną listę rozszerzeń.",
+       "config-screenshot": "zrzut ekranu",
        "mainpagetext": "<strong>Instalacja MediaWiki powiodła się.</strong>",
        "mainpagedocfooter": "Zapoznaj się z [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Podręcznikiem użytkownika] zawierającym informacje o tym jak korzystać z oprogramowania wiki.\n\n== Na początek ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Lista ustawień konfiguracyjnych]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Komunikaty o nowych wersjach MediaWiki (lista dyskusyjna)]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Przetłumacz MediaWiki na swój język]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Dowiedz się, jak walczyć ze spamem na swojej wiki]"
 }
index f61c139..21558f7 100644 (file)
  * @since 1.22
  */
 class HashRing {
-       /** @var Array (location => weight) */
+       /** @var array (location => weight) */
        protected $sourceMap = [];
-       /** @var Array (location => (start, end)) */
+       /** @var array (location => (start, end)) */
        protected $ring = [];
 
        /** @var HashRing|null */
        protected $liveRing;
-       /** @var Array (location => UNIX timestamp) */
+       /** @var array (location => UNIX timestamp) */
        protected $ejectionExpiries = [];
        /** @var int UNIX timestamp */
        protected $ejectionNextExpiry = INF;
index f50d26b..373ad93 100644 (file)
@@ -158,7 +158,7 @@ class SwiftFileBackend extends FileBackendStore {
        protected function resolveContainerPath( $container, $relStoragePath ) {
                if ( !mb_check_encoding( $relStoragePath, 'UTF-8' ) ) {
                        return null; // not UTF-8, makes it hard to use CF and the swift HTTP API
-               } elseif ( strlen( urlencode( $relStoragePath ) ) > 1024 ) {
+               } elseif ( strlen( rawurlencode( $relStoragePath ) ) > 1024 ) {
                        return null; // too long for Swift
                }
 
index 5f8d9a6..84c1182 100644 (file)
@@ -1,5 +1,7 @@
 <?php
 
+namespace Wikimedia\Http;
+
 /**
  * Utility for negotiating a value from a set of supported values using a preference list.
  * This is intended for use with HTTP headers like Accept, Accept-Language, Accept-Encoding, etc.
  *
  * @license GPL-2.0+
  * @author Daniel Kinzler
- * @author Thiemo Mättig
+ * @author Thiemo Kreuz
  */
-
-namespace Wikimedia\Http;
-
 class HttpAcceptNegotiator {
 
        /**
index d6cb340..8420f11 100644 (file)
@@ -747,10 +747,11 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
         * Make a global cache key.
         *
         * @since 1.27
-        * @param string $keys,... Key component (starting with a key collection name)
+        * @param string $class Key class
+        * @param string $component [optional] Key component (starting with a key collection name)
         * @return string Colon-delimited list of $keyspace followed by escaped components of $args
         */
-       public function makeGlobalKey() {
+       public function makeGlobalKey( $class, $component = null ) {
                return $this->makeKeyInternal( 'global', func_get_args() );
        }
 
@@ -758,10 +759,11 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
         * Make a cache key, scoped to this instance's keyspace.
         *
         * @since 1.27
-        * @param string $keys,... Key component (starting with a key collection name)
+        * @param string $class Key class
+        * @param string $component [optional] Key component (starting with a key collection name)
         * @return string Colon-delimited list of $keyspace followed by escaped components of $args
         */
-       public function makeKey() {
+       public function makeKey( $class, $component = null ) {
                return $this->makeKeyInternal( $this->keyspace, func_get_args() );
        }
 
index c85a82e..ae434c1 100644 (file)
@@ -86,11 +86,11 @@ class CachedBagOStuff extends HashBagOStuff {
                return $this->backend->deleteObjectsExpiringBefore( $date, $progressCallback );
        }
 
-       public function makeKey() {
+       public function makeKey( $class, $component = null ) {
                return call_user_func_array( [ $this->backend, __FUNCTION__ ], func_get_args() );
        }
 
-       public function makeGlobalKey() {
+       public function makeGlobalKey( $class, $component = null ) {
                return call_user_func_array( [ $this->backend, __FUNCTION__ ], func_get_args() );
        }
 
index 0188991..f7bf86b 100644 (file)
@@ -137,7 +137,7 @@ class MemcachedBagOStuff extends BagOStuff {
                );
 
                if ( $charsLeft < 0 ) {
-                       return $keyspace . ':##' . md5( implode( ':', $args ) );
+                       return $keyspace . ':BagOStuff-long-key:##' . md5( implode( ':', $args ) );
                }
 
                return $keyspace . ':' . implode( ':', $args );
index 200ab79..643f318 100644 (file)
@@ -233,11 +233,11 @@ class MultiWriteBagOStuff extends BagOStuff {
                return $ret;
        }
 
-       public function makeKey() {
+       public function makeKey( $class, $component = null ) {
                return call_user_func_array( [ $this->caches[0], __FUNCTION__ ], func_get_args() );
        }
 
-       public function makeGlobalKey() {
+       public function makeGlobalKey( $class, $component = null ) {
                return call_user_func_array( [ $this->caches[0], __FUNCTION__ ], func_get_args() );
        }
 }
index 73e4a9a..51c4669 100644 (file)
@@ -917,7 +917,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                                        // Value existed before with a different version; use variant key.
                                        // Reflect purges to $key by requiring that this key value be newer.
                                        $value = $this->doGetWithSetCallback(
-                                               'cache-variant:' . md5( $key ) . ":$version",
+                                               $this->makeGlobalKey( 'WANCache-key-variant', md5( $key ), $version ),
                                                $ttl,
                                                $callback,
                                                // Regenerate value if not newer than $key
@@ -1388,21 +1388,23 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
 
        /**
         * @see BagOStuff::makeKey()
-        * @param string $keys,... Key component (starting with a key collection name)
+        * @param string $class Key class
+        * @param string $component [optional] Key component (starting with a key collection name)
         * @return string Colon-delimited list of $keyspace followed by escaped components of $args
         * @since 1.27
         */
-       public function makeKey() {
+       public function makeKey( $class, $component = null ) {
                return call_user_func_array( [ $this->cache, __FUNCTION__ ], func_get_args() );
        }
 
        /**
         * @see BagOStuff::makeGlobalKey()
-        * @param string $keys,... Key component (starting with a key collection name)
+        * @param string $class Key class
+        * @param string $component [optional] Key component (starting with a key collection name)
         * @return string Colon-delimited list of $keyspace followed by escaped components of $args
         * @since 1.27
         */
-       public function makeGlobalKey() {
+       public function makeGlobalKey( $class, $component = null ) {
                return call_user_func_array( [ $this->cache, __FUNCTION__ ], func_get_args() );
        }
 
index dbd97a7..980a8bf 100644 (file)
@@ -103,4 +103,15 @@ interface LinkTarget {
         * @return string
         */
        public function getInterwiki();
+
+       /**
+        * Returns an informative human readable representation of the link target,
+        * for use in logging and debugging. There is no requirement for the return
+        * value to have any relationship with the input of TitleParser.
+        * @since 1.31
+        *
+        * @return string
+        */
+       public function __toString();
+
 }
index ce1df0d..1686bbb 100644 (file)
@@ -30,7 +30,6 @@
  * header format when requested.
  */
 class MailAddress {
-
        /**
         * @var string
         */
index bd78b49..2b13893 100644 (file)
@@ -97,19 +97,50 @@ class SvgHandler extends ImageHandler {
                        if ( isset( $metadata['translations'] ) ) {
                                foreach ( $metadata['translations'] as $lang => $langType ) {
                                        if ( $langType === SVGReader::LANG_FULL_MATCH ) {
-                                               $langList[] = $lang;
+                                               $langList[] = strtolower( $lang );
                                        }
                                }
                        }
                }
-               return $langList;
+               return array_unique( $langList );
        }
 
        /**
-        * What language to render file in if none selected.
+        * SVG's systemLanguage matching rules state:
+        * 'The `systemLanguage` attribute ... [e]valuates to "true" if one of the languages indicated
+        * by user preferences exactly equals one of the languages given in the value of this parameter,
+        * or if one of the languages indicated by user preferences exactly equals a prefix of one of
+        * the languages given in the value of this parameter such that the first tag character
+        * following the prefix is "-".'
         *
-        * @param File $file
-        * @return string Language code.
+        * Return the first element of $svgLanguages that matches $userPreferredLanguage
+        *
+        * @see https://www.w3.org/TR/SVG/struct.html#SystemLanguageAttribute
+        * @param string $userPreferredLanguage
+        * @param array $svgLanguages
+        * @return string|null
+        */
+       public function getMatchedLanguage( $userPreferredLanguage, array $svgLanguages ) {
+               foreach ( $svgLanguages as $svgLang ) {
+                       if ( strcasecmp( $svgLang, $userPreferredLanguage ) === 0 ) {
+                               return $svgLang;
+                       }
+                       $trimmedSvgLang = $svgLang;
+                       while ( strpos( $trimmedSvgLang, '-' ) !== false ) {
+                               $trimmedSvgLang = substr( $trimmedSvgLang, 0, strrpos( $trimmedSvgLang, '-' ) );
+                               if ( strcasecmp( $trimmedSvgLang, $userPreferredLanguage ) === 0 ) {
+                                       return $svgLang;
+                               }
+                       }
+               }
+               return null;
+       }
+
+       /**
+        * What language to render file in if none selected
+        *
+        * @param File $file Language code
+        * @return string
         */
        public function getDefaultRenderLanguage( File $file ) {
                return 'en';
@@ -479,7 +510,7 @@ class SvgHandler extends ImageHandler {
                        return ( $value > 0 );
                } elseif ( $name == 'lang' ) {
                        // Validate $code
-                       if ( $value === '' || !Language::isValidBuiltInCode( $value ) ) {
+                       if ( $value === '' || !Language::isValidCode( $value ) ) {
                                wfDebug( "Invalid user language code\n" );
 
                                return false;
@@ -499,8 +530,7 @@ class SvgHandler extends ImageHandler {
        public function makeParamString( $params ) {
                $lang = '';
                if ( isset( $params['lang'] ) && $params['lang'] !== 'en' ) {
-                       $params['lang'] = strtolower( $params['lang'] );
-                       $lang = "lang{$params['lang']}-";
+                       $lang = 'lang' . strtolower( $params['lang'] ) . '-';
                }
                if ( !isset( $params['width'] ) ) {
                        return false;
@@ -511,7 +541,7 @@ class SvgHandler extends ImageHandler {
 
        public function parseParamString( $str ) {
                $m = false;
-               if ( preg_match( '/^lang([a-z]+(?:-[a-z]+)*)-(\d+)px$/', $str, $m ) ) {
+               if ( preg_match( '/^lang([a-z]+(?:-[a-z]+)*)-(\d+)px$/i', $str, $m ) ) {
                        return [ 'width' => array_pop( $m ), 'lang' => $m[1] ];
                } elseif ( preg_match( '/^(\d+)px$/', $str, $m ) ) {
                        return [ 'width' => $m[1], 'lang' => 'en' ];
index de438da..85430d2 100644 (file)
@@ -512,7 +512,7 @@ abstract class TransformationalImageHandler extends ImageHandler {
                $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
                $method = __METHOD__;
                return $cache->getWithSetCallback(
-                       'imagemagick-version',
+                       $cache->makeGlobalKey( 'imagemagick-version' ),
                        $cache::TTL_HOUR,
                        function () use ( $method ) {
                                global $wgImageMagickConvertCommand;
index df189af..c9dc273 100644 (file)
@@ -590,7 +590,7 @@ class Article implements Page {
                                                        $outputPage->setRobotPolicy( 'noindex,nofollow' );
 
                                                        $errortext = $error->getWikiText( false, 'view-pool-error' );
-                                                       $outputPage->addWikiText( '<div class="errorbox">' . $errortext . '</div>' );
+                                                       $outputPage->addWikiText( Html::errorBox( $errortext ) );
                                                }
                                                # Connection or timeout error
                                                return;
index 639cbd0..c4baae4 100644 (file)
@@ -285,6 +285,19 @@ class ImagePage extends Article {
                return parent::getContentObject();
        }
 
+       private function getLanguageForRendering( WebRequest $request, File $file ) {
+               $handler = $this->displayImg->getHandler();
+
+               $requestLanguage = $request->getVal( 'lang' );
+               if ( !is_null( $requestLanguage ) ) {
+                       if ( $handler && $handler->validateParam( 'lang', $requestLanguage ) ) {
+                               return $requestLanguage;
+                       }
+               }
+
+               return $handler->getDefaultRenderLanguage( $this->displayImg );
+       }
+
        protected function openShowImage() {
                global $wgEnableUploads, $wgSend404Code, $wgSVGMaxSize;
 
@@ -309,14 +322,9 @@ class ImagePage extends Article {
                                $params = [ 'page' => $page ];
                        }
 
-                       $renderLang = $request->getVal( 'lang' );
+                       $renderLang = $this->getLanguageForRendering( $request, $this->displayImg );
                        if ( !is_null( $renderLang ) ) {
-                               $handler = $this->displayImg->getHandler();
-                               if ( $handler && $handler->validateParam( 'lang', $renderLang ) ) {
-                                       $params['lang'] = $renderLang;
-                               } else {
-                                       $renderLang = null;
-                               }
+                               $params['lang'] = $renderLang;
                        }
 
                        $width_orig = $this->displayImg->getWidth( $page );
@@ -544,12 +552,7 @@ EOT
 
                        $renderLangOptions = $this->displayImg->getAvailableLanguages();
                        if ( count( $renderLangOptions ) >= 1 ) {
-                               $currentLanguage = $renderLang;
-                               $defaultLang = $this->displayImg->getDefaultRenderLanguage();
-                               if ( is_null( $currentLanguage ) ) {
-                                       $currentLanguage = $defaultLang;
-                               }
-                               $out->addHTML( $this->doRenderLangOpt( $renderLangOptions, $currentLanguage, $defaultLang ) );
+                               $out->addHTML( $this->doRenderLangOpt( $renderLangOptions, $renderLang ) );
                        }
 
                        // Add cannot animate thumbnail warning
@@ -1047,60 +1050,31 @@ EOT
         * Output a drop-down box for language options for the file
         *
         * @param array $langChoices Array of string language codes
-        * @param string $curLang Language code file is being viewed in.
-        * @param string $defaultLang Language code that image is rendered in by default
+        * @param string $renderLang Language code for the language we want the file to rendered in.
         * @return string HTML to insert underneath image.
         */
-       protected function doRenderLangOpt( array $langChoices, $curLang, $defaultLang ) {
+       protected function doRenderLangOpt( array $langChoices, $renderLang ) {
                global $wgScript;
-               sort( $langChoices );
-               $curLang = LanguageCode::bcp47( $curLang );
-               $defaultLang = LanguageCode::bcp47( $defaultLang );
                $opts = '';
-               $haveCurrentLang = false;
-               $haveDefaultLang = false;
-
-               // We make a list of all the language choices in the file.
-               // Additionally if the default language to render this file
-               // is not included as being in this file (for example, in svgs
-               // usually the fallback content is the english content) also
-               // include a choice for that. Last of all, if we're viewing
-               // the file in a language not on the list, add it as a choice.
+
+               $matchedRenderLang = $this->displayImg->getMatchedLanguage( $renderLang );
+
                foreach ( $langChoices as $lang ) {
-                       $code = LanguageCode::bcp47( $lang );
-                       $name = Language::fetchLanguageName( $code, $this->getContext()->getLanguage()->getCode() );
-                       if ( $name !== '' ) {
-                               $display = $this->getContext()->msg( 'img-lang-opt', $code, $name )->text();
-                       } else {
-                               $display = $code;
-                       }
-                       $opts .= "\n" . Xml::option( $display, $code, $curLang === $code );
-                       if ( $curLang === $code ) {
-                               $haveCurrentLang = true;
-                       }
-                       if ( $defaultLang === $code ) {
-                               $haveDefaultLang = true;
-                       }
-               }
-               if ( !$haveDefaultLang ) {
-                       // Its hard to know if the content is really in the default language, or
-                       // if its just unmarked content that could be in any language.
-                       $opts = Xml::option(
-                                       $this->getContext()->msg( 'img-lang-default' )->text(),
-                               $defaultLang,
-                               $defaultLang === $curLang
-                       ) . $opts;
-               }
-               if ( !$haveCurrentLang && $defaultLang !== $curLang ) {
-                       $name = Language::fetchLanguageName( $curLang, $this->getContext()->getLanguage()->getCode() );
-                       if ( $name !== '' ) {
-                               $display = $this->getContext()->msg( 'img-lang-opt', $curLang, $name )->text();
-                       } else {
-                               $display = $curLang;
-                       }
-                       $opts = Xml::option( $display, $curLang, true ) . $opts;
+                       $opts .= $this->createXmlOptionStringForLanguage(
+                               $lang,
+                               $matchedRenderLang === $lang
+                       );
                }
 
+               // Allow for the default case in an svg <switch> that is displayed if no
+               // systemLanguage attribute matches
+               $opts .= "\n" .
+                       Xml::option(
+                               $this->getContext()->msg( 'img-lang-default' )->text(),
+                               'und',
+                               is_null( $matchedRenderLang )
+                       );
+
                $select = Html::rawElement(
                        'select',
                        [ 'id' => 'mw-imglangselector', 'name' => 'lang' ],
@@ -1119,6 +1093,27 @@ EOT
                return $langSelectLine;
        }
 
+       /**
+        * @param $lang string
+        * @param $selected bool
+        * @return string
+        */
+       private function createXmlOptionStringForLanguage( $lang, $selected ) {
+               $code = LanguageCode::bcp47( $lang );
+               $name = Language::fetchLanguageName( $code, $this->getContext()->getLanguage()->getCode() );
+               if ( $name !== '' ) {
+                       $display = $this->getContext()->msg( 'img-lang-opt', $code, $name )->text();
+               } else {
+                       $display = $code;
+               }
+               return "\n" .
+                       Xml::option(
+                               $display,
+                               $lang,
+                               $selected
+                       );
+       }
+
        /**
         * Get the width and height to display image at.
         *
diff --git a/includes/parser/RemexStripTagHandler.php b/includes/parser/RemexStripTagHandler.php
new file mode 100644 (file)
index 0000000..2839147
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+
+use RemexHtml\Tokenizer\Attributes;
+use RemexHtml\Tokenizer\TokenHandler;
+use RemexHtml\Tokenizer\Tokenizer;
+
+/**
+ * @internal
+ */
+class RemexStripTagHandler implements TokenHandler {
+       private $text = '';
+       public function getResult() {
+               return $this->text;
+       }
+
+       function startDocument( Tokenizer $t, $fns, $fn ) {
+               // Do nothing.
+       }
+       function endDocument( $pos ) {
+               // Do nothing.
+       }
+       function error( $text, $pos ) {
+               // Do nothing.
+       }
+       function characters( $text, $start, $length, $sourceStart, $sourceLength ) {
+               $this->text .= substr( $text, $start, $length );
+       }
+       function startTag( $name, Attributes $attrs, $selfClose, $sourceStart, $sourceLength ) {
+               // Do nothing.
+       }
+       function endTag( $name, $sourceStart, $sourceLength ) {
+               // Do nothing.
+       }
+       function doctype( $name, $public, $system, $quirks, $sourceStart, $sourceLength ) {
+               // Do nothing.
+       }
+       function comment( $text, $sourceStart, $sourceLength ) {
+               // Do nothing.
+       }
+}
diff --git a/includes/parser/Sanitizer.php b/includes/parser/Sanitizer.php
new file mode 100644 (file)
index 0000000..7c9f563
--- /dev/null
@@ -0,0 +1,2120 @@
+<?php
+/**
+ * HTML sanitizer for %MediaWiki.
+ *
+ * Copyright © 2002-2005 Brion Vibber <brion@pobox.com> et al
+ * https://www.mediawiki.org/
+ *
+ * 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 Parser
+ */
+
+/**
+ * HTML sanitizer for MediaWiki
+ * @ingroup Parser
+ */
+class Sanitizer {
+       /**
+        * Regular expression to match various types of character references in
+        * Sanitizer::normalizeCharReferences and Sanitizer::decodeCharReferences
+        */
+       const CHAR_REFS_REGEX =
+               '/&([A-Za-z0-9\x80-\xff]+);
+                |&\#([0-9]+);
+                |&\#[xX]([0-9A-Fa-f]+);
+                |(&)/x';
+
+       /**
+        * Acceptable tag name charset from HTML5 parsing spec
+        * https://www.w3.org/TR/html5/syntax.html#tag-open-state
+        */
+       const ELEMENT_BITS_REGEX = '!^(/?)([A-Za-z][^\t\n\v />\0]*+)([^>]*?)(/?>)([^<]*)$!';
+
+       /**
+        * Blacklist for evil uris like javascript:
+        * WARNING: DO NOT use this in any place that actually requires blacklisting
+        * for security reasons. There are NUMEROUS[1] ways to bypass blacklisting, the
+        * only way to be secure from javascript: uri based xss vectors is to whitelist
+        * things that you know are safe and deny everything else.
+        * [1]: http://ha.ckers.org/xss.html
+        */
+       const EVIL_URI_PATTERN = '!(^|\s|\*/\s*)(javascript|vbscript)([^\w]|$)!i';
+       const XMLNS_ATTRIBUTE_PATTERN = "/^xmlns:[:A-Z_a-z-.0-9]+$/";
+
+       /**
+        * Tells escapeUrlForHtml() to encode the ID using the wiki's primary encoding.
+        *
+        * @since 1.30
+        */
+       const ID_PRIMARY = 0;
+
+       /**
+        * Tells escapeUrlForHtml() to encode the ID using the fallback encoding, or return false
+        * if no fallback is configured.
+        *
+        * @since 1.30
+        */
+       const ID_FALLBACK = 1;
+
+       /**
+        * List of all named character entities defined in HTML 4.01
+        * https://www.w3.org/TR/html4/sgml/entities.html
+        * As well as &apos; which is only defined starting in XHTML1.
+        */
+       private static $htmlEntities = [
+               'Aacute'   => 193,
+               'aacute'   => 225,
+               'Acirc'    => 194,
+               'acirc'    => 226,
+               'acute'    => 180,
+               'AElig'    => 198,
+               'aelig'    => 230,
+               'Agrave'   => 192,
+               'agrave'   => 224,
+               'alefsym'  => 8501,
+               'Alpha'    => 913,
+               'alpha'    => 945,
+               'amp'      => 38,
+               'and'      => 8743,
+               'ang'      => 8736,
+               'apos'     => 39, // New in XHTML & HTML 5; avoid in output for compatibility with IE.
+               'Aring'    => 197,
+               'aring'    => 229,
+               'asymp'    => 8776,
+               'Atilde'   => 195,
+               'atilde'   => 227,
+               'Auml'     => 196,
+               'auml'     => 228,
+               'bdquo'    => 8222,
+               'Beta'     => 914,
+               'beta'     => 946,
+               'brvbar'   => 166,
+               'bull'     => 8226,
+               'cap'      => 8745,
+               'Ccedil'   => 199,
+               'ccedil'   => 231,
+               'cedil'    => 184,
+               'cent'     => 162,
+               'Chi'      => 935,
+               'chi'      => 967,
+               'circ'     => 710,
+               'clubs'    => 9827,
+               'cong'     => 8773,
+               'copy'     => 169,
+               'crarr'    => 8629,
+               'cup'      => 8746,
+               'curren'   => 164,
+               'dagger'   => 8224,
+               'Dagger'   => 8225,
+               'darr'     => 8595,
+               'dArr'     => 8659,
+               'deg'      => 176,
+               'Delta'    => 916,
+               'delta'    => 948,
+               'diams'    => 9830,
+               'divide'   => 247,
+               'Eacute'   => 201,
+               'eacute'   => 233,
+               'Ecirc'    => 202,
+               'ecirc'    => 234,
+               'Egrave'   => 200,
+               'egrave'   => 232,
+               'empty'    => 8709,
+               'emsp'     => 8195,
+               'ensp'     => 8194,
+               'Epsilon'  => 917,
+               'epsilon'  => 949,
+               'equiv'    => 8801,
+               'Eta'      => 919,
+               'eta'      => 951,
+               'ETH'      => 208,
+               'eth'      => 240,
+               'Euml'     => 203,
+               'euml'     => 235,
+               'euro'     => 8364,
+               'exist'    => 8707,
+               'fnof'     => 402,
+               'forall'   => 8704,
+               'frac12'   => 189,
+               'frac14'   => 188,
+               'frac34'   => 190,
+               'frasl'    => 8260,
+               'Gamma'    => 915,
+               'gamma'    => 947,
+               'ge'       => 8805,
+               'gt'       => 62,
+               'harr'     => 8596,
+               'hArr'     => 8660,
+               'hearts'   => 9829,
+               'hellip'   => 8230,
+               'Iacute'   => 205,
+               'iacute'   => 237,
+               'Icirc'    => 206,
+               'icirc'    => 238,
+               'iexcl'    => 161,
+               'Igrave'   => 204,
+               'igrave'   => 236,
+               'image'    => 8465,
+               'infin'    => 8734,
+               'int'      => 8747,
+               'Iota'     => 921,
+               'iota'     => 953,
+               'iquest'   => 191,
+               'isin'     => 8712,
+               'Iuml'     => 207,
+               'iuml'     => 239,
+               'Kappa'    => 922,
+               'kappa'    => 954,
+               'Lambda'   => 923,
+               'lambda'   => 955,
+               'lang'     => 9001,
+               'laquo'    => 171,
+               'larr'     => 8592,
+               'lArr'     => 8656,
+               'lceil'    => 8968,
+               'ldquo'    => 8220,
+               'le'       => 8804,
+               'lfloor'   => 8970,
+               'lowast'   => 8727,
+               'loz'      => 9674,
+               'lrm'      => 8206,
+               'lsaquo'   => 8249,
+               'lsquo'    => 8216,
+               'lt'       => 60,
+               'macr'     => 175,
+               'mdash'    => 8212,
+               'micro'    => 181,
+               'middot'   => 183,
+               'minus'    => 8722,
+               'Mu'       => 924,
+               'mu'       => 956,
+               'nabla'    => 8711,
+               'nbsp'     => 160,
+               'ndash'    => 8211,
+               'ne'       => 8800,
+               'ni'       => 8715,
+               'not'      => 172,
+               'notin'    => 8713,
+               'nsub'     => 8836,
+               'Ntilde'   => 209,
+               'ntilde'   => 241,
+               'Nu'       => 925,
+               'nu'       => 957,
+               'Oacute'   => 211,
+               'oacute'   => 243,
+               'Ocirc'    => 212,
+               'ocirc'    => 244,
+               'OElig'    => 338,
+               'oelig'    => 339,
+               'Ograve'   => 210,
+               'ograve'   => 242,
+               'oline'    => 8254,
+               'Omega'    => 937,
+               'omega'    => 969,
+               'Omicron'  => 927,
+               'omicron'  => 959,
+               'oplus'    => 8853,
+               'or'       => 8744,
+               'ordf'     => 170,
+               'ordm'     => 186,
+               'Oslash'   => 216,
+               'oslash'   => 248,
+               'Otilde'   => 213,
+               'otilde'   => 245,
+               'otimes'   => 8855,
+               'Ouml'     => 214,
+               'ouml'     => 246,
+               'para'     => 182,
+               'part'     => 8706,
+               'permil'   => 8240,
+               'perp'     => 8869,
+               'Phi'      => 934,
+               'phi'      => 966,
+               'Pi'       => 928,
+               'pi'       => 960,
+               'piv'      => 982,
+               'plusmn'   => 177,
+               'pound'    => 163,
+               'prime'    => 8242,
+               'Prime'    => 8243,
+               'prod'     => 8719,
+               'prop'     => 8733,
+               'Psi'      => 936,
+               'psi'      => 968,
+               'quot'     => 34,
+               'radic'    => 8730,
+               'rang'     => 9002,
+               'raquo'    => 187,
+               'rarr'     => 8594,
+               'rArr'     => 8658,
+               'rceil'    => 8969,
+               'rdquo'    => 8221,
+               'real'     => 8476,
+               'reg'      => 174,
+               'rfloor'   => 8971,
+               'Rho'      => 929,
+               'rho'      => 961,
+               'rlm'      => 8207,
+               'rsaquo'   => 8250,
+               'rsquo'    => 8217,
+               'sbquo'    => 8218,
+               'Scaron'   => 352,
+               'scaron'   => 353,
+               'sdot'     => 8901,
+               'sect'     => 167,
+               'shy'      => 173,
+               'Sigma'    => 931,
+               'sigma'    => 963,
+               'sigmaf'   => 962,
+               'sim'      => 8764,
+               'spades'   => 9824,
+               'sub'      => 8834,
+               'sube'     => 8838,
+               'sum'      => 8721,
+               'sup'      => 8835,
+               'sup1'     => 185,
+               'sup2'     => 178,
+               'sup3'     => 179,
+               'supe'     => 8839,
+               'szlig'    => 223,
+               'Tau'      => 932,
+               'tau'      => 964,
+               'there4'   => 8756,
+               'Theta'    => 920,
+               'theta'    => 952,
+               'thetasym' => 977,
+               'thinsp'   => 8201,
+               'THORN'    => 222,
+               'thorn'    => 254,
+               'tilde'    => 732,
+               'times'    => 215,
+               'trade'    => 8482,
+               'Uacute'   => 218,
+               'uacute'   => 250,
+               'uarr'     => 8593,
+               'uArr'     => 8657,
+               'Ucirc'    => 219,
+               'ucirc'    => 251,
+               'Ugrave'   => 217,
+               'ugrave'   => 249,
+               'uml'      => 168,
+               'upsih'    => 978,
+               'Upsilon'  => 933,
+               'upsilon'  => 965,
+               'Uuml'     => 220,
+               'uuml'     => 252,
+               'weierp'   => 8472,
+               'Xi'       => 926,
+               'xi'       => 958,
+               'Yacute'   => 221,
+               'yacute'   => 253,
+               'yen'      => 165,
+               'Yuml'     => 376,
+               'yuml'     => 255,
+               'Zeta'     => 918,
+               'zeta'     => 950,
+               'zwj'      => 8205,
+               'zwnj'     => 8204
+       ];
+
+       /**
+        * Character entity aliases accepted by MediaWiki
+        */
+       private static $htmlEntityAliases = [
+               'רלמ' => 'rlm',
+               'رلم' => 'rlm',
+       ];
+
+       /**
+        * Lazy-initialised attributes regex, see getAttribsRegex()
+        */
+       private static $attribsRegex;
+
+       /**
+        * Regular expression to match HTML/XML attribute pairs within a tag.
+        * Allows some... latitude. Based on,
+        * https://www.w3.org/TR/html5/syntax.html#before-attribute-value-state
+        * Used in Sanitizer::fixTagAttributes and Sanitizer::decodeTagAttributes
+        * @return string
+        */
+       static function getAttribsRegex() {
+               if ( self::$attribsRegex === null ) {
+                       $attribFirst = "[:_\p{L}\p{N}]";
+                       $attrib = "[:_\.\-\p{L}\p{N}]";
+                       $space = '[\x09\x0a\x0c\x0d\x20]';
+                       self::$attribsRegex =
+                               "/(?:^|$space)({$attribFirst}{$attrib}*)
+                                       ($space*=$space*
+                                       (?:
+                                               # The attribute value: quoted or alone
+                                               \"([^\"]*)(?:\"|\$)
+                                               | '([^']*)(?:'|\$)
+                                               | (((?!$space|>).)*)
+                                       )
+                               )?(?=$space|\$)/sxu";
+               }
+               return self::$attribsRegex;
+       }
+
+       /**
+        * Return the various lists of recognized tags
+        * @param array $extratags For any extra tags to include
+        * @param array $removetags For any tags (default or extra) to exclude
+        * @return array
+        */
+       public static function getRecognizedTagData( $extratags = [], $removetags = [] ) {
+               global $wgAllowImageTag;
+
+               static $htmlpairsStatic, $htmlsingle, $htmlsingleonly, $htmlnest, $tabletags,
+                       $htmllist, $listtags, $htmlsingleallowed, $htmlelementsStatic, $staticInitialised;
+
+               // Base our staticInitialised variable off of the global config state so that if the globals
+               // are changed (like in the screwed up test system) we will re-initialise the settings.
+               $globalContext = $wgAllowImageTag;
+               if ( !$staticInitialised || $staticInitialised != $globalContext ) {
+                       $htmlpairsStatic = [ # Tags that must be closed
+                               'b', 'bdi', 'del', 'i', 'ins', 'u', 'font', 'big', 'small', 'sub', 'sup', 'h1',
+                               'h2', 'h3', 'h4', 'h5', 'h6', 'cite', 'code', 'em', 's',
+                               'strike', 'strong', 'tt', 'var', 'div', 'center',
+                               'blockquote', 'ol', 'ul', 'dl', 'table', 'caption', 'pre',
+                               'ruby', 'rb', 'rp', 'rt', 'rtc', 'p', 'span', 'abbr', 'dfn',
+                               'kbd', 'samp', 'data', 'time', 'mark'
+                       ];
+                       $htmlsingle = [
+                               'br', 'wbr', 'hr', 'li', 'dt', 'dd', 'meta', 'link'
+                       ];
+
+                       # Elements that cannot have close tags. This is (not coincidentally)
+                       # also the list of tags for which the HTML 5 parsing algorithm
+                       # requires you to "acknowledge the token's self-closing flag", i.e.
+                       # a self-closing tag like <br/> is not an HTML 5 parse error only
+                       # for this list.
+                       $htmlsingleonly = [
+                               'br', 'wbr', 'hr', 'meta', 'link'
+                       ];
+
+                       $htmlnest = [ # Tags that can be nested--??
+                               'table', 'tr', 'td', 'th', 'div', 'blockquote', 'ol', 'ul',
+                               'li', 'dl', 'dt', 'dd', 'font', 'big', 'small', 'sub', 'sup', 'span',
+                               'var', 'kbd', 'samp', 'em', 'strong', 'q', 'ruby', 'bdo'
+                       ];
+                       $tabletags = [ # Can only appear inside table, we will close them
+                               'td', 'th', 'tr',
+                       ];
+                       $htmllist = [ # Tags used by list
+                               'ul', 'ol',
+                       ];
+                       $listtags = [ # Tags that can appear in a list
+                               'li',
+                       ];
+
+                       if ( $wgAllowImageTag ) {
+                               $htmlsingle[] = 'img';
+                               $htmlsingleonly[] = 'img';
+                       }
+
+                       $htmlsingleallowed = array_unique( array_merge( $htmlsingle, $tabletags ) );
+                       $htmlelementsStatic = array_unique( array_merge( $htmlsingle, $htmlpairsStatic, $htmlnest ) );
+
+                       # Convert them all to hashtables for faster lookup
+                       $vars = [ 'htmlpairsStatic', 'htmlsingle', 'htmlsingleonly', 'htmlnest', 'tabletags',
+                               'htmllist', 'listtags', 'htmlsingleallowed', 'htmlelementsStatic' ];
+                       foreach ( $vars as $var ) {
+                               $$var = array_flip( $$var );
+                       }
+                       $staticInitialised = $globalContext;
+               }
+
+               # Populate $htmlpairs and $htmlelements with the $extratags and $removetags arrays
+               $extratags = array_flip( $extratags );
+               $removetags = array_flip( $removetags );
+               $htmlpairs = array_merge( $extratags, $htmlpairsStatic );
+               $htmlelements = array_diff_key( array_merge( $extratags, $htmlelementsStatic ), $removetags );
+
+               return [
+                       'htmlpairs' => $htmlpairs,
+                       'htmlsingle' => $htmlsingle,
+                       'htmlsingleonly' => $htmlsingleonly,
+                       'htmlnest' => $htmlnest,
+                       'tabletags' => $tabletags,
+                       'htmllist' => $htmllist,
+                       'listtags' => $listtags,
+                       'htmlsingleallowed' => $htmlsingleallowed,
+                       'htmlelements' => $htmlelements,
+               ];
+       }
+
+       /**
+        * Cleans up HTML, removes dangerous tags and attributes, and
+        * removes HTML comments
+        * @param string $text
+        * @param callable $processCallback Callback to do any variable or parameter
+        *   replacements in HTML attribute values
+        * @param array|bool $args Arguments for the processing callback
+        * @param array $extratags For any extra tags to include
+        * @param array $removetags For any tags (default or extra) to exclude
+        * @param callable $warnCallback (Deprecated) Callback allowing the
+        *   addition of a tracking category when bad input is encountered.
+        *   DO NOT ADD NEW PARAMETERS AFTER $warnCallback, since it will be
+        *   removed shortly.
+        * @return string
+        */
+       public static function removeHTMLtags( $text, $processCallback = null,
+               $args = [], $extratags = [], $removetags = [], $warnCallback = null
+       ) {
+               extract( self::getRecognizedTagData( $extratags, $removetags ) );
+
+               # Remove HTML comments
+               $text = self::removeHTMLcomments( $text );
+               $bits = explode( '<', $text );
+               $text = str_replace( '>', '&gt;', array_shift( $bits ) );
+               if ( !MWTidy::isEnabled() ) {
+                       $tagstack = $tablestack = [];
+                       foreach ( $bits as $x ) {
+                               $regs = [];
+                               # $slash: Does the current element start with a '/'?
+                               # $t: Current element name
+                               # $params: String between element name and >
+                               # $brace: Ending '>' or '/>'
+                               # $rest: Everything until the next element of $bits
+                               if ( preg_match( self::ELEMENT_BITS_REGEX, $x, $regs ) ) {
+                                       list( /* $qbar */, $slash, $t, $params, $brace, $rest ) = $regs;
+                               } else {
+                                       $slash = $t = $params = $brace = $rest = null;
+                               }
+
+                               $badtag = false;
+                               $t = strtolower( $t );
+                               if ( isset( $htmlelements[$t] ) ) {
+                                       # Check our stack
+                                       if ( $slash && isset( $htmlsingleonly[$t] ) ) {
+                                               $badtag = true;
+                                       } elseif ( $slash ) {
+                                               # Closing a tag... is it the one we just opened?
+                                               MediaWiki\suppressWarnings();
+                                               $ot = array_pop( $tagstack );
+                                               MediaWiki\restoreWarnings();
+
+                                               if ( $ot != $t ) {
+                                                       if ( isset( $htmlsingleallowed[$ot] ) ) {
+                                                               # Pop all elements with an optional close tag
+                                                               # and see if we find a match below them
+                                                               $optstack = [];
+                                                               array_push( $optstack, $ot );
+                                                               MediaWiki\suppressWarnings();
+                                                               $ot = array_pop( $tagstack );
+                                                               MediaWiki\restoreWarnings();
+                                                               while ( $ot != $t && isset( $htmlsingleallowed[$ot] ) ) {
+                                                                       array_push( $optstack, $ot );
+                                                                       MediaWiki\suppressWarnings();
+                                                                       $ot = array_pop( $tagstack );
+                                                                       MediaWiki\restoreWarnings();
+                                                               }
+                                                               if ( $t != $ot ) {
+                                                                       # No match. Push the optional elements back again
+                                                                       $badtag = true;
+                                                                       MediaWiki\suppressWarnings();
+                                                                       $ot = array_pop( $optstack );
+                                                                       MediaWiki\restoreWarnings();
+                                                                       while ( $ot ) {
+                                                                               array_push( $tagstack, $ot );
+                                                                               MediaWiki\suppressWarnings();
+                                                                               $ot = array_pop( $optstack );
+                                                                               MediaWiki\restoreWarnings();
+                                                                       }
+                                                               }
+                                                       } else {
+                                                               MediaWiki\suppressWarnings();
+                                                               array_push( $tagstack, $ot );
+                                                               MediaWiki\restoreWarnings();
+
+                                                               # <li> can be nested in <ul> or <ol>, skip those cases:
+                                                               if ( !isset( $htmllist[$ot] ) || !isset( $listtags[$t] ) ) {
+                                                                       $badtag = true;
+                                                               }
+                                                       }
+                                               } else {
+                                                       if ( $t == 'table' ) {
+                                                               $tagstack = array_pop( $tablestack );
+                                                       }
+                                               }
+                                               $newparams = '';
+                                       } else {
+                                               # Keep track for later
+                                               if ( isset( $tabletags[$t] ) && !in_array( 'table', $tagstack ) ) {
+                                                       $badtag = true;
+                                               } elseif ( in_array( $t, $tagstack ) && !isset( $htmlnest[$t] ) ) {
+                                                       $badtag = true;
+                                               #  Is it a self closed htmlpair ? (T7487)
+                                               } elseif ( $brace == '/>' && isset( $htmlpairs[$t] ) ) {
+                                                       // Eventually we'll just remove the self-closing
+                                                       // slash, in order to be consistent with HTML5
+                                                       // semantics.
+                                                       // $brace = '>';
+                                                       // For now, let's just warn authors to clean up.
+                                                       if ( is_callable( $warnCallback ) ) {
+                                                               call_user_func_array( $warnCallback, [ 'deprecated-self-close-category' ] );
+                                                       }
+                                                       $badtag = true;
+                                               } elseif ( isset( $htmlsingleonly[$t] ) ) {
+                                                       # Hack to force empty tag for unclosable elements
+                                                       $brace = '/>';
+                                               } elseif ( isset( $htmlsingle[$t] ) ) {
+                                                       # Hack to not close $htmlsingle tags
+                                                       $brace = null;
+                                                       # Still need to push this optionally-closed tag to
+                                                       # the tag stack so that we can match end tags
+                                                       # instead of marking them as bad.
+                                                       array_push( $tagstack, $t );
+                                               } elseif ( isset( $tabletags[$t] ) && in_array( $t, $tagstack ) ) {
+                                                       // New table tag but forgot to close the previous one
+                                                       $text .= "</$t>";
+                                               } else {
+                                                       if ( $t == 'table' ) {
+                                                               array_push( $tablestack, $tagstack );
+                                                               $tagstack = [];
+                                                       }
+                                                       array_push( $tagstack, $t );
+                                               }
+
+                                               # Replace any variables or template parameters with
+                                               # plaintext results.
+                                               if ( is_callable( $processCallback ) ) {
+                                                       call_user_func_array( $processCallback, [ &$params, $args ] );
+                                               }
+
+                                               if ( !self::validateTag( $params, $t ) ) {
+                                                       $badtag = true;
+                                               }
+
+                                               # Strip non-approved attributes from the tag
+                                               $newparams = self::fixTagAttributes( $params, $t );
+                                       }
+                                       if ( !$badtag ) {
+                                               $rest = str_replace( '>', '&gt;', $rest );
+                                               $close = ( $brace == '/>' && !$slash ) ? ' /' : '';
+                                               $text .= "<$slash$t$newparams$close>$rest";
+                                               continue;
+                                       }
+                               }
+                               $text .= '&lt;' . str_replace( '>', '&gt;', $x );
+                       }
+                       # Close off any remaining tags
+                       while ( is_array( $tagstack ) && ( $t = array_pop( $tagstack ) ) ) {
+                               $text .= "</$t>\n";
+                               if ( $t == 'table' ) {
+                                       $tagstack = array_pop( $tablestack );
+                               }
+                       }
+               } else {
+                       # this might be possible using tidy itself
+                       foreach ( $bits as $x ) {
+                               if ( preg_match( self::ELEMENT_BITS_REGEX, $x, $regs ) ) {
+                                       list( /* $qbar */, $slash, $t, $params, $brace, $rest ) = $regs;
+
+                                       $badtag = false;
+                                       $t = strtolower( $t );
+                                       if ( isset( $htmlelements[$t] ) ) {
+                                               if ( is_callable( $processCallback ) ) {
+                                                       call_user_func_array( $processCallback, [ &$params, $args ] );
+                                               }
+
+                                               if ( $brace == '/>' && !( isset( $htmlsingle[$t] ) || isset( $htmlsingleonly[$t] ) ) ) {
+                                                       // Eventually we'll just remove the self-closing
+                                                       // slash, in order to be consistent with HTML5
+                                                       // semantics.
+                                                       // $brace = '>';
+                                                       // For now, let's just warn authors to clean up.
+                                                       if ( is_callable( $warnCallback ) ) {
+                                                               call_user_func_array( $warnCallback, [ 'deprecated-self-close-category' ] );
+                                                       }
+                                               }
+                                               if ( !self::validateTag( $params, $t ) ) {
+                                                       $badtag = true;
+                                               }
+
+                                               $newparams = self::fixTagAttributes( $params, $t );
+                                               if ( !$badtag ) {
+                                                       if ( $brace === '/>' && !isset( $htmlsingleonly[$t] ) ) {
+                                                               # Interpret self-closing tags as empty tags even when
+                                                               # HTML 5 would interpret them as start tags. Such input
+                                                               # is commonly seen on Wikimedia wikis with this intention.
+                                                               $brace = "></$t>";
+                                                       }
+
+                                                       $rest = str_replace( '>', '&gt;', $rest );
+                                                       $text .= "<$slash$t$newparams$brace$rest";
+                                                       continue;
+                                               }
+                                       }
+                               }
+                               $text .= '&lt;' . str_replace( '>', '&gt;', $x );
+                       }
+               }
+               return $text;
+       }
+
+       /**
+        * Remove '<!--', '-->', and everything between.
+        * To avoid leaving blank lines, when a comment is both preceded
+        * and followed by a newline (ignoring spaces), trim leading and
+        * trailing spaces and one of the newlines.
+        *
+        * @param string $text
+        * @return string
+        */
+       public static function removeHTMLcomments( $text ) {
+               while ( ( $start = strpos( $text, '<!--' ) ) !== false ) {
+                       $end = strpos( $text, '-->', $start + 4 );
+                       if ( $end === false ) {
+                               # Unterminated comment; bail out
+                               break;
+                       }
+
+                       $end += 3;
+
+                       # Trim space and newline if the comment is both
+                       # preceded and followed by a newline
+                       $spaceStart = max( $start - 1, 0 );
+                       $spaceLen = $end - $spaceStart;
+                       while ( substr( $text, $spaceStart, 1 ) === ' ' && $spaceStart > 0 ) {
+                               $spaceStart--;
+                               $spaceLen++;
+                       }
+                       while ( substr( $text, $spaceStart + $spaceLen, 1 ) === ' ' ) {
+                               $spaceLen++;
+                       }
+                       if ( substr( $text, $spaceStart, 1 ) === "\n"
+                               && substr( $text, $spaceStart + $spaceLen, 1 ) === "\n" ) {
+                               # Remove the comment, leading and trailing
+                               # spaces, and leave only one newline.
+                               $text = substr_replace( $text, "\n", $spaceStart, $spaceLen + 1 );
+                       } else {
+                               # Remove just the comment.
+                               $text = substr_replace( $text, '', $start, $end - $start );
+                       }
+               }
+               return $text;
+       }
+
+       /**
+        * Takes attribute names and values for a tag and the tag name and
+        * validates that the tag is allowed to be present.
+        * This DOES NOT validate the attributes, nor does it validate the
+        * tags themselves. This method only handles the special circumstances
+        * where we may want to allow a tag within content but ONLY when it has
+        * specific attributes set.
+        *
+        * @param string $params
+        * @param string $element
+        * @return bool
+        */
+       static function validateTag( $params, $element ) {
+               $params = self::decodeTagAttributes( $params );
+
+               if ( $element == 'meta' || $element == 'link' ) {
+                       if ( !isset( $params['itemprop'] ) ) {
+                               // <meta> and <link> must have an itemprop="" otherwise they are not valid or safe in content
+                               return false;
+                       }
+                       if ( $element == 'meta' && !isset( $params['content'] ) ) {
+                               // <meta> must have a content="" for the itemprop
+                               return false;
+                       }
+                       if ( $element == 'link' && !isset( $params['href'] ) ) {
+                               // <link> must have an associated href=""
+                               return false;
+                       }
+               }
+
+               return true;
+       }
+
+       /**
+        * Take an array of attribute names and values and normalize or discard
+        * illegal values for the given element type.
+        *
+        * - Discards attributes not on a whitelist for the given element
+        * - Unsafe style attributes are discarded
+        * - Invalid id attributes are re-encoded
+        *
+        * @param array $attribs
+        * @param string $element
+        * @return array
+        *
+        * @todo Check for legal values where the DTD limits things.
+        * @todo Check for unique id attribute :P
+        */
+       static function validateTagAttributes( $attribs, $element ) {
+               return self::validateAttributes( $attribs,
+                       self::attributeWhitelist( $element ) );
+       }
+
+       /**
+        * Take an array of attribute names and values and normalize or discard
+        * illegal values for the given whitelist.
+        *
+        * - Discards attributes not on the given whitelist
+        * - Unsafe style attributes are discarded
+        * - Invalid id attributes are re-encoded
+        *
+        * @param array $attribs
+        * @param array $whitelist List of allowed attribute names
+        * @return array
+        *
+        * @todo Check for legal values where the DTD limits things.
+        * @todo Check for unique id attribute :P
+        */
+       static function validateAttributes( $attribs, $whitelist ) {
+               $whitelist = array_flip( $whitelist );
+               $hrefExp = '/^(' . wfUrlProtocols() . ')[^\s]+$/';
+
+               $out = [];
+               foreach ( $attribs as $attribute => $value ) {
+                       # Allow XML namespace declaration to allow RDFa
+                       if ( preg_match( self::XMLNS_ATTRIBUTE_PATTERN, $attribute ) ) {
+                               if ( !preg_match( self::EVIL_URI_PATTERN, $value ) ) {
+                                       $out[$attribute] = $value;
+                               }
+
+                               continue;
+                       }
+
+                       # Allow any attribute beginning with "data-"
+                       # However:
+                       # * Disallow data attributes used by MediaWiki code
+                       # * Ensure that the attribute is not namespaced by banning
+                       #   colons.
+                       if ( !preg_match( '/^data-[^:]*$/i', $attribute )
+                               && !isset( $whitelist[$attribute] )
+                               || self::isReservedDataAttribute( $attribute )
+                       ) {
+                               continue;
+                       }
+
+                       # Strip javascript "expression" from stylesheets.
+                       # https://msdn.microsoft.com/en-us/library/ms537634.aspx
+                       if ( $attribute == 'style' ) {
+                               $value = self::checkCss( $value );
+                       }
+
+                       # Escape HTML id attributes
+                       if ( $attribute === 'id' ) {
+                               $value = self::escapeIdForAttribute( $value, self::ID_PRIMARY );
+                       }
+
+                       # Escape HTML id reference lists
+                       if ( $attribute === 'aria-describedby'
+                               || $attribute === 'aria-flowto'
+                               || $attribute === 'aria-labelledby'
+                               || $attribute === 'aria-owns'
+                       ) {
+                               $value = self::escapeIdReferenceList( $value );
+                       }
+
+                       // RDFa and microdata properties allow URLs, URIs and/or CURIs.
+                       // Check them for sanity.
+                       if ( $attribute === 'rel' || $attribute === 'rev'
+                               # RDFa
+                               || $attribute === 'about' || $attribute === 'property'
+                               || $attribute === 'resource' || $attribute === 'datatype'
+                               || $attribute === 'typeof'
+                               # HTML5 microdata
+                               || $attribute === 'itemid' || $attribute === 'itemprop'
+                               || $attribute === 'itemref' || $attribute === 'itemscope'
+                               || $attribute === 'itemtype'
+                       ) {
+                               // Paranoia. Allow "simple" values but suppress javascript
+                               if ( preg_match( self::EVIL_URI_PATTERN, $value ) ) {
+                                       continue;
+                               }
+                       }
+
+                       # NOTE: even though elements using href/src are not allowed directly, supply
+                       #       validation code that can be used by tag hook handlers, etc
+                       if ( $attribute === 'href' || $attribute === 'src' || $attribute === 'poster' ) {
+                               if ( !preg_match( $hrefExp, $value ) ) {
+                                       continue; // drop any href or src attributes not using an allowed protocol.
+                                       // NOTE: this also drops all relative URLs
+                               }
+                       }
+
+                       // If this attribute was previously set, override it.
+                       // Output should only have one attribute of each name.
+                       $out[$attribute] = $value;
+               }
+
+               # itemtype, itemid, itemref don't make sense without itemscope
+               if ( !array_key_exists( 'itemscope', $out ) ) {
+                       unset( $out['itemtype'] );
+                       unset( $out['itemid'] );
+                       unset( $out['itemref'] );
+               }
+               # TODO: Strip itemprop if we aren't descendants of an itemscope or pointed to by an itemref.
+
+               return $out;
+       }
+
+       /**
+        * Given an attribute name, checks whether it is a reserved data attribute
+        * (such as data-mw-foo) which is unavailable to user-generated HTML so MediaWiki
+        * core and extension code can safely use it to communicate with frontend code.
+        * @param string $attr Attribute name.
+        * @return bool
+        */
+       public static function isReservedDataAttribute( $attr ) {
+               // data-ooui is reserved for ooui.
+               // data-mw and data-parsoid are reserved for parsoid.
+               // data-mw-<name here> is reserved for extensions (or core) if
+               // they need to communicate some data to the client and want to be
+               // sure that it isn't coming from an untrusted user.
+               // We ignore the possibility of namespaces since user-generated HTML
+               // can't use them anymore.
+               return (bool)preg_match( '/^data-(ooui|mw|parsoid)/i', $attr );
+       }
+
+       /**
+        * Merge two sets of HTML attributes.  Conflicting items in the second set
+        * will override those in the first, except for 'class' attributes which
+        * will be combined (if they're both strings).
+        *
+        * @todo implement merging for other attributes such as style
+        * @param array $a
+        * @param array $b
+        * @return array
+        */
+       static function mergeAttributes( $a, $b ) {
+               $out = array_merge( $a, $b );
+               if ( isset( $a['class'] ) && isset( $b['class'] )
+                       && is_string( $a['class'] ) && is_string( $b['class'] )
+                       && $a['class'] !== $b['class']
+               ) {
+                       $classes = preg_split( '/\s+/', "{$a['class']} {$b['class']}",
+                               -1, PREG_SPLIT_NO_EMPTY );
+                       $out['class'] = implode( ' ', array_unique( $classes ) );
+               }
+               return $out;
+       }
+
+       /**
+        * Normalize CSS into a format we can easily search for hostile input
+        *  - decode character references
+        *  - decode escape sequences
+        *  - convert characters that IE6 interprets into ascii
+        *  - remove comments, unless the entire value is one single comment
+        * @param string $value the css string
+        * @return string normalized css
+        */
+       public static function normalizeCss( $value ) {
+               // Decode character references like &#123;
+               $value = self::decodeCharReferences( $value );
+
+               // Decode escape sequences and line continuation
+               // See the grammar in the CSS 2 spec, appendix D.
+               // This has to be done AFTER decoding character references.
+               // This means it isn't possible for this function to return
+               // unsanitized escape sequences. It is possible to manufacture
+               // input that contains character references that decode to
+               // escape sequences that decode to character references, but
+               // it's OK for the return value to contain character references
+               // because the caller is supposed to escape those anyway.
+               static $decodeRegex;
+               if ( !$decodeRegex ) {
+                       $space = '[\\x20\\t\\r\\n\\f]';
+                       $nl = '(?:\\n|\\r\\n|\\r|\\f)';
+                       $backslash = '\\\\';
+                       $decodeRegex = "/ $backslash
+                               (?:
+                                       ($nl) |  # 1. Line continuation
+                                       ([0-9A-Fa-f]{1,6})$space? |  # 2. character number
+                                       (.) | # 3. backslash cancelling special meaning
+                                       () | # 4. backslash at end of string
+                               )/xu";
+               }
+               $value = preg_replace_callback( $decodeRegex,
+                       [ __CLASS__, 'cssDecodeCallback' ], $value );
+
+               // Normalize Halfwidth and Fullwidth Unicode block that IE6 might treat as ascii
+               $value = preg_replace_callback(
+                       '/[!-[]-z]/u', // U+FF01 to U+FF5A, excluding U+FF3C (T60088)
+                       function ( $matches ) {
+                               $cp = UtfNormal\Utils::utf8ToCodepoint( $matches[0] );
+                               if ( $cp === false ) {
+                                       return '';
+                               }
+                               return chr( $cp - 65248 ); // ASCII range \x21-\x7A
+                       },
+                       $value
+               );
+
+               // Convert more characters IE6 might treat as ascii
+               // U+0280, U+0274, U+207F, U+029F, U+026A, U+207D, U+208D
+               $value = str_replace(
+                       [ 'ʀ', 'ɴ', 'ⁿ', 'ʟ', 'ɪ', '⁽', '₍' ],
+                       [ 'r', 'n', 'n', 'l', 'i', '(', '(' ],
+                       $value
+               );
+
+               // Let the value through if it's nothing but a single comment, to
+               // allow other functions which may reject it to pass some error
+               // message through.
+               if ( !preg_match( '! ^ \s* /\* [^*\\/]* \*/ \s* $ !x', $value ) ) {
+                       // Remove any comments; IE gets token splitting wrong
+                       // This must be done AFTER decoding character references and
+                       // escape sequences, because those steps can introduce comments
+                       // This step cannot introduce character references or escape
+                       // sequences, because it replaces comments with spaces rather
+                       // than removing them completely.
+                       $value = StringUtils::delimiterReplace( '/*', '*/', ' ', $value );
+
+                       // Remove anything after a comment-start token, to guard against
+                       // incorrect client implementations.
+                       $commentPos = strpos( $value, '/*' );
+                       if ( $commentPos !== false ) {
+                               $value = substr( $value, 0, $commentPos );
+                       }
+               }
+
+               // S followed by repeat, iteration, or prolonged sound marks,
+               // which IE will treat as "ss"
+               $value = preg_replace(
+                       '/s(?:
+                               \xE3\x80\xB1 | # U+3031
+                               \xE3\x82\x9D | # U+309D
+                               \xE3\x83\xBC | # U+30FC
+                               \xE3\x83\xBD | # U+30FD
+                               \xEF\xB9\xBC | # U+FE7C
+                               \xEF\xB9\xBD | # U+FE7D
+                               \xEF\xBD\xB0   # U+FF70
+                       )/ix',
+                       'ss',
+                       $value
+               );
+
+               return $value;
+       }
+
+       /**
+        * Pick apart some CSS and check it for forbidden or unsafe structures.
+        * Returns a sanitized string. This sanitized string will have
+        * character references and escape sequences decoded and comments
+        * stripped (unless it is itself one valid comment, in which case the value
+        * will be passed through). If the input is just too evil, only a comment
+        * complaining about evilness will be returned.
+        *
+        * Currently URL references, 'expression', 'tps' are forbidden.
+        *
+        * NOTE: Despite the fact that character references are decoded, the
+        * returned string may contain character references given certain
+        * clever input strings. These character references must
+        * be escaped before the return value is embedded in HTML.
+        *
+        * @param string $value
+        * @return string
+        */
+       static function checkCss( $value ) {
+               $value = self::normalizeCss( $value );
+
+               // Reject problematic keywords and control characters
+               if ( preg_match( '/[\000-\010\013\016-\037\177]/', $value ) ||
+                       strpos( $value, UtfNormal\Constants::UTF8_REPLACEMENT ) !== false ) {
+                       return '/* invalid control char */';
+               } elseif ( preg_match(
+                       '! expression
+                               | filter\s*:
+                               | accelerator\s*:
+                               | -o-link\s*:
+                               | -o-link-source\s*:
+                               | -o-replace\s*:
+                               | url\s*\(
+                               | image\s*\(
+                               | image-set\s*\(
+                               | attr\s*\([^)]+[\s,]+url
+                       !ix', $value ) ) {
+                       return '/* insecure input */';
+               }
+               return $value;
+       }
+
+       /**
+        * @param array $matches
+        * @return string
+        */
+       static function cssDecodeCallback( $matches ) {
+               if ( $matches[1] !== '' ) {
+                       // Line continuation
+                       return '';
+               } elseif ( $matches[2] !== '' ) {
+                       $char = UtfNormal\Utils::codepointToUtf8( hexdec( $matches[2] ) );
+               } elseif ( $matches[3] !== '' ) {
+                       $char = $matches[3];
+               } else {
+                       $char = '\\';
+               }
+               if ( $char == "\n" || $char == '"' || $char == "'" || $char == '\\' ) {
+                       // These characters need to be escaped in strings
+                       // Clean up the escape sequence to avoid parsing errors by clients
+                       return '\\' . dechex( ord( $char ) ) . ' ';
+               } else {
+                       // Decode unnecessary escape
+                       return $char;
+               }
+       }
+
+       /**
+        * Take a tag soup fragment listing an HTML element's attributes
+        * and normalize it to well-formed XML, discarding unwanted attributes.
+        * Output is safe for further wikitext processing, with escaping of
+        * values that could trigger problems.
+        *
+        * - Normalizes attribute names to lowercase
+        * - Discards attributes not on a whitelist for the given element
+        * - Turns broken or invalid entities into plaintext
+        * - Double-quotes all attribute values
+        * - Attributes without values are given the name as attribute
+        * - Double attributes are discarded
+        * - Unsafe style attributes are discarded
+        * - Prepends space if there are attributes.
+        * - (Optionally) Sorts attributes by name.
+        *
+        * @param string $text
+        * @param string $element
+        * @param bool $sorted Whether to sort the attributes (default: false)
+        * @return string
+        */
+       static function fixTagAttributes( $text, $element, $sorted = false ) {
+               if ( trim( $text ) == '' ) {
+                       return '';
+               }
+
+               $decoded = self::decodeTagAttributes( $text );
+               $stripped = self::validateTagAttributes( $decoded, $element );
+
+               if ( $sorted ) {
+                       ksort( $stripped );
+               }
+
+               return self::safeEncodeTagAttributes( $stripped );
+       }
+
+       /**
+        * Encode an attribute value for HTML output.
+        * @param string $text
+        * @return string HTML-encoded text fragment
+        */
+       static function encodeAttribute( $text ) {
+               $encValue = htmlspecialchars( $text, ENT_QUOTES );
+
+               // Whitespace is normalized during attribute decoding,
+               // so if we've been passed non-spaces we must encode them
+               // ahead of time or they won't be preserved.
+               $encValue = strtr( $encValue, [
+                       "\n" => '&#10;',
+                       "\r" => '&#13;',
+                       "\t" => '&#9;',
+               ] );
+
+               return $encValue;
+       }
+
+       /**
+        * Encode an attribute value for HTML tags, with extra armoring
+        * against further wiki processing.
+        * @param string $text
+        * @return string HTML-encoded text fragment
+        */
+       static function safeEncodeAttribute( $text ) {
+               $encValue = self::encodeAttribute( $text );
+
+               # Templates and links may be expanded in later parsing,
+               # creating invalid or dangerous output. Suppress this.
+               $encValue = strtr( $encValue, [
+                       '<'    => '&lt;',   // This should never happen,
+                       '>'    => '&gt;',   // we've received invalid input
+                       '"'    => '&quot;', // which should have been escaped.
+                       '{'    => '&#123;',
+                       '}'    => '&#125;', // prevent unpaired language conversion syntax
+                       '['    => '&#91;',
+                       "''"   => '&#39;&#39;',
+                       'ISBN' => '&#73;SBN',
+                       'RFC'  => '&#82;FC',
+                       'PMID' => '&#80;MID',
+                       '|'    => '&#124;',
+                       '__'   => '&#95;_',
+               ] );
+
+               # Stupid hack
+               $encValue = preg_replace_callback(
+                       '/((?i)' . wfUrlProtocols() . ')/',
+                       [ 'Sanitizer', 'armorLinksCallback' ],
+                       $encValue );
+               return $encValue;
+       }
+
+       /**
+        * Given a value, escape it so that it can be used in an id attribute and
+        * return it.  This will use HTML5 validation if $wgExperimentalHtmlIds is
+        * true, allowing anything but ASCII whitespace.  Otherwise it will use
+        * HTML 4 rules, which means a narrow subset of ASCII, with bad characters
+        * escaped with lots of dots.
+        *
+        * To ensure we don't have to bother escaping anything, we also strip ', ",
+        * & even if $wgExperimentalIds is true.  TODO: Is this the best tactic?
+        * We also strip # because it upsets IE, and % because it could be
+        * ambiguous if it's part of something that looks like a percent escape
+        * (which don't work reliably in fragments cross-browser).
+        *
+        * @deprecated since 1.30, use one of this class' escapeIdFor*() functions
+        *
+        * @see https://www.w3.org/TR/html401/types.html#type-name Valid characters
+        *   in the id and name attributes
+        * @see https://www.w3.org/TR/html401/struct/links.html#h-12.2.3 Anchors with
+        *   the id attribute
+        * @see https://www.w3.org/TR/html5/dom.html#the-id-attribute
+        *   HTML5 definition of id attribute
+        *
+        * @param string $id Id to escape
+        * @param string|array $options String or array of strings (default is array()):
+        *   'noninitial': This is a non-initial fragment of an id, not a full id,
+        *       so don't pay attention if the first character isn't valid at the
+        *       beginning of an id.  Only matters if $wgExperimentalHtmlIds is
+        *       false.
+        *   'legacy': Behave the way the old HTML 4-based ID escaping worked even
+        *       if $wgExperimentalHtmlIds is used, so we can generate extra
+        *       anchors and links won't break.
+        * @return string
+        */
+       static function escapeId( $id, $options = [] ) {
+               global $wgExperimentalHtmlIds;
+               $options = (array)$options;
+
+               if ( $wgExperimentalHtmlIds && !in_array( 'legacy', $options ) ) {
+                       $id = preg_replace( '/[ \t\n\r\f_\'"&#%]+/', '_', $id );
+                       $id = trim( $id, '_' );
+                       if ( $id === '' ) {
+                               // Must have been all whitespace to start with.
+                               return '_';
+                       } else {
+                               return $id;
+                       }
+               }
+
+               // HTML4-style escaping
+               static $replace = [
+                       '%3A' => ':',
+                       '%' => '.'
+               ];
+
+               $id = urlencode( strtr( $id, ' ', '_' ) );
+               $id = strtr( $id, $replace );
+
+               if ( !preg_match( '/^[a-zA-Z]/', $id ) && !in_array( 'noninitial', $options ) ) {
+                       // Initial character must be a letter!
+                       $id = "x$id";
+               }
+               return $id;
+       }
+
+       /**
+        * Given a section name or other user-generated or otherwise unsafe string, escapes it to be
+        * a valid HTML id attribute.
+        *
+        * WARNING: unlike escapeId(), the output of this function is not guaranteed to be HTML safe,
+        * be sure to use proper escaping.
+        *
+        * @param string $id String to escape
+        * @param int $mode One of ID_* constants, specifying whether the primary or fallback encoding
+        *     should be used.
+        * @return string|bool Escaped ID or false if fallback encoding is requested but it's not
+        *     configured.
+        *
+        * @since 1.30
+        */
+       public static function escapeIdForAttribute( $id, $mode = self::ID_PRIMARY ) {
+               global $wgFragmentMode;
+
+               if ( !isset( $wgFragmentMode[$mode] ) ) {
+                       if ( $mode === self::ID_PRIMARY ) {
+                               throw new UnexpectedValueException( '$wgFragmentMode is configured with no primary mode' );
+                       }
+                       return false;
+               }
+
+               $internalMode = $wgFragmentMode[$mode];
+
+               return self::escapeIdInternal( $id, $internalMode );
+       }
+
+       /**
+        * Given a section name or other user-generated or otherwise unsafe string, escapes it to be
+        * a valid URL fragment.
+        *
+        * WARNING: unlike escapeId(), the output of this function is not guaranteed to be HTML safe,
+        * be sure to use proper escaping.
+        *
+        * @param string $id String to escape
+        * @return string Escaped ID
+        *
+        * @since 1.30
+        */
+       public static function escapeIdForLink( $id ) {
+               global $wgFragmentMode;
+
+               if ( !isset( $wgFragmentMode[self::ID_PRIMARY] ) ) {
+                       throw new UnexpectedValueException( '$wgFragmentMode is configured with no primary mode' );
+               }
+
+               $mode = $wgFragmentMode[self::ID_PRIMARY];
+
+               $id = self::escapeIdInternal( $id, $mode );
+
+               return $id;
+       }
+
+       /**
+        * Given a section name or other user-generated or otherwise unsafe string, escapes it to be
+        * a valid URL fragment for external interwikis.
+        *
+        * @param string $id String to escape
+        * @return string Escaped ID
+        *
+        * @since 1.30
+        */
+       public static function escapeIdForExternalInterwiki( $id ) {
+               global $wgExternalInterwikiFragmentMode;
+
+               $id = self::escapeIdInternal( $id, $wgExternalInterwikiFragmentMode );
+
+               return $id;
+       }
+
+       /**
+        * Helper for escapeIdFor*() functions. Performs most of the actual escaping.
+        *
+        * @param string $id String to escape
+        * @param string $mode One of modes from $wgFragmentMode
+        * @return string
+        */
+       private static function escapeIdInternal( $id, $mode ) {
+               switch ( $mode ) {
+                       case 'html5':
+                               $id = str_replace( ' ', '_', $id );
+                               break;
+                       case 'legacy':
+                               // This corresponds to 'noninitial' mode of the old escapeId()
+                               static $replace = [
+                                       '%3A' => ':',
+                                       '%' => '.'
+                               ];
+
+                               $id = urlencode( str_replace( ' ', '_', $id ) );
+                               $id = strtr( $id, $replace );
+                               break;
+                       case 'html5-legacy':
+                               $id = preg_replace( '/[ \t\n\r\f_\'"&#%]+/', '_', $id );
+                               $id = trim( $id, '_' );
+                               if ( $id === '' ) {
+                                       // Must have been all whitespace to start with.
+                                       $id = '_';
+                               }
+                               break;
+                       default:
+                               throw new InvalidArgumentException( "Invalid mode '$mode' passed to '" . __METHOD__ );
+               }
+
+               return $id;
+       }
+
+       /**
+        * Given a string containing a space delimited list of ids, escape each id
+        * to match ids escaped by the escapeId() function.
+        *
+        * @todo remove $options completely in 1.32
+        *
+        * @since 1.27
+        *
+        * @param string $referenceString Space delimited list of ids
+        * @param string|array $options Deprecated and does nothing.
+        * @return string
+        */
+       static function escapeIdReferenceList( $referenceString, $options = [] ) {
+               if ( $options ) {
+                       wfDeprecated( __METHOD__ . ' with $options', '1.31' );
+               }
+               # Explode the space delimited list string into an array of tokens
+               $references = preg_split( '/\s+/', "{$referenceString}", -1, PREG_SPLIT_NO_EMPTY );
+
+               # Escape each token as an id
+               foreach ( $references as &$ref ) {
+                       $ref = self::escapeIdForAttribute( $ref );
+               }
+
+               # Merge the array back to a space delimited list string
+               # If the array is empty, the result will be an empty string ('')
+               $referenceString = implode( ' ', $references );
+
+               return $referenceString;
+       }
+
+       /**
+        * Given a value, escape it so that it can be used as a CSS class and
+        * return it.
+        *
+        * @todo For extra validity, input should be validated UTF-8.
+        *
+        * @see https://www.w3.org/TR/CSS21/syndata.html Valid characters/format
+        *
+        * @param string $class
+        * @return string
+        */
+       static function escapeClass( $class ) {
+               // Convert ugly stuff to underscores and kill underscores in ugly places
+               return rtrim( preg_replace(
+                       [ '/(^[0-9\\-])|[\\x00-\\x20!"#$%&\'()*+,.\\/:;<=>?@[\\]^`{|}~]|\\xC2\\xA0/', '/_+/' ],
+                       '_',
+                       $class ), '_' );
+       }
+
+       /**
+        * Given HTML input, escape with htmlspecialchars but un-escape entities.
+        * This allows (generally harmless) entities like &#160; to survive.
+        *
+        * @param string $html HTML to escape
+        * @return string Escaped input
+        */
+       static function escapeHtmlAllowEntities( $html ) {
+               $html = self::decodeCharReferences( $html );
+               # It seems wise to escape ' as well as ", as a matter of course.  Can't
+               # hurt. Use ENT_SUBSTITUTE so that incorrectly truncated multibyte characters
+               # don't cause the entire string to disappear.
+               $html = htmlspecialchars( $html, ENT_QUOTES | ENT_SUBSTITUTE );
+               return $html;
+       }
+
+       /**
+        * Regex replace callback for armoring links against further processing.
+        * @param array $matches
+        * @return string
+        */
+       private static function armorLinksCallback( $matches ) {
+               return str_replace( ':', '&#58;', $matches[1] );
+       }
+
+       /**
+        * Return an associative array of attribute names and values from
+        * a partial tag string. Attribute names are forced to lowercase,
+        * character references are decoded to UTF-8 text.
+        *
+        * @param string $text
+        * @return array
+        */
+       public static function decodeTagAttributes( $text ) {
+               if ( trim( $text ) == '' ) {
+                       return [];
+               }
+
+               $attribs = [];
+               $pairs = [];
+               if ( !preg_match_all(
+                       self::getAttribsRegex(),
+                       $text,
+                       $pairs,
+                       PREG_SET_ORDER ) ) {
+                       return $attribs;
+               }
+
+               foreach ( $pairs as $set ) {
+                       $attribute = strtolower( $set[1] );
+                       $value = self::getTagAttributeCallback( $set );
+
+                       // Normalize whitespace
+                       $value = preg_replace( '/[\t\r\n ]+/', ' ', $value );
+                       $value = trim( $value );
+
+                       // Decode character references
+                       $attribs[$attribute] = self::decodeCharReferences( $value );
+               }
+               return $attribs;
+       }
+
+       /**
+        * Build a partial tag string from an associative array of attribute
+        * names and values as returned by decodeTagAttributes.
+        *
+        * @param array $assoc_array
+        * @return string
+        */
+       public static function safeEncodeTagAttributes( $assoc_array ) {
+               $attribs = [];
+               foreach ( $assoc_array as $attribute => $value ) {
+                       $encAttribute = htmlspecialchars( $attribute );
+                       $encValue = self::safeEncodeAttribute( $value );
+
+                       $attribs[] = "$encAttribute=\"$encValue\"";
+               }
+               return count( $attribs ) ? ' ' . implode( ' ', $attribs ) : '';
+       }
+
+       /**
+        * Pick the appropriate attribute value from a match set from the
+        * attribs regex matches.
+        *
+        * @param array $set
+        * @throws MWException When tag conditions are not met.
+        * @return string
+        */
+       private static function getTagAttributeCallback( $set ) {
+               if ( isset( $set[5] ) ) {
+                       # No quotes.
+                       return $set[5];
+               } elseif ( isset( $set[4] ) ) {
+                       # Single-quoted
+                       return $set[4];
+               } elseif ( isset( $set[3] ) ) {
+                       # Double-quoted
+                       return $set[3];
+               } elseif ( !isset( $set[2] ) ) {
+                       # In XHTML, attributes must have a value so return an empty string.
+                       # See "Empty attribute syntax",
+                       # https://www.w3.org/TR/html5/syntax.html#syntax-attribute-name
+                       return "";
+               } else {
+                       throw new MWException( "Tag conditions not met. This should never happen and is a bug." );
+               }
+       }
+
+       /**
+        * @param string $text
+        * @return string
+        */
+       private static function normalizeWhitespace( $text ) {
+               return preg_replace(
+                       '/\r\n|[\x20\x0d\x0a\x09]/',
+                       ' ',
+                       $text );
+       }
+
+       /**
+        * Normalizes whitespace in a section name, such as might be returned
+        * by Parser::stripSectionName(), for use in the id's that are used for
+        * section links.
+        *
+        * @param string $section
+        * @return string
+        */
+       static function normalizeSectionNameWhitespace( $section ) {
+               return trim( preg_replace( '/[ _]+/', ' ', $section ) );
+       }
+
+       /**
+        * Ensure that any entities and character references are legal
+        * for XML and XHTML specifically. Any stray bits will be
+        * &amp;-escaped to result in a valid text fragment.
+        *
+        * a. named char refs can only be &lt; &gt; &amp; &quot;, others are
+        *   numericized (this way we're well-formed even without a DTD)
+        * b. any numeric char refs must be legal chars, not invalid or forbidden
+        * c. use lower cased "&#x", not "&#X"
+        * d. fix or reject non-valid attributes
+        *
+        * @param string $text
+        * @return string
+        * @private
+        */
+       static function normalizeCharReferences( $text ) {
+               return preg_replace_callback(
+                       self::CHAR_REFS_REGEX,
+                       [ 'Sanitizer', 'normalizeCharReferencesCallback' ],
+                       $text );
+       }
+
+       /**
+        * @param string $matches
+        * @return string
+        */
+       static function normalizeCharReferencesCallback( $matches ) {
+               $ret = null;
+               if ( $matches[1] != '' ) {
+                       $ret = self::normalizeEntity( $matches[1] );
+               } elseif ( $matches[2] != '' ) {
+                       $ret = self::decCharReference( $matches[2] );
+               } elseif ( $matches[3] != '' ) {
+                       $ret = self::hexCharReference( $matches[3] );
+               }
+               if ( is_null( $ret ) ) {
+                       return htmlspecialchars( $matches[0] );
+               } else {
+                       return $ret;
+               }
+       }
+
+       /**
+        * If the named entity is defined in the HTML 4.0/XHTML 1.0 DTD,
+        * return the equivalent numeric entity reference (except for the core &lt;
+        * &gt; &amp; &quot;). If the entity is a MediaWiki-specific alias, returns
+        * the HTML equivalent. Otherwise, returns HTML-escaped text of
+        * pseudo-entity source (eg &amp;foo;)
+        *
+        * @param string $name
+        * @return string
+        */
+       static function normalizeEntity( $name ) {
+               if ( isset( self::$htmlEntityAliases[$name] ) ) {
+                       return '&' . self::$htmlEntityAliases[$name] . ';';
+               } elseif ( in_array( $name, [ 'lt', 'gt', 'amp', 'quot' ] ) ) {
+                       return "&$name;";
+               } elseif ( isset( self::$htmlEntities[$name] ) ) {
+                       return '&#' . self::$htmlEntities[$name] . ';';
+               } else {
+                       return "&amp;$name;";
+               }
+       }
+
+       /**
+        * @param int $codepoint
+        * @return null|string
+        */
+       static function decCharReference( $codepoint ) {
+               $point = intval( $codepoint );
+               if ( self::validateCodepoint( $point ) ) {
+                       return sprintf( '&#%d;', $point );
+               } else {
+                       return null;
+               }
+       }
+
+       /**
+        * @param int $codepoint
+        * @return null|string
+        */
+       static function hexCharReference( $codepoint ) {
+               $point = hexdec( $codepoint );
+               if ( self::validateCodepoint( $point ) ) {
+                       return sprintf( '&#x%x;', $point );
+               } else {
+                       return null;
+               }
+       }
+
+       /**
+        * Returns true if a given Unicode codepoint is a valid character in
+        * both HTML5 and XML.
+        * @param int $codepoint
+        * @return bool
+        */
+       private static function validateCodepoint( $codepoint ) {
+               # U+000C is valid in HTML5 but not allowed in XML.
+               # U+000D is valid in XML but not allowed in HTML5.
+               # U+007F - U+009F are disallowed in HTML5 (control characters).
+               return $codepoint == 0x09
+                       || $codepoint == 0x0a
+                       || ( $codepoint >= 0x20 && $codepoint <= 0x7e )
+                       || ( $codepoint >= 0xa0 && $codepoint <= 0xd7ff )
+                       || ( $codepoint >= 0xe000 && $codepoint <= 0xfffd )
+                       || ( $codepoint >= 0x10000 && $codepoint <= 0x10ffff );
+       }
+
+       /**
+        * Decode any character references, numeric or named entities,
+        * in the text and return a UTF-8 string.
+        *
+        * @param string $text
+        * @return string
+        */
+       public static function decodeCharReferences( $text ) {
+               return preg_replace_callback(
+                       self::CHAR_REFS_REGEX,
+                       [ 'Sanitizer', 'decodeCharReferencesCallback' ],
+                       $text );
+       }
+
+       /**
+        * Decode any character references, numeric or named entities,
+        * in the next and normalize the resulting string. (T16952)
+        *
+        * This is useful for page titles, not for text to be displayed,
+        * MediaWiki allows HTML entities to escape normalization as a feature.
+        *
+        * @param string $text Already normalized, containing entities
+        * @return string Still normalized, without entities
+        */
+       public static function decodeCharReferencesAndNormalize( $text ) {
+               global $wgContLang;
+               $text = preg_replace_callback(
+                       self::CHAR_REFS_REGEX,
+                       [ 'Sanitizer', 'decodeCharReferencesCallback' ],
+                       $text,
+                       -1, //limit
+                       $count
+               );
+
+               if ( $count ) {
+                       return $wgContLang->normalize( $text );
+               } else {
+                       return $text;
+               }
+       }
+
+       /**
+        * @param string $matches
+        * @return string
+        */
+       static function decodeCharReferencesCallback( $matches ) {
+               if ( $matches[1] != '' ) {
+                       return self::decodeEntity( $matches[1] );
+               } elseif ( $matches[2] != '' ) {
+                       return self::decodeChar( intval( $matches[2] ) );
+               } elseif ( $matches[3] != '' ) {
+                       return self::decodeChar( hexdec( $matches[3] ) );
+               }
+               # Last case should be an ampersand by itself
+               return $matches[0];
+       }
+
+       /**
+        * Return UTF-8 string for a codepoint if that is a valid
+        * character reference, otherwise U+FFFD REPLACEMENT CHARACTER.
+        * @param int $codepoint
+        * @return string
+        * @private
+        */
+       static function decodeChar( $codepoint ) {
+               if ( self::validateCodepoint( $codepoint ) ) {
+                       return UtfNormal\Utils::codepointToUtf8( $codepoint );
+               } else {
+                       return UtfNormal\Constants::UTF8_REPLACEMENT;
+               }
+       }
+
+       /**
+        * If the named entity is defined in the HTML 4.0/XHTML 1.0 DTD,
+        * return the UTF-8 encoding of that character. Otherwise, returns
+        * pseudo-entity source (eg "&foo;")
+        *
+        * @param string $name
+        * @return string
+        */
+       static function decodeEntity( $name ) {
+               if ( isset( self::$htmlEntityAliases[$name] ) ) {
+                       $name = self::$htmlEntityAliases[$name];
+               }
+               if ( isset( self::$htmlEntities[$name] ) ) {
+                       return UtfNormal\Utils::codepointToUtf8( self::$htmlEntities[$name] );
+               } else {
+                       return "&$name;";
+               }
+       }
+
+       /**
+        * Fetch the whitelist of acceptable attributes for a given element name.
+        *
+        * @param string $element
+        * @return array
+        */
+       static function attributeWhitelist( $element ) {
+               $list = self::setupAttributeWhitelist();
+               return isset( $list[$element] )
+                       ? $list[$element]
+                       : [];
+       }
+
+       /**
+        * Foreach array key (an allowed HTML element), return an array
+        * of allowed attributes
+        * @return array
+        */
+       static function setupAttributeWhitelist() {
+               static $whitelist;
+
+               if ( $whitelist !== null ) {
+                       return $whitelist;
+               }
+
+               $common = [
+                       # HTML
+                       'id',
+                       'class',
+                       'style',
+                       'lang',
+                       'dir',
+                       'title',
+
+                       # WAI-ARIA
+                       'aria-describedby',
+                       'aria-flowto',
+                       'aria-label',
+                       'aria-labelledby',
+                       'aria-owns',
+                       'role',
+
+                       # RDFa
+                       # These attributes are specified in section 9 of
+                       # https://www.w3.org/TR/2008/REC-rdfa-syntax-20081014
+                       'about',
+                       'property',
+                       'resource',
+                       'datatype',
+                       'typeof',
+
+                       # Microdata. These are specified by
+                       # https://html.spec.whatwg.org/multipage/microdata.html#the-microdata-model
+                       'itemid',
+                       'itemprop',
+                       'itemref',
+                       'itemscope',
+                       'itemtype',
+               ];
+
+               $block = array_merge( $common, [ 'align' ] );
+               $tablealign = [ 'align', 'valign' ];
+               $tablecell = [
+                       'abbr',
+                       'axis',
+                       'headers',
+                       'scope',
+                       'rowspan',
+                       'colspan',
+                       'nowrap', # deprecated
+                       'width', # deprecated
+                       'height', # deprecated
+                       'bgcolor', # deprecated
+               ];
+
+               # Numbers refer to sections in HTML 4.01 standard describing the element.
+               # See: https://www.w3.org/TR/html4/
+               $whitelist = [
+                       # 7.5.4
+                       'div'        => $block,
+                       'center'     => $common, # deprecated
+                       'span'       => $common,
+
+                       # 7.5.5
+                       'h1'         => $block,
+                       'h2'         => $block,
+                       'h3'         => $block,
+                       'h4'         => $block,
+                       'h5'         => $block,
+                       'h6'         => $block,
+
+                       # 7.5.6
+                       # address
+
+                       # 8.2.4
+                       'bdo'        => $common,
+
+                       # 9.2.1
+                       'em'         => $common,
+                       'strong'     => $common,
+                       'cite'       => $common,
+                       'dfn'        => $common,
+                       'code'       => $common,
+                       'samp'       => $common,
+                       'kbd'        => $common,
+                       'var'        => $common,
+                       'abbr'       => $common,
+                       # acronym
+
+                       # 9.2.2
+                       'blockquote' => array_merge( $common, [ 'cite' ] ),
+                       'q'          => array_merge( $common, [ 'cite' ] ),
+
+                       # 9.2.3
+                       'sub'        => $common,
+                       'sup'        => $common,
+
+                       # 9.3.1
+                       'p'          => $block,
+
+                       # 9.3.2
+                       'br'         => array_merge( $common, [ 'clear' ] ),
+
+                       # https://www.w3.org/TR/html5/text-level-semantics.html#the-wbr-element
+                       'wbr'        => $common,
+
+                       # 9.3.4
+                       'pre'        => array_merge( $common, [ 'width' ] ),
+
+                       # 9.4
+                       'ins'        => array_merge( $common, [ 'cite', 'datetime' ] ),
+                       'del'        => array_merge( $common, [ 'cite', 'datetime' ] ),
+
+                       # 10.2
+                       'ul'         => array_merge( $common, [ 'type' ] ),
+                       'ol'         => array_merge( $common, [ 'type', 'start', 'reversed' ] ),
+                       'li'         => array_merge( $common, [ 'type', 'value' ] ),
+
+                       # 10.3
+                       'dl'         => $common,
+                       'dd'         => $common,
+                       'dt'         => $common,
+
+                       # 11.2.1
+                       'table'      => array_merge( $common,
+                                                               [ 'summary', 'width', 'border', 'frame',
+                                                                               'rules', 'cellspacing', 'cellpadding',
+                                                                               'align', 'bgcolor',
+                                                               ] ),
+
+                       # 11.2.2
+                       'caption'    => $block,
+
+                       # 11.2.3
+                       'thead'      => $common,
+                       'tfoot'      => $common,
+                       'tbody'      => $common,
+
+                       # 11.2.4
+                       'colgroup'   => array_merge( $common, [ 'span' ] ),
+                       'col'        => array_merge( $common, [ 'span' ] ),
+
+                       # 11.2.5
+                       'tr'         => array_merge( $common, [ 'bgcolor' ], $tablealign ),
+
+                       # 11.2.6
+                       'td'         => array_merge( $common, $tablecell, $tablealign ),
+                       'th'         => array_merge( $common, $tablecell, $tablealign ),
+
+                       # 12.2
+                       # NOTE: <a> is not allowed directly, but the attrib
+                       # whitelist is used from the Parser object
+                       'a'          => array_merge( $common, [ 'href', 'rel', 'rev' ] ), # rel/rev esp. for RDFa
+
+                       # 13.2
+                       # Not usually allowed, but may be used for extension-style hooks
+                       # such as <math> when it is rasterized, or if $wgAllowImageTag is
+                       # true
+                       'img'        => array_merge( $common, [ 'alt', 'src', 'width', 'height', 'srcset' ] ),
+
+                       'video'      => array_merge( $common, [ 'poster', 'controls', 'preload', 'width', 'height' ] ),
+                       'source'     => array_merge( $common, [ 'type', 'src' ] ),
+                       'track'      => array_merge( $common, [ 'type', 'src', 'srclang', 'kind', 'label' ] ),
+
+                       # 15.2.1
+                       'tt'         => $common,
+                       'b'          => $common,
+                       'i'          => $common,
+                       'big'        => $common,
+                       'small'      => $common,
+                       'strike'     => $common,
+                       's'          => $common,
+                       'u'          => $common,
+
+                       # 15.2.2
+                       'font'       => array_merge( $common, [ 'size', 'color', 'face' ] ),
+                       # basefont
+
+                       # 15.3
+                       'hr'         => array_merge( $common, [ 'width' ] ),
+
+                       # HTML Ruby annotation text module, simple ruby only.
+                       # https://www.w3.org/TR/html5/text-level-semantics.html#the-ruby-element
+                       'ruby'       => $common,
+                       # rbc
+                       'rb'         => $common,
+                       'rp'         => $common,
+                       'rt'         => $common, # array_merge( $common, array( 'rbspan' ) ),
+                       'rtc'        => $common,
+
+                       # MathML root element, where used for extensions
+                       # 'title' may not be 100% valid here; it's XHTML
+                       # https://www.w3.org/TR/REC-MathML/
+                       'math'       => [ 'class', 'style', 'id', 'title' ],
+
+                       // HTML 5 section 4.5
+                       'figure'     => $common,
+                       'figcaption' => $common,
+
+                       # HTML 5 section 4.6
+                       'bdi' => $common,
+
+                       # HTML5 elements, defined by:
+                       # https://html.spec.whatwg.org/multipage/semantics.html#the-data-element
+                       'data' => array_merge( $common, [ 'value' ] ),
+                       'time' => array_merge( $common, [ 'datetime' ] ),
+                       'mark' => $common,
+
+                       // meta and link are only permitted by removeHTMLtags when Microdata
+                       // is enabled so we don't bother adding a conditional to hide these
+                       // Also meta and link are only valid in WikiText as Microdata elements
+                       // (ie: validateTag rejects tags missing the attributes needed for Microdata)
+                       // So we don't bother including $common attributes that have no purpose.
+                       'meta' => [ 'itemprop', 'content' ],
+                       'link' => [ 'itemprop', 'href', 'title' ],
+               ];
+
+               return $whitelist;
+       }
+
+       /**
+        * Take a fragment of (potentially invalid) HTML and return
+        * a version with any tags removed, encoded as plain text.
+        *
+        * Warning: this return value must be further escaped for literal
+        * inclusion in HTML output as of 1.10!
+        *
+        * @param string $html HTML fragment
+        * @return string
+        */
+       static function stripAllTags( $html ) {
+               // Use RemexHtml to tokenize $html and extract the text
+               $handler = new RemexStripTagHandler;
+               $tokenizer = new RemexHtml\Tokenizer\Tokenizer( $handler, $html, [
+                       'ignoreErrors' => true,
+                       // don't ignore char refs, we want them to be decoded
+                       'ignoreNulls' => true,
+                       'skipPreprocess' => true,
+               ] );
+               $tokenizer->execute();
+               $text = $handler->getResult();
+
+               $text = self::normalizeWhitespace( $text );
+               return $text;
+       }
+
+       /**
+        * Hack up a private DOCTYPE with HTML's standard entity declarations.
+        * PHP 4 seemed to know these if you gave it an HTML doctype, but
+        * PHP 5.1 doesn't.
+        *
+        * Use for passing XHTML fragments to PHP's XML parsing functions
+        *
+        * @return string
+        */
+       static function hackDocType() {
+               $out = "<!DOCTYPE html [\n";
+               foreach ( self::$htmlEntities as $entity => $codepoint ) {
+                       $out .= "<!ENTITY $entity \"&#$codepoint;\">";
+               }
+               $out .= "]>\n";
+               return $out;
+       }
+
+       /**
+        * @param string $url
+        * @return mixed|string
+        */
+       static function cleanUrl( $url ) {
+               # Normalize any HTML entities in input. They will be
+               # re-escaped by makeExternalLink().
+               $url = self::decodeCharReferences( $url );
+
+               # Escape any control characters introduced by the above step
+               $url = preg_replace_callback( '/[\][<>"\\x00-\\x20\\x7F\|]/',
+                       [ __CLASS__, 'cleanUrlCallback' ], $url );
+
+               # Validate hostname portion
+               $matches = [];
+               if ( preg_match( '!^([^:]+:)(//[^/]+)?(.*)$!iD', $url, $matches ) ) {
+                       list( /* $whole */, $protocol, $host, $rest ) = $matches;
+
+                       // Characters that will be ignored in IDNs.
+                       // https://tools.ietf.org/html/rfc3454#section-3.1
+                       // Strip them before further processing so blacklists and such work.
+                       $strip = "/
+                               \\s|          # general whitespace
+                               \xc2\xad|     # 00ad SOFT HYPHEN
+                               \xe1\xa0\x86| # 1806 MONGOLIAN TODO SOFT HYPHEN
+                               \xe2\x80\x8b| # 200b ZERO WIDTH SPACE
+                               \xe2\x81\xa0| # 2060 WORD JOINER
+                               \xef\xbb\xbf| # feff ZERO WIDTH NO-BREAK SPACE
+                               \xcd\x8f|     # 034f COMBINING GRAPHEME JOINER
+                               \xe1\xa0\x8b| # 180b MONGOLIAN FREE VARIATION SELECTOR ONE
+                               \xe1\xa0\x8c| # 180c MONGOLIAN FREE VARIATION SELECTOR TWO
+                               \xe1\xa0\x8d| # 180d MONGOLIAN FREE VARIATION SELECTOR THREE
+                               \xe2\x80\x8c| # 200c ZERO WIDTH NON-JOINER
+                               \xe2\x80\x8d| # 200d ZERO WIDTH JOINER
+                               [\xef\xb8\x80-\xef\xb8\x8f] # fe00-fe0f VARIATION SELECTOR-1-16
+                               /xuD";
+
+                       $host = preg_replace( $strip, '', $host );
+
+                       // IPv6 host names are bracketed with [].  Url-decode these.
+                       if ( substr_compare( "//%5B", $host, 0, 5 ) === 0 &&
+                               preg_match( '!^//%5B([0-9A-Fa-f:.]+)%5D((:\d+)?)$!', $host, $matches )
+                       ) {
+                               $host = '//[' . $matches[1] . ']' . $matches[2];
+                       }
+
+                       // @todo FIXME: Validate hostnames here
+
+                       return $protocol . $host . $rest;
+               } else {
+                       return $url;
+               }
+       }
+
+       /**
+        * @param array $matches
+        * @return string
+        */
+       static function cleanUrlCallback( $matches ) {
+               return urlencode( $matches[0] );
+       }
+
+       /**
+        * Does a string look like an e-mail address?
+        *
+        * This validates an email address using an HTML5 specification found at:
+        * http://www.whatwg.org/html/states-of-the-type-attribute.html#valid-e-mail-address
+        * Which as of 2011-01-24 says:
+        *
+        *   A valid e-mail address is a string that matches the ABNF production
+        *   1*( atext / "." ) "@" ldh-str *( "." ldh-str ) where atext is defined
+        *   in RFC 5322 section 3.2.3, and ldh-str is defined in RFC 1034 section
+        *   3.5.
+        *
+        * This function is an implementation of the specification as requested in
+        * T24449.
+        *
+        * Client-side forms will use the same standard validation rules via JS or
+        * HTML 5 validation; additional restrictions can be enforced server-side
+        * by extensions via the 'isValidEmailAddr' hook.
+        *
+        * Note that this validation doesn't 100% match RFC 2822, but is believed
+        * to be liberal enough for wide use. Some invalid addresses will still
+        * pass validation here.
+        *
+        * @since 1.18
+        *
+        * @param string $addr E-mail address
+        * @return bool
+        */
+       public static function validateEmail( $addr ) {
+               $result = null;
+               if ( !Hooks::run( 'isValidEmailAddr', [ $addr, &$result ] ) ) {
+                       return $result;
+               }
+
+               // Please note strings below are enclosed in brackets [], this make the
+               // hyphen "-" a range indicator. Hence it is double backslashed below.
+               // See T28948
+               $rfc5322_atext = "a-z0-9!#$%&'*+\\-\/=?^_`{|}~";
+               $rfc1034_ldh_str = "a-z0-9\\-";
+
+               $html5_email_regexp = "/
+               ^                      # start of string
+               [$rfc5322_atext\\.]+    # user part which is liberal :p
+               @                      # 'apostrophe'
+               [$rfc1034_ldh_str]+       # First domain part
+               (\\.[$rfc1034_ldh_str]+)*  # Following part prefixed with a dot
+               $                      # End of string
+               /ix"; // case Insensitive, eXtended
+
+               return (bool)preg_match( $html5_email_regexp, $addr );
+       }
+}
diff --git a/includes/profiler/ProfilerFunctions.php b/includes/profiler/ProfilerFunctions.php
deleted file mode 100644 (file)
index cc71630..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-<?php
-/**
- * Core profiling functions. Have to exist before basically anything.
- *
- * 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 Profiler
- */
-
-/**
- * Get system resource usage of current request context.
- * Invokes the getrusage(2) system call, requesting RUSAGE_SELF if on PHP5
- * or RUSAGE_THREAD if on HHVM. Returns false if getrusage is not available.
- *
- * @since 1.24
- * @return array|bool Resource usage data or false if no data available.
- */
-function wfGetRusage() {
-       if ( !function_exists( 'getrusage' ) ) {
-               return false;
-       } elseif ( defined( 'HHVM_VERSION' ) && PHP_OS === 'Linux' ) {
-               return getrusage( 2 /* RUSAGE_THREAD */ );
-       } else {
-               return getrusage( 0 /* RUSAGE_SELF */ );
-       }
-}
-
-/**
- * Begin profiling of a function
- * @param string $functionname Name of the function we will profile
- * @deprecated since 1.25
- */
-function wfProfileIn( $functionname ) {
-}
-
-/**
- * Stop profiling of a function
- * @param string $functionname Name of the function we have profiled
- * @deprecated since 1.25
- */
-function wfProfileOut( $functionname = 'missing' ) {
-}
index ee8d841..1ad1ab0 100644 (file)
@@ -96,12 +96,9 @@ class SkinFallbackTemplate extends BaseTemplate {
         * warning message and page content.
         */
        public function execute() {
-               $this->html( 'headelement' ) ?>
-
-               <div class="warningbox">
-                       <?php echo $this->buildHelpfulInformationMessage() ?>
-               </div>
-
+               $this->html( 'headelement' );
+               echo Html::warningBox( $this->buildHelpfulInformationMessage() );
+       ?>
                <form action="<?php $this->text( 'wgScript' ) ?>">
                        <input type="hidden" name="title" value="<?php $this->text( 'searchtitle' ) ?>" />
                        <h3><label for="searchInput"><?php $this->msg( 'search' ) ?></label></h3>
index 476c452..eb0f0aa 100644 (file)
@@ -451,9 +451,8 @@ class SpecialEditTags extends UnlistedSpecialPage {
         */
        protected function failure( $status ) {
                $this->getOutput()->setPageTitle( $this->msg( 'actionfailed' ) );
-               $this->getOutput()->addWikiText( '<div class="errorbox">' .
-                       $status->getWikiText( 'tags-edit-failure' ) .
-                       '</div>'
+               $this->getOutput()->addWikiText(
+                       Html::errorBox( $status->getWikiText( 'tags-edit-failure' ) )
                );
                $this->showForm();
        }
index 46d7cf7..02d6d00 100644 (file)
@@ -235,18 +235,18 @@ class MovePageForm extends UnlistedSpecialPage {
                }
 
                if ( count( $err ) ) {
-                       $out->addHTML( "<div class='errorbox'>\n" );
                        $action_desc = $this->msg( 'action-move' )->plain();
-                       $out->addWikiMsg( 'permissionserrorstext-withaction', count( $err ), $action_desc );
+                       $errMsgHtml = $this->msg( 'permissionserrorstext-withaction',
+                               count( $err ), $action_desc )->parseAsBlock();
 
                        if ( count( $err ) == 1 ) {
                                $errMsg = $err[0];
                                $errMsgName = array_shift( $errMsg );
 
                                if ( $errMsgName == 'hookaborted' ) {
-                                       $out->addHTML( "<p>{$errMsg[0]}</p>\n" );
+                                       $errMsgHtml .= "<p>{$errMsg[0]}</p>\n";
                                } else {
-                                       $out->addWikiMsgArray( $errMsgName, $errMsg );
+                                       $errMsgHtml .= $this->msg( $errMsgName, $errMsg )->parseAsBlock();
                                }
                        } else {
                                $errStr = [];
@@ -260,9 +260,9 @@ class MovePageForm extends UnlistedSpecialPage {
                                        }
                                }
 
-                               $out->addHTML( '<ul><li>' . implode( "</li>\n<li>", $errStr ) . "</li></ul>\n" );
+                               $errMsgHtml .= '<ul><li>' . implode( "</li>\n<li>", $errStr ) . "</li></ul>\n";
                        }
-                       $out->addHTML( "</div>\n" );
+                       $out->addHTML( Html::errorBox( $errMsgHtml ) );
                }
 
                if ( $this->oldTitle->isProtected( 'move' ) ) {
index 8ad1630..7fa74af 100644 (file)
@@ -50,8 +50,8 @@ class SpecialPreferences extends SpecialPage {
                        return;
                }
 
-               $out->addModules( 'mediawiki.special.preferences' );
-               $out->addModuleStyles( 'mediawiki.special.preferences.styles' );
+               $out->addModules( 'mediawiki.special.preferences.ooui' );
+               $out->addModuleStyles( 'mediawiki.special.preferences.styles.ooui' );
 
                $session = $this->getRequest()->getSession();
                if ( $session->get( 'specialPreferencesSaveSuccess' ) ) {
@@ -83,37 +83,19 @@ class SpecialPreferences extends SpecialPage {
 
                $htmlForm = $this->getFormObject( $user, $this->getContext() );
                $htmlForm->setSubmitCallback( [ 'Preferences', 'tryUISubmit' ] );
-               $sectionTitles = $htmlForm->getPreferenceSections();
-
-               $prefTabs = '';
-               foreach ( $sectionTitles as $key ) {
-                       $prefTabs .= Html::rawElement( 'li',
-                               [
-                                       'role' => 'presentation',
-                                       'class' => ( $key === 'personal' ) ? 'selected' : null
-                               ],
-                               Html::rawElement( 'a',
-                                       [
-                                               'id' => 'preftab-' . $key,
-                                               'role' => 'tab',
-                                               'href' => '#mw-prefsection-' . $key,
-                                               'aria-controls' => 'mw-prefsection-' . $key,
-                                               'aria-selected' => ( $key === 'personal' ) ? 'true' : 'false',
-                                               'tabIndex' => ( $key === 'personal' ) ? 0 : -1,
-                                       ],
-                                       $htmlForm->getLegend( $key )
-                               )
-                       );
+
+               $prefTabs = [];
+               foreach ( $htmlForm->getPreferenceSections() as $key ) {
+                       $prefTabs[] = [
+                               'name' => $key,
+                               'label' => $htmlForm->getLegend( $key ),
+                       ];
                }
+               $out->addJsConfigVars( 'wgPreferencesTabs', $prefTabs );
+
+               // TODO: Render fake tabs here to avoid FOUC.
+               // $out->addHTML( $fakeTabs );
 
-               $out->addHTML(
-                       Html::rawElement( 'ul',
-                               [
-                                       'id' => 'preftoc',
-                                       'role' => 'tablist'
-                               ],
-                               $prefTabs )
-               );
                $htmlForm->show();
        }
 
@@ -136,7 +118,7 @@ class SpecialPreferences extends SpecialPage {
 
                $context = new DerivativeContext( $this->getContext() );
                $context->setTitle( $this->getPageTitle( 'reset' ) ); // Reset subpage
-               $htmlForm = new HTMLForm( [], $context, 'prefs-restore' );
+               $htmlForm = HTMLForm::factory( 'ooui', [], $context, 'prefs-restore' );
 
                $htmlForm->setSubmitTextMsg( 'restoreprefs' );
                $htmlForm->setSubmitDestructive();
index 99880de..358a309 100644 (file)
@@ -62,8 +62,9 @@ class SpecialRecentChangesLinked extends SpecialRecentChanges {
                $outputPage = $this->getOutput();
                $title = Title::newFromText( $target );
                if ( !$title || $title->isExternal() ) {
-                       $outputPage->addHTML( '<div class="errorbox">' . $this->msg( 'allpagesbadtitle' )
-                                       ->parse() . '</div>' );
+                       $outputPage->addHTML(
+                               Html::errorBox( $this->msg( 'allpagesbadtitle' )->parse() )
+                       );
 
                        return false;
                }
index 3e89686..964a261 100644 (file)
@@ -74,7 +74,7 @@ class SpecialResetTokens extends FormSpecialPage {
 
        public function onSuccess() {
                $this->getOutput()->wrapWikiMsg(
-                       "<div class='successbox'>\n$1\n</div>",
+                       Html::successBox( '$1' ),
                        'resettokens-done'
                );
        }
index e1d4dd1..8edebf2 100644 (file)
@@ -636,9 +636,10 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
        protected function failure( $status ) {
                // Messages: revdelete-failure, logdelete-failure
                $this->getOutput()->setPageTitle( $this->msg( 'actionfailed' ) );
-               $this->getOutput()->addWikiText( '<div class="errorbox">' .
-                       $status->getWikiText( $this->typeLabels['failure'] ) .
-                       '</div>'
+               $this->getOutput()->addWikiText(
+                       Html::errorBox(
+                               $status->getWikiText( $this->typeLabels['failure'] )
+                       )
                );
                $this->showForm();
        }
index 09210e4..b3a58cb 100644 (file)
@@ -365,16 +365,12 @@ class SpecialSearch extends SpecialPage {
                if ( $hasErrors ) {
                        list( $error, $warning ) = $textStatus->splitByErrorType();
                        if ( $error->getErrors() ) {
-                               $out->addHTML( Html::rawElement(
-                                       'div',
-                                       [ 'class' => 'errorbox' ],
+                               $out->addHTML( Html::errorBox(
                                        $error->getHTML( 'search-error' )
                                ) );
                        }
                        if ( $warning->getErrors() ) {
-                               $out->addHTML( Html::rawElement(
-                                       'div',
-                                       [ 'class' => 'warningbox' ],
+                               $out->addHTML( Html::warningBox(
                                        $warning->getHTML( 'search-warning' )
                                ) );
                        }
index 0a712ef..cf8c3f5 100644 (file)
@@ -140,7 +140,7 @@ class UserrightsPage extends SpecialPage {
                $this->setHeaders();
                $this->outputHeader();
 
-               $out->addModuleStyles( 'mediawiki.special' );
+               $out->addModuleStyles( 'mediawiki.special.userrights.styles' );
                $this->addHelpLink( 'Help:Assigning permissions' );
 
                $this->switchForm();
@@ -835,7 +835,10 @@ class UserrightsPage extends SpecialPage {
                        }
                        $ret .= "\t<td style='vertical-align:top;'>\n";
                        foreach ( $column as $group => $checkbox ) {
-                               $attr = $checkbox['disabled'] ? [ 'disabled' => 'disabled' ] : [];
+                               $attr = [ 'class' => 'mw-userrights-groupcheckbox' ];
+                               if ( $checkbox['disabled'] ) {
+                                       $attr['disabled'] = 'disabled';
+                               }
 
                                $member = UserGroupMembership::getGroupMemberName( $group, $user->getName() );
                                if ( $checkbox['irreversible'] ) {
@@ -847,10 +850,6 @@ class UserrightsPage extends SpecialPage {
                                }
                                $checkboxHtml = Xml::checkLabel( $text, "wpGroup-" . $group,
                                        "wpGroup-" . $group, $checkbox['set'], $attr );
-                               $ret .= "\t\t" . ( ( $checkbox['disabled'] && $checkbox['disabled-expiry'] )
-                                       ? Xml::tags( 'div', [ 'class' => 'mw-userrights-disabled' ], $checkboxHtml )
-                                       : Xml::tags( 'div', [], $checkboxHtml )
-                               ) . "\n";
 
                                if ( $this->canProcessExpiries() ) {
                                        $uiUser = $this->getUser();
@@ -920,7 +919,10 @@ class UserrightsPage extends SpecialPage {
                                                $expiryHtml .= $expiryFormOptions->getHTML() . '<br />';
 
                                                // Add custom expiry field
-                                               $attribs = [ 'id' => "mw-input-wpExpiry-$group-other" ];
+                                               $attribs = [
+                                                       'id' => "mw-input-wpExpiry-$group-other",
+                                                       'class' => 'mw-userrights-expiryfield',
+                                               ];
                                                if ( $checkbox['disabled-expiry'] ) {
                                                        $attribs['disabled'] = 'disabled';
                                                }
@@ -939,8 +941,12 @@ class UserrightsPage extends SpecialPage {
                                                'id' => "mw-userrights-nested-wpGroup-$group",
                                                'class' => 'mw-userrights-nested',
                                        ];
-                                       $ret .= "\t\t\t" . Xml::tags( 'div', $divAttribs, $expiryHtml ) . "\n";
+                                       $checkboxHtml .= "\t\t\t" . Xml::tags( 'div', $divAttribs, $expiryHtml ) . "\n";
                                }
+                               $ret .= "\t\t" . ( ( $checkbox['disabled'] && $checkbox['disabled-expiry'] )
+                                       ? Xml::tags( 'div', [ 'class' => 'mw-userrights-disabled' ], $checkboxHtml )
+                                       : Xml::tags( 'div', [], $checkboxHtml )
+                               ) . "\n";
                        }
                        $ret .= "\t</td>\n";
                }
index d4e5ef4..28cfb8b 100644 (file)
  * @file
  */
 
-use MediaWiki\MediaWikiServices;
-
 /**
  * Form to edit user preferences.
  */
-class PreferencesForm extends HTMLForm {
+class PreferencesForm extends OOUIHTMLForm {
        // Override default value from HTMLForm
        protected $mSubSectionBeforeFields = false;
 
@@ -71,8 +69,6 @@ class PreferencesForm extends HTMLForm {
         * @return string
         */
        function getButtons() {
-               $attrs = [ 'id' => 'mw-prefs-restoreprefs' ];
-
                if ( !$this->getModifiedUser()->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) {
                        return '';
                }
@@ -82,9 +78,14 @@ class PreferencesForm extends HTMLForm {
                if ( $this->getModifiedUser()->isAllowed( 'editmyoptions' ) ) {
                        $t = $this->getTitle()->getSubpage( 'reset' );
 
-                       $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
-                       $html .= "\n" . $linkRenderer->makeLink( $t, $this->msg( 'restoreprefs' )->text(),
-                               Html::buttonAttributes( $attrs, [ 'mw-ui-quiet' ] ) );
+                       $html .= new OOUI\ButtonWidget( [
+                               'infusable' => true,
+                               'id' => 'mw-prefs-restoreprefs',
+                               'label' => $this->msg( 'restoreprefs' )->text(),
+                               'href' => $t->getLinkURL(),
+                               'flags' => [ 'destructive' ],
+                               'framed' => false,
+                       ] );
 
                        $html = Xml::tags( 'div', [ 'class' => 'mw-prefs-buttons' ], $html );
                }
index 73bc5f8..c06eea0 100644 (file)
@@ -174,6 +174,10 @@ class RemexCompatMunger implements TreeHandler {
                        $length, $sourceStart, $sourceLength );
        }
 
+       private function trace( $msg ) {
+               // echo "[RCM] $msg\n";
+       }
+
        /**
         * Insert or reparent an element. Create p-wrappers or split the tag stack
         * as necessary.
@@ -242,6 +246,7 @@ class RemexCompatMunger implements TreeHandler {
                if ( $under && $parentData->isPWrapper && !$inline ) {
                        // [B/b] The element is non-inline and the parent is a p-wrapper,
                        // close the parent and insert into its parent instead
+                       $this->trace( 'insert B/b' );
                        $newParent = $this->serializer->getParentNode( $parent );
                        $parent = $newParent;
                        $parentData = $parent->snData;
@@ -255,12 +260,14 @@ class RemexCompatMunger implements TreeHandler {
                        // [CS/b, DS/i] The parent is splittable and the current element is
                        // inline in block context, or if the current element is a block
                        // under a p-wrapper, split the tag stack.
+                       $this->trace( $inline ? 'insert DS/i' : 'insert CS/b' );
                        $newRef = $this->splitTagStack( $newRef, $inline, $sourceStart );
                        $parent = $newRef;
                        $parentData = $parent->snData;
                } elseif ( $under && $parentData->needsPWrapping && $inline ) {
                        // [A/i] If the element is inline and we are in body/blockquote,
                        // we need to create a p-wrapper
+                       $this->trace( 'insert A/i' );
                        $newRef = $this->insertPWrapper( $newRef, $sourceStart );
                        $parent = $newRef;
                        $parentData = $parent->snData;
@@ -268,9 +275,12 @@ class RemexCompatMunger implements TreeHandler {
                        // [CU/b] If the element is non-inline and (despite attempting to
                        // split above) there is still an ancestor p-wrap, disable that
                        // p-wrap
+                       $this->trace( 'insert CU/b' );
                        $this->disablePWrapper( $parent, $sourceStart );
+               } else {
+                       // [A/b, B/i, C/i, D/b, DU/i] insert as normal
+                       $this->trace( 'insert normal' );
                }
-               // else [A/b, B/i, C/i, D/b, DU/i] insert as normal
 
                // An element with element children is a non-blank element
                $parentData->nonblankNodeCount++;
@@ -457,6 +467,20 @@ class RemexCompatMunger implements TreeHandler {
 
        public function reparentChildren( Element $element, Element $newParent, $sourceStart ) {
                $self = $element->userData;
+               if ( $self->snData->childPElement ) {
+                       // Reparent under the p-wrapper instead, so that e.g.
+                       //   <blockquote><mw:p-wrap>...</mw:p-wrap></blockquote>
+                       // becomes
+                       //   <blockquote><mw:p-wrap><i>...</i></mw:p-wrap></blockquote>
+
+                       // The formatting element should not be the parent of the p-wrap.
+                       // Without this special case, the insertElement() of the <i> below
+                       // would be diverted into the p-wrapper, causing infinite recursion
+                       // (T178632)
+                       $this->reparentChildren( $self->snData->childPElement, $newParent, $sourceStart );
+                       return;
+               }
+
                $children = $self->children;
                $self->children = [];
                $this->insertElement( TreeBuilder::UNDER, $element, $newParent, false, $sourceStart, 0 );
@@ -464,6 +488,7 @@ class RemexCompatMunger implements TreeHandler {
                $newParentId = $newParentNode->id;
                foreach ( $children as $child ) {
                        if ( is_object( $child ) ) {
+                               $this->trace( "reparent <{$child->name}>" );
                                $child->parentId = $newParentId;
                        }
                }
index d614a38..08d148f 100644 (file)
@@ -75,4 +75,43 @@ class RemexMungerData {
        public function __set( $name, $value ) {
                throw new \Exception( "Cannot set property \"$name\"" );
        }
+
+       /**
+        * Get a text representation of the current state of the serializer, for
+        * debugging.
+        *
+        * @return string
+        */
+       public function dump() {
+               if ( $this->childPElement ) {
+                       $parts[] = 'childPElement=' . $this->childPElement->getDebugTag();
+               }
+               if ( $this->ancestorPNode ) {
+                       $parts[] = "ancestorPNode=<{$this->ancestorPNode->name}>";
+               }
+               if ( $this->wrapBaseNode ) {
+                       $parts[] = "wrapBaseNode=<{$this->wrapBaseNode->name}>";
+               }
+               if ( $this->currentCloneElement ) {
+                       $parts[] = "currentCloneElement=" . $this->currentCloneElement->getDebugTag();
+               }
+               if ( $this->isPWrapper ) {
+                       $parts[] = 'isPWrapper';
+               }
+               if ( $this->isSplittable ) {
+                       $parts[] = 'isSplittable';
+               }
+               if ( $this->needsPWrapping ) {
+                       $parts[] = 'needsPWrapping';
+               }
+               if ( $this->nonblankNodeCount ) {
+                       $parts[] = "nonblankNodeCount={$this->nonblankNodeCount}";
+               }
+               $s = "RemexMungerData {\n";
+               foreach ( $parts as $part ) {
+                       $s .= "  $part\n";
+               }
+               $s .= "}\n";
+               return $s;
+       }
 }
index 7c370f1..77c1953 100644 (file)
@@ -34,22 +34,27 @@ use Wikimedia\Assert\Assert;
  * @since 1.23
  */
 class TitleValue implements LinkTarget {
+
        /**
+        * @deprecated in 1.31. This class is immutable. Use the getter for access.
         * @var int
         */
        protected $namespace;
 
        /**
+        * @deprecated in 1.31. This class is immutable. Use the getter for access.
         * @var string
         */
        protected $dbkey;
 
        /**
+        * @deprecated in 1.31. This class is immutable. Use the getter for access.
         * @var string
         */
        protected $fragment;
 
        /**
+        * @deprecated in 1.31. This class is immutable. Use the getter for access.
         * @var string
         */
        protected $interwiki;
@@ -89,6 +94,7 @@ class TitleValue implements LinkTarget {
        }
 
        /**
+        * @since 1.23
         * @return int
         */
        public function getNamespace() {
@@ -105,6 +111,7 @@ class TitleValue implements LinkTarget {
        }
 
        /**
+        * @since 1.23
         * @return string
         */
        public function getFragment() {
@@ -122,6 +129,7 @@ class TitleValue implements LinkTarget {
        /**
         * Returns the title's DB key, as supplied to the constructor,
         * without namespace prefix or fragment.
+        * @since 1.23
         *
         * @return string
         */
@@ -132,6 +140,7 @@ class TitleValue implements LinkTarget {
        /**
         * Returns the title in text form,
         * without namespace prefix or fragment.
+        * @since 1.23
         *
         * This is computed from the DB key by replacing any underscores with spaces.
         *
@@ -185,6 +194,7 @@ class TitleValue implements LinkTarget {
         * Returns a string representation of the title, for logging. This is purely informative
         * and must not be used programmatically. Use the appropriate TitleFormatter to generate
         * the correct string representation for a given use.
+        * @since 1.23
         *
         * @return string
         */
index 25625e7..b898d8a 100644 (file)
@@ -437,7 +437,7 @@ class BotPassword implements IDBAccessObject {
         * @return Status On success, the good status's value is the new Session object
         */
        public static function login( $username, $password, WebRequest $request ) {
-               global $wgEnableBotPasswords;
+               global $wgEnableBotPasswords, $wgPasswordAttemptThrottle;
 
                if ( !$wgEnableBotPasswords ) {
                        return Status::newFatal( 'botpasswords-disabled' );
@@ -462,6 +462,20 @@ class BotPassword implements IDBAccessObject {
                        return Status::newFatal( 'nosuchuser', $name );
                }
 
+               // Throttle
+               $throttle = null;
+               if ( !empty( $wgPasswordAttemptThrottle ) ) {
+                       $throttle = new MediaWiki\Auth\Throttler( $wgPasswordAttemptThrottle, [
+                               'type' => 'botpassword',
+                               'cache' => ObjectCache::getLocalClusterInstance(),
+                       ] );
+                       $result = $throttle->increase( $user->getName(), $request->getIP(), __METHOD__ );
+                       if ( $result ) {
+                               $msg = wfMessage( 'login-throttled' )->durationParams( $result['wait'] );
+                               return Status::newFatal( $msg );
+                       }
+               }
+
                // Get the bot password
                $bp = self::newFromUser( $user, $appId );
                if ( !$bp ) {
@@ -480,6 +494,9 @@ class BotPassword implements IDBAccessObject {
                }
 
                // Ok! Create the session.
+               if ( $throttle ) {
+                       $throttle->clear( $user->getName(), $request->getIP() );
+               }
                return Status::newGood( $provider->newSessionForRequest( $user, $bp, $request ) );
        }
 }
index 854ebbd..37a80f2 100644 (file)
@@ -27,6 +27,7 @@ use MediaWiki\Session\Token;
 use MediaWiki\Auth\AuthManager;
 use MediaWiki\Auth\AuthenticationResponse;
 use MediaWiki\Auth\AuthenticationRequest;
+use MediaWiki\User\UserIdentity;
 use Wikimedia\ScopedCallback;
 use Wikimedia\Rdbms\Database;
 use Wikimedia\Rdbms\DBExpectedError;
@@ -48,7 +49,7 @@ define( 'EDIT_TOKEN_SUFFIX', Token::SUFFIX );
  * for rendering normal pages are set in the cookie to minimize use
  * of the database.
  */
-class User implements IDBAccessObject {
+class User implements IDBAccessObject, UserIdentity {
        /**
         * @const int Number of characters in user_token field.
         */
diff --git a/includes/user/UserIdentity.php b/includes/user/UserIdentity.php
new file mode 100644 (file)
index 0000000..57a0408
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+/**
+ * Interface for objects representing user identity.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\User;
+
+/**
+ * Interface for objects representing user identity.
+ *
+ * This represents the identity of a user in the context of page revisions and log entries.
+ *
+ * @since 1.31
+ */
+interface UserIdentity {
+
+       /**
+        * @since 1.31
+        *
+        * @return int The user ID. May be 0 for anonymous users or for users with no local account.
+        */
+       public function getId();
+
+       /**
+        * @since 1.31
+        *
+        * @return string The user's logical name. May be an IPv4 or IPv6 address for anonymous users.
+        */
+       public function getName();
+
+       // TODO: in the future, we should also provide access to the actor ID here.
+       // TODO: we may want to (optionally?) provide a global ID, see CentralIdLookup.
+
+}
diff --git a/includes/watcheditem/WatchedItem.php b/includes/watcheditem/WatchedItem.php
new file mode 100644 (file)
index 0000000..bfd1d61
--- /dev/null
@@ -0,0 +1,200 @@
+<?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 Watchlist
+ */
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Linker\LinkTarget;
+
+/**
+ * Representation of a pair of user and title for watchlist entries.
+ *
+ * @author Tim Starling
+ * @author Addshore
+ *
+ * @ingroup Watchlist
+ */
+class WatchedItem {
+
+       /**
+        * @deprecated since 1.27, see User::IGNORE_USER_RIGHTS
+        */
+       const IGNORE_USER_RIGHTS = User::IGNORE_USER_RIGHTS;
+
+       /**
+        * @deprecated since 1.27, see User::CHECK_USER_RIGHTS
+        */
+       const CHECK_USER_RIGHTS = User::CHECK_USER_RIGHTS;
+
+       /**
+        * @deprecated Internal class use only
+        */
+       const DEPRECATED_USAGE_TIMESTAMP = -100;
+
+       /**
+        * @var bool
+        * @deprecated Internal class use only
+        */
+       public $checkRights = User::CHECK_USER_RIGHTS;
+
+       /**
+        * @var Title
+        * @deprecated Internal class use only
+        */
+       private $title;
+
+       /**
+        * @var LinkTarget
+        */
+       private $linkTarget;
+
+       /**
+        * @var User
+        */
+       private $user;
+
+       /**
+        * @var null|string the value of the wl_notificationtimestamp field
+        */
+       private $notificationTimestamp;
+
+       /**
+        * @param User $user
+        * @param LinkTarget $linkTarget
+        * @param null|string $notificationTimestamp the value of the wl_notificationtimestamp field
+        * @param bool|null $checkRights DO NOT USE - used internally for backward compatibility
+        */
+       public function __construct(
+               User $user,
+               LinkTarget $linkTarget,
+               $notificationTimestamp,
+               $checkRights = null
+       ) {
+               $this->user = $user;
+               $this->linkTarget = $linkTarget;
+               $this->notificationTimestamp = $notificationTimestamp;
+               if ( $checkRights !== null ) {
+                       $this->checkRights = $checkRights;
+               }
+       }
+
+       /**
+        * @return User
+        */
+       public function getUser() {
+               return $this->user;
+       }
+
+       /**
+        * @return LinkTarget
+        */
+       public function getLinkTarget() {
+               return $this->linkTarget;
+       }
+
+       /**
+        * Get the notification timestamp of this entry.
+        *
+        * @return bool|null|string
+        */
+       public function getNotificationTimestamp() {
+               // Back compat for objects constructed using self::fromUserTitle
+               if ( $this->notificationTimestamp === self::DEPRECATED_USAGE_TIMESTAMP ) {
+                       // wfDeprecated( __METHOD__, '1.27' );
+                       if ( $this->checkRights && !$this->user->isAllowed( 'viewmywatchlist' ) ) {
+                               return false;
+                       }
+                       $item = MediaWikiServices::getInstance()->getWatchedItemStore()
+                               ->loadWatchedItem( $this->user, $this->linkTarget );
+                       if ( $item ) {
+                               $this->notificationTimestamp = $item->getNotificationTimestamp();
+                       } else {
+                               $this->notificationTimestamp = false;
+                       }
+               }
+               return $this->notificationTimestamp;
+       }
+
+       /**
+        * Back compat pre 1.27 with the WatchedItemStore introduction
+        * @todo remove in 1.28/9
+        * -------------------------------------------------
+        */
+
+       /**
+        * @return Title
+        * @deprecated Internal class use only
+        */
+       public function getTitle() {
+               if ( !$this->title ) {
+                       $this->title = Title::newFromLinkTarget( $this->linkTarget );
+               }
+               return $this->title;
+       }
+
+       /**
+        * @deprecated since 1.27 Use the constructor, WatchedItemStore::getWatchedItem()
+        *             or WatchedItemStore::loadWatchedItem()
+        */
+       public static function fromUserTitle( $user, $title, $checkRights = User::CHECK_USER_RIGHTS ) {
+               wfDeprecated( __METHOD__, '1.27' );
+               return new self( $user, $title, self::DEPRECATED_USAGE_TIMESTAMP, (bool)$checkRights );
+       }
+
+       /**
+        * @deprecated since 1.27 Use User::addWatch()
+        * @return bool
+        */
+       public function addWatch() {
+               wfDeprecated( __METHOD__, '1.27' );
+               $this->user->addWatch( $this->getTitle(), $this->checkRights );
+               return true;
+       }
+
+       /**
+        * @deprecated since 1.27 Use User::removeWatch()
+        * @return bool
+        */
+       public function removeWatch() {
+               wfDeprecated( __METHOD__, '1.27' );
+               if ( $this->checkRights && !$this->user->isAllowed( 'editmywatchlist' ) ) {
+                       return false;
+               }
+               $this->user->removeWatch( $this->getTitle(), $this->checkRights );
+               return true;
+       }
+
+       /**
+        * @deprecated since 1.27 Use User::isWatched()
+        * @return bool
+        */
+       public function isWatched() {
+               wfDeprecated( __METHOD__, '1.27' );
+               return $this->user->isWatched( $this->getTitle(), $this->checkRights );
+       }
+
+       /**
+        * @deprecated since 1.27 Use WatchedItemStore::duplicateAllAssociatedEntries()
+        */
+       public static function duplicateEntries( Title $oldTitle, Title $newTitle ) {
+               wfDeprecated( __METHOD__, '1.27' );
+               $store = MediaWikiServices::getInstance()->getWatchedItemStore();
+               $store->duplicateAllAssociatedEntries( $oldTitle, $newTitle );
+       }
+
+}
diff --git a/includes/watcheditem/WatchedItemQueryService.php b/includes/watcheditem/WatchedItemQueryService.php
new file mode 100644 (file)
index 0000000..d0f45be
--- /dev/null
@@ -0,0 +1,684 @@
+<?php
+
+use Wikimedia\Rdbms\IDatabase;
+use MediaWiki\Linker\LinkTarget;
+use Wikimedia\Assert\Assert;
+use Wikimedia\Rdbms\LoadBalancer;
+
+/**
+ * Class performing complex database queries related to WatchedItems.
+ *
+ * @since 1.28
+ *
+ * @file
+ * @ingroup Watchlist
+ *
+ * @license GNU GPL v2+
+ */
+class WatchedItemQueryService {
+
+       const DIR_OLDER = 'older';
+       const DIR_NEWER = 'newer';
+
+       const INCLUDE_FLAGS = 'flags';
+       const INCLUDE_USER = 'user';
+       const INCLUDE_USER_ID = 'userid';
+       const INCLUDE_COMMENT = 'comment';
+       const INCLUDE_PATROL_INFO = 'patrol';
+       const INCLUDE_SIZES = 'sizes';
+       const INCLUDE_LOG_INFO = 'loginfo';
+
+       // FILTER_* constants are part of public API (are used in ApiQueryWatchlist and
+       // ApiQueryWatchlistRaw classes) and should not be changed.
+       // Changing values of those constants will result in a breaking change in the API
+       const FILTER_MINOR = 'minor';
+       const FILTER_NOT_MINOR = '!minor';
+       const FILTER_BOT = 'bot';
+       const FILTER_NOT_BOT = '!bot';
+       const FILTER_ANON = 'anon';
+       const FILTER_NOT_ANON = '!anon';
+       const FILTER_PATROLLED = 'patrolled';
+       const FILTER_NOT_PATROLLED = '!patrolled';
+       const FILTER_UNREAD = 'unread';
+       const FILTER_NOT_UNREAD = '!unread';
+       const FILTER_CHANGED = 'changed';
+       const FILTER_NOT_CHANGED = '!changed';
+
+       const SORT_ASC = 'ASC';
+       const SORT_DESC = 'DESC';
+
+       /**
+        * @var LoadBalancer
+        */
+       private $loadBalancer;
+
+       /** @var WatchedItemQueryServiceExtension[]|null */
+       private $extensions = null;
+
+       /**
+        * @var CommentStore|null */
+       private $commentStore = null;
+
+       public function __construct( LoadBalancer $loadBalancer ) {
+               $this->loadBalancer = $loadBalancer;
+       }
+
+       /**
+        * @return WatchedItemQueryServiceExtension[]
+        */
+       private function getExtensions() {
+               if ( $this->extensions === null ) {
+                       $this->extensions = [];
+                       Hooks::run( 'WatchedItemQueryServiceExtensions', [ &$this->extensions, $this ] );
+               }
+               return $this->extensions;
+       }
+
+       /**
+        * @return IDatabase
+        * @throws MWException
+        */
+       private function getConnection() {
+               return $this->loadBalancer->getConnectionRef( DB_REPLICA, [ 'watchlist' ] );
+       }
+
+       private function getCommentStore() {
+               if ( !$this->commentStore ) {
+                       $this->commentStore = new CommentStore( 'rc_comment' );
+               }
+               return $this->commentStore;
+       }
+
+       /**
+        * @param User $user
+        * @param array $options Allowed keys:
+        *        'includeFields'       => string[] RecentChange fields to be included in the result,
+        *                                 self::INCLUDE_* constants should be used
+        *        'filters'             => string[] optional filters to narrow down resulted items
+        *        'namespaceIds'        => int[] optional namespace IDs to filter by
+        *                                 (defaults to all namespaces)
+        *        'allRevisions'        => bool return multiple revisions of the same page if true,
+        *                                 only the most recent if false (default)
+        *        'rcTypes'             => int[] which types of RecentChanges to include
+        *                                 (defaults to all types), allowed values: RC_EDIT, RC_NEW,
+        *                                 RC_LOG, RC_EXTERNAL, RC_CATEGORIZE
+        *        'onlyByUser'          => string only list changes by a specified user
+        *        'notByUser'           => string do not incluide changes by a specified user
+        *        'dir'                 => string in which direction to enumerate, accepted values:
+        *                                 - DIR_OLDER list newest first
+        *                                 - DIR_NEWER list oldest first
+        *        'start'               => string (format accepted by wfTimestamp) requires 'dir' option,
+        *                                 timestamp to start enumerating from
+        *        '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
+        *        'watchlistOwnerToken' => string a watchlist token used to access another user's
+        *                                 watchlist, used with 'watchlistOwnerToken' option
+        *        'limit'               => int maximum numbers of items to return
+        *        'usedInGenerator'     => bool include only RecentChange id field required by the
+        *                                 generator ('rc_cur_id' or 'rc_this_oldid') if true, or all
+        *                                 id fields ('rc_cur_id', 'rc_this_oldid', 'rc_last_oldid')
+        *                                 if false (default)
+        * @param array|null &$startFrom Continuation value: [ string $rcTimestamp, int $rcId ]
+        * @return array of pairs ( WatchedItem $watchedItem, string[] $recentChangeInfo ),
+        *         where $recentChangeInfo contains the following keys:
+        *         - 'rc_id',
+        *         - 'rc_namespace',
+        *         - 'rc_title',
+        *         - 'rc_timestamp',
+        *         - 'rc_type',
+        *         - 'rc_deleted',
+        *         Additional keys could be added by specifying the 'includeFields' option
+        */
+       public function getWatchedItemsWithRecentChangeInfo(
+               User $user, array $options = [], &$startFrom = null
+       ) {
+               $options += [
+                       'includeFields' => [],
+                       'namespaceIds' => [],
+                       'filters' => [],
+                       'allRevisions' => false,
+                       'usedInGenerator' => false
+               ];
+
+               Assert::parameter(
+                       !isset( $options['rcTypes'] )
+                               || !array_diff( $options['rcTypes'], [ RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL, RC_CATEGORIZE ] ),
+                       '$options[\'rcTypes\']',
+                       'must be an array containing only: RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL and/or RC_CATEGORIZE'
+               );
+               Assert::parameter(
+                       !isset( $options['dir'] ) || in_array( $options['dir'], [ self::DIR_OLDER, self::DIR_NEWER ] ),
+                       '$options[\'dir\']',
+                       'must be DIR_OLDER or DIR_NEWER'
+               );
+               Assert::parameter(
+                       !isset( $options['start'] ) && !isset( $options['end'] ) && $startFrom === null
+                               || isset( $options['dir'] ),
+                       '$options[\'dir\']',
+                       'must be provided when providing the "start" or "end" options or the $startFrom parameter'
+               );
+               Assert::parameter(
+                       !isset( $options['startFrom'] ),
+                       '$options[\'startFrom\']',
+                       'must not be provided, use $startFrom instead'
+               );
+               Assert::parameter(
+                       !isset( $startFrom ) || ( is_array( $startFrom ) && count( $startFrom ) === 2 ),
+                       '$startFrom',
+                       'must be a two-element array'
+               );
+               if ( array_key_exists( 'watchlistOwner', $options ) ) {
+                       Assert::parameterType(
+                               User::class,
+                               $options['watchlistOwner'],
+                               '$options[\'watchlistOwner\']'
+                       );
+                       Assert::parameter(
+                               isset( $options['watchlistOwnerToken'] ),
+                               '$options[\'watchlistOwnerToken\']',
+                               'must be provided when providing watchlistOwner option'
+                       );
+               }
+
+               $db = $this->getConnection();
+
+               $tables = $this->getWatchedItemsWithRCInfoQueryTables( $options );
+               $fields = $this->getWatchedItemsWithRCInfoQueryFields( $options );
+               $conds = $this->getWatchedItemsWithRCInfoQueryConds( $db, $user, $options );
+               $dbOptions = $this->getWatchedItemsWithRCInfoQueryDbOptions( $options );
+               $joinConds = $this->getWatchedItemsWithRCInfoQueryJoinConds( $options );
+
+               if ( $startFrom !== null ) {
+                       $conds[] = $this->getStartFromConds( $db, $options, $startFrom );
+               }
+
+               foreach ( $this->getExtensions() as $extension ) {
+                       $extension->modifyWatchedItemsWithRCInfoQuery(
+                               $user, $options, $db,
+                               $tables,
+                               $fields,
+                               $conds,
+                               $dbOptions,
+                               $joinConds
+                       );
+               }
+
+               $res = $db->select(
+                       $tables,
+                       $fields,
+                       $conds,
+                       __METHOD__,
+                       $dbOptions,
+                       $joinConds
+               );
+
+               $limit = isset( $dbOptions['LIMIT'] ) ? $dbOptions['LIMIT'] : INF;
+               $items = [];
+               $startFrom = null;
+               foreach ( $res as $row ) {
+                       if ( --$limit <= 0 ) {
+                               $startFrom = [ $row->rc_timestamp, $row->rc_id ];
+                               break;
+                       }
+
+                       $items[] = [
+                               new WatchedItem(
+                                       $user,
+                                       new TitleValue( (int)$row->rc_namespace, $row->rc_title ),
+                                       $row->wl_notificationtimestamp
+                               ),
+                               $this->getRecentChangeFieldsFromRow( $row )
+                       ];
+               }
+
+               foreach ( $this->getExtensions() as $extension ) {
+                       $extension->modifyWatchedItemsWithRCInfo( $user, $options, $db, $items, $res, $startFrom );
+               }
+
+               return $items;
+       }
+
+       /**
+        * For simple listing of user's watchlist items, see WatchedItemStore::getWatchedItemsForUser
+        *
+        * @param User $user
+        * @param array $options Allowed keys:
+        *        'sort'         => string optional sorting by namespace ID and title
+        *                          one of the self::SORT_* constants
+        *        'namespaceIds' => int[] optional namespace IDs to filter by (defaults to all namespaces)
+        *        'limit'        => int maximum number of items to return
+        *        'filter'       => string optional filter, one of the self::FILTER_* contants
+        *        'from'         => LinkTarget requires 'sort' key, only return items starting from
+        *                          those related to the link target
+        *        'until'        => LinkTarget requires 'sort' key, only return items until
+        *                          those related to the link target
+        *        'startFrom'    => LinkTarget requires 'sort' key, only return items starting from
+        *                          those related to the link target, allows to skip some link targets
+        *                          specified using the form option
+        * @return WatchedItem[]
+        */
+       public function getWatchedItemsForUser( User $user, array $options = [] ) {
+               if ( $user->isAnon() ) {
+                       // TODO: should this just return an empty array or rather complain loud at this point
+                       // as e.g. ApiBase::getWatchlistUser does?
+                       return [];
+               }
+
+               $options += [ 'namespaceIds' => [] ];
+
+               Assert::parameter(
+                       !isset( $options['sort'] ) || in_array( $options['sort'], [ self::SORT_ASC, self::SORT_DESC ] ),
+                       '$options[\'sort\']',
+                       'must be SORT_ASC or SORT_DESC'
+               );
+               Assert::parameter(
+                       !isset( $options['filter'] ) || in_array(
+                               $options['filter'], [ self::FILTER_CHANGED, self::FILTER_NOT_CHANGED ]
+                       ),
+                       '$options[\'filter\']',
+                       'must be FILTER_CHANGED or FILTER_NOT_CHANGED'
+               );
+               Assert::parameter(
+                       !isset( $options['from'] ) && !isset( $options['until'] ) && !isset( $options['startFrom'] )
+                       || isset( $options['sort'] ),
+                       '$options[\'sort\']',
+                       'must be provided if any of "from", "until", "startFrom" options is provided'
+               );
+
+               $db = $this->getConnection();
+
+               $conds = $this->getWatchedItemsForUserQueryConds( $db, $user, $options );
+               $dbOptions = $this->getWatchedItemsForUserQueryDbOptions( $options );
+
+               $res = $db->select(
+                       'watchlist',
+                       [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
+                       $conds,
+                       __METHOD__,
+                       $dbOptions
+               );
+
+               $watchedItems = [];
+               foreach ( $res as $row ) {
+                       // todo these could all be cached at some point?
+                       $watchedItems[] = new WatchedItem(
+                               $user,
+                               new TitleValue( (int)$row->wl_namespace, $row->wl_title ),
+                               $row->wl_notificationtimestamp
+                       );
+               }
+
+               return $watchedItems;
+       }
+
+       private function getRecentChangeFieldsFromRow( stdClass $row ) {
+               // This can be simplified to single array_filter call filtering by key value,
+               // once we stop supporting PHP 5.5
+               $allFields = get_object_vars( $row );
+               $rcKeys = array_filter(
+                       array_keys( $allFields ),
+                       function ( $key ) {
+                               return substr( $key, 0, 3 ) === 'rc_';
+                       }
+               );
+               return array_intersect_key( $allFields, array_flip( $rcKeys ) );
+       }
+
+       private function getWatchedItemsWithRCInfoQueryTables( array $options ) {
+               $tables = [ 'recentchanges', 'watchlist' ];
+               if ( !$options['allRevisions'] ) {
+                       $tables[] = 'page';
+               }
+               if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
+                       $tables += $this->getCommentStore()->getJoin()['tables'];
+               }
+               return $tables;
+       }
+
+       private function getWatchedItemsWithRCInfoQueryFields( array $options ) {
+               $fields = [
+                       'rc_id',
+                       'rc_namespace',
+                       'rc_title',
+                       'rc_timestamp',
+                       'rc_type',
+                       'rc_deleted',
+                       'wl_notificationtimestamp'
+               ];
+
+               $rcIdFields = [
+                       'rc_cur_id',
+                       'rc_this_oldid',
+                       'rc_last_oldid',
+               ];
+               if ( $options['usedInGenerator'] ) {
+                       if ( $options['allRevisions'] ) {
+                               $rcIdFields = [ 'rc_this_oldid' ];
+                       } else {
+                               $rcIdFields = [ 'rc_cur_id' ];
+                       }
+               }
+               $fields = array_merge( $fields, $rcIdFields );
+
+               if ( in_array( self::INCLUDE_FLAGS, $options['includeFields'] ) ) {
+                       $fields = array_merge( $fields, [ 'rc_type', 'rc_minor', 'rc_bot' ] );
+               }
+               if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ) {
+                       $fields[] = 'rc_user_text';
+               }
+               if ( in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ) {
+                       $fields[] = 'rc_user';
+               }
+               if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
+                       $fields += $this->getCommentStore()->getJoin()['fields'];
+               }
+               if ( in_array( self::INCLUDE_PATROL_INFO, $options['includeFields'] ) ) {
+                       $fields = array_merge( $fields, [ 'rc_patrolled', 'rc_log_type' ] );
+               }
+               if ( in_array( self::INCLUDE_SIZES, $options['includeFields'] ) ) {
+                       $fields = array_merge( $fields, [ 'rc_old_len', 'rc_new_len' ] );
+               }
+               if ( in_array( self::INCLUDE_LOG_INFO, $options['includeFields'] ) ) {
+                       $fields = array_merge( $fields, [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ] );
+               }
+
+               return $fields;
+       }
+
+       private function getWatchedItemsWithRCInfoQueryConds(
+               IDatabase $db,
+               User $user,
+               array $options
+       ) {
+               $watchlistOwnerId = $this->getWatchlistOwnerId( $user, $options );
+               $conds = [ 'wl_user' => $watchlistOwnerId ];
+
+               if ( !$options['allRevisions'] ) {
+                       $conds[] = $db->makeList(
+                               [ 'rc_this_oldid=page_latest', 'rc_type=' . RC_LOG ],
+                               LIST_OR
+                       );
+               }
+
+               if ( $options['namespaceIds'] ) {
+                       $conds['wl_namespace'] = array_map( 'intval', $options['namespaceIds'] );
+               }
+
+               if ( array_key_exists( 'rcTypes', $options ) ) {
+                       $conds['rc_type'] = array_map( 'intval', $options['rcTypes'] );
+               }
+
+               $conds = array_merge(
+                       $conds,
+                       $this->getWatchedItemsWithRCInfoQueryFilterConds( $user, $options )
+               );
+
+               $conds = array_merge( $conds, $this->getStartEndConds( $db, $options ) );
+
+               if ( !isset( $options['start'] ) && !isset( $options['end'] ) ) {
+                       if ( $db->getType() === 'mysql' ) {
+                               // This is an index optimization for mysql
+                               $conds[] = 'rc_timestamp > ' . $db->addQuotes( '' );
+                       }
+               }
+
+               $conds = array_merge( $conds, $this->getUserRelatedConds( $db, $user, $options ) );
+
+               $deletedPageLogCond = $this->getExtraDeletedPageLogEntryRelatedCond( $db, $user );
+               if ( $deletedPageLogCond ) {
+                       $conds[] = $deletedPageLogCond;
+               }
+
+               return $conds;
+       }
+
+       private function getWatchlistOwnerId( User $user, array $options ) {
+               if ( array_key_exists( 'watchlistOwner', $options ) ) {
+                       /** @var User $watchlistOwner */
+                       $watchlistOwner = $options['watchlistOwner'];
+                       $ownersToken = $watchlistOwner->getOption( 'watchlisttoken' );
+                       $token = $options['watchlistOwnerToken'];
+                       if ( $ownersToken == '' || !hash_equals( $ownersToken, $token ) ) {
+                               throw ApiUsageException::newWithMessage( null, 'apierror-bad-watchlist-token', 'bad_wltoken' );
+                       }
+                       return $watchlistOwner->getId();
+               }
+               return $user->getId();
+       }
+
+       private function getWatchedItemsWithRCInfoQueryFilterConds( User $user, array $options ) {
+               $conds = [];
+
+               if ( in_array( self::FILTER_MINOR, $options['filters'] ) ) {
+                       $conds[] = 'rc_minor != 0';
+               } elseif ( in_array( self::FILTER_NOT_MINOR, $options['filters'] ) ) {
+                       $conds[] = 'rc_minor = 0';
+               }
+
+               if ( in_array( self::FILTER_BOT, $options['filters'] ) ) {
+                       $conds[] = 'rc_bot != 0';
+               } elseif ( in_array( self::FILTER_NOT_BOT, $options['filters'] ) ) {
+                       $conds[] = 'rc_bot = 0';
+               }
+
+               if ( in_array( self::FILTER_ANON, $options['filters'] ) ) {
+                       $conds[] = 'rc_user = 0';
+               } elseif ( in_array( self::FILTER_NOT_ANON, $options['filters'] ) ) {
+                       $conds[] = 'rc_user != 0';
+               }
+
+               if ( $user->useRCPatrol() || $user->useNPPatrol() ) {
+                       // TODO: not sure if this should simply ignore patrolled filters if user does not have the patrol
+                       // right, or maybe rather fail loud at this point, same as e.g. ApiQueryWatchlist does?
+                       if ( in_array( self::FILTER_PATROLLED, $options['filters'] ) ) {
+                               $conds[] = 'rc_patrolled != 0';
+                       } elseif ( in_array( self::FILTER_NOT_PATROLLED, $options['filters'] ) ) {
+                               $conds[] = 'rc_patrolled = 0';
+                       }
+               }
+
+               if ( in_array( self::FILTER_UNREAD, $options['filters'] ) ) {
+                       $conds[] = 'rc_timestamp >= wl_notificationtimestamp';
+               } elseif ( in_array( self::FILTER_NOT_UNREAD, $options['filters'] ) ) {
+                       // TODO: should this be changed to use Database::makeList?
+                       $conds[] = 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp';
+               }
+
+               return $conds;
+       }
+
+       private function getStartEndConds( IDatabase $db, array $options ) {
+               if ( !isset( $options['start'] ) && !isset( $options['end'] ) ) {
+                       return [];
+               }
+
+               $conds = [];
+
+               if ( isset( $options['start'] ) ) {
+                       $after = $options['dir'] === self::DIR_OLDER ? '<=' : '>=';
+                       $conds[] = 'rc_timestamp ' . $after . ' ' .
+                               $db->addQuotes( $db->timestamp( $options['start'] ) );
+               }
+               if ( isset( $options['end'] ) ) {
+                       $before = $options['dir'] === self::DIR_OLDER ? '>=' : '<=';
+                       $conds[] = 'rc_timestamp ' . $before . ' ' .
+                               $db->addQuotes( $db->timestamp( $options['end'] ) );
+               }
+
+               return $conds;
+       }
+
+       private function getUserRelatedConds( IDatabase $db, User $user, array $options ) {
+               if ( !array_key_exists( 'onlyByUser', $options ) && !array_key_exists( 'notByUser', $options ) ) {
+                       return [];
+               }
+
+               $conds = [];
+
+               if ( array_key_exists( 'onlyByUser', $options ) ) {
+                       $conds['rc_user_text'] = $options['onlyByUser'];
+               } elseif ( array_key_exists( 'notByUser', $options ) ) {
+                       $conds[] = 'rc_user_text != ' . $db->addQuotes( $options['notByUser'] );
+               }
+
+               // Avoid brute force searches (T19342)
+               $bitmask = 0;
+               if ( !$user->isAllowed( 'deletedhistory' ) ) {
+                       $bitmask = Revision::DELETED_USER;
+               } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
+                       $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED;
+               }
+               if ( $bitmask ) {
+                       $conds[] = $db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask";
+               }
+
+               return $conds;
+       }
+
+       private function getExtraDeletedPageLogEntryRelatedCond( IDatabase $db, User $user ) {
+               // LogPage::DELETED_ACTION hides the affected page, too. So hide those
+               // entirely from the watchlist, or someone could guess the title.
+               $bitmask = 0;
+               if ( !$user->isAllowed( 'deletedhistory' ) ) {
+                       $bitmask = LogPage::DELETED_ACTION;
+               } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) {
+                       $bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED;
+               }
+               if ( $bitmask ) {
+                       return $db->makeList( [
+                               'rc_type != ' . RC_LOG,
+                               $db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask",
+                       ], LIST_OR );
+               }
+               return '';
+       }
+
+       private function getStartFromConds( IDatabase $db, array $options, array $startFrom ) {
+               $op = $options['dir'] === self::DIR_OLDER ? '<' : '>';
+               list( $rcTimestamp, $rcId ) = $startFrom;
+               $rcTimestamp = $db->addQuotes( $db->timestamp( $rcTimestamp ) );
+               $rcId = (int)$rcId;
+               return $db->makeList(
+                       [
+                               "rc_timestamp $op $rcTimestamp",
+                               $db->makeList(
+                                       [
+                                               "rc_timestamp = $rcTimestamp",
+                                               "rc_id $op= $rcId"
+                                       ],
+                                       LIST_AND
+                               )
+                       ],
+                       LIST_OR
+               );
+       }
+
+       private function getWatchedItemsForUserQueryConds( IDatabase $db, User $user, array $options ) {
+               $conds = [ 'wl_user' => $user->getId() ];
+               if ( $options['namespaceIds'] ) {
+                       $conds['wl_namespace'] = array_map( 'intval', $options['namespaceIds'] );
+               }
+               if ( isset( $options['filter'] ) ) {
+                       $filter = $options['filter'];
+                       if ( $filter === self::FILTER_CHANGED ) {
+                               $conds[] = 'wl_notificationtimestamp IS NOT NULL';
+                       } else {
+                               $conds[] = 'wl_notificationtimestamp IS NULL';
+                       }
+               }
+
+               if ( isset( $options['from'] ) ) {
+                       $op = $options['sort'] === self::SORT_ASC ? '>' : '<';
+                       $conds[] = $this->getFromUntilTargetConds( $db, $options['from'], $op );
+               }
+               if ( isset( $options['until'] ) ) {
+                       $op = $options['sort'] === self::SORT_ASC ? '<' : '>';
+                       $conds[] = $this->getFromUntilTargetConds( $db, $options['until'], $op );
+               }
+               if ( isset( $options['startFrom'] ) ) {
+                       $op = $options['sort'] === self::SORT_ASC ? '>' : '<';
+                       $conds[] = $this->getFromUntilTargetConds( $db, $options['startFrom'], $op );
+               }
+
+               return $conds;
+       }
+
+       /**
+        * Creates a query condition part for getting only items before or after the given link target
+        * (while ordering using $sort mode)
+        *
+        * @param IDatabase $db
+        * @param LinkTarget $target
+        * @param string $op comparison operator to use in the conditions
+        * @return string
+        */
+       private function getFromUntilTargetConds( IDatabase $db, LinkTarget $target, $op ) {
+               return $db->makeList(
+                       [
+                               "wl_namespace $op " . $target->getNamespace(),
+                               $db->makeList(
+                                       [
+                                               'wl_namespace = ' . $target->getNamespace(),
+                                               "wl_title $op= " . $db->addQuotes( $target->getDBkey() )
+                                       ],
+                                       LIST_AND
+                               )
+                       ],
+                       LIST_OR
+               );
+       }
+
+       private function getWatchedItemsWithRCInfoQueryDbOptions( array $options ) {
+               $dbOptions = [];
+
+               if ( array_key_exists( 'dir', $options ) ) {
+                       $sort = $options['dir'] === self::DIR_OLDER ? ' DESC' : '';
+                       $dbOptions['ORDER BY'] = [ 'rc_timestamp' . $sort, 'rc_id' . $sort ];
+               }
+
+               if ( array_key_exists( 'limit', $options ) ) {
+                       $dbOptions['LIMIT'] = (int)$options['limit'] + 1;
+               }
+
+               return $dbOptions;
+       }
+
+       private function getWatchedItemsForUserQueryDbOptions( array $options ) {
+               $dbOptions = [];
+               if ( array_key_exists( 'sort', $options ) ) {
+                       $dbOptions['ORDER BY'] = [
+                               "wl_namespace {$options['sort']}",
+                               "wl_title {$options['sort']}"
+                       ];
+                       if ( count( $options['namespaceIds'] ) === 1 ) {
+                               $dbOptions['ORDER BY'] = "wl_title {$options['sort']}";
+                       }
+               }
+               if ( array_key_exists( 'limit', $options ) ) {
+                       $dbOptions['LIMIT'] = (int)$options['limit'];
+               }
+               return $dbOptions;
+       }
+
+       private function getWatchedItemsWithRCInfoQueryJoinConds( array $options ) {
+               $joinConds = [
+                       'watchlist' => [ 'INNER JOIN',
+                               [
+                                       'wl_namespace=rc_namespace',
+                                       'wl_title=rc_title'
+                               ]
+                       ]
+               ];
+               if ( !$options['allRevisions'] ) {
+                       $joinConds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
+               }
+               if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
+                       $joinConds += $this->getCommentStore()->getJoin()['joins'];
+               }
+               return $joinConds;
+       }
+
+}
diff --git a/includes/watcheditem/WatchedItemQueryServiceExtension.php b/includes/watcheditem/WatchedItemQueryServiceExtension.php
new file mode 100644 (file)
index 0000000..93d5033
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+
+use Wikimedia\Rdbms\ResultWrapper;
+use Wikimedia\Rdbms\IDatabase;
+
+/**
+ * Extension mechanism for WatchedItemQueryService
+ *
+ * @since 1.29
+ *
+ * @file
+ * @ingroup Watchlist
+ *
+ * @license GNU GPL v2+
+ */
+interface WatchedItemQueryServiceExtension {
+
+       /**
+        * Modify the WatchedItemQueryService::getWatchedItemsWithRecentChangeInfo()
+        * query before it's made.
+        *
+        * @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 array $options Options from
+        *  WatchedItemQueryService::getWatchedItemsWithRecentChangeInfo()
+        * @param IDatabase $db Database connection being used for the query
+        * @param array &$tables Tables for Database::select()
+        * @param array &$fields Fields for Database::select()
+        * @param array &$conds Conditions for Database::select()
+        * @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
+       );
+
+       /**
+        * Modify the results from WatchedItemQueryService::getWatchedItemsWithRecentChangeInfo()
+        * before they're returned.
+        *
+        * @param User $user
+        * @param array $options Options from
+        *  WatchedItemQueryService::getWatchedItemsWithRecentChangeInfo()
+        * @param IDatabase $db Database connection being used for the query
+        * @param array &$items array of pairs ( WatchedItem $watchedItem, string[] $recentChangeInfo ).
+        *  May be truncated if necessary, in which case $startFrom must be updated.
+        * @param ResultWrapper|bool $res Database query result
+        * @param array|null &$startFrom Continuation value. If you truncate $items, set this to
+        *  [ $recentChangeInfo['rc_timestamp'], $recentChangeInfo['rc_id'] ] from the first item
+        *  removed.
+        */
+       public function modifyWatchedItemsWithRCInfo( User $user, array $options, IDatabase $db,
+               array &$items, $res, &$startFrom
+       );
+
+}
diff --git a/includes/watcheditem/WatchedItemStore.php b/includes/watcheditem/WatchedItemStore.php
new file mode 100644 (file)
index 0000000..094297c
--- /dev/null
@@ -0,0 +1,875 @@
+<?php
+
+use Wikimedia\Rdbms\IDatabase;
+use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
+use MediaWiki\Linker\LinkTarget;
+use MediaWiki\MediaWikiServices;
+use Wikimedia\Assert\Assert;
+use Wikimedia\ScopedCallback;
+use Wikimedia\Rdbms\LoadBalancer;
+
+/**
+ * Storage layer class for WatchedItems.
+ * Database interaction & caching
+ * TODO caching should be factored out into a CachingWatchedItemStore class
+ *
+ * Uses database because this uses User::isAnon
+ *
+ * @group Database
+ *
+ * @author Addshore
+ * @since 1.27
+ */
+class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterface {
+
+       /**
+        * @var LoadBalancer
+        */
+       private $loadBalancer;
+
+       /**
+        * @var ReadOnlyMode
+        */
+       private $readOnlyMode;
+
+       /**
+        * @var HashBagOStuff
+        */
+       private $cache;
+
+       /**
+        * @var array[] Looks like $cacheIndex[Namespace ID][Target DB Key][User Id] => 'key'
+        * The index is needed so that on mass changes all relevant items can be un-cached.
+        * For example: Clearing a users watchlist of all items or updating notification timestamps
+        *              for all users watching a single target.
+        */
+       private $cacheIndex = [];
+
+       /**
+        * @var callable|null
+        */
+       private $deferredUpdatesAddCallableUpdateCallback;
+
+       /**
+        * @var callable|null
+        */
+       private $revisionGetTimestampFromIdCallback;
+
+       /**
+        * @var StatsdDataFactoryInterface
+        */
+       private $stats;
+
+       /**
+        * @param LoadBalancer $loadBalancer
+        * @param HashBagOStuff $cache
+        * @param ReadOnlyMode $readOnlyMode
+        */
+       public function __construct(
+               LoadBalancer $loadBalancer,
+               HashBagOStuff $cache,
+               ReadOnlyMode $readOnlyMode
+       ) {
+               $this->loadBalancer = $loadBalancer;
+               $this->cache = $cache;
+               $this->readOnlyMode = $readOnlyMode;
+               $this->stats = new NullStatsdDataFactory();
+               $this->deferredUpdatesAddCallableUpdateCallback = [ 'DeferredUpdates', 'addCallableUpdate' ];
+               $this->revisionGetTimestampFromIdCallback = [ 'Revision', 'getTimestampFromId' ];
+       }
+
+       public function setStatsdDataFactory( StatsdDataFactoryInterface $stats ) {
+               $this->stats = $stats;
+       }
+
+       /**
+        * Overrides the DeferredUpdates::addCallableUpdate callback
+        * This is intended for use while testing and will fail if MW_PHPUNIT_TEST is not defined.
+        *
+        * @param callable $callback
+        *
+        * @see DeferredUpdates::addCallableUpdate for callback signiture
+        *
+        * @return ScopedCallback to reset the overridden value
+        * @throws MWException
+        */
+       public function overrideDeferredUpdatesAddCallableUpdateCallback( callable $callback ) {
+               if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
+                       throw new MWException(
+                               'Cannot override DeferredUpdates::addCallableUpdate callback in operation.'
+                       );
+               }
+               $previousValue = $this->deferredUpdatesAddCallableUpdateCallback;
+               $this->deferredUpdatesAddCallableUpdateCallback = $callback;
+               return new ScopedCallback( function () use ( $previousValue ) {
+                       $this->deferredUpdatesAddCallableUpdateCallback = $previousValue;
+               } );
+       }
+
+       /**
+        * 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 ) {
+               return $this->cache->makeKey(
+                       (string)$target->getNamespace(),
+                       $target->getDBkey(),
+                       (string)$user->getId()
+               );
+       }
+
+       private function cache( WatchedItem $item ) {
+               $user = $item->getUser();
+               $target = $item->getLinkTarget();
+               $key = $this->getCacheKey( $user, $target );
+               $this->cache->set( $key, $item );
+               $this->cacheIndex[$target->getNamespace()][$target->getDBkey()][$user->getId()] = $key;
+               $this->stats->increment( 'WatchedItemStore.cache' );
+       }
+
+       private function uncache( User $user, LinkTarget $target ) {
+               $this->cache->delete( $this->getCacheKey( $user, $target ) );
+               unset( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()][$user->getId()] );
+               $this->stats->increment( 'WatchedItemStore.uncache' );
+       }
+
+       private function uncacheLinkTarget( LinkTarget $target ) {
+               $this->stats->increment( 'WatchedItemStore.uncacheLinkTarget' );
+               if ( !isset( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()] ) ) {
+                       return;
+               }
+               foreach ( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()] as $key ) {
+                       $this->stats->increment( 'WatchedItemStore.uncacheLinkTarget.items' );
+                       $this->cache->delete( $key );
+               }
+       }
+
+       private function uncacheUser( User $user ) {
+               $this->stats->increment( 'WatchedItemStore.uncacheUser' );
+               foreach ( $this->cacheIndex as $ns => $dbKeyArray ) {
+                       foreach ( $dbKeyArray as $dbKey => $userArray ) {
+                               if ( isset( $userArray[$user->getId()] ) ) {
+                                       $this->stats->increment( 'WatchedItemStore.uncacheUser.items' );
+                                       $this->cache->delete( $userArray[$user->getId()] );
+                               }
+                       }
+               }
+       }
+
+       /**
+        * @param User $user
+        * @param LinkTarget $target
+        *
+        * @return WatchedItem|false
+        */
+       private function getCached( User $user, LinkTarget $target ) {
+               return $this->cache->get( $this->getCacheKey( $user, $target ) );
+       }
+
+       /**
+        * Return an array of conditions to select or update the appropriate database
+        * row.
+        *
+        * @param User $user
+        * @param LinkTarget $target
+        *
+        * @return array
+        */
+       private function dbCond( User $user, LinkTarget $target ) {
+               return [
+                       'wl_user' => $user->getId(),
+                       'wl_namespace' => $target->getNamespace(),
+                       'wl_title' => $target->getDBkey(),
+               ];
+       }
+
+       /**
+        * @param int $dbIndex DB_MASTER or DB_REPLICA
+        *
+        * @return IDatabase
+        * @throws MWException
+        */
+       private function getConnectionRef( $dbIndex ) {
+               return $this->loadBalancer->getConnectionRef( $dbIndex, [ 'watchlist' ] );
+       }
+
+       /**
+        * @since 1.31
+        */
+       public function countWatchedItems( User $user ) {
+               $dbr = $this->getConnectionRef( DB_REPLICA );
+               $return = (int)$dbr->selectField(
+                       'watchlist',
+                       'COUNT(*)',
+                       [
+                               'wl_user' => $user->getId()
+                       ],
+                       __METHOD__
+               );
+
+               return $return;
+       }
+
+       /**
+        * @since 1.27
+        */
+       public function countWatchers( LinkTarget $target ) {
+               $dbr = $this->getConnectionRef( DB_REPLICA );
+               $return = (int)$dbr->selectField(
+                       'watchlist',
+                       'COUNT(*)',
+                       [
+                               'wl_namespace' => $target->getNamespace(),
+                               'wl_title' => $target->getDBkey(),
+                       ],
+                       __METHOD__
+               );
+
+               return $return;
+       }
+
+       /**
+        * @since 1.27
+        */
+       public function countVisitingWatchers( LinkTarget $target, $threshold ) {
+               $dbr = $this->getConnectionRef( DB_REPLICA );
+               $visitingWatchers = (int)$dbr->selectField(
+                       'watchlist',
+                       'COUNT(*)',
+                       [
+                               'wl_namespace' => $target->getNamespace(),
+                               'wl_title' => $target->getDBkey(),
+                               'wl_notificationtimestamp >= ' .
+                               $dbr->addQuotes( $dbr->timestamp( $threshold ) ) .
+                               ' OR wl_notificationtimestamp IS NULL'
+                       ],
+                       __METHOD__
+               );
+
+               return $visitingWatchers;
+       }
+
+       /**
+        * @since 1.27
+        */
+       public function countWatchersMultiple( array $targets, array $options = [] ) {
+               $dbOptions = [ 'GROUP BY' => [ 'wl_namespace', 'wl_title' ] ];
+
+               $dbr = $this->getConnectionRef( DB_REPLICA );
+
+               if ( array_key_exists( 'minimumWatchers', $options ) ) {
+                       $dbOptions['HAVING'] = 'COUNT(*) >= ' . (int)$options['minimumWatchers'];
+               }
+
+               $lb = new LinkBatch( $targets );
+               $res = $dbr->select(
+                       'watchlist',
+                       [ 'wl_title', 'wl_namespace', 'watchers' => 'COUNT(*)' ],
+                       [ $lb->constructSet( 'wl', $dbr ) ],
+                       __METHOD__,
+                       $dbOptions
+               );
+
+               $watchCounts = [];
+               foreach ( $targets as $linkTarget ) {
+                       $watchCounts[$linkTarget->getNamespace()][$linkTarget->getDBkey()] = 0;
+               }
+
+               foreach ( $res as $row ) {
+                       $watchCounts[$row->wl_namespace][$row->wl_title] = (int)$row->watchers;
+               }
+
+               return $watchCounts;
+       }
+
+       /**
+        * @since 1.27
+        */
+       public function countVisitingWatchersMultiple(
+               array $targetsWithVisitThresholds,
+               $minimumWatchers = null
+       ) {
+               $dbr = $this->getConnectionRef( DB_REPLICA );
+
+               $conds = $this->getVisitingWatchersCondition( $dbr, $targetsWithVisitThresholds );
+
+               $dbOptions = [ 'GROUP BY' => [ 'wl_namespace', 'wl_title' ] ];
+               if ( $minimumWatchers !== null ) {
+                       $dbOptions['HAVING'] = 'COUNT(*) >= ' . (int)$minimumWatchers;
+               }
+               $res = $dbr->select(
+                       'watchlist',
+                       [ 'wl_namespace', 'wl_title', 'watchers' => 'COUNT(*)' ],
+                       $conds,
+                       __METHOD__,
+                       $dbOptions
+               );
+
+               $watcherCounts = [];
+               foreach ( $targetsWithVisitThresholds as list( $target ) ) {
+                       /* @var LinkTarget $target */
+                       $watcherCounts[$target->getNamespace()][$target->getDBkey()] = 0;
+               }
+
+               foreach ( $res as $row ) {
+                       $watcherCounts[$row->wl_namespace][$row->wl_title] = (int)$row->watchers;
+               }
+
+               return $watcherCounts;
+       }
+
+       /**
+        * Generates condition for the query used in a batch count visiting watchers.
+        *
+        * @param IDatabase $db
+        * @param array $targetsWithVisitThresholds array of pairs (LinkTarget, last visit threshold)
+        * @return string
+        */
+       private function getVisitingWatchersCondition(
+               IDatabase $db,
+               array $targetsWithVisitThresholds
+       ) {
+               $missingTargets = [];
+               $namespaceConds = [];
+               foreach ( $targetsWithVisitThresholds as list( $target, $threshold ) ) {
+                       if ( $threshold === null ) {
+                               $missingTargets[] = $target;
+                               continue;
+                       }
+                       /* @var LinkTarget $target */
+                       $namespaceConds[$target->getNamespace()][] = $db->makeList( [
+                               'wl_title = ' . $db->addQuotes( $target->getDBkey() ),
+                               $db->makeList( [
+                                       'wl_notificationtimestamp >= ' . $db->addQuotes( $db->timestamp( $threshold ) ),
+                                       'wl_notificationtimestamp IS NULL'
+                               ], LIST_OR )
+                       ], LIST_AND );
+               }
+
+               $conds = [];
+               foreach ( $namespaceConds as $namespace => $pageConds ) {
+                       $conds[] = $db->makeList( [
+                               'wl_namespace = ' . $namespace,
+                               '(' . $db->makeList( $pageConds, LIST_OR ) . ')'
+                       ], LIST_AND );
+               }
+
+               if ( $missingTargets ) {
+                       $lb = new LinkBatch( $missingTargets );
+                       $conds[] = $lb->constructSet( 'wl', $db );
+               }
+
+               return $db->makeList( $conds, LIST_OR );
+       }
+
+       /**
+        * @since 1.27
+        */
+       public function getWatchedItem( User $user, LinkTarget $target ) {
+               if ( $user->isAnon() ) {
+                       return false;
+               }
+
+               $cached = $this->getCached( $user, $target );
+               if ( $cached ) {
+                       $this->stats->increment( 'WatchedItemStore.getWatchedItem.cached' );
+                       return $cached;
+               }
+               $this->stats->increment( 'WatchedItemStore.getWatchedItem.load' );
+               return $this->loadWatchedItem( $user, $target );
+       }
+
+       /**
+        * @since 1.27
+        */
+       public function loadWatchedItem( User $user, LinkTarget $target ) {
+               // Only loggedin user can have a watchlist
+               if ( $user->isAnon() ) {
+                       return false;
+               }
+
+               $dbr = $this->getConnectionRef( DB_REPLICA );
+               $row = $dbr->selectRow(
+                       'watchlist',
+                       'wl_notificationtimestamp',
+                       $this->dbCond( $user, $target ),
+                       __METHOD__
+               );
+
+               if ( !$row ) {
+                       return false;
+               }
+
+               $item = new WatchedItem(
+                       $user,
+                       $target,
+                       wfTimestampOrNull( TS_MW, $row->wl_notificationtimestamp )
+               );
+               $this->cache( $item );
+
+               return $item;
+       }
+
+       /**
+        * @since 1.27
+        */
+       public function getWatchedItemsForUser( User $user, array $options = [] ) {
+               $options += [ 'forWrite' => false ];
+
+               $dbOptions = [];
+               if ( array_key_exists( 'sort', $options ) ) {
+                       Assert::parameter(
+                               ( in_array( $options['sort'], [ self::SORT_ASC, self::SORT_DESC ] ) ),
+                               '$options[\'sort\']',
+                               'must be SORT_ASC or SORT_DESC'
+                       );
+                       $dbOptions['ORDER BY'] = [
+                               "wl_namespace {$options['sort']}",
+                               "wl_title {$options['sort']}"
+                       ];
+               }
+               $db = $this->getConnectionRef( $options['forWrite'] ? DB_MASTER : DB_REPLICA );
+
+               $res = $db->select(
+                       'watchlist',
+                       [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
+                       [ 'wl_user' => $user->getId() ],
+                       __METHOD__,
+                       $dbOptions
+               );
+
+               $watchedItems = [];
+               foreach ( $res as $row ) {
+                       // @todo: Should we add these to the process cache?
+                       $watchedItems[] = new WatchedItem(
+                               $user,
+                               new TitleValue( (int)$row->wl_namespace, $row->wl_title ),
+                               $row->wl_notificationtimestamp
+                       );
+               }
+
+               return $watchedItems;
+       }
+
+       /**
+        * @since 1.27
+        */
+       public function isWatched( User $user, LinkTarget $target ) {
+               return (bool)$this->getWatchedItem( $user, $target );
+       }
+
+       /**
+        * @since 1.27
+        */
+       public function getNotificationTimestampsBatch( User $user, array $targets ) {
+               $timestamps = [];
+               foreach ( $targets as $target ) {
+                       $timestamps[$target->getNamespace()][$target->getDBkey()] = false;
+               }
+
+               if ( $user->isAnon() ) {
+                       return $timestamps;
+               }
+
+               $targetsToLoad = [];
+               foreach ( $targets as $target ) {
+                       $cachedItem = $this->getCached( $user, $target );
+                       if ( $cachedItem ) {
+                               $timestamps[$target->getNamespace()][$target->getDBkey()] =
+                                       $cachedItem->getNotificationTimestamp();
+                       } else {
+                               $targetsToLoad[] = $target;
+                       }
+               }
+
+               if ( !$targetsToLoad ) {
+                       return $timestamps;
+               }
+
+               $dbr = $this->getConnectionRef( DB_REPLICA );
+
+               $lb = new LinkBatch( $targetsToLoad );
+               $res = $dbr->select(
+                       'watchlist',
+                       [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
+                       [
+                               $lb->constructSet( 'wl', $dbr ),
+                               'wl_user' => $user->getId(),
+                       ],
+                       __METHOD__
+               );
+
+               foreach ( $res as $row ) {
+                       $timestamps[$row->wl_namespace][$row->wl_title] =
+                               wfTimestampOrNull( TS_MW, $row->wl_notificationtimestamp );
+               }
+
+               return $timestamps;
+       }
+
+       /**
+        * @since 1.27
+        */
+       public function addWatch( User $user, LinkTarget $target ) {
+               $this->addWatchBatchForUser( $user, [ $target ] );
+       }
+
+       /**
+        * @since 1.27
+        */
+       public function addWatchBatchForUser( User $user, array $targets ) {
+               if ( $this->readOnlyMode->isReadOnly() ) {
+                       return false;
+               }
+               // Only loggedin user can have a watchlist
+               if ( $user->isAnon() ) {
+                       return false;
+               }
+
+               if ( !$targets ) {
+                       return true;
+               }
+
+               $rows = [];
+               $items = [];
+               foreach ( $targets as $target ) {
+                       $rows[] = [
+                               'wl_user' => $user->getId(),
+                               'wl_namespace' => $target->getNamespace(),
+                               'wl_title' => $target->getDBkey(),
+                               'wl_notificationtimestamp' => null,
+                       ];
+                       $items[] = new WatchedItem(
+                               $user,
+                               $target,
+                               null
+                       );
+                       $this->uncache( $user, $target );
+               }
+
+               $dbw = $this->getConnectionRef( DB_MASTER );
+               foreach ( array_chunk( $rows, 100 ) as $toInsert ) {
+                       // Use INSERT IGNORE to avoid overwriting the notification timestamp
+                       // if there's already an entry for this page
+                       $dbw->insert( 'watchlist', $toInsert, __METHOD__, 'IGNORE' );
+               }
+               // Update process cache to ensure skin doesn't claim that the current
+               // page is unwatched in the response of action=watch itself (T28292).
+               // This would otherwise be re-queried from a slave by isWatched().
+               foreach ( $items as $item ) {
+                       $this->cache( $item );
+               }
+
+               return true;
+       }
+
+       /**
+        * @since 1.27
+        */
+       public function removeWatch( User $user, LinkTarget $target ) {
+               // Only logged in user can have a watchlist
+               if ( $this->readOnlyMode->isReadOnly() || $user->isAnon() ) {
+                       return false;
+               }
+
+               $this->uncache( $user, $target );
+
+               $dbw = $this->getConnectionRef( DB_MASTER );
+               $dbw->delete( 'watchlist',
+                       [
+                               'wl_user' => $user->getId(),
+                               'wl_namespace' => $target->getNamespace(),
+                               'wl_title' => $target->getDBkey(),
+                       ], __METHOD__
+               );
+               $success = (bool)$dbw->affectedRows();
+
+               return $success;
+       }
+
+       /**
+        * @since 1.27
+        */
+       public function setNotificationTimestampsForUser( User $user, $timestamp, array $targets = [] ) {
+               // Only loggedin user can have a watchlist
+               if ( $user->isAnon() ) {
+                       return false;
+               }
+
+               $dbw = $this->getConnectionRef( DB_MASTER );
+
+               $conds = [ 'wl_user' => $user->getId() ];
+               if ( $targets ) {
+                       $batch = new LinkBatch( $targets );
+                       $conds[] = $batch->constructSet( 'wl', $dbw );
+               }
+
+               if ( $timestamp !== null ) {
+                       $timestamp = $dbw->timestamp( $timestamp );
+               }
+
+               $success = $dbw->update(
+                       'watchlist',
+                       [ 'wl_notificationtimestamp' => $timestamp ],
+                       $conds,
+                       __METHOD__
+               );
+
+               $this->uncacheUser( $user );
+
+               return $success;
+       }
+
+       /**
+        * @since 1.27
+        */
+       public function updateNotificationTimestamp( User $editor, LinkTarget $target, $timestamp ) {
+               $dbw = $this->getConnectionRef( DB_MASTER );
+               $uids = $dbw->selectFieldValues(
+                       'watchlist',
+                       'wl_user',
+                       [
+                               'wl_user != ' . intval( $editor->getId() ),
+                               'wl_namespace' => $target->getNamespace(),
+                               'wl_title' => $target->getDBkey(),
+                               'wl_notificationtimestamp IS NULL',
+                       ],
+                       __METHOD__
+               );
+
+               $watchers = array_map( 'intval', $uids );
+               if ( $watchers ) {
+                       // Update wl_notificationtimestamp for all watching users except the editor
+                       $fname = __METHOD__;
+                       DeferredUpdates::addCallableUpdate(
+                               function () use ( $timestamp, $watchers, $target, $fname ) {
+                                       global $wgUpdateRowsPerQuery;
+
+                                       $dbw = $this->getConnectionRef( DB_MASTER );
+                                       $factory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+                                       $ticket = $factory->getEmptyTransactionTicket( __METHOD__ );
+
+                                       $watchersChunks = array_chunk( $watchers, $wgUpdateRowsPerQuery );
+                                       foreach ( $watchersChunks as $watchersChunk ) {
+                                               $dbw->update( 'watchlist',
+                                                       [ /* SET */
+                                                               'wl_notificationtimestamp' => $dbw->timestamp( $timestamp )
+                                                       ], [ /* WHERE - TODO Use wl_id T130067 */
+                                                               'wl_user' => $watchersChunk,
+                                                               'wl_namespace' => $target->getNamespace(),
+                                                               'wl_title' => $target->getDBkey(),
+                                                       ], $fname
+                                               );
+                                               if ( count( $watchersChunks ) > 1 ) {
+                                                       $factory->commitAndWaitForReplication(
+                                                               __METHOD__, $ticket, [ 'domain' => $dbw->getDomainID() ]
+                                                       );
+                                               }
+                                       }
+                                       $this->uncacheLinkTarget( $target );
+                               },
+                               DeferredUpdates::POSTSEND,
+                               $dbw
+                       );
+               }
+
+               return $watchers;
+       }
+
+       /**
+        * @since 1.27
+        */
+       public function resetNotificationTimestamp( User $user, Title $title, $force = '', $oldid = 0 ) {
+               // Only loggedin user can have a watchlist
+               if ( $this->readOnlyMode->isReadOnly() || $user->isAnon() ) {
+                       return false;
+               }
+
+               $item = null;
+               if ( $force != 'force' ) {
+                       $item = $this->loadWatchedItem( $user, $title );
+                       if ( !$item || $item->getNotificationTimestamp() === null ) {
+                               return false;
+                       }
+               }
+
+               // If the page is watched by the user (or may be watched), update the timestamp
+               $job = new ActivityUpdateJob(
+                       $title,
+                       [
+                               'type'      => 'updateWatchlistNotification',
+                               'userid'    => $user->getId(),
+                               'notifTime' => $this->getNotificationTimestamp( $user, $title, $item, $force, $oldid ),
+                               'curTime'   => time()
+                       ]
+               );
+
+               // Try to run this post-send
+               // Calls DeferredUpdates::addCallableUpdate in normal operation
+               call_user_func(
+                       $this->deferredUpdatesAddCallableUpdateCallback,
+                       function () use ( $job ) {
+                               $job->run();
+                       }
+               );
+
+               $this->uncache( $user, $title );
+
+               return true;
+       }
+
+       private function getNotificationTimestamp( User $user, Title $title, $item, $force, $oldid ) {
+               if ( !$oldid ) {
+                       // No oldid given, assuming latest revision; clear the timestamp.
+                       return null;
+               }
+
+               if ( !$title->getNextRevisionID( $oldid ) ) {
+                       // Oldid given and is the latest revision for this title; clear the timestamp.
+                       return null;
+               }
+
+               if ( $item === null ) {
+                       $item = $this->loadWatchedItem( $user, $title );
+               }
+
+               if ( !$item ) {
+                       // This can only happen if $force is enabled.
+                       return null;
+               }
+
+               // 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
+               );
+
+               // We need to go one second to the future because of various strict comparisons
+               // throughout the codebase
+               $ts = new MWTimestamp( $notificationTimestamp );
+               $ts->timestamp->add( new DateInterval( 'PT1S' ) );
+               $notificationTimestamp = $ts->getTimestamp( TS_MW );
+
+               if ( $notificationTimestamp < $item->getNotificationTimestamp() ) {
+                       if ( $force != 'force' ) {
+                               return false;
+                       } else {
+                               // This is a little silly…
+                               return $item->getNotificationTimestamp();
+                       }
+               }
+
+               return $notificationTimestamp;
+       }
+
+       /**
+        * @since 1.27
+        */
+       public function countUnreadNotifications( User $user, $unreadLimit = null ) {
+               $queryOptions = [];
+               if ( $unreadLimit !== null ) {
+                       $unreadLimit = (int)$unreadLimit;
+                       $queryOptions['LIMIT'] = $unreadLimit;
+               }
+
+               $dbr = $this->getConnectionRef( DB_REPLICA );
+               $rowCount = $dbr->selectRowCount(
+                       'watchlist',
+                       '1',
+                       [
+                               'wl_user' => $user->getId(),
+                               'wl_notificationtimestamp IS NOT NULL',
+                       ],
+                       __METHOD__,
+                       $queryOptions
+               );
+
+               if ( !isset( $unreadLimit ) ) {
+                       return $rowCount;
+               }
+
+               if ( $rowCount >= $unreadLimit ) {
+                       return true;
+               }
+
+               return $rowCount;
+       }
+
+       /**
+        * @since 1.27
+        */
+       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() );
+       }
+
+       /**
+        * @since 1.27
+        */
+       public function duplicateEntry( LinkTarget $oldTarget, LinkTarget $newTarget ) {
+               $dbw = $this->getConnectionRef( DB_MASTER );
+
+               $result = $dbw->select(
+                       'watchlist',
+                       [ 'wl_user', 'wl_notificationtimestamp' ],
+                       [
+                               'wl_namespace' => $oldTarget->getNamespace(),
+                               'wl_title' => $oldTarget->getDBkey(),
+                       ],
+                       __METHOD__,
+                       [ 'FOR UPDATE' ]
+               );
+
+               $newNamespace = $newTarget->getNamespace();
+               $newDBkey = $newTarget->getDBkey();
+
+               # Construct array to replace into the watchlist
+               $values = [];
+               foreach ( $result as $row ) {
+                       $values[] = [
+                               'wl_user' => $row->wl_user,
+                               'wl_namespace' => $newNamespace,
+                               'wl_title' => $newDBkey,
+                               'wl_notificationtimestamp' => $row->wl_notificationtimestamp,
+                       ];
+               }
+
+               if ( !empty( $values ) ) {
+                       # Perform replace
+                       # Note that multi-row replace is very efficient for MySQL but may be inefficient for
+                       # some other DBMSes, mostly due to poor simulation by us
+                       $dbw->replace(
+                               'watchlist',
+                               [ [ 'wl_user', 'wl_namespace', 'wl_title' ] ],
+                               $values,
+                               __METHOD__
+                       );
+               }
+       }
+
+}
diff --git a/includes/watcheditem/WatchedItemStoreInterface.php b/includes/watcheditem/WatchedItemStoreInterface.php
new file mode 100644 (file)
index 0000000..d5a3d7c
--- /dev/null
@@ -0,0 +1,291 @@
+<?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 Watchlist
+ */
+use MediaWiki\Linker\LinkTarget;
+
+/**
+ * @author Addshore
+ * @since 1.31 interface created. WatchedItemStore implementation available since 1.27
+ */
+interface WatchedItemStoreInterface {
+
+       /**
+        * @since 1.31
+        */
+       const SORT_ASC = 'ASC';
+
+       /**
+        * @since 1.31
+        */
+       const SORT_DESC = 'DESC';
+
+       /**
+        * Count the number of individual items that are watched by the user.
+        * If a subject and corresponding talk page are watched this will return 2.
+        *
+        * @since 1.31
+        *
+        * @param User $user
+        *
+        * @return int
+        */
+       public function countWatchedItems( User $user );
+
+       /**
+        * @since 1.31
+        *
+        * @param LinkTarget $target
+        *
+        * @return int
+        */
+       public function countWatchers( LinkTarget $target );
+
+       /**
+        * Number of page watchers who also visited a "recent" edit
+        *
+        * @since 1.31
+        *
+        * @param LinkTarget $target
+        * @param mixed $threshold timestamp accepted by wfTimestamp
+        *
+        * @return int
+        * @throws DBUnexpectedError
+        * @throws MWException
+        */
+       public function countVisitingWatchers( LinkTarget $target, $threshold );
+
+       /**
+        * @since 1.31
+        *
+        * @param LinkTarget[] $targets
+        * @param array $options Allowed keys:
+        *        'minimumWatchers' => int
+        *
+        * @return array multi dimensional like $return[$namespaceId][$titleString] = int $watchers
+        *         All targets will be present in the result. 0 either means no watchers or the number
+        *         of watchers was below the minimumWatchers option if passed.
+        */
+       public function countWatchersMultiple( array $targets, array $options = [] );
+
+       /**
+        * Number of watchers of each page who have visited recent edits to that page
+        *
+        * @since 1.31
+        *
+        * @param array $targetsWithVisitThresholds array of pairs (LinkTarget $target, mixed
+        *     $threshold),
+        *        $threshold is:
+        *        - a timestamp of the recent edit if $target exists (format accepted by wfTimestamp)
+        *        - null if $target doesn't exist
+        * @param int|null $minimumWatchers
+        *
+        * @return array multi-dimensional like $return[$namespaceId][$titleString] = $watchers,
+        *         where $watchers is an int:
+        *         - if the page exists, number of users watching who have visited the page recently
+        *         - if the page doesn't exist, number of users that have the page on their watchlist
+        *         - 0 means there are no visiting watchers or their number is below the
+        *     minimumWatchers
+        *         option (if passed).
+        */
+       public function countVisitingWatchersMultiple(
+               array $targetsWithVisitThresholds,
+               $minimumWatchers = null
+       );
+
+       /**
+        * Get an item (may be cached)
+        *
+        * @since 1.31
+        *
+        * @param User $user
+        * @param LinkTarget $target
+        *
+        * @return WatchedItem|false
+        */
+       public function getWatchedItem( User $user, LinkTarget $target );
+
+       /**
+        * Loads an item from the db
+        *
+        * @since 1.31
+        *
+        * @param User $user
+        * @param LinkTarget $target
+        *
+        * @return WatchedItem|false
+        */
+       public function loadWatchedItem( User $user, LinkTarget $target );
+
+       /**
+        * @since 1.31
+        *
+        * @param User $user
+        * @param array $options Allowed keys:
+        *        'forWrite' => bool defaults to false
+        *        'sort' => string optional sorting by namespace ID and title
+        *                     one of the self::SORT_* constants
+        *
+        * @return WatchedItem[]
+        */
+       public function getWatchedItemsForUser( User $user, array $options = [] );
+
+       /**
+        * Must be called separately for Subject & Talk namespaces
+        *
+        * @since 1.31
+        *
+        * @param User $user
+        * @param LinkTarget $target
+        *
+        * @return bool
+        */
+       public function isWatched( User $user, LinkTarget $target );
+
+       /**
+        * @since 1.31
+        *
+        * @param User $user
+        * @param LinkTarget[] $targets
+        *
+        * @return array multi-dimensional like $return[$namespaceId][$titleString] = $timestamp,
+        *         where $timestamp is:
+        *         - string|null value of wl_notificationtimestamp,
+        *         - false if $target is not watched by $user.
+        */
+       public function getNotificationTimestampsBatch( User $user, array $targets );
+
+       /**
+        * Must be called separately for Subject & Talk namespaces
+        *
+        * @since 1.31
+        *
+        * @param User $user
+        * @param LinkTarget $target
+        */
+       public function addWatch( User $user, LinkTarget $target );
+
+       /**
+        * @since 1.31
+        *
+        * @param User $user
+        * @param LinkTarget[] $targets
+        *
+        * @return bool success
+        */
+       public function addWatchBatchForUser( User $user, array $targets );
+
+       /**
+        * Removes the an entry for the User watching the LinkTarget
+        * Must be called separately for Subject & Talk namespaces
+        *
+        * @since 1.31
+        *
+        * @param User $user
+        * @param LinkTarget $target
+        *
+        * @return bool success
+        * @throws DBUnexpectedError
+        * @throws MWException
+        */
+       public function removeWatch( User $user, LinkTarget $target );
+
+       /**
+        * @since 1.31
+        *
+        * @param User $user The user to set the timestamp 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,
+               $timestamp,
+               array $targets = []
+       );
+
+       /**
+        * @since 1.31
+        *
+        * @param User $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 );
+
+       /**
+        * Reset the notification timestamp of this entry
+        *
+        * @since 1.31
+        *
+        * @param User $user
+        * @param Title $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
+        * @param int $oldid The revision id being viewed. If not given or 0, latest revision is
+        *     assumed.
+        *
+        * @return bool success
+        */
+       public function resetNotificationTimestamp( User $user, Title $title, $force = '', $oldid = 0 );
+
+       /**
+        * @since 1.31
+        *
+        * @param User $user
+        * @param int $unreadLimit
+        *
+        * @return int|bool The number of unread notifications
+        *                  true if greater than or equal to $unreadLimit
+        */
+       public function countUnreadNotifications( User $user, $unreadLimit = null );
+
+       /**
+        * Check if the given title already is watched by the user, and if so
+        * add a watch for the new title.
+        *
+        * To be used for page renames and such.
+        *
+        * @since 1.31
+        *
+        * @param LinkTarget $oldTarget
+        * @param LinkTarget $newTarget
+        */
+       public function duplicateAllAssociatedEntries( LinkTarget $oldTarget, LinkTarget $newTarget );
+
+       /**
+        * Check if the given title already is watched by the user, and if so
+        * add a watch for the new title.
+        *
+        * To be used for page renames and such.
+        * This must be called separately for Subject and Talk pages
+        *
+        * @since 1.31
+        *
+        * @param LinkTarget $oldTarget
+        * @param LinkTarget $newTarget
+        */
+       public function duplicateEntry( LinkTarget $oldTarget, LinkTarget $newTarget );
+
+}
index 67c0ca7..a84c4b8 100644 (file)
@@ -20,6 +20,8 @@
  */
 use MediaWiki\MediaWikiServices;
 
+use MediaWiki\Logger\LoggerFactory;
+
 /**
  * Base class for language conversion.
  * @ingroup Language
@@ -37,6 +39,7 @@ class LanguageConverter {
         */
        static public $languagesWithVariants = [
                'en',
+               'crh',
                'gan',
                'iu',
                'kk',
@@ -351,26 +354,34 @@ class LanguageConverter {
                if ( $this->guessVariant( $text, $toVariant ) ) {
                        return $text;
                }
-
                /* we convert everything except:
-                * 1. HTML markups (anything between < and >)
-                * 2. HTML entities
-                * 3. placeholders created by the parser
-                */
-               $marker = '|' . Parser::MARKER_PREFIX . '[\-a-zA-Z0-9]+';
+                  1. HTML markups (anything between < and >)
+                  2. HTML entities
+                  3. placeholders created by the parser
+                  IMPORTANT: Beware of failure from pcre.backtrack_limit (T124404).
+                  Minimize use of backtracking where possible.
+               */
+               $marker = '|' . Parser::MARKER_PREFIX . '[^\x7f]++\x7f';
 
                // this one is needed when the text is inside an HTML markup
-               $htmlfix = '|<[^>]+$|^[^<>]*>';
+               $htmlfix = '|<[^>\004]++(?=\004$)|^[^<>]*+>';
+
+               // Optimize for the common case where these tags have
+               // few or no children. Thus try and possesively get as much as
+               // possible, and only engage in backtracking when we hit a '<'.
 
                // disable convert to variants between <code> tags
-               $codefix = '<code>.+?<\/code>|';
+               $codefix = '<code>[^<]*+(?:(?:(?!<\/code>).)[^<]*+)*+<\/code>|';
                // disable conversion of <script> tags
-               $scriptfix = '<script.*?>.*?<\/script>|';
+               $scriptfix = '<script[^>]*+>[^<]*+(?:(?:(?!<\/script>).)[^<]*+)*+<\/script>|';
                // disable conversion of <pre> tags
-               $prefix = '<pre.*?>.*?<\/pre>|';
+               $prefix = '<pre[^>]*+>[^<]*+(?:(?:(?!<\/pre>).)[^<]*+)*+<\/pre>|';
+               // The "|.*+)" at the end, is in case we missed some part of html syntax,
+               // we will fail securely (hopefully) by matching the rest of the string.
+               $htmlFullTag = '<(?:[^>=]*+(?>[^>=]*+=\s*+(?:"[^"]*"|\'[^\']*\'|[^\'">\s]*+))*+[^>=]*+>|.*+)|';
 
-               $reg = '/' . $codefix . $scriptfix . $prefix .
-                       '<[^>]+>|&[a-zA-Z#][a-z0-9]+;' . $marker . $htmlfix . '/s';
+               $reg = '/' . $codefix . $scriptfix . $prefix . $htmlFullTag .
+                       '&[a-zA-Z#][a-z0-9]++;' . $marker . $htmlfix . '|\004$/s';
                $startPos = 0;
                $sourceBlob = '';
                $literalBlob = '';
@@ -378,18 +389,45 @@ class LanguageConverter {
                // Guard against delimiter nulls in the input
                // (should never happen: see T159174)
                $text = str_replace( "\000", '', $text );
+               $text = str_replace( "\004", '', $text );
 
                $markupMatches = null;
                $elementMatches = null;
+
+               // We add a marker (\004) at the end of text, to ensure we always match the
+               // entire text (Otherwise, pcre.backtrack_limit might cause silent failure)
                while ( $startPos < strlen( $text ) ) {
-                       if ( preg_match( $reg, $text, $markupMatches, PREG_OFFSET_CAPTURE, $startPos ) ) {
+                       if ( preg_match( $reg, $text . "\004", $markupMatches, PREG_OFFSET_CAPTURE, $startPos ) ) {
                                $elementPos = $markupMatches[0][1];
                                $element = $markupMatches[0][0];
+                               if ( $element === "\004" ) {
+                                       // We hit the end.
+                                       $elementPos = strlen( $text );
+                                       $element = '';
+                               } elseif ( substr( $element, -1 ) === "\004" ) {
+                                       // This can sometimes happen if we have
+                                       // unclosed html tags (For example
+                                       // when converting a title attribute
+                                       // during a recursive call that contains
+                                       // a &lt; e.g. <div title="&lt;">.
+                                       $element = substr( $element, 0, -1 );
+                               }
                        } else {
-                               $elementPos = strlen( $text );
-                               $element = '';
+                               // If we hit here, then Language Converter could be tricked
+                               // into doing an XSS, so we refuse to translate.
+                               // If non-crazy input manages to reach this code path,
+                               // we should consider it a bug.
+                               $log = LoggerFactory::getInstance( 'languageconverter' );
+                               $log->error( "Hit pcre.backtrack_limit in " . __METHOD__
+                                       . ". Disabling language conversion for this page.",
+                                       [
+                                               "method" => __METHOD__,
+                                               "variant" => $toVariant,
+                                               "startOfText" => substr( $text, 0, 500 )
+                                       ]
+                               );
+                               return $text;
                        }
-
                        // Queue the part before the markup for translation in a batch
                        $sourceBlob .= substr( $text, $startPos, $elementPos - $startPos ) . "\000";
 
@@ -398,9 +436,16 @@ class LanguageConverter {
 
                        // Translate any alt or title attributes inside the matched element
                        if ( $element !== ''
-                               && preg_match( '/^(<[^>\s]*)\s([^>]*)(.*)$/', $element, $elementMatches )
+                               && preg_match( '/^(<[^>\s]*+)\s([^>]*+)(.*+)$/', $element, $elementMatches )
                        ) {
+                               // FIXME, this decodes entities, so if you have something
+                               // like <div title="foo&lt;bar"> the bar won't get
+                               // translated since after entity decoding it looks like
+                               // unclosed html and we call this method recursively
+                               // on attributes.
                                $attrs = Sanitizer::decodeTagAttributes( $elementMatches[2] );
+                               // Ensure self-closing tags stay self-closing.
+                               $close = substr( $elementMatches[2], -1 ) === '/' ? ' /' : '';
                                $changed = false;
                                foreach ( [ 'title', 'alt' ] as $attrName ) {
                                        if ( !isset( $attrs[$attrName] ) ) {
@@ -419,7 +464,7 @@ class LanguageConverter {
                                }
                                if ( $changed ) {
                                        $element = $elementMatches[1] . Html::expandAttributes( $attrs ) .
-                                               $elementMatches[3];
+                                               $close . $elementMatches[3];
                                }
                        }
                        $literalBlob .= $element . "\000";
@@ -631,29 +676,43 @@ class LanguageConverter {
                $out = '';
                $length = strlen( $text );
                $shouldConvert = !$this->guessVariant( $text, $variant );
-
-               while ( $startPos < $length ) {
-                       $pos = strpos( $text, '-{', $startPos );
-
-                       if ( $pos === false ) {
+               $continue = 1;
+
+               $noScript = '<script.*?>.*?<\/script>(*SKIP)(*FAIL)';
+               $noStyle = '<style.*?>.*?<\/style>(*SKIP)(*FAIL)';
+               // @codingStandardsIgnoreStart Generic.Files.LineLength.TooLong
+               $noHtml = '<(?:[^>=]*+(?>[^>=]*+=\s*+(?:"[^"]*"|\'[^\']*\'|[^\'">\s]*+))*+[^>=]*+>|.*+)(*SKIP)(*FAIL)';
+               // @codingStandardsIgnoreEnd
+               while ( $startPos < $length && $continue ) {
+                       $continue = preg_match(
+                               // Only match -{ outside of html.
+                               "/$noScript|$noStyle|$noHtml|-\{/",
+                               $text,
+                               $m,
+                               PREG_OFFSET_CAPTURE,
+                               $startPos
+                       );
+
+                       if ( !$continue ) {
                                // No more markup, append final segment
                                $fragment = substr( $text, $startPos );
                                $out .= $shouldConvert ? $this->autoConvert( $fragment, $variant ) : $fragment;
                                return $out;
                        }
 
-                       // Markup found
+                       // Offset of the match of the regex pattern.
+                       $pos = $m[0][1];
+
                        // Append initial segment
                        $fragment = substr( $text, $startPos, $pos - $startPos );
                        $out .= $shouldConvert ? $this->autoConvert( $fragment, $variant ) : $fragment;
-
-                       // Advance position
+                       // -{ marker found, not in attribute
+                       // Advance position up to -{ marker.
                        $startPos = $pos;
-
                        // Do recursive conversion
+                       // Note: This passes $startPos by reference, and advances it.
                        $out .= $this->recursiveConvertRule( $text, $variant, $startPos, $depth + 1 );
                }
-
                return $out;
        }
 
diff --git a/languages/classes/LanguageCrh.php b/languages/classes/LanguageCrh.php
new file mode 100644 (file)
index 0000000..f384471
--- /dev/null
@@ -0,0 +1,296 @@
+<?php
+/**
+ * Crimean Tatar (Qırımtatarca) specific code.
+ *
+ * Adapted from https://crh.wikipedia.org/wiki/Qullan%C4%B1c%C4%B1:Don_Alessandro/Translit
+ *
+ * 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 Language
+ */
+
+/**
+ * Crimean Tatar (Qırımtatarca) converter routines
+ *
+ * @ingroup Language
+ */
+class CrhConverter extends LanguageConverter {
+       // Defines working character ranges
+       const WORD_BEGINS = '\r\s\"\'\(\)\-<>\[\]\/.,:;!?';
+       const WORD_ENDS = '\r\s\"\'\(\)\-<>\[\]\/.,:;!?';
+
+       // Cyrillic
+       const C_UC = 'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ'; # Crimean Tatar Cyrillic uppercase
+       const C_LC = 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя'; # Crimean Tatar Cyrillic lowercase
+       const C_CONS_UC = 'БВГДЖЗЙКЛМНПРСТФХЦЧШЩCÑ'; # Crimean Tatar Cyrillic + CÑ uppercase consonants
+       const C_CONS_LC = 'бвгджзйклмнпрстфхцчшщcñ'; # Crimean Tatar Cyrillic + CÑ lowercase consonants
+       const C_M_CONS = 'бгкмпшcБГКМПШC'; # Crimean Tatar Cyrillic M-type consonants
+
+       # Crimean Tatar Cyrillic + CÑ consonants
+       const C_CONS = 'бвгджзйклмнпрстфхцчшщcñБВГДЖЗЙКЛМНПРСТФХЦЧШЩCÑ';
+
+       // Latin
+       const L_UC = 'AÂBCÇDEFGĞHIİJKLMNÑOÖPQRSŞTUÜVYZ'; # Crimean Tatar Latin uppercase
+       const L_LC = 'aâbcçdefgğhıijklmnñoöpqrsştuüvyz'; # Crimean Tatar Latin lowercase
+       const L_N_CONS_UC = 'ÇNRSTZ'; # Crimean Tatar Latin N-type upper case consonants
+       const L_N_CONS_LC = 'çnrstz'; # Crimean Tatar Latin N-type lower case consonants
+       const L_N_CONS = 'çnrstzÇNRSTZ'; # Crimean Tatar Latin N-type consonants
+       const L_M_CONS = 'bcgkmpşBCGKMPŞ'; # Crimean Tatar Latin M-type consonants
+       const L_CONS_UC = 'BCÇDFGHJKLMNÑPRSŞTVZ'; # Crimean Tatar Latin uppercase consonants
+       const L_CONS_LC = 'bcçdfghjklmnñprsştvz'; # Crimean Tatar Latin lowercase consonants
+       const L_CONS = 'bcçdfghjklmnñprsştvzBCÇDFGHJKLMNÑPRSŞTVZ'; # Crimean Tatar Latin consonants
+       const L_VOW_UC = 'AÂEIİOÖUÜ'; # Crimean Tatar Latin uppercase vowels
+       const L_VOW = 'aâeıioöuüAÂEIİOÖUÜ'; # Crimean Tatar Latin vowels
+       const L_F_UC = 'EİÖÜ'; # Crimean Tatar Latin uppercase front vowels
+       const L_F = 'eiöüEİÖÜ'; # Crimean Tatar Latin front vowels
+
+       public $mCyrillicToLatin = [
+
+               ## these are independent of location in the word, but have
+               ## to go first so other transforms don't bleed them
+               'гъ' => 'ğ', 'Гъ' => 'Ğ', 'ГЪ' => 'Ğ',
+               'къ' => 'q', 'Къ' => 'Q', 'КЪ' => 'Q',
+               'нъ' => 'ñ', 'Нъ' => 'Ñ', 'НЪ' => 'Ñ',
+               'дж' => 'c', 'Дж' => 'C', 'ДЖ' => 'C',
+
+               'А' => 'A', 'а' => 'a', 'Б' => 'B', 'б' => 'b',
+               'В' => 'V', 'в' => 'v', 'Г' => 'G', 'г' => 'g',
+               'Д' => 'D', 'д' => 'd', 'Ж' => 'J', 'ж' => 'j',
+               'З' => 'Z', 'з' => 'z', 'И' => 'İ', 'и' => 'i',
+               'Й' => 'Y', 'й' => 'y', 'К' => 'K', 'к' => 'k',
+               'Л' => 'L', 'л' => 'l', 'М' => 'M', 'м' => 'm',
+               'Н' => 'N', 'н' => 'n', 'П' => 'P', 'п' => 'p',
+               'Р' => 'R', 'р' => 'r', 'С' => 'S', 'с' => 's',
+               'Т' => 'T', 'т' => 't', 'Ф' => 'F', 'ф' => 'f',
+               'Х' => 'H', 'х' => 'h', 'Ч' => 'Ç', 'ч' => 'ç',
+               'Ш' => 'Ş', 'ш' => 'ş', 'Ы' => 'I', 'ы' => 'ı',
+               'Э' => 'E', 'э' => 'e', 'Е' => 'E', 'е' => 'e',
+               'Я' => 'Â', 'я' => 'â', 'У' => 'U', 'у' => 'u',
+               'О' => 'O', 'о' => 'o',
+
+               'Ё' => 'Yo', 'ё' => 'yo', 'Ю' => 'Yu', 'ю' => 'yu',
+               'Ц' => 'Ts', 'ц' => 'ts', 'Щ' => 'Şç', 'щ' => 'şç',
+               'Ь' => '', 'ь' => '', 'Ъ' => '', 'ъ' => '',
+
+       ];
+
+       public $mLatinToCyrillic = [
+               'Â' => 'Я', 'â' => 'я', 'B' => 'Б', 'b' => 'б',
+               'Ç' => 'Ч', 'ç' => 'ч', 'D' => 'Д', 'd' => 'д',
+               'F' => 'Ф', 'f' => 'ф', 'G' => 'Г', 'g' => 'г',
+               'H' => 'Х', 'h' => 'х', 'I' => 'Ы', 'ı' => 'ы',
+               'İ' => 'И', 'i' => 'и', 'J' => 'Ж', 'j' => 'ж',
+               'K' => 'К', 'k' => 'к', 'L' => 'Л', 'l' => 'л',
+               'M' => 'М', 'm' => 'м', 'N' => 'Н', 'n' => 'н',
+               'O' => 'О', 'o' => 'о', 'P' => 'П', 'p' => 'п',
+               'R' => 'Р', 'r' => 'р', 'S' => 'С', 's' => 'с',
+               'Ş' => 'Ш', 'ş' => 'ш', 'T' => 'Т', 't' => 'т',
+               'V' => 'В', 'v' => 'в', 'Z' => 'З', 'z' => 'з',
+
+               'ya' => 'я', 'Ya' => 'Я', 'YA' => 'Я',
+               'ye' => 'е', 'YE' => 'Е', 'Ye' => 'Е',
+
+               // hack, hack, hack
+               'A' => 'А', 'a' => 'а', 'E' => 'Е', 'e' => 'е',
+               'Ö' => 'О', 'ö' => 'о', 'U' => 'У', 'u' => 'у',
+               'Ü' => 'У', 'ü' => 'у', 'Y' => 'Й', 'y' => 'й',
+
+               'C' => 'Дж', 'c' => 'дж', 'Ğ' => 'Гъ', 'ğ' => 'гъ',
+               'Ñ' => 'Нъ', 'ñ' => 'нъ', 'Q' => 'Къ', 'q' => 'къ',
+
+               ];
+
+       public $mExceptions = [];
+       public $mCyrl2LatnPatterns = [];
+       public $mLatn2CyrlPatterns = [];
+       public $mCyrlCleanUpRegexes = [];
+
+       public $mExceptionsLoaded = false;
+
+       function loadDefaultTables() {
+               $this->mTables = [
+                       'crh-latn' => new ReplacementArray( $this->mCyrillicToLatin ),
+                       'crh-cyrl' => new ReplacementArray( $this->mLatinToCyrillic ),
+                       'crh' => new ReplacementArray()
+               ];
+       }
+
+       function postLoadTables() {
+               $this->loadExceptions();
+       }
+
+       function loadExceptions() {
+               if ( $this->mExceptionsLoaded ) {
+                       return;
+               }
+
+               $this->mExceptionsLoaded = true;
+               $crhExceptions = new MediaWiki\Languages\Data\CrhExceptions();
+               list( $this->mExceptions, $this->mCyrl2LatnPatterns, $this->mLatn2CyrlPatterns,
+                       $this->mCyrlCleanUpRegexes ) = $crhExceptions->loadExceptions( self::L_LC . self::C_LC,
+                       self::L_UC . self::C_UC );
+       }
+
+       /**
+        * A function wrapper:
+        *   - if there is no selected variant, leave the link
+        *     names as they were
+        *   - do not try to find variants for usernames
+        *
+        * @param string &$link
+        * @param Title &$nt
+        * @param bool $ignoreOtherCond
+        */
+       function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
+               // check for user namespace
+               if ( is_object( $nt ) ) {
+                       $ns = $nt->getNamespace();
+                       if ( $ns == NS_USER || $ns == NS_USER_TALK ) {
+                               return;
+                       }
+               }
+
+               $oldlink = $link;
+               parent::findVariantLink( $link, $nt, $ignoreOtherCond );
+               if ( $this->getPreferredVariant() == $this->mMainLanguageCode ) {
+                       $link = $oldlink;
+               }
+       }
+
+       /**
+        *  It translates text into variant, specials:
+        *    - ommiting roman numbers
+        *
+        * @param string $text
+        * @param bool $toVariant
+        *
+        * @throws MWException
+        * @return string
+        */
+       function translate( $text, $toVariant ) {
+               $letters = '';
+               switch ( $toVariant ) {
+                       case 'crh-cyrl':
+                               $letters = self::L_UC . self::L_LC . "\'";
+                               break;
+                       case 'crh-latn':
+                               $letters = self::C_UC . self::C_LC . "";
+                               break;
+                       default:
+                               return $text;
+                               break;
+               }
+
+               if ( !$this->mTablesLoaded ) {
+                       $this->loadTables();
+               }
+
+               if ( !isset( $this->mTables[$toVariant] ) ) {
+                       throw new MWException( "Broken variant table: " . implode( ',', array_keys( $this->mTables ) ) );
+               }
+
+               // check for roman numbers like VII, XIX...
+               $roman = '/^M{0,3}(C[DM]|D{0,1}C{0,3})(X[LC]|L{0,1}X{0,3})(I[VX]|V{0,1}I{0,3})$/u';
+
+               # match any sub-string of the relevant letters and convert it
+               $matches = preg_split( '/(\b|^)[^' . $letters . ']+(\b|$)/u',
+                       $text, -1, PREG_SPLIT_OFFSET_CAPTURE );
+               $mstart = 0;
+               $ret = '';
+               foreach ( $matches as $m ) {
+                       # copy over the non-matching bit
+                       $ret .= substr( $text, $mstart, $m[1] - $mstart );
+                       # skip certain classes of strings
+
+                       if ( array_key_exists( $m[0], $this->mExceptions ) ) {
+                               # if it's an exception, just copy down the right answer
+                               $ret .= $this->mExceptions[$m[0]];
+                       } elseif ( ! $m[0] || # empty strings
+                                        preg_match( $roman, $m[0] ) || # roman numerals
+                                        preg_match( '/[^' . $letters . ']/', $m[0] ) # mixed orthography
+                                       ) {
+                               $ret .= $m[0];
+                       } else {
+                               # convert according to the rules
+                               $token = $this->regsConverter( $m[0], $toVariant );
+                               $ret .= parent::translate( $token, $toVariant );
+                       }
+                       $mstart = $m[1] + strlen( $m[0] );
+               }
+
+               # pick up stray quote marks
+               switch ( $toVariant ) {
+                       case 'crh-cyrl':
+                               $ret = strtr( $ret, [ '“' => '«', '”' => '»', ] );
+                               $ret = $this->regsConverter( $ret, 'cyrl-cleanup' );
+                               break;
+                       case 'crh-latn':
+                               $ret = strtr( $ret, [ '«' => '"', '»' => '"', ] );
+                               break;
+               }
+
+               return $ret;
+       }
+
+       private function regsConverter( $text, $toVariant ) {
+               if ( $text == '' ) return $text;
+
+               $pat = [];
+               $rep = [];
+               switch ( $toVariant ) {
+                       case 'crh-latn':
+                               foreach ( $this->mCyrl2LatnPatterns as $pat => $rep ) {
+                                       $text = preg_replace( $pat, $rep, $text );
+                               }
+                               return $text;
+                       case 'crh-cyrl':
+                               foreach ( $this->mLatn2CyrlPatterns as $pat => $rep ) {
+                                       $text = preg_replace( $pat, $rep, $text );
+                               }
+                               return $text;
+                       case 'cyrl-cleanup':
+                               foreach ( $this->mCyrlCleanUpRegexes as $pat => $rep ) {
+                                       $text = preg_replace( $pat, $rep, $text );
+                               }
+                               return $text;
+                       default:
+                               return $text;
+               }
+       }
+
+}
+
+/**
+ * Crimean Tatar (Qırımtatarca)
+ *
+ * @ingroup Language
+ */
+class LanguageCrh extends Language {
+
+       function __construct() {
+               parent::__construct();
+
+               $variants = [ 'crh', 'crh-cyrl', 'crh-latn' ];
+               $variantfallbacks = [
+                       'crh' => 'crh-latn',
+                       'crh-cyrl' => 'crh-latn',
+                       'crh-latn' => 'crh-cyrl',
+               ];
+
+               $this->mConverter = new CrhConverter( $this, 'crh', $variants, $variantfallbacks );
+       }
+}
diff --git a/languages/data/CrhExceptions.php b/languages/data/CrhExceptions.php
new file mode 100644 (file)
index 0000000..dc4b1ef
--- /dev/null
@@ -0,0 +1,830 @@
+<?php
+/**
+ * Exceptions Tables for Crimean Tatar (crh / Qırımtatarca)
+ *
+ * Adapted from https://crh.wikipedia.org/wiki/Qullan%C4%B1c%C4%B1:Don_Alessandro/Translit
+ *
+ * @file
+ */
+
+namespace MediaWiki\Languages\Data;
+
+use \CrhConverter as Crh;
+
+class CrhExceptions {
+
+       function __construct() {
+               $this->loadRegs();
+       }
+
+       public $exceptionMap = [];
+       public $Cyrl2LatnPatterns = [];
+       public $Latn2CyrlPatterns = [];
+
+       private $lc2uc;
+       private $uc2lc;
+
+       private function initLcUc( $lcChars, $ucChars, $reinit = false ) {
+               # bail if we've already done this, unless we are re-initializing
+               if ( !$reinit && $this->lc2uc && $this->uc2lc ) {
+                       return;
+               }
+
+               # split up the lc and uc lists in a unicode-friendly way
+               $myLc = [];
+               preg_match_all( '/./u', $lcChars, $myLc );
+               $myLc = $myLc[0];
+
+               $myUc = [];
+               preg_match_all( '/./u', $ucChars, $myUc );
+               $myUc = $myUc[0];
+
+               # map lc to uc and vice versa
+               $this->lc2uc = array_combine( array_values( $myLc ), array_values( $myUc ) );
+               $this->uc2lc = array_combine( array_values( $myUc ), array_values( $myLc ) );
+       }
+
+       private function myLc( $string ) {
+               return strtr( $string, $this->uc2lc );
+       }
+
+       private function myUc( $string ) {
+               return strtr( $string, $this->lc2uc );
+       }
+
+       private function myUcWord( $string ) {
+               return $this->myUc( mb_substr( $string, 0, 1 ) ) . $this->myLc( mb_substr( $string, 1 ) );
+       }
+
+       private function addMappings( $mapArray, &$A2B, &$B2A, $exactCase = false,
+                       $prePat = '', $postPat = '' ) {
+               foreach ( $mapArray as $WordA => $WordB ) {
+                       $ucA = $this->myUc( $WordA );
+                       $ucWordA = $this->myUcWord( $WordA );
+                       $ucB = $this->myUc( $WordB );
+                       $ucWordB = $this->myUcWord( $WordB );
+
+                       # if there are regexes, only map toward backregs
+                       if ( ! preg_match( '/\$[1-9]/', $WordA ) ) {
+                               $A2B[ $prePat . $WordA . $postPat ] = $WordB;
+                               if ( ! $exactCase ) {
+                                       $A2B[ $prePat . $ucWordA . $postPat ] = $ucWordB;
+                                       $A2B[ $prePat . $ucA . $postPat ] = $ucB;
+                               }
+                       }
+
+                       if ( ! preg_match( '/\$[1-9]/', $WordB ) ) {
+                               $B2A[ $prePat . $WordB . $postPat ] = $WordA;
+                               if ( ! $exactCase ) {
+                                       $B2A[ $prePat . $ucWordB . $postPat ] = $ucWordA;
+                                       $B2A[ $prePat . $ucB . $postPat ] = $ucA;
+                               }
+                       }
+               }
+       }
+
+       function loadExceptions( $lcChars, $ucChars ) {
+               # init lc and uc, as needed
+               $this->initLcUc( $lcChars, $ucChars );
+               # load C2L and L2C whole-word exceptions into the same array, since it's just a look up
+               # no regex prefix/suffix needed
+               $this->addMappings( $this->wordMappings, $this->exceptionMap, $this->exceptionMap );
+               $this->addMappings( $this->exactCaseMappings, $this->exceptionMap, $this->exceptionMap, true );
+
+               # load C2L and L2C bidirectional prefix mappings
+               $this->addMappings( $this->prefixMapping,
+                       $this->Cyrl2LatnPatterns, $this->Latn2CyrlPatterns, false, '/^', '/u' );
+               $this->addMappings( $this->suffixMapping,
+                       $this->Cyrl2LatnPatterns, $this->Latn2CyrlPatterns, false, '/', '$/u' );
+
+               # tack on one-way mappings to the ends of the prefix and suffix patterns
+               $this->Cyrl2LatnPatterns += $this->Cyrl2LatnRegexes;
+               $this->Latn2CyrlPatterns += $this->Latn2CyrlRegexes;
+
+               return [ $this->exceptionMap, $this->Cyrl2LatnPatterns,
+                       $this->Latn2CyrlPatterns, $this->CyrlCleanUpRegexes ];
+       }
+
+       # map Cyrillic to Latin and back, whole word match only
+       # variants: all lowercase, all uppercase, first letter capitalized
+       # items with capture group refs (e.g., $1) are only mapped from the
+       # regex to the reference
+       private $wordMappings = [
+
+               #### originally Cyrillic to Latin
+               'аджыумер' => 'acıümer', 'аджыусеин' => 'acıüsein', 'алейкум' => 'aleyküm',
+               'бейуде' => 'beyüde', 'боливия' => 'boliviya', 'большевик' => 'bolşevik', 'борис' => 'boris',
+               'борнен' => 'bornen', 'бугун' => 'bugün', 'бузкесен' => 'buzkesen', 'буксир' => 'buksir',
+               'бульбуль' => 'bülbül', 'бульвар' => 'bulvar', 'бульдозер' => 'buldozer', 'бульон' => 'bulyon',
+               'бунен' => 'bunen', 'буннен' => 'bunnen', 'бус-бутюн' => 'büs-bütün',
+               'бутерброд' => 'buterbrod', 'буфер' => 'bufer', 'буфет' => 'bufet', 'гонъюл' => 'göñül',
+               'горизонт' => 'gorizont', 'госпиталь' => 'gospital', 'гуливер' => 'guliver', 'гуна' => 'güna',
+               'гунях' => 'günâh', 'гургуль' => 'gürgül', 'гуя' => 'güya', 'демирёл' => 'demiryol',
+               'джуньджу' => 'cüncü', 'ёлнен' => 'yolnen', 'зумбуль' => 'zümbül', 'ильи' => 'ilyi', 'ишунь' =>
+               'işün', 'кодекс' => 'kodeks', 'кодифик' => 'kodifik', 'койлю' => 'köylü', 'коккоз' =>
+               'kökköz', 'коккозь' => 'kökköz', 'коккозю' => 'kökközü', 'кокос' => 'kokos',
+               'коллег' => 'kolleg', 'коллект' => 'kollekt', 'коллекц' => 'kollekts', 'кольцов' => 'koltsov',
+               'комбин' => 'kombin', 'комедия' => 'komediya', 'коменда' => 'komenda', 'комета' => 'kometa',
+               'комис' => 'komis', 'комит' => 'komit', 'комите' => 'komite', 'коммент' => 'komment',
+               'коммерс' => 'kommers', 'коммерц' => 'kommerts', 'компенс' => 'kompens', 'компил' => 'kompil',
+               'компьютер' => 'kompyuter', 'конвейер' => 'konveyer', 'конвен' => 'konven',
+               'конверт' => 'konvert', 'конденс' => 'kondens', 'кондитер' => 'konditer',
+               'кондиц' => 'kondits', 'коник' => 'konik', 'консерв' => 'konserv', 'контейнер' => 'konteyner',
+               'континент' => 'kontinent', 'конфе' => 'konfe', 'конфискац' => 'konfiskats',
+               'концен' => 'kontsen', 'концерт' => 'kontsert', 'конъюктур' => 'konyuktur',
+               'коньки' => 'konki', 'коньяк' => 'konyak', 'копирле' => 'kopirle', 'копия' => 'kopiya',
+               'корбекул' => 'körbekül', 'кореиз' => 'koreiz', 'коренн' => 'korenn', 'корея' => 'koreya',
+               'коридор' => 'koridor', 'корнеев' => 'korneyev', 'корре' => 'korre', 'корьбекул' =>
+               'körbekül', 'косме' => 'kosme', 'космик' => 'kosmik', 'костюм' => 'kostüm', 'котельн' =>
+               'koteln', 'котировка' => 'kotirovka', 'котлет' => 'kotlet', 'кочергин' => 'koçergin',
+               'коше' => 'köşe', 'кудрин' => 'kudrin', 'кузнец' => 'kuznets', 'кулинар' => 'kulinar',
+               'кулич' => 'kuliç', 'кульминац' => 'kulminats', 'культив' => 'kultiv',
+               'культура' => 'kultura', 'куркулет' => 'kürkület', 'курсив' => 'kursiv', 'кушку' => 'küşkü',
+               'куюк' => 'küyük', 'къарагоз' => 'qaragöz', 'къолязма' => 'qolyazma', 'къуртумер' =>
+               'qurtümer', 'къуртусеин' => 'qurtüsein', 'марьино' => 'maryino', 'медьюн' => 'medyun',
+               'месули' => 'mesüli', 'месуль' => 'mesül', 'мефкуре' => 'mefküre', 'могедек' => 'mögedek',
+               'муур' => 'müür', 'муче' => 'müçe', 'муюз' => 'müyüz', 'огнево' => 'ognevo',
+               'одеколон' => 'odekolon', 'одеса' => 'odesa', 'одесса' => 'odessa', 'озерки' => 'ozerki',
+               'озерн' => 'ozern', 'озёрн' => 'ozörn', 'океан' => 'okean', 'оленев' => 'olenev',
+               'олимп' => 'olimp', 'ольчер' => 'ölçer', 'онен' => 'onen', 'оннен' => 'onnen',
+               'опера' => 'opera', 'оптим' => 'optim', 'опци' => 'optsi', 'опция' => 'optsiya',
+               'орден' => 'orden', 'ордер' => 'order', 'ореанда' => 'oreanda', 'орех' => 'oreh',
+               'оригинал' => 'original', 'ориент' => 'oriyent', 'оркестр' => 'orkestr', 'орлин' => 'orlin',
+               'офис' => 'ofis', 'офицер' => 'ofitser', 'офсет' => 'ofset', 'оюннен' => 'oyunnen', 'побед' =>
+               'pobed', 'полево' => 'polevo', 'поли' => 'poli', 'полюшко' => 'polüşko',
+               'помидор' => 'pomidor', 'пониз' => 'poniz', 'порфир' => 'porfir', 'потелов' => 'potelov',
+               'почетн' => 'poçetn', 'почётн' => 'poçötn', 'публик' => 'publik', 'публиц' => 'publits',
+               'пушкин' => 'puşkin', 'сеитумер' => 'seitümer', 'сеитусеин' => 'seitüsein', 'сеитягъя' =>
+               'seityağya', 'сеитягья' => 'seityagya', 'сеитяхья' => 'seityahya', 'сеитяя' => 'seityaya',
+               'сейитумер' => 'seyitümer', 'сейитусеин' => 'seyitüsein', 'сейитягъя' => 'seyityağya',
+               'сейитягья' => 'seyityagya', 'сейитяхья' => 'seyityahya', 'сейитяя' => 'seyityaya',
+               'ультимат' => 'ultimat', 'ультра' => 'ultra', 'ульянов' => 'ulyanov', 'универ' => 'univer',
+               'уника' => 'unika', 'унтер' => 'unter', 'урьян' => 'uryan', 'уткин' => 'utkin', 'учебн' =>
+               'uçebn', 'шовини' => 'şovini', 'шоссе' => 'şosse', 'шубин' => 'şubin', 'шунен' => 'şunen',
+               'шуннен' => 'şunnen', 'щёлкино' => 'şçolkino', 'эмирусеин' => 'emirüsein',
+               'юзбашы' => 'yüzbaşı', 'юзйыл' => 'yüzyıl', 'юртер' => 'yurter', 'ющенко' => 'yuşçenko',
+
+               'кою' => 'köyü', 'кок' => 'kök', 'ком-кок' => 'köm-kök', 'коп' => 'köp', 'ог' => 'ög',
+               'юрип' => 'yürip', 'юз' => 'yüz', 'юк' => 'yük', 'буюп' => 'büyüp', 'буюк' => 'büyük',
+               'джонк' => 'cönk', 'джонкю' => 'cönkü', 'устке' => 'üstke', 'устте' => 'üstte',
+               'усттен' => 'üstten',
+
+               # шофёр needs to come after шофер to override it in the Latin-to-Cyrillic direction
+               'шофер' => 'şoför',
+               'шофёр' => 'şoför',
+
+               #### originally Latin to Cyrillic (deduped from above)
+
+               # слова на -аль
+               # words in -аль
+               'актуаль' => 'aktual', 'диагональ' => 'diagonal', 'документаль' => 'dokumental',
+               'эмсаль' => 'emsal', 'фааль' => 'faal', 'феодаль' => 'feodal', 'фестиваль' => 'festival',
+               'горизонталь' => 'gorizontal', 'хроникаль' => 'hronikal', 'идеаль' => 'ideal',
+               'инструменталь' => 'instrumental', 'икъмаль' => 'iqmal', 'икъбаль' => 'iqbal',
+               'истикъбаль' => 'istiqbal', 'истикъляль' => 'istiqlâl', 'италия' => 'italiya',
+               'италья' => 'italya', 'ишгъаль' => 'işğal', 'кафедраль' => 'kafedral', 'казуаль' => 'kazual',
+               'коллегиаль' => 'kollegial', 'колоссаль' => 'kolossal', 'коммуналь' => 'kommunal',
+               'кординаль' => 'kordinal', 'криминаль' => 'kriminal', 'легаль' => 'legal', 'леталь' => 'letal',
+               'либераль' => 'liberal', 'локаль' => 'lokal', 'магистраль' => 'magistral',
+               'материаль' => 'material', 'машиналь' => 'maşinal', 'меаль' => 'meal',
+               'медальон' => 'medalyon', 'медаль' => 'medal', 'меридиональ' => 'meridional',
+               'мешъаль' => 'meşal', 'минераль' => 'mineral', 'минималь' => 'minimal', 'мисаль' => 'misal',
+               'модаль' => 'modal', 'музыкаль' => 'muzıkal', 'номиналь' => 'nominal', 'нормаль' => 'normal',
+               'оптималь' => 'optimal', 'орбиталь' => 'orbital', 'оригиналь' => 'original',
+               'педаль' => 'pedal', 'пропорциональ' => 'proportsional', 'профессиональ' => 'professional',
+               'радикаль' => 'radikal', 'рациональ' => 'ratsional', 'реаль' => 'real',
+               'региональ' => 'regional', 'суаль' => 'sual', 'шималь' => 'şimal',
+               'территориаль' => 'territorial', 'тимсаль' => 'timsal', 'тоталь' => 'total',
+               'уникаль' => 'unikal', 'универсаль' => 'universal', 'вертикаль' => 'vertikal',
+               'виртуаль' => 'virtual', 'визуаль' => 'vizual', 'вуаль' => 'vual', 'зональ' => 'zonal',
+               'зуаль' => 'zual',
+
+               # слова с мягким знаком перед а, о, у, э
+               # Words with a soft sign before а, о, у, э
+               'бильакис' => 'bilakis', 'маальэсеф' => 'maalesef',
+               'мельун' => 'melun', 'озьара' => 'özara', 'вельасыл' => 'velasıl',
+               'ельаякъ' => 'yelayaq',
+               # these are ordered so C2L is correct (the later Latin one)
+               'февкъульаде' => 'fevqülade','февкъульаде' => 'fevqulade',
+
+               # другие слова с мягким знаком
+               # Other words with a soft sign
+               'альбатрос' => 'albatros', 'альбинос' => 'albinos', 'альбом' => 'albom',
+               'альбумин' => 'albumin', 'алфавит' => 'alfavit', 'альфа' => 'alfa', 'альманах' => 'almanah',
+               'альпинист' => 'alpinist', 'альтерн' => 'altern', 'альтру' => 'altru', 'альвеола' => 'alveola',
+               'ансамбль' => 'ansambl', 'аньане' => 'anane', 'асфальт' => 'asfalt', 'бальнео' => 'balneo',
+               'баарь' => 'baar', 'базальт' => 'bazalt', 'бинокль' => 'binokl', 'джурьат' => 'curat',
+               'джурьат' => 'cürat', 'девальв' => 'devalv', 'факульт' => 'fakult', 'фальсиф' => 'falsif',
+               'фольклор' => 'folklor', 'гальван' => 'galvan', 'геральд' => 'gerald', 'женьшень' => 'jenşen',
+               'инвентарь' => 'inventar', 'кальк' => 'kalk', 'кальмар' => 'kalmar', 'консульт' => 'konsult',
+               'контроль' => 'kontrol', 'кульмин' => 'kulmin', 'культур' => 'kultur', 'лагерь' => 'lager',
+               'макъбуль' => 'maqbul', 'макъуль' => 'maqul', 'мальт' => 'malt', 'мальземе' => 'malzeme',
+               'меджуль' => 'mecul', 'мешгуль' => 'meşgül', 'мешгъуль' => 'meşğul', 'мульти' => 'multi',
+               'мусульман' => 'musulman', 'нефть' => 'neft', 'пальто' => 'palto', 'пароль' => 'parol',
+               'патруль' => 'patrul', 'пенальти' => 'penalti', 'къальби' => 'qalbi', 'къальпке' => 'qalpke',
+               'къальплер' => 'qalpler', 'къальпни' => 'qalpni', 'къальпте' => 'qalpte', 'къаарь' => 'qaar',
+               'ресуль' => 'resul', 'рыцарь' => 'rıtsar', 'рояль' => 'royal', 'саарь' => 'saar',
+               'спираль' => 'spiral', 'сульх' => 'sulh', 'сумбуль' => 'sumbul', 'суньий' => 'suniy',
+               'темаюль' => 'temayul', 'шампунь' => 'şampun', 'вальс' => 'vals', 'вальц' => 'valts',
+               'ведомость' => 'vedomost', 'зулькъарнейн' => 'zulqarneyn', 'январь' => 'yanvar',
+               'февраль' => 'fevral', 'июнь' => 'iyün', 'сентябрь' => 'sentâbr', 'октябрь' => 'oktâbr',
+               'ноябрь' => 'noyabr', 'декабрь' => 'dekabr',
+
+               # слова с твёрдым знаком
+               # Words with a solid sign
+               'бидъат' => 'bidat', 'бузъюрек' => 'buzyürek', 'атешъюрек' => 'ateşyürek',
+               'алъянакъ' => 'alyanaq', 'демиръёл' => 'demiryol', 'деръал' => 'deral', 'инъекц' => 'inyekts',
+               'мефъум' => 'mefum', 'мешъум' => 'meşum', 'объект' => 'obyekt', 'разъезд' => 'razyezd',
+               'субъект' => 'subyekt', 'хавъяр' => 'havyar', 'ямъям' => 'yamyam',
+
+               # слова с буквой щ
+               # words with щ
+               'ящик' => 'yaşçik', 'мещан' => 'meşçan',
+
+               # слова с буквой ц
+               # words with ц
+               'акциз' => 'aktsiz', 'ацет' => 'atset', 'блиц' => 'blits', 'бруцеллёз' => 'brutsellöz',
+               'доцент' => 'dotsent', 'фармацевт' => 'farmatsevt', 'глицер' => 'glitser',
+               'люцерна' => 'lütserna', 'лицей' => 'litsey', 'меццо' => 'metstso', 'наци' => 'natsi',
+               'проце' => 'protse', 'рецеп' => 'retsep', 'реценз' => 'retsenz', 'теплица' => 'teplitsa',
+               'вице' => 'vitse', 'цепс' => 'tseps', 'швейцар' => 'şveytsar',
+
+               # слова без буквы тс
+               # words with тс
+               'агъартс' => 'ağarts', 'агъыртс' => 'ağırts', 'бильдиртс' => 'bildirts', 'битсин' => 'bitsin',
+               'буюльтс' => 'büyülts', 'буютс' => 'büyüts', 'гебертс' => 'geberts', 'делиртс' => 'delirts',
+               'эгрильтс' => 'egrilts', 'эксильтс' => 'eksilts', 'эшитс' => 'eşits', 'иритс' => 'irits',
+               'иситс' => 'isits', 'ичиртс' => 'içirts', 'кертсин' => 'kertsin', 'кенишлетс' => 'kenişlets',
+               'кийсетс' => 'kiysets', 'копюртс' => 'köpürts', 'косьтертс' => 'kösterts',
+               'кучертс' => 'küçerts', 'кучюльтс' => 'küçülts', 'пертсин' => 'pertsin', 'къайтс' => 'qayts',
+               'къутсуз' => 'qutsuz', 'орьтс' => 'örts', 'отьс' => 'öts', 'тартс' => 'tarts',
+               'тутсун' => 'tutsun', 'тюнъюльтс' => 'tüñülts', 'тюртс' => 'türts', 'янъартс' => 'yañarts',
+               'ебертс' => 'yeberts', 'етсин' => 'yetsin', 'ешертс' => 'yeşerts', 'йиритс' => 'yirits',
+
+               # разные исключения
+               # different exceptions
+               'бейуде' => 'beyude', 'бугунь' => 'bugün', 'бюджет' => 'bücet', 'бюллет' => 'büllet',
+               'бюро' => 'büro', 'бюст' => 'büst', 'джонк' => 'cönk', 'диалог' => 'dialog',
+               'гонъюль' => 'göñül', 'ханымэфенди' => 'hanımefendi', 'каньон' => 'kanyon', 'кирил' => 'kiril',
+               'кирил' => 'kirill', 'кёрджа' => 'körca', 'кой' => 'köy', 'кулеръюзь' => 'küleryüz',
+               'маалле' => 'маальle', 'майор' => 'mayor', 'маниал' => 'manиаль', 'мефкуре' => 'mefküre',
+               'месуль' => 'mesul', 'месуль' => 'mesül', 'муурь' => 'müür',
+               'нормала' => 'нормальa', 'нумюне' => 'nümüne', 'проект' => 'proekt', 'район' => 'rayon',
+               'сойады' => 'soyadı', 'спортсмен' => 'sportsmen', 'услюп' => 'üslüp', 'услюб' => 'üslüb',
+               'вакъиал' => 'vaqиаль', 'юзйыллыкъ' => 'yüzyıllıq',
+
+               # имена собственные
+               # proper names
+               'адольф' => 'adolf', 'альберт' => 'albert', 'бешуй' => 'beşüy', 'эмирусеин' => 'emirüsein',
+               'флотск' => 'flotsk', 'гайана' => 'gayana', 'грэсовский' => 'gresovskiy', 'гриц' => 'grits',
+               'гурджи' => 'gürci', 'игорь' => 'igor', 'ильич' => 'ilyiç', 'ильин' => 'ilyin',
+               'исмаил' => 'ismail', 'киттс' => 'kitts', 'комсомольск' => 'komsomolsk',
+               'корьбекулю' => 'körbekülü', 'корьбекуль' => 'körbekül', 'куницын' => 'kunitsın',
+               'львив' => 'lviv', 'львов' => 'lvov', 'марьино' => 'maryino', 'махульдюр' => 'mahuldür',
+               'павел' => 'pavel', 'пантикапейон' => 'pantikapeyon', 'къарагозь' => 'qaragöz',
+               'къуртсейит' => 'qurtseyit', 'къуртсеит' => 'qurtseit', 'къуртумер' => 'qurtümer',
+               'сейитумер' => 'seyitümer', 'сеитумер' => 'seitümer', 'смаил' => 'smail',
+               'советск' => 'sovetsk', 'шемьи-заде' => 'şemi-zade', 'щёлкино' => 'şçolkino',
+               'тсвана' => 'tsvana', 'учьэвли' => 'üçevli', 'йохан' => 'yohan', 'йорк' => 'york',
+               'ющенко' => 'yuşçenko', 'льная' => 'lnaya', 'льное' => 'lnoye', 'льный' => 'lnıy',
+               'льская' => 'lskaya', 'льский' => 'lskiy', 'льское' => 'lskoye', 'ополь' => 'opol',
+
+               # originally Latin to Cyrillic, deduped from above
+               'ань' => 'an', 'аньге' => 'ange', 'аньде' => 'ande', 'аньки' => 'anki', 'кёр' => 'kör',
+               'мэр' => 'mer', 'этсин' => 'etsin',
+
+               # exceptions added after speaker review
+               # see https://www.mediawiki.org/wiki/User:TJones_(WMF)/T23582
+               'аджизленювинъиз' => 'acizlenüviñiz', 'акъшам' => 'aqşam', 'алчакъгонъюлли' => 'alçaqgöñülli',
+               'аньанелер' => 'ananeler', 'аньанелеримиз' => 'ananelerimiz',
+               'аньанелеримизден' => 'ananelerimizden', 'аньанелеримизни' => 'ananelerimizni',
+               'аньанели' => 'ananeli', 'асфальтке' => 'asfaltke', 'баарьде' => 'baarde', 'бахтсыз' => 'bahtsız',
+               'берилюви' => 'berilüvi', 'берювден' => 'berüvden', 'берювни' => 'berüvni',
+               'большевиклер' => 'bolşevikler', 'большевиклерге' => 'bolşeviklerge', 'болюк' => 'bölük',
+               'болюнген' => 'bölüngen', 'болюнгенини' => 'bölüngenini', 'болюшип' => 'bölüşip',
+               'бугуннинъ' => 'bugünniñ', 'бугуньден' => 'bugünden', 'бугуньки' => 'bugünki',
+               'букюльген' => 'bükülgen', 'букюльди' => 'büküldi', 'буллюр' => 'büllür',
+               'бурюмчик' => 'bürümçik', 'бурюнген' => 'bürüngen', 'бутюн' => 'bütün', 'бутюнлей' => 'bütünley',
+               'буюген' => 'büyügen', 'буюй' => 'büyüy', 'волость' => 'volost', 'волостьларгъа' => 'volostlarğa',
+               'гонъюлини' => 'göñülini', 'гонъюлли' => 'göñülli', 'гонъюллилер' => 'göñülliler',
+               'госпиталинде' => 'gospitalinde', 'госпитальге' => 'gospitalge', 'госпитальде' => 'gospitalde',
+               'гренадёр' => 'grenadör', 'гугюм' => 'gügüm', 'гугюмлер' => 'gügümler',
+               'гугюмлери' => 'gügümleri', 'гугюмлерини' => 'gügümlerini', 'гурьсюльди' => 'gürsüldi',
+               'гурюльдештилер' => 'gürüldeştiler', 'гурюльти' => 'gürülti', 'гурюльтили' => 'gürültili',
+               'гурюльтисидир' => 'gürültisidir', 'дарульмуаллиминде' => 'darülmualliminde',
+               'дарульмуаллимининде' => 'darülmuallimininde', 'дарульмуаллиминнинъ' => 'darülmualliminniñ',
+               'дёгюльген' => 'dögülgen', 'декабрьде' => 'dekabrde', 'дёндюрилип' => 'döndürilip',
+               'дёнермиз' => 'dönermiz', 'дёнмектелер' => 'dönmekteler', 'денъишюв' => 'deñişüv',
+               'дёрдю' => 'dördü', 'дёрдюмиз' => 'dördümiz', 'дёрдюнджи' => 'dördünci', 'дёрт' => 'dört',
+               'дертлешювге' => 'dertleşüvge', 'джесюр' => 'cesür', 'джесюране' => 'cesürane',
+               'джесюрликлерини' => 'cesürliklerini', 'джонегенлерини' => 'cönegenlerini',
+               'джонедим' => 'cönedim', 'джонейлер' => 'cöneyler', 'джурьатсызлыгъына' => 'cüratsızlığına',
+               'дюгюнлер' => 'dügünler', 'дюгюнлерле' => 'dügünlerle', 'дюдюк' => 'düdük', 'дюльбер' => 'dülber',
+               'дюльбери' => 'dülberi', 'дюльберлер' => 'dülberler', 'дюльберлернинъ' => 'dülberlerniñ',
+               'дюльгер' => 'dülger', 'дюльгерге' => 'dülgerge', 'дюльгерлернинъки' => 'dülgerlerniñki',
+               'дюльгерни' => 'dülgerni', 'дюльгернинъ' => 'dülgerniñ', 'дюмбюрдетти' => 'dümbürdetti',
+               'дюмен' => 'dümen', 'дюмени' => 'dümeni', 'дюнья' => 'dünya', 'дюньявий' => 'dünyaviy',
+               'дюньяда' => 'dünyada', 'дюньяларгъа' => 'dünyalarğa', 'дюньяларда' => 'dünyalarda',
+               'дюньяны' => 'dünyanı', 'дюньянынъ' => 'dünyanıñ', 'дюньясы' => 'dünyası',
+               'ельаякълылар' => 'yelayaqlılar', 'елькъуваны' => 'yelquvanı', 'ильич' => 'i̇liç',
+               'ичюн' => 'içün', 'ичюнми' => 'içünmi', 'келюви' => 'kelüvi', 'келювини' => 'kelüvini',
+               'келювинъизде' => 'kelüviñizde', 'келювни' => 'kelüvni', 'кемирювлер' => 'kemirüvler',
+               'кесювде' => 'kesüvde', 'кетюв' => 'ketüv', 'кетювге' => 'ketüvge', 'кетюви' => 'ketüvi',
+               'кетювимни' => 'ketüvimni', 'кетювлер' => 'ketüvler', 'кетювлери' => 'ketüvleri',
+               'кетювлеринънинъ' => 'ketüvleriñniñ', 'кетювнинъ' => 'ketüvniñ', 'кирюв' => 'kirüv',
+               'князь' => 'knâz', 'козькъапакъларыны' => 'közqapaqlarını', 'козьлю' => 'közlü', 'козю' => 'közü',
+               'козюме' => 'közüme', 'козюнде' => 'közünde', 'козюне' => 'közüne', 'козюнен' => 'közünen',
+               'козюнинъ' => 'közüniñ', 'козюнъни' => 'közüñni', 'койлюде' => 'köylüde',
+               'койлюлер' => 'köylüler', 'койлюлерде' => 'köylülerde', 'койлюлерни' => 'köylülerni',
+               'койлюлернинъ' => 'köylülerniñ', 'койлюнинъ' => 'köylüniñ', 'коккозьге' => 'kökközge',
+               'коккозьде' => 'kökközde', 'коккозьдеки' => 'kökközdeki', 'коккозьден' => 'kökközden',
+               'кокюс' => 'köküs', 'кокюси' => 'köküsi', 'кокюсим' => 'köküsim', 'кокюсиме' => 'köküsime',
+               'кокюсинъе' => 'köküsiñe', 'комиссарлар' => 'komissarlar', 'комиссарлары' => 'komissarları',
+               'комитетининъ' => 'komitetiniñ', 'концлагерь' => 'kontslager', 'копьмеди' => 'köpmedi',
+               'копьти' => 'köpti', 'копюр' => 'köpür', 'копюрге' => 'köpürge', 'копюрден' => 'köpürden',
+               'копюри' => 'köpüri', 'копюрнинъ' => 'köpürniñ', 'коридорда' => 'koridorda',
+               'корьсюн' => 'körsün', 'корюв' => 'körüv', 'корюльген' => 'körülgen', 'корюнди' => 'köründi',
+               'корюндинъ' => 'köründiñ', 'корюне' => 'körüne', 'корюнип' => 'körünip',
+               'корюнмеген' => 'körünmegen', 'корюнмеди' => 'körünmedi', 'корюнмедилер' => 'körünmediler',
+               'корюнмей' => 'körünmey', 'корюнмейсинъиз' => 'körünmeysiñiz', 'корюнмекте' => 'körünmekte',
+               'корюнмектелер' => 'körünmekteler', 'корюнъиз' => 'körüñiz', 'корюше' => 'körüşe',
+               'корюшеджекмиз' => 'körüşecekmiz', 'корюшим' => 'körüşim', 'корюшип' => 'körüşip',
+               'корюширмиз' => 'körüşirmiz', 'корюшкен' => 'körüşken', 'корюшкенде' => 'körüşkende',
+               'корюшмеге' => 'körüşmege', 'корюшмегенимиз' => 'körüşmegenimiz', 'корюштик' => 'körüştik',
+               'корюштим' => 'körüştim', 'корюшюв' => 'körüşüv', 'корюшювде' => 'körüşüvde',
+               'корюшювден' => 'körüşüvden', 'корюшюви' => 'körüşüvi', 'корюшювимден' => 'körüşüvimden',
+               'корюшювимизге' => 'körüşüvimizge', 'корюшювимизден' => 'körüşüvimizden',
+               'костюми' => 'kostümi', 'кузю' => 'küzü', 'кулькюден' => 'külküden', 'кулькюнинъ' => 'külküniñ',
+               'кулькюсининъ' => 'külküsiniñ', 'кулю' => 'külü', 'кулюмсиреген' => 'külümsiregen',
+               'кулюмсиреди' => 'külümsiredi', 'кулюмсиредим' => 'külümsiredim', 'кулюмсирей' => 'külümsirey',
+               'кулюмсирейим' => 'külümsireyim', 'кулюмсиреп' => 'külümsirep', 'кулюни' => 'külüni',
+               'кулюнчли' => 'külünçli', 'кулюшинде' => 'külüşinde', 'кулюштилер' => 'külüştiler',
+               'кумюш' => 'kümüş', 'куньдюз' => 'kündüz', 'куньдюзлери' => 'kündüzleri', 'куньлюк' => 'künlük',
+               'куню' => 'künü', 'кунюмде' => 'künümde', 'кунюнде' => 'kününde', 'кунюндеми' => 'künündemi',
+               'кунюнъ' => 'künüñ', 'курькчю' => 'kürkçü', 'курьсю' => 'kürsü', 'курьсюге' => 'kürsüge',
+               'курьсюлер' => 'kürsüler', 'курючтен' => 'kürüçten', 'кутюклерни' => 'kütüklerni',
+               'кутюкли' => 'kütükli', 'кучьлю' => 'küçlü', 'кучьлюклер' => 'küçlükler',
+               'кучьсюнмезсинъ' => 'küçsünmezsiñ', 'кучюджик' => 'küçücik', 'кучюк' => 'küçük',
+               'кучюм' => 'küçüm', 'кучюмле' => 'küçümle', 'кучюнден' => 'küçünden', 'кучюни' => 'küçüni',
+               'къаарьлене' => 'qaarlene', 'къаарьли' => 'qaarli', 'къальбим' => 'qalbim',
+               'къальбимни' => 'qalbimni', 'къальбинде' => 'qalbinde', 'къальпли' => 'qalpli',
+               'къальптен' => 'qalpten', 'къалюбелядан' => 'qalübelâdan', 'къулюбенъде' => 'qulübeñde',
+               'лёман' => 'löman', 'львованынъ' => 'lvovanıñ', 'лютфи' => 'lütfi', 'лютфиге' => 'lütfige',
+               'лютфини' => 'lütfini', 'мазюн' => 'mazün', 'малюм' => 'malüm', 'малюмат' => 'malümat',
+               'махлюкъаттан' => 'mahlüqattan', 'махлюкътан' => 'mahlüqtan', 'махульдюрге' => 'mahuldürge',
+               'махульдюрде' => 'mahuldürde', 'махульдюрдеки' => 'mahuldürdeki',
+               'махульдюрден' => 'mahuldürden', 'махульдюрли' => 'mahuldürli',
+               'махульдюрлилер' => 'mahuldürliler', 'махульдюрлилермиз' => 'mahuldürlilermiz',
+               'махульдюрми' => 'mahuldürmi', 'махульдюрни' => 'mahuldürni', 'мевджут' => 'mevcut',
+               'мезкюр' => 'mezkür', 'мектюп' => 'mektüp', 'мектюпни' => 'mektüpni', 'мектюпте' => 'mektüpte',
+               'мелитопольге' => 'melitopolge', 'мемнюн' => 'memnün', 'мемнюниетле' => 'memnüniyetle',
+               'мемнюним' => 'memnünim', 'мемнюнмиз' => 'memnünmiz', 'менсюп' => 'mensüp',
+               'мешгъульмиз' => 'meşğulmiz', 'мулькюни' => 'mülküni', 'мумкюн' => 'mümkün',
+               'мумкюнми' => 'mümkünmi', 'мусульманлар' => 'musulmanlar', 'мусульманлармы' => 'musulmanlarmı',
+               'мухкемлендирюв' => 'mühkemlendirüv', 'мушкюль' => 'müşkül', 'ничюн' => 'niçün',
+               'ничюндир' => 'niçündir', 'нумюнеси' => 'nümünesi', 'огю' => 'ögü', 'огюз' => 'ögüz',
+               'огюмде' => 'ögümde', 'огюмдеки' => 'ögümdeki', 'огюме' => 'ögüme', 'огюмизге' => 'ögümizge',
+               'огюмизде' => 'ögümizde', 'огюмиздеки' => 'ögümizdeki', 'огюмни' => 'ögümni',
+               'огюнде' => 'ögünde', 'огюндеки' => 'ögündeki', 'огюндекиси' => 'ögündekisi',
+               'огюнден' => 'ögünden', 'огюне' => 'ögüne', 'огюнъизде' => 'ögüñizde', 'огютини' => 'ögütini',
+               'огютлерини' => 'ögütlerini', 'озю' => 'özü', 'озюм' => 'özüm', 'озюмден' => 'özümden',
+               'озюме' => 'özüme', 'озюмизни' => 'özümizni', 'озюмизнинъ' => 'özümizniñ',
+               'озюмизнинъки' => 'özümizniñki', 'озюмнен' => 'özümnen', 'озюмни' => 'özümni',
+               'озюмнинъ' => 'özümniñ', 'озюнде' => 'özünde', 'озюнден' => 'özünden', 'озюне' => 'özüne',
+               'озюнен' => 'özünen', 'озюни' => 'özüni', 'озюнинъ' => 'özüniñ', 'озюнинъкими' => 'özüniñkimi',
+               'озюнъ' => 'özüñ', 'озюнъе' => 'özüñe', 'озюнъиз' => 'özüñiz', 'озюнъиздеки' => 'özüñizdeki',
+               'озюнъни' => 'özüñni', 'оксюз' => 'öksüz', 'окюндим' => 'ökündim', 'ольдюрип' => 'öldürip',
+               'ольдюрмек' => 'öldürmek', 'ольдюрювде' => 'öldürüvde', 'ольчюде' => 'ölçüde', 'олюм' => 'ölüm',
+               'олюмден' => 'ölümden', 'олюмлер' => 'ölümler', 'омюр' => 'ömür', 'омюрге' => 'ömürge',
+               'омюри' => 'ömüri', 'опькеленюв' => 'öpkelenüv', 'орьтилюви' => 'örtilüvi', 'орьтюли' => 'örtüli',
+               'орюли' => 'örüli', 'орюлип' => 'örülip', 'осюв' => 'ösüv', 'осюмлик' => 'ösümlik',
+               'отькерювни' => 'ötkerüvni', 'отькюр' => 'ötkür', 'офицери' => 'ofitseri',
+               'офицерим' => 'ofitserim', 'офицерлер' => 'ofitserler', 'пальтосыны' => 'paltosını',
+               'пальтосынынъ' => 'paltosınıñ', 'пекинюв' => 'pekinüv', 'пекитювнинъ' => 'pekitüvniñ',
+               'пиширюв' => 'pişirüv', 'повидло' => 'povidlo', 'полис' => 'polis', 'полициясы' => 'politsiyası',
+               'помещик' => 'pomeşçik', 'потюк' => 'potük', 'потюклеринен' => 'potüklerinen',
+               'пулемёт' => 'pülemöt', 'пулемётларны' => 'pülemötlarnı', 'режиссёр' => 'rejissör',
+               'ролюнде' => 'rolünde', 'севастопольнинъ' => 'sevastopolniñ', 'сёгди' => 'sögdi', 'сёз' => 'söz',
+               'сёзлер' => 'sözler', 'сёзлери' => 'sözleri', 'сёзлерим' => 'sözlerim',
+               'сёзлеримден' => 'sözlerimden', 'сёзлериме' => 'sözlerime', 'сёзлеримни' => 'sözlerimni',
+               'сёзлеримнинъ' => 'sözlerimniñ', 'сёзлеринде' => 'sözlerinde', 'сёзлерине' => 'sözlerine',
+               'сёзлерини' => 'sözlerini', 'сёзлерининъ' => 'sözleriniñ', 'сёзлеринъиз' => 'sözleriñiz',
+               'сёзлеринъизни' => 'sözleriñizni', 'сёзлернен' => 'sözlernen', 'сёзлерни' => 'sözlerni',
+               'сёзлернинъ' => 'sözlerniñ', 'сёзнен' => 'söznen', 'сёзни' => 'sözni', 'сёзчиклер' => 'sözçikler',
+               'сёзчиклерден' => 'sözçiklerden', 'сёзю' => 'sözü', 'сёзюмен' => 'sözümen',
+               'сёзюмнинъ' => 'sözümniñ', 'сёзюне' => 'sözüne', 'сёзюни' => 'sözüni', 'сёзюнинъ' => 'sözüniñ',
+               'сёйле' => 'söyle', 'сёйлегенде' => 'söylegende', 'сёйлегенлеринден' => 'söylegenlerinden',
+               'сёйледи' => 'söyledi', 'сёйлей' => 'söyley', 'сёйленди' => 'söylendi',
+               'сёйленмеге' => 'söylenmege', 'сёйленмекте' => 'söylenmekte', 'сёйленъиз' => 'söyleñiz',
+               'сёнген' => 'söngen', 'сёнди' => 'söndi', 'сёндюрди' => 'söndürdi',
+               'сёндюрильген' => 'söndürilgen', 'сёндюрип' => 'söndürip', 'сентябрьнинъ' => 'sentâbrniñ',
+               'сергюзешт' => 'sergüzeşt', 'сергюзештлерни' => 'sergüzeştlerni',
+               'ставропольге' => 'stavropolge', 'сулькевич' => 'sulkeviç', 'сурьат' => 'surat',
+               'суфлёр' => 'suflör', 'сюеги' => 'süyegi', 'сюеклерге' => 'süyeklerge',
+               'сюйрекледи' => 'süyrekledi', 'сюйреле' => 'süyrele', 'сюйрен' => 'süyren',
+               'сюйренге' => 'süyrenge', 'сюйренде' => 'süyrende', 'сюйреп' => 'süyrep', 'сюйрю' => 'süyrü',
+               'сюкюнет' => 'sükünet', 'сюкюнети' => 'süküneti', 'сюкюнетте' => 'sükünette', 'сюкют' => 'süküt',
+               'сюляле' => 'sülâle', 'сюрген' => 'sürgen', 'сюрди' => 'sürdi', 'сюрмеди' => 'sürmedi',
+               'сюрюльмеген' => 'sürülmegen', 'сют' => 'süt', 'тебессюм' => 'tebessüm', 'тёкип' => 'tökip',
+               'тёкти' => 'tökti', 'тёкюльген' => 'tökülgen', 'тёкюльди' => 'töküldi',
+               'тёкюндиси' => 'tökündisi', 'тёле' => 'töle', 'тёледим' => 'töledim', 'телюке' => 'telüke',
+               'телюкели' => 'telükeli', 'тенеффюс' => 'teneffüs', 'тенеффюслер' => 'teneffüsler',
+               'тёпеге' => 'töpege', 'тёпелери' => 'töpeleri', 'тёпелерине' => 'töpelerine',
+               'тёпели' => 'töpeli', 'тёпеси' => 'töpesi', 'тёпесинден' => 'töpesinden',
+               'тёпесини' => 'töpesini', 'тёрге' => 'törge', 'тёрде' => 'törde', 'тёрдеки' => 'tördeki',
+               'тёрюне' => 'törüne', 'тешеббюсим' => 'teşebbüsim', 'тёшегинден' => 'töşeginden',
+               'тёшегине' => 'töşegine', 'тёшек' => 'töşek', 'тешеккюр' => 'teşekkür',
+               'тешеккюрлер' => 'teşekkürler', 'тёшекни' => 'töşekni', 'тёшектен' => 'töşekten',
+               'тёшели' => 'töşeli', 'тёшемек' => 'töşemek', 'тёшеп' => 'töşep', 'теэссюф' => 'teessüf',
+               'тюбю' => 'tübü', 'тюбюнде' => 'tübünde', 'тюбюндеки' => 'tübündeki', 'тюз' => 'tüz',
+               'тюзельгенге' => 'tüzelgenge', 'тюзельтмек' => 'tüzeltmek', 'тюземликлер' => 'tüzemlikler',
+               'тюзетип' => 'tüzetip', 'тюзетирим' => 'tüzetirim', 'тюзеткен' => 'tüzetken',
+               'тюзетмеге' => 'tüzetmege', 'тюзетмесенъ' => 'tüzetmeseñ', 'тюзетти' => 'tüzetti',
+               'тюзетюв' => 'tüzetüv', 'тюкенмез' => 'tükenmez', 'тюкюриктен' => 'tükürikten',
+               'тюкян' => 'tükân', 'тюкяны' => 'tükânı', 'тюкянында' => 'tükânında', 'тюм' => 'tüm',
+               'тюневин' => 'tünevin', 'тюневинки' => 'tünevinki', 'тюпсюз' => 'tüpsüz', 'тюрк' => 'türk',
+               'тюрклернинъ' => 'türklerniñ', 'тюркнинъ' => 'türkniñ', 'тюркче' => 'türkçe', 'тюркю' => 'türkü',
+               'тюркюлерини' => 'türkülerini', 'тюркюнинъ' => 'türküniñ', 'тюрлю' => 'türlü',
+               'тюртип' => 'türtip', 'тюрттинъиз' => 'türttiñiz', 'тютемекте' => 'tütemekte', 'тютюн' => 'tütün',
+               'тютюнджи' => 'tütünci', 'тюфеги' => 'tüfegi', 'тюфегини' => 'tüfegini', 'тюфек' => 'tüfek',
+               'тюфеклеринен' => 'tüfeklerinen', 'тюфеклернен' => 'tüfeklernen', 'тюфеклерни' => 'tüfeklerni',
+               'тюфекнен' => 'tüfeknen', 'тюфексиз' => 'tüfeksiz', 'тюш' => 'tüş', 'тюше' => 'tüşe',
+               'тюшеджек' => 'tüşecek', 'тюшеджексинъми' => 'tüşeceksiñmi', 'тюшем' => 'tüşem',
+               'тюшип' => 'tüşip', 'тюшкен' => 'tüşken', 'тюшкенде' => 'tüşkende', 'тюшкенлер' => 'tüşkenler',
+               'тюшмеге' => 'tüşmege', 'тюшмейим' => 'tüşmeyim', 'тюшмейлер' => 'tüşmeyler',
+               'тюшмек' => 'tüşmek', 'тюшмекте' => 'tüşmekte', 'тюшмеси' => 'tüşmesi', 'тюшсе' => 'tüşse',
+               'тюшти' => 'tüşti', 'тюштик' => 'tüştik', 'тюштилер' => 'tüştiler', 'тюштими' => 'tüştimi',
+               'тюштинъиз' => 'tüştiñiz', 'тюшювден' => 'tüşüvden', 'тюшюджек' => 'tüşücek',
+               'тюшюнген' => 'tüşüngen', 'тюшюнгендже' => 'tüşüngence', 'тюшюндже' => 'tüşünce',
+               'тюшюнджеге' => 'tüşüncege', 'тюшюнджелер' => 'tüşünceler', 'тюшюнджелери' => 'tüşünceleri',
+               'тюшюнджелерим' => 'tüşüncelerim', 'тюшюнджели' => 'tüşünceli', 'тюшюнджеси' => 'tüşüncesi',
+               'тюшюнди' => 'tüşündi', 'тюшюндим' => 'tüşündim', 'тюшюне' => 'tüşüne',
+               'тюшюнелер' => 'tüşüneler', 'тюшюнесинъиз' => 'tüşünesiñiz', 'тюшюнип' => 'tüşünip',
+               'тюшюнмеге' => 'tüşünmege', 'тюшюнмезсинъ' => 'tüşünmezsiñ', 'тюшюнмей' => 'tüşünmey',
+               'тюшюнмемек' => 'tüşünmemek', 'тюшюргенлер' => 'tüşürgenler', 'тюшюрди' => 'tüşürdi',
+               'тюшюрдик' => 'tüşürdik', 'тюшюре' => 'tüşüre', 'тюшюрип' => 'tüşürip', 'тюшюрмек' => 'tüşürmek',
+               'уджюм' => 'ücüm', 'удюр' => 'üdür', 'узюле' => 'üzüle', 'узюлип' => 'üzülip',
+               'узюльгенини' => 'üzülgenini', 'узюльди' => 'üzüldi', 'уйрюлип' => 'üyrülip',
+               'укюмет' => 'ükümet', 'укюмети' => 'ükümeti', 'укюметими' => 'ükümetimi',
+               'укюметимиз' => 'ükümetimiz', 'укюметини' => 'ükümetini', 'укюметининъ' => 'ükümetiniñ',
+               'укюметке' => 'ükümetke', 'укюметкеми' => 'ükümetkemi', 'укюметми' => 'ükümetmi',
+               'укюметнинъ' => 'ükümetniñ', 'укюметтен' => 'ükümetten', 'укюмран' => 'ükümran',
+               'улькюн' => 'ülkün', 'умюдим' => 'ümüdim', 'умют' => 'ümüt', 'умютлери' => 'ümütleri',
+               'умютсизден' => 'ümütsizden', 'усть' => 'üst', 'устьке' => 'üstke', 'устьлеринде' => 'üstlerinde',
+               'устьлериндеки' => 'üstlerindeki', 'устьлерине' => 'üstlerine', 'устьлерини' => 'üstlerini',
+               'устюрткъа' => 'üsturtqa', 'усьнюхаткъа' => 'üsnühatqa', 'усьнюхаты' => 'üsnühatı',
+               'усьтю' => 'üstü', 'усьтюмде' => 'üstümde', 'усьтюмдеки' => 'üstümdeki', 'усьтюме' => 'üstüme',
+               'усьтюнде' => 'üstünde', 'усьтюндеки' => 'üstündeki', 'усьтюндемиз' => 'üstündemiz',
+               'усьтюне' => 'üstüne', 'усьтюни' => 'üstüni', 'усьтюнлик' => 'üstünlik',
+               'усьтюнъизге' => 'üstüñizge', 'утёкунь' => 'ütökün', 'уфюрди' => 'üfürdi', 'учю' => 'üçü',
+               'учюмиз' => 'üçümiz', 'учюн' => 'üçün', 'учюнджи' => 'üçünci', 'учюнджисининъ' => 'üçüncisiniñ',
+               'ушюй' => 'üşüy', 'ушюмез' => 'üşümez', 'ушюмезсинъ' => 'üşümezsiñ',
+               'факультетинде' => 'fakultetinde', 'факультетине' => 'fakultetine',
+               'февральнинъ' => 'fevralniñ', 'харьковдаки' => 'harkovdaki', 'харьковдан' => 'harkovdan',
+               'чёкти' => 'çökti', 'чёкюрли' => 'çökürli', 'чёкюч' => 'çöküç', 'чёллюкке' => 'çöllükke',
+               'чёль' => 'çöl', 'чёльде' => 'çölde', 'чёльмек' => 'çölmek', 'чёткю' => 'çötkü',
+               'чёчамийлер' => 'çöçamiyler', 'чюнки' => 'çünki', 'чюрюди' => 'çürüdi', 'чюрюк' => 'çürük',
+               'шукюр' => 'şükür', 'шукюрлер' => 'şükürler', 'этюв' => 'etüv', 'этювден' => 'etüvden',
+               'этюви' => 'etüvi', 'этюдлар' => 'etüdlar', 'юзден' => 'yüzden', 'юзлеп' => 'yüzlep',
+               'юзлерини' => 'yüzlerini', 'юзлернен' => 'yüzlernen', 'юзлюги' => 'yüzlügi',
+               'юзлюкке' => 'yüzlükke', 'юзю' => 'yüzü', 'юзюм' => 'yüzüm', 'юзюме' => 'yüzüme',
+               'юзюмен' => 'yüzümen', 'юзюмни' => 'yüzümni', 'юзюнде' => 'yüzünde', 'юзюни' => 'yüzüni',
+               'юзюнинъ' => 'yüzüniñ', 'юзюнъ' => 'yüzüñ', 'юзюнъизге' => 'yüzüñizge', 'юклю' => 'yüklü',
+               'юксельтюв' => 'yükseltüv', 'юньлю' => 'yünlü', 'юньлюдже' => 'yünlüce',
+               'юртсеверлик' => 'yurtseverlik', 'юртюде' => 'yürtüde', 'юрьтю' => 'yürtü',
+               'юрьтюге' => 'yürtüge', 'юрьтюнинъ' => 'yürtüniñ', 'юрюльсе' => 'yürülse', 'юрюнъиз' => 'yürüñiz',
+               'юрюш' => 'yürüş', 'юрюши' => 'yürüşi', 'юрюшим' => 'yürüşim', 'юрюшини' => 'yürüşini',
+               'юрюшнен' => 'yürüşnen', 'юрюшни' => 'yürüşni',
+       ];
+
+       # map Cyrillic to Latin and back, whole word match only
+       # no variants: map exactly as is
+       # items with capture group refs (e.g., $1) are only mapped from the
+       # regex to the reference
+       private $exactCaseMappings = [
+               # аббревиатуры
+               # abbreviations
+               'ОБСЕ' => 'OBSE', 'КъМДж' => 'QMC', 'КъАЭ' => 'QAE', 'ГъСМК' => 'ĞSMK', 'ШСДжБ' => 'ŞSCB',
+               'КъМШСДж' => 'QMŞSC', 'КъДМПУ' => 'QDMPU', 'КъМПУ' => 'QMPU', 'КъЮШ' => 'QYŞ', 'ЮШ' => 'YŞ',
+       ];
+
+       # map Cyrillic to Latin and back, match end of word
+       # variants: all lowercase, all uppercase, first letter capitalized
+       # "first letter capitalized" variant was in the source
+       # items with capture group refs (e.g., $1) are only mapped from the
+       # regex to the reference
+       private $suffixMapping = [
+               # originally C2L
+               'иаль' => 'ial', 'нуль' => 'nul', 'кой' => 'köy', 'койнинъ' => 'köyniñ', 'койни' => 'köyni',
+               'койге' => 'köyge', 'койде' => 'köyde', 'койдеки' => 'köydeki', 'койден' => 'köyden',
+               'козь' => 'köz',
+
+               # originally L2C, here swapped
+               'етсин' => 'etsin',
+
+       ];
+
+       # map Cyrillic to Latin and back, match beginning of word
+       # variants: all lowercase, all uppercase, first letter capitalized
+       # items with capture group refs (e.g., $1) are only mapped from the
+       # regex to the reference
+       private $prefixMapping = [
+               # originally C2L
+               'буюк([^ъ])' => 'büyük$1', 'бую([гдйлмнпрстчшc])(и)' => 'büyü$1$2',
+               'буют([^ыа])' => 'büyüt$1', 'джонк([^ъ])' => 'cönk$1', 'коюм' => 'köyüm', 'коюнъ' => 'köyüñ',
+               'коюн([ди])' => 'köyün$1', 'куе' => 'küye', 'куркке' => 'kürkke', 'куркни' => 'kürkni',
+               'куркте' => 'kürkte', 'куркчи' => 'kürkçi', 'куркчю' => 'kürkçü',
+
+               # арабизмы на муи- муэ- / Arabic муи- муэ-
+               'му([иэИЭ])' => 'mü$1',
+
+               # originally L2C, here swapped
+               'итъаль' => 'ital',
+               'роль$1' => 'rol([^ü])',
+               'усть$1' => 'üst([knt])',
+
+       ];
+
+       private $Cyrl2LatnRegexes = [];
+       private $Latn2CyrlRegexes = [];
+
+       function loadRegs() {
+               // Regexes as keys need to be declared in a function.
+               $this->Cyrl2LatnRegexes = [
+                       ############################
+                       # относятся ко всему слову #
+                       # whole words              #
+                       ############################
+                       '/\b([34])(\-)юнджи\b/u' => '$1$2ünci',
+                       '/\b([34])(\-)ЮНДЖИ\b/u' => '$1$2ÜNCİ',
+
+                       # отдельно стоящие Ё и Я
+                       # stand-alone Ё and Я
+                       '/\bЯ\b/u' => 'Ya',
+                       '/\bЁ\b/u' => 'Yo',
+
+                       ############################
+                       # относятся к началу слова #
+                       # word prefixes            #
+                       ############################
+                       '/\bКъЮШн/u' => 'QYŞn',
+                       '/\bЮШн/u' => 'YŞn',
+
+                       # о => ö
+                       '/\b(['.Crh::C_M_CONS.'])о(['.Crh::C_CONS.'])(['.Crh::C_CONS.'])([еиэюьü])/u' => '$1ö$2$3$4',
+                       '/\bо(['.Crh::C_CONS.'])(['.Crh::C_CONS.'])([еиэюьü])/u' => 'ö$1$2$3',
+                       '/\b(['.Crh::C_M_CONS.'])О(['.Crh::C_CONS.'])(['.Crh::C_CONS.'])([еиэюьüЕИЭЮЬÜ])/u' =>
+                               '$1Ö$2$3$4',
+                       '/\bО(['.Crh::C_CONS.'])(['.Crh::C_CONS.'])([еиэюьüЕИЭЮЬÜ])/u' => 'Ö$1$2$3',
+
+                       '/\b(['.Crh::C_M_CONS.'])о(['.Crh::C_CONS.'])([еиэюьü])/u' => '$1ö$2$3',
+                       '/\bо(['.Crh::C_CONS.'])([еиэюьü])/u' => 'ö$1$2',
+                       '/\b(['.Crh::C_M_CONS.'])О(['.Crh::C_CONS.'])([еиэюьüЕИЭЮЬÜ])/u' => '$1Ö$2$3',
+                       '/\bО(['.Crh::C_CONS.'])([еиэюьüЕИЭЮЬÜ])/u' => 'Ö$1$2',
+
+                       # ё => yö
+                       '/\bё(['.Crh::C_CONS.'])(['.Crh::C_CONS.'])([ьеюü])/u' => 'yö$1$2$3',
+                       '/\bЁ(['.Crh::C_CONS_LC.'])(['.Crh::C_CONS_LC.'])([ьеюü])/u' => 'Yö$1$2$3',
+                       '/\bЁ(['.Crh::C_CONS_UC.'])(['.Crh::C_CONS_UC.'])([ЬЕЮÜ])/u' => 'YÖ$1$2$3',
+                       '/\bё(['.Crh::C_CONS.'])([ьеюü])/u' => 'yö$1$2',
+                       '/\bЁ(['.Crh::C_CONS_LC.'])([ьеюü])/u' => 'Yö$1$2',
+                       '/\bЁ(['.Crh::C_CONS_UC.'])([ЬЕЮÜ])/u' => 'YÖ$1$2',
+
+                       # у => ü, ую => üyü
+                       '/\b(['.Crh::C_M_CONS.'])у(['.Crh::C_CONS.'])(['.Crh::C_CONS.'])([еиэюьü])/u' => '$1ü$2$3$4',
+                       '/\bу(['.Crh::C_CONS.'])(['.Crh::C_CONS.'])([еиэюьü])/u' => 'ü$1$2$3',
+                       '/\bую(['.Crh::C_CONS.'])(['.Crh::C_CONS.'])([еиэюьü])/u' => 'üyü$1$2$3',
+                       '/\b(['.Crh::C_M_CONS.'])У(['.Crh::C_CONS.'])(['.Crh::C_CONS.'])([еиэюьüЕИЭЮЬÜ])/u' =>
+                               '$1Ü$2$3$4',
+                       '/\bУ(['.Crh::C_CONS.'])(['.Crh::C_CONS.'])([еиэюьüЕИЭЮЬÜ])/u' => 'Ü$1$2$3',
+                       '/\bУю(['.Crh::C_CONS.'])(['.Crh::C_CONS.'])([еиэюьü])/u' => 'Üyü$1$2$2',
+                       '/\bУЮ(['.Crh::C_CONS.'])(['.Crh::C_CONS.'])([еиэюьü])/u' => 'ÜYÜ$1$2$3',
+
+                       '/\b(['.Crh::C_M_CONS.'])у(['.Crh::C_CONS.'])([еиэюьü])/u' => '$1ü$2$3',
+                       '/\bу(['.Crh::C_CONS.'])([еиэюьü])/u' => 'ü$1$2',
+                       '/\bую(['.Crh::C_CONS.'])([еиэюьü])/u' => 'üyü$1$2',
+                       '/\b(['.Crh::C_M_CONS.'])У(['.Crh::C_CONS.'])([еиэюьüЕИЭЮЬÜ])/u' => '$1Ü$2$3',
+                       '/\bУ(['.Crh::C_CONS.'])([еиэюьüЕИЭЮЬÜ])/u' => 'Ü$1$2',
+                       '/\bУю(['.Crh::C_CONS.'])([еиэюьü])/u' => 'Üyü$1$2',
+                       '/\bУЮ(['.Crh::C_CONS.'])([еиэюьü])/u' => 'ÜYÜ$1$2',
+
+                       # ю => yü
+                       '/\b([аыоуеиёюАЫОУЕИЁЮ]?)ю(['.Crh::C_CONS.'])(['.Crh::C_CONS.'])([ьеюü])/u' => '$1yü$2$3$4',
+                       '/\b([АЫОУЕИЁЮ]?)Ю(['.Crh::C_CONS_LC.'])(['.Crh::C_CONS_LC.'])([ьеюü])/u' => '$1Yü$2$3$4',
+                       '/\b([АЫОУЕИЁЮ]?)Ю(['.Crh::C_CONS_UC.'])(['.Crh::C_CONS_UC.'])([ЬЕЮÜ])/u' => '$1YÜ$2$3$4',
+                       '/\b([аыоуеиёюАЫОУЕИЁЮ]?)ю(['.Crh::C_CONS.'])([ьеюü])/u' => '$1yü$2$3',
+                       '/\b([АЫОУЕИЁЮ]?)Ю(['.Crh::C_CONS_LC.'])([ьеюü])/u' => '$1Yü$2$3',
+                       '/\b([АЫОУЕИЁЮ]?)Ю(['.Crh::C_CONS_UC.'])([ЬЕЮÜ])/u' => '$1YÜ$2$3',
+
+                       # e => ye, я => ya
+                       '/\bе/u' => 'ye',
+                       '/\bЕ(['.Crh::C_LC.'cğñqöü])/u' => 'Ye$1',
+                       '/\bЕ(['.Crh::C_UC.'CĞÑQÖÜ])/u' => 'YE$1',
+                       '/\bя/u' => 'ya',
+                       '/\bЯ(['.Crh::C_LC.'cğñqöü])/u' => 'Ya$1',
+                       '/\bЯ(['.Crh::C_UC.'CĞÑQÖÜ])/u' => 'YA$1',
+                       '/([аеёиоуыэюяйьъaeöüАЕЁИОУЫЭЮЯЙЬЪAEÖÜ])е/u' => '$1ye',
+                       '/([аеёиоуыэюяйьъaeöüАЕЁИОУЫЭЮЯЙЬЪAEÖÜ])Е(['.Crh::C_LC.'cğñqöü])/u' => '$1Ye$2',
+                       '/([аеёиоуыэюяйьъaeöüАЕЁИОУЫЭЮЯЙЬЪAEÖÜ])Е(['.Crh::C_UC.'CĞÑQÖÜ])/u' => '$1YE$2',
+                       '/([аеёиоуыэюяйьъaeöüğqАЕЁИОУЫЭЮЯЙЬЪAEÖÜĞQ])я/u' => '$1ya',
+                       '/([аеёиоуыэюяйьъaeöüğqАЕЁИОУЫЭЮЯЙЬЪAEÖÜĞQ])Я(['.Crh::C_LC.'cğñqöü])/u' => '$1Ya$2',
+                       '/([аеёиоуыэюяйьъaeöüğqАЕЁИОУЫЭЮЯЙЬЪAEÖÜĞQ])Я(['.Crh::C_UC.'CĞÑQÖÜ])/u' => '$1YA$2',
+
+                       ###############################
+                       # не зависят от места в слове #
+                       # position independent        #
+                       ###############################
+
+                       # слова на -льон
+                       # words with -льон
+                       '/льон/u' => 'lyon',
+                       '/ЛЬОН/u' => 'LYON',
+
+                       '/козь([^я])/u' => 'köz$1',
+                       '/Козь([^я])/u' => 'Köz$1',
+                       '/КОЗЬ([^Я])/u' => 'KÖZ$1',
+
+                       # Ö, Ü 1-й заход: ё, ю после согласных > ö, ü
+                       # Ö, Ü 1st instance: ё, ю after consonants > ö, ü
+                       '/(['.Crh::C_CONS.'])ю/u' => '$1ü',
+                       '/(['.Crh::C_CONS.'])Ю/u' => '$1Ü',
+                       '/(['.Crh::C_CONS.'])ё/u' => '$1ö',
+                       '/(['.Crh::C_CONS.'])Ё/u' => '$1Ö',
+
+                       # остальные вхождения о, у, ё, ю
+                       # other occurences of о, у, ё, ю
+                       '/Ё(['.Crh::C_UC.'CĞÑQÖÜ])/u' => 'YO$2',
+                       '/Ю(['.Crh::C_UC.'CĞÑQÖÜ])/u' => 'YU$2',
+
+                       # Ц & Щ
+                       '/Ц(['.Crh::C_UC.'CĞÑQÖÜ])/u' => 'TS$2',
+                       '/Щ(['.Crh::C_UC.'CĞÑQÖÜ])/u' => 'ŞÇ$2',
+               ];
+
+               $this->Latn2CyrlRegexes = [
+                       # буква Ё - первый заход
+                       # расставляем Ь после согласных
+                       '/^([yY])ö(['.Crh::L_N_CONS.'])([aAuU'.Crh::L_CONS.']|$)/u' => '$1ö$2ь$3',
+                       '/^([yY])Ö(['.Crh::L_N_CONS.'])([aAuU'.Crh::L_CONS.']|$)/u' => '$1Ö$2Ь$3',
+                       '/^AQŞ(['.Crh::WORD_ENDS.'ngd])/u' => 'АКъШ$1',
+
+                       # буква Ю - первый заход
+                       # расставляем Ь после согласных
+                       '/^([yY])ü(['.Crh::L_N_CONS.'])([aAuU'.Crh::L_CONS.']|$)/u' => '$1ü$2ь$3',
+                       '/^([yY])Ü(['.Crh::L_N_CONS.'])([aAuU'.Crh::L_CONS.']|$)/u' => '$1Ü$2Ь$3',
+
+                       '/^([bcgkpşBCGKPŞ])ö(['.Crh::L_N_CONS.'])(['.Crh::L_CONS.']|$)/u' => '$1ö$2ь$3',
+                       '/^([bcgkpşBCGKPŞ])Ö(['.Crh::L_N_CONS.'])(['.Crh::L_CONS.']|$)/u' => '$1Ö$2Ь$3',
+                       '/^([bcgkpşBCGKPŞ])Ö(['.Crh::L_N_CONS.'])(['.Crh::L_CONS.']|$)/u' => '$1Ö$2Ь$3',
+                       '/^([bcgkpşBCGKPŞ])ü(['.Crh::L_N_CONS.'])(['.Crh::L_CONS.']|$)/u' => '$1ü$2ь$3',
+                       '/^([bcgkpşBCGKPŞ])Ü(['.Crh::L_N_CONS.'])(['.Crh::L_CONS.']|$)/u' => '$1Ü$2Ь$3',
+                       '/^([bcgkpşBCGKPŞ])Ü(['.Crh::L_N_CONS.'])(['.Crh::L_CONS.']|$)/u' => '$1Ü$2Ь$3',
+
+                        # ö и ü в начале слова
+                        # случаи, когда нужен Ь
+                       '/^ö(['.Crh::L_N_CONS.'pP])(['.Crh::L_CONS.']|$)/u' => 'ö$1ь$2',
+                       '/^Ö(['.Crh::L_N_CONS_LC.'p])(['.Crh::L_CONS.']|$)/u' => 'Ö$1ь$2',
+                       '/^Ö(['.Crh::L_N_CONS_UC.'P])(['.Crh::L_CONS.']|$)/u' => 'Ö$1Ь$2',
+                       '/^ü(['.Crh::L_N_CONS.'])(['.Crh::L_CONS.']|$)/u' => 'ü$1ь$2',
+                       '/^Ü(['.Crh::L_N_CONS_LC.'])(['.Crh::L_CONS.']|$)/u' => 'Ü$1ь$2',
+                       '/^Ü(['.Crh::L_N_CONS_UC.'])(['.Crh::L_CONS.']|$)/u' => 'Ü$1Ь$2',
+
+                       '/ts$/u' => 'ц',
+                       '/şç$/u' => 'щ',
+                       '/Ş[çÇ]$/u' => 'Щ',
+                       '/T[sS]$/u' => 'Ц',
+
+                       # Ь после Л
+                       # add Ь after Л
+                       '/(['.Crh::L_F.'])l(['.Crh::L_CONS_LC.']|$)/u' => '$1ль$2',
+                       '/(['.Crh::L_F_UC.'])L(['.Crh::L_CONS.']|$)/u' => '$1ЛЬ$2',
+
+                       # относятся к началу слова
+                       '/^ts/u' => 'ц',
+                       '/^T[sS]/u' => 'Ц',
+
+                       '/^şç/u' => 'щ',
+                       '/^Ş[çÇ]/u' => 'Щ',
+
+                       # Э
+                       '/(^|['.Crh::L_VOW.'аеэяАЕЭЯ])e/u' => '$1э',
+                       '/(^|['.Crh::L_VOW_UC.'АЕЭЯ])E/u' => '$1Э',
+
+                       '/^(['.Crh::L_M_CONS.'])ö/u' => '$1о',
+                       '/^(['.Crh::L_M_CONS.'])Ö/u' => '$1О',
+                       '/^(['.Crh::L_M_CONS.'])ü/u' => '$1у',
+                       '/^(['.Crh::L_M_CONS.'])Ü/u' => '$1У',
+
+                       '/^ö/u' => 'о',
+                       '/^Ö/u' => 'О',
+                       '/^ü/u' => 'у',
+                       '/^Ü/u' => 'У',
+
+                       # некоторые исключения
+                       # some exceptions
+                       '/maal([^e])/u' => 'мааль$1',
+                       '/Maal([^e])/u' => 'Мааль$1',
+                       '/MAAL([^E])/u' => 'МААЛЬ$1',
+                       '/küf([^eü])/u' => 'куфь$1',
+                       '/Küf([^eü])/u' => 'Куфь$1',
+                       '/KÜF([^EÜ])/u' => 'КУФЬ$1',
+                       '/köz([^eü])/u' => 'козь$1',
+                       '/Köz([^eü])/u' => 'Козь$1',
+                       '/KÖZ([^EÜ])/u' => 'КОЗЬ$1',
+
+                       # Punctuation
+                       '/#|No\./' => '№',
+
+                       # некоторые случаи употребления Ц
+                       '/tsi([^zñ])/u' => 'ци$1',
+                       '/T[sS][iİ]([^zZñÑ])/u' => 'ЦИ$1',
+                       '/ts([ou])/u' => 'ц$1',
+                       '/T[sS]([oOuU])/u' => 'Ц$1',
+                       '/ts(['.Crh::L_CONS.'])/u' => 'ц$1',
+                       '/T[sS](['.Crh::L_CONS.'])/u' => 'Ц$1',
+                       '/(['.Crh::L_CONS.'])ts/u' => '$1ц',
+                       '/(['.Crh::L_CONS.'])T[sS]/u' => '$1Ц',
+                       '/tsиал/u' => 'циал',
+                       '/TSИАЛ/u' => 'ЦИАЛ',
+
+                       # убираем ьi
+                       # remove ьi (note Cyrillic ь and Latin i)
+                       '/[ьЬ]([iİ])/u' => '$1',
+
+                       # ya & ye
+                       '/(['.Crh::L_CONS.'])ya/u' => '$1ья',
+                       '/(['.Crh::L_CONS.'])Y[aA]/u' => '$1ЬЯ',
+                       '/(['.Crh::L_CONS.'])ye/u' => '$1ье',
+                       '/(['.Crh::L_CONS.'])Y[eE]/u' => '$1ЬЕ',
+
+                        # расставляем Ь перед Ё
+                        # place Ь in front of Ё
+                       '/(['.Crh::L_CONS.'])y[oö]/u' => '$1ьё',
+                       '/(['.Crh::L_CONS.'])Y[oOöÖ]/u' => '$1ЬЁ',
+                        # оставшиеся вхождения yo и yö
+                        # remaining occurrences of yo and yö
+                       '/y[oö]/u' => 'ё',
+                       '/[yY][oOöÖ]/u' => 'Ё',
+
+                        # расставляем Ь перед Ю
+                        # place Ь in front of Ю
+                       '/(['.Crh::L_CONS.'])y[uü]/u' => '$1ью',
+                       '/(['.Crh::L_CONS.'])Y[uUüÜ]/u' => '$1ЬЮ',
+                        # оставшиеся вхождения yu и yü
+                        # remaining occurrences of yu and yü
+                       '/y[uü]/u' => 'ю',
+                       '/[yY][uUüÜ]/u' => 'Ю',
+
+                       # убираем ьa
+                       # remove ьa (note Cyrillic ь and Latin a)
+                       '/[ьЬ]([aA])/u' => '$1',
+
+                       # дж
+                       '/C(['.Crh::L_UC.Crh::C_UC.'Ъ])/u' => 'ДЖ$1',
+
+                       # гъ, къ, нъ
+                       # гъ, къ, нъ
+                       '/Ğ(['.Crh::L_UC.Crh::C_UC.'Ъ])/u' => 'ГЪ$1',
+                       '/Q(['.Crh::L_UC.Crh::C_UC.'Ъ])/u' => 'КЪ$1',
+                       '/Ñ(['.Crh::L_UC.Crh::C_UC.'Ъ])/u' => 'НЪ$1',
+
+               ];
+       }
+
+       private $CyrlCleanUpRegexes = [
+               '/([клнрст])ь\1/u' => '$1$1',
+               '/([КЛНРСТ])Ь\1/u' => '$1$1',
+               '/К[ьЬ]к/u' => 'Кк',
+               '/Л[ьЬ]л/u' => 'Лл',
+               '/Н[ьЬ]н/u' => 'Нн',
+               '/Р[ьЬ]р/u' => 'Рр',
+               '/С[ьЬ]с/u' => 'Сс',
+               '/Т[ьЬ]т/u' => 'Тт',
+
+               # убираем ьы и ь..ы
+               # remove ьы и ь..ы
+               '/[ьЬ]ы/u' => 'ы',
+               '/ЬЫ/u' => 'Ы',
+               '/[ьЬ]([гдклмнпрстчшГДКЛМНПРСТЧШ])ы/u' => '$1ы',
+               '/Ь([гдклмнпрстчшГДКЛМНПРСТЧШ])Ы/u' => '$1Ы',
+               '/[ьЬ]([гкнГКН])([ъЪ])ы/u' => '$1$2ы',
+               '/Ь([ГКН])ЪЫ/u' => '$1ЪЫ',
+
+               # убираем йь
+               # remove йь
+               '/йь/u' => 'й',
+               '/ЙЬ/u' => 'Й',
+
+               # частичное решение проблемы слова юз - 100
+               # Partial solution of the problem of the word юз ("100")
+               # notice that these are cross-word patterns
+               '/эки юзь/u' => 'эки юз', '/Эки юзь/u' => 'Эки юз', '/ЭКИ ЮЗЬ/u' => 'ЭКИ ЮЗ',
+               '/учь юзь/u' => 'учь юз', '/Учь юзь/u' => 'Учь юз', '/УЧЬ ЮЗЬ/u' => 'УЧЬ ЮЗ',
+               '/дёрт юзь/u' => 'дёрт юз', '/Дёрт юзь/u' => 'Дёрт юз', '/ДЁРТ ЮЗЬ/u' => 'ДЁРТ ЮЗ',
+               '/беш юзь/u' => 'беш юз', '/Беш юзь/u' => 'Беш юз', '/БЕШ ЮЗЬ/u' => 'БЕШ ЮЗ',
+               '/алты юзь/u' => 'алты юз', '/Алты юзь/u' => 'Алты юз', '/АЛТЫ ЮЗЬ/u' => 'АЛТЫ ЮЗ',
+               '/еди юзь/u' => 'еди юз', '/Еди юзь/u' => 'Еди юз', '/ЕДИ ЮЗЬ/u' => 'ЕДИ ЮЗ',
+               '/секиз юзь/u' => 'секиз юз', '/Секиз юзь/u' => 'Секиз юз', '/СЕКИЗ ЮЗЬ/u' => 'СЕКИЗ ЮЗ',
+               '/докъуз юзь/u' => 'докъуз юз', '/Докъуз юзь/u' => 'Докъуз юз', '/ДОКЪУЗ ЮЗЬ/u' => 'ДОКЪУЗ ЮЗ',
+       ];
+}
index cb59739..c3a50ea 100644 (file)
        "nstab-help": "buhci tu kamu a kasabelih",
        "nstab-category": "kakuniza",
        "mainpage-nstab": "saayaway a belih",
+       "nosuchactiontext": "URL matuzu’ay a saungay la’cus.\nURL nu misu hakay misulit mungangaw, saca sapecec la’cusay a siket.\nuyniyan hakay ku {{SITENAME}} pisaungay a sisetyimo simunday.",
        "nosuchspecialpage": "inayi’ tina sazumaay a kasabelih",
        "nospecialpagetext": "<strong> milunguc nu misu a sazumaay a kasabelih la’cus.</strong>\n\nmaydih miala kapahay a sazumaay a kasabelih  piazihan-tu-sulit kapah tayza [[Special:SpecialPages|{{int:specialpages}}]].",
        "error": "mungangaw",
        "databaseerror-query": "palalitemuh tu kawaw: $1",
        "databaseerror-function": "sakapaluwaluway: $1",
        "databaseerror-error": "mungangaw: $1",
+       "transaction-duration-limit-exceeded": "saka miliyas kayadah sapahalhal mautang, zayhan suliten a tuki ($1) mangasiw $2 beit a kelec, tina saungay mapateziptu. \namahica kisu misumad yadah kasacacay sacacay, kapah mitanam mawawada’ mikawaw.",
        "laggedslavemode": "<strong> patalaw:</strong> kasabelih hakay caay yamalyilu capi demiad misabaluh.",
        "readonly": "pamutektu sulu nu nasulitan",
        "enterlockreason": "pisuliten pamutek a mahicaay, yamalyilu sausi miliyaw mawawah a tuki",
+       "readonlytext": "nasulitan-sulu pamutek ayza, la’cus cunusen saca misumad tu kalunasulitan, imahini hakay miteka laliday a nasulitan-sulu masanga’ay a kawaw, mapahezektusa kapahtu masasa malecek. \n\npamutek nasulitan-sulu a sisetyimo mikuwanay buhci tu kamu: $1",
+       "missing-article": "nasulitan-sulu inayi’ talaayaway mazateng kasabelih sulit, kalungangan ku \"$1\" $2.\n\nsawsawni zayhan kisu masasiket ta masipuay a sasizuma saca nazipa’an kasabelih.\n\namahica kisu malitumuhay caay kuniay a pulita, hakay kisu maazih zwanti munday.\npisulitan URL puenengan, zumasatu pasayza [[Special:ListUsers/sysop|mikuwanay ]] mitubeli tina munday.",
        "missingarticle-rev": "(masumad#: $1)",
        "readonly_lag": "nasulitan-sulu lunuk miazih, imahini mihalhal mikilulay nasulitan-sulu malecad naamahiza misulit kalunasulitan katukuh sausi nasulitan-sulu",
+       "nonwrite-api-promise-error": "caay suliten API saungay ’Promise-Non-Write-API-Action’ HTTP satangahan mapatayzaan yuzasa kya lunguc patyaza API masulit i bacu-saupu.",
        "internalerror": "ilabuay a mungangaw",
        "internalerror_info": "ilabuay a mungangaw: $1",
        "internalerror-fatal-exception": "pihaceng a bahal nikalahizaan \"$1\"",
        "title-invalid-interwiki": "milungucay a kasabelih satangahan yamalyilu la’cus pisaungay i satangahan a milakuid Wiki masasiket.",
        "title-invalid-talk-namespace": "milungucay a kasabelih satangahan nimicaliw hakay inayi’ay a sasukamu belih",
        "title-invalid-characters": "milungucay a kasabelih satangahan yamalyilu la’cusay a tatebanan-nisulitan: \"$1\".",
+       "title-invalid-relative": "ilabu’ nu pyawti izaw ku malecaday pazazan. malecaday pazazanay kasabelih satangahan (./, ../) uzuma la’cus, misaungayay saazih la’cus misuped-miala malecad pazazan.",
+       "title-invalid-magic-tilde": "milunguc tu kasabelih satangahan izaw ku la’cusay a kaliwaza masalaing bacu  (<nowiki>~~~</nowiki>)",
        "title-invalid-too-long": "namilungucay a kasabelih satangahan mangasiw, satangahan pisaungay UTF-8 sakababalic a bang gu amana mangasiw $1 {{PLURAL:$1|wyiyincu}}.",
        "title-invalid-leading-colon": "milungucay a kasabelih  satangahan yamalyilu la’cusay a mahaw-bacu i lalingatuan.",
+       "perfcached": "isasa’ay sa ku kabilil-miala kalunasulitan, hakay caay sabaluhay. kabilil-miala kalunasulitan sayadah taneng misuped {{PLURAL:$1|1 ku heci|$1 a heci}}.\n\n(kabilil-miala kalunasulitan: tinnaw saca zikecan dinwa, saka dayum cengse atu tangan misaungay, ilabu’ nu pazateng-sakaluk cacay nanunuz izaway madayumay a kalunasulitan.)",
        "perfcachedts": "isasa’ay u saduba’ kalunasulitan, sazikuz misabaluh tuki sa u $1. saduba’ kalunasulitan sayadah sa kapah misuped  {{PLURAL:$4|1 ku heci|$4 ku heci}}.",
        "viewsource": "ciwsace yuensma-kodo",
        "viewsource-title": "ciwsace $1 a sakatizeng banggu",
        "actionthrottled": "makelec saungay tuway",
+       "actionthrottledtext": "kisu i apuyu’ay lawad mileku misaungay kinapinapina, matabesiw ku sisetyimo a tatukian, matalaw a patahtah sa, a kelecen ku nu misu pisaungay.\npihalhalen pinapina ku lawad mitanam aca.",
        "protectedpagetext": "tina kasabelih masetin midiput  mitena’ mikawaway-kalumyiti saca zuma saungay tuway.",
        "viewsourcetext": "kapah kisu miciwsace atu mikopi tina kasabelih a sakatizeng banggu.",
        "viewyourtext": "kapah kisu miciwsace atu kopi ilabu’ tina kasabelih <strong> kisu mikawaway-kalumyiti </strong> yuensma-kodo.",
+       "protectedinterface": "tina kasabelih sapinipabeli tina Wiki zwanti taypuolayta a sulit,zumasatu mapasetin tu ku midiputay amitena’ talahaday a sumad.\nanu maydih micunus saca misumad Wiki a belih,tayza [https://translatewiki.net/ translatewiki.net] a MediaWiki sanuniyazu’en cwanan.",
+       "translateinterface": "anu maydih misumad Wiki a mibelih,katayza [https://translatewiki.net/ translatewiki.net] a MediaWiki sanuniyazu’en cwanan",
+       "cascadeprotected": "tina kasabelih madiput tuway, la’cus mikawaway-kalumyiti, izay tina kasabelih mawawah nu isasa’ay a \"patatusulay a diput\" mapili’ay a {{PLURAL:$1|cacay belih|yadah belih}} midiput  kasabelih nicaliwan:\n$2",
        "namespaceprotected": "inayi’ ku tungus kisu mikawaway-kalumyiti <strong>$1</strong> pangangananay a salaedan a kasabelih.",
        "customcssprotected": "inayi’ ku tungus kisu mikawaway-kalumyiti tina CSS kasabelih, zayhan tina kasabelih yamalyilu ku zuma misaungayay teked a setin.",
+       "customjsprotected": "kisu sa inayi’ ku tungus mikawaway-kalumyiti tina JavaScript kasabelih, zayhan tina kasabelih yamalyilu tu zuma misaungayay nu teked a setin.",
        "mycustomcssprotected": "inayi’ tungus mikawaway-kalumyiti tina CSS kasabelih.",
        "mycustomjsprotected": "inayi’ tungus kisu mikawaway-kalumyiti tina JavaScript kasabelih.",
        "myprivateinfoprotected": "inayi’ tungus kisu mikawaway-kalumyiti cesyun nu misu.",
        "mypreferencesprotected": "inayi’ tungus kisu mikawaway-kalumyiti setin tu kanamuhan nu misu.",
        "ns-specialprotected": "sazumaay a kasabelih la’cusay  mikawaway-kalumyiti.",
+       "titleprotected": "tina pyawti mapa [[User:$1|$1]] tu midiput a mitena’ patizeng, mahicaay u <em>$2</em>.",
+       "filereadonlyerror": "la’cus misumad tangan \"$1\" zayhan tangan sulu \"$2\" ayza itiza i asip-dada’ muse.\n\npamutek a sisetyimo mikuwanay buhci tu kamu:\"$3\".",
        "invalidtitle-knownnamespace": "pangangananay a salaedan \"$2\" atu kalungangan \"$3\" u la’cusay a satangahan",
        "invalidtitle-unknownnamespace": "caay kapulita pangangananay a salaedan bacu $1 atu kalungangan \"$2\" ku la’cusay a satangahan",
        "exception-nologin": "caay henay patalabu",
        "logouttext": "<strong>matahkal tu kisu ayza. </strong>\n\npiazihen, izaw ku kasabelih matuling paazih patalabu a setyitase, katukuh kisu palawpes saazihay-sakaluk kabilil-miala.",
        "cannotlogoutnow-text": "anu pisaungay $1 la’cus katahkal.",
        "welcomeuser": "manamuh tu tayniay, $1!",
+       "welcomecreation-msg": "canghaw nu misu kapah mapatizeng tuway.\ntaneng tayza [[Special:Preferences|setin tu kanamuhan]] misabaluh numisuay i {{SITENAME}} a tekeday a asetin.",
        "yourname": "misaungayay a kalungangan:",
        "userlogin-yourname": "misaungayay a kalungangan",
        "userlogin-yourname-ph": "pisulitan tu nu misay a misaungayay a kalungangan",
        "createaccount": "panganganen ku canghaw",
        "userlogin-resetpassword-link": "maliyuh ku mima kisu haw?",
        "userlogin-helplink2": "patalabu miedap",
+       "userlogin-loggedin": "mapatalabu tu kisu ayza {{GENDER:$1|$1}} misaungayay,\nkapisaungay tu cudad isasa’ aazihan cudad mibalic patalabu tu zumaay misaungayay.",
        "userlogin-reauth": "kanca kisu patalabu aca amisawantanen kisu ku {{GENDER:$1|$1}}.",
        "userlogin-createanother": "patizeng tu zumaay canghaw",
        "createacct-emailrequired": "imyiyo(email) puenengan",
        "userexists": "nasulitan nu misu a misaungayay a kalungangan izaw tu, pipili’ zuma a kalungangan",
        "loginerror": "patalabu mungangaw",
        "createacct-error": "canghaw patizeng mungangaw",
+       "nocookiesnew": "misaungayay a canghaw mapatizeng malahci, uyzasa caay henay kisu patalabu.\napatalabu {{SITENAME}} misaungayay maydih pisaungay Cookies, \nCookies nu misu caay henay miwawah.\nnazikuz tu mawawah pisaungay baluhay misaungayay a kalungangan atu mima nu misu patalabu.",
+       "createacct-loginerror": "malahci tu ku pangangan tu canghaw, uyzasa la’cus lunuk sa patalabu.\nkapipalalid [[Special:UserLogin|kaulima ku misaungyay patalabu]].",
        "noname": "namasuilit numisuay a misaungayay a kalungangan la’cus.",
        "loginsuccesstitle": "patalabutu",
+       "loginsuccess": "<strong>{{GENDER:|kisu}} mala tu \"$1\" a kika patalabu {{SITENAME}}.</strong>",
+       "nosuchuser": "caay katepa’ kalungangan ku \"$1\" a misaungayay.\nmisaungayay a kalungangan izaw ku tabakiay atu adidi’ay a sulit,\npikinsa kisu pasasuala tu sulit tatenga’ay tu haw? saca [[Special:CreateAccount| patizeng baluhay a canghaw]].",
        "nouserspecified": "kanca matuzu’ay cacay misaungayay a kalungangan kisu.",
        "login-userblocked": "tina misaungayay mapalangat tuway, caay mahasa patalabu tuway.",
        "wrongpassword": "nasulitan nu misu a mima mungangaw, pitaneng aca.",
        "password-login-forbidden": "tina misaungayay a kalungangan  atu mima masatezep pisaungay tuway.",
        "mailmypassword": "miliyaw miteka setin mima",
        "passwordremindertitle": "{{SITENAME}} a baluhay nanunuz mima",
+       "passwordremindertext": "caay kapulita a tademaw (hakay kisu I, namakay IP puenengan $1) milunguc miliyaw patizeng i {{SITENAME}} ($4) a mima.\npabeli’ misaungayay \"$2\" a nanunuz mima setin tu \"$3\".\namahica tina ku kisu misaungayay, maydih kisu tansul patalabu atu pisetin cacay baluhay mima,\nnanunuz mima nu misu a ilabu’ {{PLURAL:$5|cacay|$5}} demiad mangasiw.\n\namahica caay kisu milunguc miliyaw patizeng mima, saca mazateng tu kisu ku mima, caay henay amisumad,\nkapah kisu sekipo tina palatuh zumasatu palalid pisaungay mahizaay saayaway mima nu misu.",
        "noemail": "misaungayay \"$1\" inayi’ imyiyo(email) puenengan nasulitan.",
        "noemailcreate": "maydih kisu nipabeli cacay kapahay a imyiyo(email) puenengan.",
        "passwordsent": "misaungayay \"$1\" a baluhay mima mapatahkal tu i saayaway a imyiyo(email) puenengan, kapihalhal henay maala tu tigami miliyaw patalabu aca",
+       "blocked-mailpassword": "numisu a IP puenengan malangat tu caay kahasa mikawaway-kalumyiti, satezep tu namakay tini IP puenengan a mima panukasan sasahicaan a mitena’ patahtah.",
+       "eauthentsent": "patigami tuway ku malucekay a tigami ta kisu misetinay a imyiyo(email) puenengan.\nanu caayhen milayap zuma a imyiyo(email), kanca kisu mikilul tigami a micuzu’ay tu kawaw, pilucek tina canghaw tatenga’ u numisuay.",
        "mailerror": "pabahel imyiyo(email) mungangaw: $1",
+       "acct_creation_throttle_hit": "pisaungay IP puenengan nu misu ayza ta wiki a labang i capi demiad $2 patizeng tuway {{PLURAL:$1|1 canghaw|$1 canghaw}}, mangasiw tu kakatukuhan mahasa a pabaw-sahezek.\nsisa, ayza caay palilid pisaungay tina IP puenengan a labang patizeng amahicahica canghaw.",
        "emailauthenticated": "imyiyo(email) puenengan nu misu malucek tu i $2 $3.",
        "emailnotauthenticated": "imyiyo(email) puenengan mu misu caay henay malucek, cayhenay patigami kisu isasa’ay a sasahicaan a imyiyo(email).",
        "noemailprefs": "i numisuay a setin tu kanamuhan misetin imyiyo(email) puenengan, kya kapah kisu pisaungay uyniyay sasahicaan.",
        "cannotchangeemail": "tina Wiki satezep misumad canghaw a imyiyo(email) puenengan.",
        "emaildisabled": "tina calay-kakacawan(wangcan) caay kakapah pabahel imyiyo(email).",
        "accountcreated": "panganganen tu ku canghaw",
+       "accountcreatedtext": "misaungayay canghaw [[{{ns:User}}:$1|$1]] ([[{{ns:User talk}}:$1|sasukamu]]) matizeng tuway.",
        "createaccount-title": "{{SITENAME}} a canghaw patizeng",
+       "createaccount-text": "caay kapulita a tademaw pisaungay imyiyo(email) puenengan nu misu i {{SITENAME}} ($4) patizeng tu cacay canghaw kalungangan saka \"$2\", mima \"$3\".\nkanca kisu tansul patalabu atu misumad mima.\n\namahica kya canghaw sa ku patizeng mungangaw a kamu, kapah kisu sekipo tina palatuh.",
        "login-throttled": "capi demiad mitanam mangasiw patalabu.\npihalhal henay $1 pitaneng aca.",
        "login-abort-generic": "patalabu kisu mungangaw - satezep tuway",
        "login-migrated-generic": "u canghaw nu misu maliyas tu, zumasatu tina Wiki nu misu misaungayay a kalungangan inayi’ tu.",
        "changepassword-success": "mima nu misu masumad tu!",
        "changepassword-throttled": "capi demiad mitanam mangasiw patalabu.\npihalhal henay $1 pitaneng aca.",
        "botpasswords": "kikay-tademaw a mima",
+       "botpasswords-summary": "<em>kikay-tademaw mima</em> kapah i caay kamaydih ku canghaw a sausi patalabu mima zasatu, mahasa micaliaw API misuped-miala misaungayay canghaw. taneng mikelec pisaungay kikay-tademaw mima patalabuay a misaungayay tungus. \n\namahica i caay katineng mahica iayaw nu amisetin kikay-tademaw mima caay kanca pisaungay tina sasahicaan. zumasatau caay kanca izaw amahicahica tademaw amiala kikay-tademaw mima.",
        "botpasswords-disabled": "kikay-tademaw a mima mapasatezep tuway.",
        "botpasswords-no-central-id": "amisaungay kikay-tademaw a mima, kanca kisu patalabu canghaw sanat kuwan sisetyimo.",
        "botpasswords-existing": "naizawtu ku kikay-tademaw a mima",
        "botpasswords-updated-body": "pabeli misaungayay \"$2\" a kikay-tademaw \"$1\" a kikay-tademaw mima masabaluh tuway.",
        "botpasswords-deleted-title": "masipu tu kikay-tademaw mima",
        "botpasswords-deleted-body": "pabeli misaungayay \"$2\" a kikay-tademaw \"$1\" a kikay-tademaw mima masipu tuway.",
+       "botpasswords-newpassword": "sapipatalabu <strong>$1</strong> a baluhay mima ku <strong>$2</strong>. <em>pisulitan tina mima sapabeli anucilacila miazih tu tatenga’ay pisaungay.</em> <br> (sasutili’ay maluman kikay-tademaw a patalabu kalungangan kanca malecad tu nu sazikuzay misaungayay a kalungangan, kapah tu kisu pisaungay  <strong>$3</strong> mala misaungayay a kalungangan <strong>$4</strong> mala mima.)",
        "botpasswords-no-provider": "BotPasswordsSessionProvider la’cus pisaungay.",
        "botpasswords-restriction-failed": "kikay-tademaw a mima kelec makai tu ayzaay a patalabu.",
        "botpasswords-invalid-name": "matuzu’ay a misaungayay a kalungangan caay yamalyilu kikay-tademaw mima maliyasay a tatebanan nu nisulitan  (\"$1\")",
        "resetpass-temp-password": "nanunuz a mima:",
        "resetpass-abort-generic": "sacunus satezip tu misumad  mima a saungay.",
        "resetpass-expired": "mima nu misu mangasiw tu. pisetin baluhayay a mima amipatalabu.",
+       "resetpass-expired-soft": "mima nu misu mangasiw tuway, maydih miliyaw misetin. pisetin baluhay mima nu miu ayza, saca sapecec \"{{int:authprovider-resetpass-skip-label}}\" sawsawni miliyaw patizeng aca.",
+       "resetpass-validity-soft": "mima nu misu la’cus tu:$1 \n\nayza sa pisetin baluhay mima nu misu,saca sapecec \"{{int:authprovider-resetpass-skip-label}}\" sawsawni miliyaw patizeng aca.",
        "passwordreset": "miliyaw miteka setin mima",
        "passwordreset-text-one": "pahezek tina aazihan cudad, micaliw tu imyiyo(email) pabahel nanuzu a mima sapaliyaw misetin mima nu misu.",
        "passwordreset-text-many": "{{PLURAL:$1|pihezek tina aazihan cudad amicaliaw imyiyo(email) milayap nanunuz a mima.}}",
        "passwordreset-email": "imyiyo(email) puenengan:",
        "passwordreset-emailtitle": "i {{SITENAME}} a canghaw pulita kalunasulitan",
        "passwordreset-emailtext-ip": "caay kapulita a tademaw (hakay kisu i, namakay IP puenengan $1) milunguc miliyaw patizeng i {{SITENAME}} ($4) a mima, isasa’ay sa ku mahizaay tina imyiyo(email) puenengan a misaungayay {{PLURAL:$3|canghaw}}:\n\n$2\n\n{{PLURAL:$3|tina nanunuzay a mima|uyniyay nanunuzay a mima}} nay ilabu’ nu {{PLURAL:$5|cacay a demiad|$5 a demiad}} kakatukuhan, kanca kisu tansul patalabu atu misaimed baluhayay a mima.\n\namahica caay kisu milunguc miliyaw patizeng mima, saca mazateng tu kisu ku mima, caay henay amisumad, kapah kisu sekipo tina palatuh zumasatu palalid pisaungay mahizaay saayaway mima nu misu.",
+       "passwordreset-emailtext-user": "misaungayay $1 milunguc miliyaw patizeng i {{SITENAME}} ($4) a mima, isasa’ay ku misaungay mahizaay tina imyiyo(email) puenengan {{PLURAL:$3|canghaw}}:\n\n$2\n\n{{PLURAL:$3|tina nanunuz mima|uyniyay nanunuz mima}} a ilabu’ nu {{PLURAL:$5|cacay demiad|$5 demiad}} ngaliw, kanca kisu tansul patalabu atu misumad baluhay mima. amahica caay sa ku kisu milunguc miliyaw patizeng mima, saca mazateng tu kisu kya mima, atu caay amisumad, kapah kisu sekipo tina palatuh zumasatu palalid  pisaungay mahizaay saayaway a mima nu misu.",
        "passwordreset-emailelement": "misaungayay a kalungangan:\n$1\n\nnanunuzay a mima:\n$2",
        "passwordreset-emailsentusername": "amahica tina tatenga’ ku misaungayay tu kalungangan masulitay imyiyo(email) puenengan nu misu, apatahkal miliyaw patizeng tu mima a tigami kisu.",
        "passwordreset-nocaller": "manakanca nipabeli caller",
        "changeemail-no-info": "kanca kisu patalabu kyu taneng kakelulan kisu misuped-miala tina kasabelih.",
        "changeemail-oldemail": "ayzaay a imyiyo(email) puengengan:",
        "changeemail-newemail": "baluhay imyiyo(email) puenengan:",
+       "changeemail-newemail-help": "amahica kisu maydihh misipu imyiyo(email) puenengan nu misu, tina langat-pisinga’an kanca painayi’. amahica  misipu imyiyo(email) puenengan nu misu la’cus miliyaw patizeng mapawanay a mima, zumasatu caay kalayap namakay tina wiki a imyiyo(email).",
        "changeemail-none": "(nayi’)",
        "changeemail-password": "i {{SITENAME}} a mima nu misu:",
        "changeemail-submit": "misumad imyiyo(email)",
        "changeemail-nochange": "pisulitan caay kalecaday baluhay imyiyo(email) puenengan.",
        "resettokens": "miliyaw patizeng sabuhat a mima",
+       "resettokens-text": "kapah tu kisu itini miliyaw patizeng sapisuped-miala canghaw nu misu mahizaay midimut kasikazan kalunasulitan a mima-sabuhat.\n\namahica kisu malimuh pasimel mima-sabuhat tu tawan saca canghaw nu misu madebung mapeci’ tu tawan, kanca miliyaw patizeng tina mima-sabuhat.",
        "resettokens-no-tokens": "inayi’ ku miliyaw tu setinay a mima-sabuhat.",
        "resettokens-tokens": "sabuhat:",
        "resettokens-token-label": "$1 (ayza sa ku $2)",
+       "resettokens-watchlist-token": "sapiaca tu aazihan [[Special:Watchlist|miazihay a piazihan tu sulit]] Atom/RSS mima-sabuhat",
        "resettokens-done": "maliyaw tu patizeng mima-sabuhat",
        "resettokens-resetbutton": "miliyaw patizeng mapili’ay a mima-sabuhat",
        "bold_sample": "kibetulay a sulit",
        "anoneditwarning": "<strong>patalaw:</strong>caay henay kisu patalabu. anu miteka mikawaway tu kalumyiti, IP adolyise nu misu ama mitilak. anu kisu <strong>[$1  patalabu ]</strong> acasa <strong>[$2 panganganen ku canghaw ]</strong>, misuay mikawaway tu kalumyiti payni tu nu misuay misaungayay kalungangan sacuzu’ ,izaway zuma kapahayay.",
        "anonpreviewwarning": "<em>caay henay patalabu kisu. misuped kasabelih a mahiya ku nu misu i IP puenengan sulit tu nakawawan tina kasabelih ilabu’ nu mikawaway-kalumyiti nazipa’an. </em>",
        "missingsummary": "<strong> pacekil:</strong>caay henay kisu suliten  mikawaway-kalumyiti pecu’ nu lacul.\namahica kisu amisapecec aca \"$1\" kinacacay, atakud han pecu’ nu lacul kakelul misuped mikawaway-kalumyiti nu misu.",
+       "selfredirect": "<strong>patalaw:</strong> imahini kisu patizeng  misasiket ta miliyaw patatuzu’ nu misu.\nhakay kisu matuzu’ay patelac maydih miliyaw patatuzu’ a papatuzu'an kasabelih saca mikawaway-kalumyiti patelac kasabelih.\namahica kisu sapecec aca \"$1\" sakacacay, apalalid patizeng miliyaw patatuzu’.",
        "missingcommenttext": "pisuliten buhci tu kamu isasa’.",
+       "missingcommentheader": "<strong>pacekil:</strong> caay henay kisu suliten tina buhci tu kamu a taazihan tu kawaw.\namahica kisu mipecec aca \"$1\" cacay, amatakud han taazihan tu kawaw / satangahan kakelul sa misuped buhci tu kamu nu misu.",
        "summary-preview": "mikawaway-kalumyiti pecu’ nu lacul pataayaway miazih:",
        "subject-preview": "pataayaway miazih  taazihan tu kawaw:",
        "previewerrortext": "mitanam pataayaway miazih sumad nu misu sa mungangaw.",
        "blockedtitle": "misaungayay malangat tuway",
        "blockedtext": "<strong> misaungayay a kalungangan nu misu saca IP puenengan malangat tuway.</strong>\n\nmalangat tu kisu nay $1,\nmahicaay ku <em>$2</em>.\n\n*  milangat miteka a tuki :$8\n*  milangat pahezek a tuki :$6\n*  mahizaay malangat a tukusay:$7\n\nkapah kisu patakus $1 saca zumaay a [[{{MediaWiki:Grouppage-sysop}}|mikuwanay]] matatengil malangatay a mahizaay a munday.\namahica kisu i [[Special:Preferences|setin tu kanamuhan]] misetin tu cacay kapahay a imyiyo(email) puenengan, zumasatu caay henay malangat tigami sasahicaan, kya taneng kisu micaliw \" imyiyo(Email) patakus tina misaungayay \" a sasahicaan apatakus mahizaay mikuwanay.\nIP puenengan nu misu ayzasa ku $3, tina malangatay a ID ku #$5.\nanu palita kisu sa, pakeliten ipabaway a pulita kalunasulitan.",
        "autoblockedtext": "zayhan nasawniay zuma a cacay misaungayay malangat nu $1, IP puenengan nu misu lunuk  malangat tuway.\nkya mahicaay saku:\n\n:<em>$2</em>\n\n*  milangat miteka a tuki :$8\n*  milangat pahezek a tuki :$6\n*  mahizaay malangat a tusukay:$7\n\nkapah kisu patakus $1 saca zumaay a [[{{MediaWiki:Grouppage-sysop}}|mikuwanay]] matatengil milangatay a mahizaay munday.\namahica kisu i [[Special:Preferences|setin tu kanamuhan]] misetin tu cacay silaheciay a imyiyo(email) puenengan, zumasatu caay henay malangat tigami sasahicaan, kya taneng kisu micaliw \" imyiyo(Email) patakus tina misaungayay \" a sasahicaan apatakus mahizaay mikuwanay. \nIP puenengan nu misu ayzasa ku $3, tina malangatay a ID ku #$5.\nanu palita kisu sa, pakeliten ipabaway a pulita kalunasulitan",
+       "systemblockedtext": "misaungayay a kalungangan nu misu saca IP puenengan nay MediaWiki lunuk malangat tuway,mahicaay mahiza isasa’:\n\n:<em>$2</em>\n\n* milangat miteka a tuki: $8\n* milangat pahezek a tuki: $6\n* malangatay a misaungayay: $7\n\nIP puenengan nu misu ayza sa ku $3.\npipabeli ku ipabaway a cesyun i mipalita",
        "blockednoreason": "caay pasaheci tu mahicaay",
        "whitelistedittext": "piayawen $1 sakapah mikawaway-kalumyiti kasabelih.",
+       "confirmedittext": "iayaw mikawaway-kalumyiti tina belih, kanca kisu milucek imyiyo(email) puenengan nu misu.\npicaliw [[Special:Preferences|setin tu kanamuhan]]  misetin atu sawantanen imyiyo(email) puenengan nu misu.",
        "nosuchsectiontitle": "caykatepa tu tusil",
        "nosuchsectiontext": "mitanam kisu mikawaway-kalumyiti a tusil inayi’. hakay miciwsace kasabelih kisu sa malimad saca masipu.",
        "loginreqtitle": "maydih patalabu",
        "loginreqlink": "patalabu",
        "loginreqpagetext": "$1 iayaw kyu kapah miciwsace zuma kasabelih.",
        "accmailtitle": "mima patigamitu",
+       "accmailtext": "[[User talk:$1|$1]] a kakibalucu’ mima patigamitu katukuh $2, kapah tu napatalabu tayza <em>[[Special:ChangePassword|misumad mima]] kasabelih misumad </em>.",
        "newarticle": "(baluhay)",
        "newarticletext": "masasiket kisu tu nayi’ay tu kasabelih.\namipatizeng tina kasabelih, kaisasa mikawaway tu kalumyiti atilad misulit ku lacul (kahica nu kawaw piazih tu tatenga’ay [$1 misaungay a buhci tu kamu  kasabelih ]).\namahica caay padeteng tayza tina kasabelih kisu haw, pihaymaw sapecec saazihay a <strong>tatiku</strong> pipenecan.",
        "anontalkpagetext": "<em>tina matatengil kasabelih apabeli caay henay panganganen ku canghaw a paceba panganganay a misaungayay pisaungay</em>\nsisa kanca kami pisaungay IP puenengan ataazihan kika, uyzasa malecaday a IP puenengan hakay nay katuuday caykalecaday a misaungayay kapulungan.\namahica kisu ku paceba panganganay a misaungayay zumasatu kunizateng buhci tu kamu a lacul inayi’ mahizaay kisu, [[Special:CreateAccount|patizeng baluhay canghaw]] saca [[Special:UserLogin|patalabu]] miliyas masiwala tu zuma paceba panganganay a misaungayay.",
        "noarticletext": "kina kasabelih inayi’ lacul ayza,kapah tu kisu i zumaay a kasabelih [[Special:Search/{{PAGENAME}}| mikilim kina kasabelih pyawti ]]、<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}}  mikilim sasuala nasulitan nakawawan ] caay sa[{{fullurl:{{FULLPAGENAME}}|action=edit}} patizeng kina kasabelih ]</span>.",
        "noarticletext-nopermission": "tina kasabelih ayza inayi’ lacul,\nkapah tu kisu i zuma kasabelih [[Special:Search/{{PAGENAME}}| kilim kina kasabelih pyawti ]],acasa <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}}  kilim sasuala nasulitan nakawawan ]</span>,uyzasa  inayi’  ku tungus patizeng tina kasabelih.",
+       "missing-revision": "kasabelih kalungangan \"{{FULLPAGENAME}}\" a #$1 masumad baziyong inayi’.\n\nsawsawni zayhan masasiket ta mangasiway nasulitan nazipa’an kasabelih,kya kasabelih masipu tuway.\nkahica nu kawaw piazih tu tatenga’ay [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} misipu nazipa’an]",
        "userpage-userdoesnotexist": "misaungayay canghaw \"$1\" caay henay  pangangan。\namahica maydih kisu patizeng/mikawaway-kalumyiti tina a kasabelih, kaliayaw mikinsa tatenga’ay tu hakiza.",
        "userpage-userdoesnotexist-view": "misaungayay canghaw \"$1\" caay henay mapangangan.",
        "blocked-notice-logextract": "tina misaungayay malangat tu ayza.\nisasa’ay ku capi demiad malangatay a nasulitan apabeli miazih tu tatenga’ay:",
        "clearyourcache": "<strong> azihen:</strong>izikuzay misuped kisu kanca palawpes saazihay-sakaluk kabilil-miala ngay maazih sabaluhay sumad.\n* <strong>Firefox / Safari:</strong> pecec <em>Shift</em> sa sapecec <em> miliyaw lisimeten </em>, saca pecec <em>Ctrl-F5</em> saca <em>Ctrl-R</em> (Mac sa ku <em>⌘-R</em>) \n* <strong>Google Chrome:</strong> pecec <em>Ctrl-Shift-R</em> (Mac sa ku <em>⌘-Shift-R</em>) \n* <strong>Internet Explorer:</strong> pecec <em>Ctrl</em> sa sapecec <em> miliyaw lisimeten </em>, saca pecec <em>Ctrl-F5</em>\n* <strong>Opera:</strong> taayaw <em> pili’  →  setin </em> (i Mac ku <em>Opera →  setin tu kanamuhan </em>) nazikuzan sa katukuh aca <em> midimut kasikazan & kazahkezan → palawpes azih  kalunasulitan → kabilil-miala tuway a zunga atu tangan </em>",
+       "usercssyoucanpreview": "<strong> pacekil:</strong>iayaw nu misuped pisaungay \"{{int:showpreview}}\" pipenecan amitanam baluhay CSS nu misu.",
+       "userjsyoucanpreview": "<strong>pacekil:</strong>iayaw misuped misaungay \"{{int:showpreview}}\"  pipenecan amitanam baluhay JavaScript nu misu.",
+       "usercsspreview": "<strong>mipataayaway miazih tunu misu misaungayay CSS kisu ayza, CSS caay henay misuped!</strong>",
        "userjspreview": "strong>imhini pataayaway miazih kisu numisuay misaungayay a JavaScript.\nJavaScript caay henay misuped!</strong>",
        "sitecsspreview": "<strong>imhini kisu ayza i pataayaway miazih tina CSS, CSS caay henay suped!</strong>",
        "sitejspreview": "<strong> mipataayaway miazih tina JavaScript kisu ayza, JavaScript caay henay misuped!</strong>",
+       "userinvalidcssjstitle": "<strong>patalaw:</strong> inayi’ tina nuhekalan yangse \"$1\".\npakuniza misanga’ a .css atu .js kasabelih maydih pisaungay adidi’ay ku sulit satangahan, tinaku:{{ns:user}}:Foo/vector.css atu {{ns:user}}:Foo/Vector.css caay kalecad",
        "updated": "(misabaluh tuway)",
        "note": "<strong>azihen:</strong>",
        "previewnote": "<strong>imahini kisu mapataayaway miazih, misu a pisumad caay henay kasinga’</strong>",
        "continue-editing": "taayaw mikawaway-kalumyiti nikatatapal",
        "previewconflict": "tina pataayaway miazih paazih kisu ipabaway a sulit mikawaway-kalumyiti nikatatapal a lacul masuped tuway amapaazihay aheci.",
        "session_fail_preview": "<strong>ahicanaca! u nanu tina patalabu a tuki matahkal kalunasulitan mahedaw, la’cus kami lisimet nasanga’ mikawaway-kalumyiti.</strong>\n\n<em> {{SITENAME}} mawawah tu saayaway HTML muse, sisa tina pataayaway miazih kya zunga caay paazih amiliyas JavaScript madebung.</em>\n\n<strong>anu maydih kisu tatenga’ taneng misanga’ tina mikawaway-kalumyiti, pitaneng aca.</strong>\namahica caay malahci henay, nazikuz tu [[Special:UserLogout|katahkal]] piliyaw miteka patalabu, zumasatu pilucek saazihay-sakaluk setin ku \"mahasa tina calay-subal(wangyi) a cookie\".",
+       "session_fail_preview_html": "ahicanaca! u nanu kasasiketan a kalunasulitan mahedaw, kyu la’cus kami malisimet numisuay a mikawaway-kalumyiti saungay.\nhakay kisu makatahkal tuway.<strong> pilucek kisu i patalabu setyitase henay, zumasatu pitaneng aca </strong>.\npitaneng aca.\namahica munganaw saan,nazikuzan nu [[Special:UserLogout|katahkal]] miliyaw patalabu, zumasatu piluceki kisu saazihay-sakaluk mahasa tina calay-kakacawan(wangcan) a Cookie.",
+       "token_suffix_mismatch": "<strong>zayhan kisu misaungay a saazihay-sakaluk papeci’ tuway mikawaway-kalumyiti mima-sabuhat a sazumaay a kasabelih, mikawaway-kalumyiti nu misu  makai tuway.</strong>\nsaka miliyas mapeci’ kasabelih lacul, makai tuway tina mikawaway-kalumyiti saungay, \namatahkal tina munday sawsawni zayhan kisu misaungay simunday a paceba pangangan calay-belih(wangyi) kutay sefu-kikay.",
+       "edit_form_incomplete": "<strong>u liyad mikawaway-kalumyiti a lacul caay henay ta sefu-kikay, kapikinsa tu nu misu mikawaway-kalumyiti lacul caay hen kaleku atu pitaneng henay. </strong>",
        "editing": "mikawaway tu kalumyiti  $1 ayza",
        "creating": "patizeng ayza $1",
        "editingsection": "mikawaway tu kalumyiti ayza $1 (tusil)",
        "editingcomment": "imahini mikawaway-kalumyiti $1 (baluhay tusil)",
        "editconflict": "mikawaway-kalumyiti sasula’cus: $1",
+       "explainconflict": "nazikuz kisu miteka mikawaway-kalumyiti izawtu zumaay tatemaw misuped tu tina kasabelih.\nipabaway a sulit misabacu paazih tu ayzaay a kasabelih a sulit lacul,\nnamasumad nu misu a sulit lacul paazih isasa’ sulit misabacu.\nkanca kisu amisumad a sulit lacul mikabu ta masupeday a sulit lacul.\namahica kisu kakelul sapecec \"$1\" <strong>dada’</strong> ipabaway a sulit misabacu a lacul mapasuped.",
        "yourtext": "numisuay a sulit",
        "storedversion": "misupedtu tu sumad",
        "editingold": "<strong>patalaw: imahini kisu mikawaway-kalumyiti kasabelih maluman a sumad baziyong.</strong>\namahica kisu suped, itini masumad tu haw masumaday a amahicahica a lacul amahedaw.",
        "yourdiff": "sasizuma",
+       "copyrightwarning": "piazihen, sacahamin i {{SITENAME}} namasanga’ay a paanin akilulen $2 sapabeli tu kinli a cedang patiyak (kahica nu kawaw piazih i $1).\namahica kisu caay maydih nisanga’ay tu cudad nu misu kilul tu nizateng masumad atu payakayak, amana itini patahka ku nasulitan.<br />\npiketun kisu tu tamiyan itini nisanga’ay tu cudad lacul nay sulitan nu misu, saca miala inayi’ nisanga’an niza tu tungus a kawaw midiput a binawlan a subal saca paybalucu’ laculaculan.\n<strong>amana i inayi’ pabeli tu kinli a pulita patahkal nasulitan!</strong>",
        "editpage-cannot-use-custom-model": "tina kasabelih a lacul tatudungen-misanga’ amana pasumad.",
+       "longpageerror": "<strong> mungangaw: patayzaan ku sulit lacul nu misu pulung sa {{PLURAL:$1|1 KB|$1 KB}}, mangasiw sisetyimo pabaw-sahezek {{PLURAL:$2|1 KB|$2 KB}}. </strong>\n\nla’cus misuped.",
+       "readonlywarning": "<strong> patalaw: nasulitan-sulu pamutek samiteka midiput, sisa la’cus misuped nasanga’ kisu ayza mikawaway-kalumyiti saungay.</strong>\nkapah kisu mikopi numisuay a sulit zumasatu pazepit tu sulit tangan misuped, kanay misuped aca mikawaway-kalumyiti nu misu.\n\npamutek nasulitan-sulu a sisetyimo mikuwanay izaw isasa’ay a buhci tu kamu: $1",
        "semiprotectedpagewarning": "<strong> azihen:</strong>tina kasabelih madiput tuway, mapanganganay a misaungayay dada’ kapah mikawaway-kalumyiti.\nisasa’ sa, pabeli capi demiad a nasulitan nazipa’an kya kapah miazih tu tatenga’ay",
+       "cascadeprotectedwarning": "<strong>patalaw:</strong> u nanu tina bilih sipakabit nu isasa’ay a {{PLURAL:$1|kasabelih}}, sisa patatusul midiput. maala a dada’[[Special:ListGroupRights|sazumaay a tungus]] a misaungayay kya kapah mikawaway-kalumyiti.",
+       "titleprotectedwarning": "<strong> patalaw: tina kasabelih madiput tuway, maydih [[Special:ListGroupRights|sazumaay a tungus]] kyu taneng patizeng.</strong>\nisasa’ay nipabeli capi demiad nasulitan nazipa’an ngay miazih tu tatenga’ay",
        "templatesused": "uyni kasabelih pisaungay tu isasaay {{PLURAL:$1|taazihan mitudung}}:",
        "templatesusedpreview": "tina pataayaway miazih pisaungay kya isasa’ay a {{PLURAL:$1|taazihan mitudung}}",
        "templatesusedsection": "tina belih pisaungay tu isasa’ay a {{PLURAL:$1|taazihan mitudung}}:",
        "template-protected": "(madiputay)",
        "template-semiprotected": "(madiputay a kasabelih - satizep mikawaway tu kalumyiti)",
        "hiddencategories": "kina kasabelih tungusay nu {{PLURAL:$1|1 midimut kakuniza }}mamikawaw:",
+       "nocreatetext": "{{SITENAME}} makelec tuway patizeng baluhayay a kasabelih a sasahicaan. {{GENDER:|kisu|}} taneng tatiku atu mikawaway-kalumyiti izaway a kasabelih, saca [[Special:UserLogin|patalabu saca patizeng baluhay canghaw]].",
        "nocreate-loggedin": "inayi’ tungus kisu patizeng  baluhayay a kasabelih.",
        "sectioneditnotsupported-title": "caay pidama mikawaway-kalumyiti tusil",
        "sectioneditnotsupported-text": "tina kasabelih caay midama mikawaway-kalumyiti tusil.",
        "permissionserrors": "mungangaw ku tungus",
        "permissionserrorstext": "makay isasa’ay {{PLURAL:$1|mahicaay}}, inayi’ ku tungus kisu miteka ayzaay pisaungay",
        "permissionserrorstext-withaction": "namakay isasaay {{PLURAL:$1|mahicaay}}, inayi’ kisu situngus miteka $2 miteka tuway misaungay:",
+       "contentmodelediterror": "You cannot edit this revision because its content model is <code>$1</code>, which differs from the current content model of the page <code>$2</code>.",
        "recreate-moveddeleted-warn": "<strong> patalaw: imahini kisu miliyaw patizeng nasawniay masipuay tu kasabelih. </strong>\n\nkanca kisu mizateng palalid mikawaway-kalumyiti tina bilih haw?\nitini nipabeli masipu atu milimad nasulitan nazipa’an sapiazih tu tatenga’ay",
        "moveddeleted-notice": "kina kasabelih masipu tu.\nisasa nipabeli kina kasabelihay a masipu atu milimad nasulitan nakawawan, taneng miazih tu tatenga’ay.",
+       "moveddeleted-notice-recent": "ahicanaca, tina a kasabelih ayaw sahenay masipu tu (caay sungaliw 24 a tuki).\nisasa’ sa nipabeli tina kasabelih a masipu atu milimad nasulitan nazipa’an sapihica miazih tu tatenga’ay.",
        "log-fulllog": "ciwsace leku nasulitan-nazipa’an",
        "edit-hook-aborted": "mikawaway-kalumyiti masatezep tuway nay Hook.\nzumasatu caay patukil inayi’ amahicahica buhci tu kamu.",
        "edit-gone-missing": "la’cus misabaluh kasabelih.\nkya kasabelih hakay masipu tuway.",
        "content-failed-to-parse": "tingalaw $2 kese a $1 kamu-sakilul a lacul mungangaw: $3",
        "invalid-content-data": "lacul kalunasulitan la’cus",
        "content-not-allowed-here": "kasabelih [[$2]] caay mahasa pisaungay \"$1\" lacul nu kamu-sakilul",
+       "editwarning-warning": "miliyas tina kasabelih hakay amahedaw kisu nu iayaw namisanga’ay sacahamin a sumad.\namahica kisu patalabu tuway, kapah kisu i setin tu kanamuhan a \"{{int:prefs-editing}}\" kasacacay edeben tina patalaw.",
        "editpage-invalidcontentmodel-title": "caay pidama tu lacul nu tatudungen misanga’",
        "editpage-invalidcontentmodel-text": "caay kamidama lacul tatudungen-misanga’ \"$1\".",
        "editpage-notsupportedcontentformat-title": "caay pidama tuyni a lacul nu kese",
        "content-json-empty-object": "inayi’ay a tuutuud",
        "content-json-empty-array": "inayi’ay a papazengan tu nisulitan",
        "deprecated-self-close-category": "pisaungay la’cus Self-closed HTML aazihen-paya a kasabelih",
+       "deprecated-self-close-category-desc": "kasabelih yamalyilu la’cusay a Self-closed HTML aazihen-paya, tinaku <code>&lt;b/></code> or <code>&lt;span/></code>. uyniyay aazihen-paya a muse  amasumad malecad lalkuan nu HTML5,sisa wikitext a misaungay a pakayzaan mapasatezep tuway.",
+       "duplicate-args-warning": "<strong>patalaw:</strong> [[:$1]] miawza [[:$2]] a \"$3\" aazihen-sulyang pisaungay mangsiw tu cacay,pisaungay dada’ nipabeli sazikuzay a aazihen-sulyang sulyang.",
        "duplicate-args-category": "anu taazihan-mitudung muawaw haw pisaungay misaliyaway a aazihen-sulyang a kasabelih",
+       "duplicate-args-category-desc": "kya kasabelih yamalyilu misaliyaw pisaungay aazihen-sulyang a taazihan mitudung miawza, tinaku <code><nowiki>{{foo|bar=1|bar=2}}</nowiki></code> saca  <code><nowiki>{{foo|bar|1=baz}}</nowiki></code>.",
        "expensive-parserfunction-category": "pisaungay sayadah eluc sisetyimo katahkalan nu kalisiw a mapulita kasabelih",
+       "post-expand-template-inclusion-warning": "<strong>patalaw:</strong> nicaliwan taazihan mitudung zikuz tabaki adidi’ matabesiw ku pikelec. uzuma taazihan mitudung lacul a caay papisaungay.",
        "post-expand-template-inclusion-category": "nicaliwan taazihan-mitudung mangasiw kelec nu kasabelih",
+       "post-expand-template-argument-warning": "<strong> patalaw:</strong> tina kasabelih izaw cacay pabaway taazihan mitudung aazihen-sulyang tadatanayu’.\ntadatanayu’ay aazihen-sulyang tansul sa masekipo.",
        "post-expand-template-argument-category": "taazihan-mitudung aazihen-sulyang izaw layad masekipoay a kasabelih",
        "parser-template-loop-warning": "masedap taazihan-mitudung musaliyut: [[$1]]",
        "template-loop-category": "sitatutungay masaliyut a kasabelih",
        "parser-unstrip-loop-warning": "masedap tu Unstrip masaliyut.",
        "parser-unstrip-recursion-limit": "Unstrip musaliyuliyud mangasiw tu kelec ($1)",
        "converter-manual-rule-error": "imahini palima-saungay kamu miliyaw likec masedap tu mungangaw",
+       "undo-success": "tina mikawaway-kalumyiti kapah tu mapatiku.\nkapikinsa tu zikuz sasutili’ay sapat nu sulit, malucekay tu ukakaiyan kaidihan isu patiku, satu suped tu zikuz misumad tu pahezek mikawaway-kalumyiti patiku.",
        "undo-failure": "nay mikawaway-kalumyiti a sumad izaw sasula’cus,tina mikawaway-kalumyiti caay patiku.",
        "undo-norev": "uyni mikawaway-kalumyiti inayi’ saca masipu tuway, la’cus patiku",
        "undo-nochange": "tina mikawaway-kalumyiti mapatiku tuway.",
+       "undo-summary": "palawpes nay [[Special:Contributions/$2|$2]] ([[User talk:$2|sasukamu]]) sapisanga’ay a sumad $1",
        "undo-summary-username-hidden": "patiku midimut misaungayay sumad $1",
+       "cantcreateaccount-text": "namakay IP puenengan a subal <strong>$1</strong>, yamalyilu IP puenengan nu misu (<strong>$1</strong>) napatizengay a canghaw malangat tu ku [[User:$3|$3]].\n\n$3 malangatay a mahicaay ku <em>$2</em>",
+       "cantcreateaccount-range-text": "namakay IP puenengan a subal <strong>$1</strong>, yamalyilu IP puenengan nu misu (<strong>$4</strong>) napatizengay a canghaw malangat tu ku [[User:$3|$3]].\n\n$3 malangatay a mahicaay ku <em>$2</em>",
        "viewpagelogs": "ciwsace kina kasabelih a nasulitan nakawawan",
        "nohistory": "tina kasabilih inayi’ amahicahica tu sumad a nasulitan.",
        "currentrev": "sabaluhay masumad",
        "history-feed-title": "masumad nu ayaway a nazipa’an",
        "history-feed-description": "tina Wiki tina kasabelih nu masumaday a nazipa’an",
        "history-feed-item-nocomment": "$1 i $2",
+       "history-feed-empty": "milungucay a kasabelih inayi’,\nhakay masipu tuway saca maliyaw pangangan.\npitanam [[Special:Search|mikilim tina kakacawan]] maala zuma mahizaay baluhayay a kasabelih.",
        "history-edit-tags": "mikawaway-kalumyiti mipili’ masumad nu ayaway a aazihen a paya tuway",
        "rev-deleted-comment": "(masipu tu mikawaway-kalumyiti  pecu’ nu lacul)",
        "rev-deleted-user": "(misipu misaungayay a kalungangan tuway)",
        "rev-deleted-event": "(masipu tu nasulitan-nazipa’an nu paazih tu sulit)",
        "rev-deleted-user-contribs": "[misaungayay a kalungangan saca IP puenengan masipu tuway - madimut paanin piazihan-tu-sulit a mikawaway-kalumyiti]",
+       "rev-deleted-text-permission": "tina kasabelih masumad <strong> masipu </strong>.\nkapah tayza[{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} misipu nasulitan nazipa’an] miala pulita cesyun.",
+       "rev-suppressed-text-permission": "tina kasabelih sumad nay <strong> masatezep paazih </strong>.\ntaneng tayza [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} satezep paazih nasulitan nazipa’an] maala pulita cesyun.",
+       "rev-deleted-text-unhide": "tina kasabelih a sumad nay <strong> masipu </strong>.\nkapah tayza [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} misipu nasulitan nazipa’an] maala pulita cesyun.\namahica kisu palalid, kapahtu kisu [$1 miciwsace tina sumad].",
+       "rev-suppressed-text-unhide": "tina kasabelih a sumad nay <strong> masatezep paazih </strong>。\nkapah tayza [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} satezep paazih nasulitan nazipa’an] miala pulita cesyun。\namahica kisu apalalid,kapah tu kisu [$1 miciwsace tina sumad]",
+       "rev-suppressed-text-view": "tina kasabelih a sumad nay <strong> masatezep paazih </strong>.\nkapah kisu lalid miciwsace misumad,kapah tayza [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} satezep paazih nazipa’an ] miala pulita cesyun.",
+       "rev-deleted-no-diff": "zayhan kasabelih ilabu’ a cacay a sumad nu ayaway nay <strong> masipu tuway </strong>, la’cus kisu miciwsace   sasizuma.\ntaneng tayza [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}}] masipu nasulitan nazipa’an maala pulitaay a cesyun.",
+       "rev-suppressed-no-diff": "zayhan kasabelih ilabu’ a cacay a sumad nu ayaway nay <strong>masipu tuway</strong>, la’cus kisu miciwsace sasizuma.",
+       "rev-deleted-unhide-diff": "miciwsace sasizuma ilabu’ay ku cacay masumad nu ayaway nay <strong> masipu tu </strong>.\nkapah tyaza [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} masipu nasulitan nazipa’an] maala pulita  kalunasulitan.\namahica kisu apalalid, kapah tu kisu [$1 miwsace tina zuma].",
+       "rev-suppressed-unhide-diff": "miciwsace sasizuma ilabu’ay a cacay masumad nu ayaway nay <strong>masatezep paazih</strong>.\nkapah tayza [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} satezep paazih nasulitan nazipa’an] maala pulitaya a cesyun.\namahica kisu palalid, kapah tu kisu [$1 miciwsace tina sasizuma].",
+       "rev-deleted-diff-view": "ciwsace sasizuma ilabu’ay a cacay sumad nu ayaway nay <strong> masipu </strong>tu.\nkapah kisu palalid miciwsace sasizuma,kapah tayza [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} masipu nasulitan nazipa’an] miala pulita cesyun.",
+       "rev-suppressed-diff-view": "miciwsace sasizuma ilabu’ay a cacay masumad nu ayaway nay<strong> masatezep paazih </strong>.\nkapah kisu palalid miciwsace sasizuma,kapah tayza [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} misatezep paazih nasulitan nazipa’an] miala pulita cesyun.",
        "rev-delundel": "misumad ku maazihay",
        "rev-showdeleted": "paazih",
        "revisiondelete": "masipu/palawpes misipu masumad nu ayaway",
        "revdelete-nooldid-title": "la’cusay a pamutekan masumad nu ayaway",
+       "revdelete-nooldid-text": "inayi’ matuzu’ay kisu amahicahica tu amisaungay tina sasahicaan pamutekan masumad nu ayaway nu ayaway, saca  matuzu’ay sumad inayi’ay, saca kisu mitanam midimut ayza a sumad",
        "revdelete-no-file": "matuzu’ay a tangan inayi’ tu.",
+       "revdelete-show-file-confirm": "sakaydihan kisu miciwsace tu tangan \"<nowiki>$1</nowiki>\" masipuay tu masumad i $2 $3?",
        "revdelete-show-file-submit": "hang",
        "revdelete-selected-text": "mapili’ tuway [[:$2]] tebanay{{PLURAL:$1|cacayay|yadahay}} masumad nu ayaway",
        "revdelete-selected-file": "mapili’ tu [[:$2]] labu’ay {{PLURAL:$1|cacay|yadahay}} tangan baziyong:",
        "revdelete-text-file": "masipuay a tangan baziyong uyzatu matahkal i tangan nazipa’an, uyzasa lacul caay pawawah misuped-miala.",
        "logdelete-text": "masipuay tu nasulitan a saungay nu binawlan a tahkal henay i nasulitan ilabu’, nika liyad a lacul a caay kawawah paazih tu katuuday misuped-miala.",
        "revdelete-text-others": "amahica caay misetin uzumaay a sakacucek, zuma mikuwanay izaw tu ku tungus miciwsace atu palawpes masipu midimutay a lacul.",
+       "revdelete-confirm": "piluceki matineng kisu haw tina saungay amatatuzu’ay heci? atu nasaungay isu matatungus haw?\n[[{{MediaWiki:Policy-url}}|amikuwanay-pasayzaay]] kakilulen.",
+       "revdelete-suppress-text": "satezep paazih kanca <strong>dada’</strong> isasa’ay a zasatu pisaungay:\n* midimut palawacu’ay a cesyun \n* caay katanengay teked tademaw a kalunasulitan \n*:<em> ngangan nu puenengan, bangku nu telay, banggu nu mibunsiw atuzuma./em>",
        "revdelete-legend": "misetin paazih a kelec",
        "revdelete-hide-text": "masumad nu ayaway a sulit",
        "revdelete-hide-image": "midimut lacul nu tangan",
        "revdelete-modify-missing": "misumad kasacacay ID $1 mungangaw: kalunasulitan-sulu inayi’ kya kalunasulitan",
        "revdelete-no-change": "<strong>patalaw:</strong> i $1 $2 a kasacacay malunguc tu pisumad paazihay a setin.",
        "revdelete-concurrent-change": "misumad i $1 $2 kasacacay mungangaway: itisuwan mitanam misumad pataayaw tu setyitase, mapasumad tu.\npikinsa tu nasulitan nazipa’an.",
+       "revdelete-only-restricted": "a midimut i $1 $2 a kasacacay mala mungangaway: amana kisu kacaay henay pipili’ miazih tu setin kasiwantan a satezep mikuwanay miciwsace kasacacay.",
        "revdelete-reason-dropdown": "* lalid maazihay a misipuay a mahicaay \n** midebung nisanga’an niza tu tungus a kawaw \n** caay matatungusay a buhci tu kamu saca tekeday a cesyun\n** caay matatungusay a misaungayay a kalungangan  \n** midimut yamalyilu palawacu’ay a cesyun",
        "revdelete-otherreason": "zumaay/nicunusay a mahicaay:",
        "revdelete-reasonotherlist": "zuma a mahicaay",
        "revdelete-edit-reasonlist": "masipu a mahicaay nu mikawaway-kalumyiti",
        "revdelete-offender": "masumad nu ayaway  masacudaday:",
        "suppressionlog": "satezep paazih nasulitan nazipa’an",
+       "suppressionlogtext": "isasa’ay a piazihan-tu-sulit u mikuwanay micaliw tu masipuay saca malangat ku midimutay a lacul.\npitayza [[Special:BlockList|milangat piazihan-tu-sulit]] miala ayzasa malangatay a piazihan-tu-sulit.",
        "mergehistory": "mikabu kasabelih nazipa’an",
        "mergehistory-box": "mikabu tatusa kasabelih a sumad:",
        "mergehistory-from": "saangangan kasabelih:",
        "mergehistory-into": "pabalucu’an kasabelih:",
        "mergehistory-list": "kapah mikabuay a mikawaway-kalumyiti nazipa’an",
+       "mergehistory-merge": "isasa’say a [[:$1]] sumad kapah tu mikabu ta [[:$2]].\npisaungay cacay-pili’ atilad mipili’ mikabu matuzu’ay a sumad saca iayaw nu matuzu’ay demiad a sumad.\npihaymawi, amahica ayzasa misaungay pasubana’ tu miidangay masasiket apalawpes tina saungay.",
        "mergehistory-go": "paazih kapah mikabuay a mikawaway-kalumyiti",
        "mergehistory-empty": "inayi’ amikabuay a sumad.",
        "mergehistory-done": "$1 izaw $3 {{PLURAL:$3|baziyong}} masumad nu ayaway makabu tu i [[:$2]]",
        "diff-empty": "(inayi’ sasizuma)",
        "diff-multi-sameuser": "(malecaday misaungayay {{PLURAL:$1|ilaed izaw ku $1 a sumad}}inayi’ paazih)",
        "diff-multi-otherusers": "({{PLURAL:$1|cacay ilabu’ay a sumad|$1 ilabu’ay a sumad}} nay {{PLURAL:$2|uzumaay a misaungay|$2 misaungayay}} caay paazih)",
+       "difference-missing-revision": "caykatepa’ sasizuma ($1) a {{PLURAL:$2|1 masumad nu ayaway|$2 masumad nu ayaway}}.\n\nsawsawni tina zayhan sasizuma misiket sungaliw, kasabelih masipu tu.\nkahica nu kawaw cesyun piazih [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} masipu nasulitan nazipa’an].",
        "searchresults": "heci nu makatepa",
        "searchresults-title": "$1 heci nu makatepa",
        "titlematches": "kasabelih satangah matatungus",
        "search-interwiki-more-results": "sayadahay a heci",
        "search-relatedarticle": "mahizaay",
        "searchall": "hamin",
+       "showingresults": "isasa’ay paazih nay saka <strong>$2</strong> miteka, pulung {{PLURAL:$1|<strong>1</strong>ku heci|<strong>$1</strong> ku heci}}",
+       "showingresultsinrange": "isasa’ paazih nay saka <strong>$2</strong> ta saka <strong>$3</strong> a {{PLURAL:$1|<strong>$1</strong> heci}}:",
        "search-showingresults": "{{PLURAL:$4|saka <strong>$1</strong> a heci, pulung <strong>$3</strong>|saka <strong>$1-$2</strong> a heci, pulung <strong>$3</strong>}}",
        "search-nonefound": "nayi’ matatungusay palalitemuh tu kawaw maheciay.",
        "search-nonefound-thiswiki": "itini kakacawan inayi’ matatungus tina palalitemuh tu kawaw  sakacucek a heci.",
        "powersearch-togglenone": "nayi’",
        "powersearch-remember": "pibalucu’en mapiliay sapikilim anucilacila",
        "search-external": "mikilim i hekal",
+       "searchdisabled": "{{SITENAME}} mapasatezep mikilimay a sasahicaan.\nkapah kisu misumad micaliw Google kilim.\npiazihi, i Google makilimay a {{SITENAME}} kasabelih lacul hakay caay sabaluhayay.",
        "search-error": "imahini mikilim sa mungangaw:$1",
        "search-warning": "imahini mikilim sa mungangaw:$1",
        "preferences": "setin tu kanamuhan",
        "recentchangesdays-max": "sayadahay $1 {{PLURAL:$1|a demiad}}",
        "recentchangescount": "pataayaw tu kawaw paazihay nu mikawaway-kalumyiti nikayadah:",
        "prefs-help-recentchangescount": "uyni yamalyilu capi demiad a sumad, kasabelih nasulitan-nazipa’an atu nazipa’an",
+       "prefs-help-watchlist-token2": "miaca aazihan numisuay a miazihay a piazihan-tu-sulit maydihay a mima-sabuhat.\namahicahica tademaw matineng ku mama-sabuhat dada’ taneng maasip numisuay a miazihay a piazihan-tu-sulit,sisa amana kilul tu nizateng kasasimel tu zuma a tadeamw.\namahica izaw ku maydih [[Special:ResetTokens|kapah kisu miliyaw patizeng mima-sabuhat]].",
        "savedprefs": "masuped setin tu kanamuhan nu misu.",
        "savedrights": "masuped tu {{GENDER:$1|$1}} misaungayay a tungus.",
        "timezonelegend": "tatukian a kakitizaan:",
        "email": "imyiyo(email)",
        "prefs-help-realname": "tatenga’ay ngangan mipili’ tu sasulitan pisinga’an. \nanu pabeli, tatenga’ay ngangan hakay kapah malumisuay nalimaan a sapangangan",
        "prefs-help-email": "imyiyo(email) puenengan ku mipili’ sasulitan langat-pisinga’an.\nnika i miliyaw patizeng mima sa matineng pisaungay,hakay kisu kalamapawan tu mima.",
+       "prefs-help-email-others": "hakay kapah kisu mipili’ zuma misaungayay micaliw imyiyo(email) nu misu, misaungayay kasabelih saca  sasukamu kasabelih a siket masukazih tisuwan.\nimyiyo(email) puenengan nu misu caay pabeli’ tu zuma amitakus tisuwanay a misaungayay.",
        "prefs-help-email-required": "imyiyo(email) puenengan manakanca suliten naca kasacacay.",
        "prefs-info": "anganganay a cesyun",
        "prefs-i18n": "masakitakiay",
        "prefs-tokenwatchlist": "sabuhat",
        "prefs-diffs": "sasizuma",
        "prefs-help-prefershttps": "uyni setin tu kanamuhan amalahci tu anucila patalabu kisu.",
+       "prefswarning-warning": "kisu tu nu misu a setin tu kanamuhan mikawawan a misumad caay henay singa’.\nanu caay kisu pipecec \"$1\" miliyas tina kasabelih, a caay misabulah numisuay a setin tu kanamuhan.",
        "prefs-tabs-navigation-hint": "pacekil:kapah kisu pisaungay pasawili, pasawanan a pecec miketun mibalic kasabelih-paya.",
        "userrights": "tungus nu misaungayay",
        "userrights-lookup-user": "mipili’ misaungayay",
        "userrights-user-editname": "pisuliti misaungayay a kalungangan:",
        "editusergroup": "pacumud misaungayay luyaluy",
+       "editinguser": "misumad {{GENDER:$1| misaungayay}} <strong>[[User:$1|$1]]</strong> a misaungayay a tungus $2",
+       "viewinguserrights": "ciwsace {{GENDER:$1|misaungayay}} <strong>[[User:$1|$1]]</strong> a misaungayay tungus $2",
        "userrights-editusergroup": "mikawaway-kalumyiti {{GENDER:$1|misaungayay}} cyucu",
        "userrights-viewusergroup": "ciwsace {{GENDER:$1|misaungayay}} luyaluy",
        "saveusergroups": "misuped {{GENDER:$1|misaungayay}} luyaluy",
        "userrights-groupsmember": "canan a luyaluy:",
        "userrights-groupsmember-auto": "litinay yamalyilu luyaluy:",
+       "userrights-groups-help": "kapah kisu misaimed tina misaungayay mikitinay a luyaluy:\n* mahatizaay a mapili’ay atilad dayhiw kya misaungayay tungusay kya luyaluy.\n*caay hatizaay a mapili’ay atilad dayhiw kya misaungayay caay tungusay kya luyaluy.\n* izaw * bacu sacuzu’ dayhiw anu micunus kya luyaluy sa caykatu masipu,mabelih sa mihizatu.\n* izaw # bacu sacuzu’ dayhiw kisu dada’ patazikuzen ku tuki tina luyaluy sakawaway a kakatekuhan, la’cus kisu pataayaw.",
        "userrights-reason": "mahicaay:",
        "userrights-no-interwiki": "inayi’ tungus kisu mikawaway-kalumyiti zuma Wiki misaungayay a tungus.",
        "userrights-nodatabase": "kalunasulitan-sulu $1 inayi’ saca inayi’ itiniay a angangan-kikay.",
        "userrights-expiry": "kakatekuhan:",
        "userrights-expiry-existing": "ayzaay a kakatekuhan: $3, $2",
        "userrights-expiry-othertime": "zuma a tatukian:",
+       "userrights-expiry-options": "1 a demiad:1 day,1 lipay:1 week,1 a bulad:1 month,3 a bulad:3 months,6 a bulad:6 months,1 a mihca:1 year",
        "userrights-invalid-expiry": "luyaluy \"$1\" a kakatekuhan la’cus tu.",
        "userrights-expiry-in-past": "luyaluy \"$1\" a kakatekuhan mangliw tu.",
+       "userrights-cannot-shorten-expiry": "la’cus kisu maluayaw i luyaluy \"$1\" sakawaway i kakatekuhan. izaway dada’ micunusen atu misipu tina a luyaluy tungusay misaungayay kapah tu a maliayaw i kakatekuhan.",
        "userrights-conflict": "misaungayay tungus misumad sasula’cus! piciwsace atu malucekay sumad nu misu",
        "group": "luyaluy:",
        "group-user": "misaungayay",
        "right-unblockself": "mihulak tu ku langat nu maku",
        "right-protect": "misumad midiput tindud atu mikawaway-kalumyiti mapatatusul midiputay a kasabelih",
        "right-editprotected": "mikawaway-kalumyiti midiput tu kasalaylay ku \"{{int:protect-level-sysop}}\" a kasabelih.",
+       "right-editsemiprotected": "mikawaway-kalumyiti midiput kasalaylay u \"{{int:protect-level-autoconfirmed}}\" u kasabelih",
        "right-editcontentmodel": "mikawaway-kalumyiti kasabelih a lacul tatudungen-misanga’",
        "right-editinterface": "mikawaway-kalumyiti misaungayay taypuolayta",
        "right-editusercss": "mikawaway-kalumyiti zuma misaungayay a CSS tangan",
        "rcfilters-highlightmenu-help": "mipili’ tu palita sacuzu’ tina susin a kalukulit",
        "rcfilters-filterlist-noresults": "caykatepa sakacucek nu misapili’",
        "rcfilters-noresults-conflict": "zayhan kilim sakacucek sasula’cus,cay katepa ku heci",
+       "rcfilters-state-message-subset": "tina sakacucek nu misapili’ inayi’ ku laheci, zayhan kya heci yamalyilu isasa’ subal saahebalay a {{PLURAL:$2|sakacucek nu misapili’}} cacay ku ilabu’ay (mitanam kasiwantan sacuzu’ amipili’):$1",
+       "rcfilters-state-message-fullcoverage": "mipili’ i luyaluy labu’ay sacahamin sakacucek nu misapili’ atu inayi’ mipili’ sa malecad, dayhiw tina sakacucek nu misapili’caay kalahci. luyaluy yamalyilu tu: $1",
        "rcfilters-filtergroup-authorship": "paaninay a masacudaday",
        "rcfilters-filter-editsbyself-label": "numisuay a mikawaway-kalumyiti",
        "rcfilters-filter-editsbyself-description": "numisuay a paanin",
        "rcfilters-filter-categorization-description": "mamicunus i kakuniza saca nay kakuniza masipuay a kasabelih nasulitanㄡ",
        "rcfilters-filter-logactions-label": "saungay a nasulitan nazipa’an",
        "rcfilters-filter-logactions-description": "mikuwan saungay, patizeng canghaw, misipu kasabelih, patapabaw...",
+       "rcfilters-hideminor-conflicts-typeofchange-global": "\"mikilulay mikawaway-kalumyiti\" a sakacucek nu misapili’ sasula’cus tu cacay saca yadah sumad nikalahizaan sakacucek nu misapili’,zayhan izaw hatizaay sumad nikalahizaan la’cus patuzu’ay tu \"mikilulay\". sasula’cusay a sakacucek nu misapili’ sacuzu’ tu i pabaw nu pisaungay a subal nu sakacucek nu misapili’.",
        "rcfilters-hideminor-conflicts-typeofchange": "izaw ku zumaay misumad nikalahizaan la’cus matuzu’ay mala \"mikilulay\", sisa tina sakacucek nu misapili’ atu isasa’ay a sumad nikalahizaan sakacucek nu misapili’ sasula’cus: $1",
+       "rcfilters-typeofchange-conflicts-hideminor": "tina misumad nikalahizaan sakacucek nu misapili’ atu \"mikilulay mikawaway-kalumyiti\" sakacucek nu misapili’sasula’cus, uzuma misumad nikalahizaan la’cus matuzu’ay ku \"mikilulay\"",
        "rcfilters-filtergroup-lastRevision": "sabaluhay masumad",
        "rcnotefrom": "isasa’ay a {{PLURAL:$5|ku}}nay <strong>$3 $4</strong> a sumad  (sayadah paazih <strong>$1</strong>).",
        "rclistfrom": "paazih nay $3 $2 baluhayay a sumad katukuh ayza",
        "upload_directory_missing": "ma’ngadis patapabaw dilyikotoling ($1) zumasa calay-belih(wangyi) sefu-kikay inayi’ tungus mipatizeng.",
        "upload_directory_read_only": "calay-belih(wangyi) sefu-sakaluk inayi’ patapabaw dilyikotoling ($1) a suliten  tungus.",
        "uploaderror": "patapabaw mungangaw",
+       "upload-recreate-warning": "<strong>patalaw: nanu ayaw izaw ku tangan misaungay tina kalungangan mapasipu tuway saca  malimad ta zuma a kakitizaan.</strong>\n\nitini pabeli masipuay atu malimaday a nasulitan nazipa’an dayum paazih tu tatenga’ay:",
        "uploadtext": "pisaungay isasa’ay a aazihan cudad ngay patapabaw tangan.\namiciwsace saca mikilim iayaw patapabaway a tangan, taneng tayza [[Special:FileList|tangan patapabaw piazihan tu sulit]], (miliyaw miteka)  patapabaw a i [[Special:Log/upload| patapabaw nasulitan nazipa’an]] sulitan, zumasatu masipu ngay i [[Special:Log/delete|masipu nazipa’an]] sulitan.\n\na i kasabelih micaliwan tu tangan, kapah pisaungay isasa’ay ilabu’ ku cacay a sasakawawen masasiket:\n* <strong><code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:File.jpg]]</nowiki></code></strong> paazih leku ditek a zunga \n* <strong><code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:File.png|200px|thumb|left|alt text]]</nowiki></code></strong>amipazeng cacay 200 syangsu ahebalay a zunga ilabu’ nu misabacu, zumasatu paazih \"alt text\" sapihica ku sapuelac \n* <strong><code><nowiki>[[</nowiki>{{ns:media}}<nowiki>:File.ogg]]</nowiki></code></strong> kakelul masasiket ta zunga caay paazih pataayaway miazih",
        "upload-permitted": "mahasa tangan {{PLURAL:$2|nikalahizaan}}: $1.",
        "upload-preferred": "patahkal nizateng a tangan {{PLURAL:$2|nikalahizaan}}: $1",
        "upload-prohibited": "satezep a tangan {{PLURAL:$2|nikalahizaan}}: $1",
        "uploadlogpage": "patapabaw nasulitan nazipa’an",
+       "uploadlogpagetext": "isasa’ay a piazihan-tu-sulit ku capi demiad patapabaway a tangan.\npiciwsace [[Special:NewFiles|sabaluhay tangan-sulu nu zunga]] kyu kapah palakuliten ku aazihen a sasakawawen miciwsace.",
        "filename": "kalungangan nu tangan",
        "filedesc": "pecu’ nu lacul",
        "fileuploadsummary": "pecu’ nu lacul:",
        "largefileserver": "tina tangan hacica-tabaki mangsiw sefu-kikay setin a mahasaay a subal.",
        "emptyfile": "patapabaway tu tangan nu misu nayay ilabu.\nhakay u tangan a kalungangan mungangaw ku sulitan.\nkapikinsa maydih kisu patapabaw tu nayaay a tangan.",
        "windows-nonascii-filename": "tina Wiki caay midama pisaungay sazumaay bacu a tangan kalungangan.",
+       "fileexists": "izawtu malecad kalungangan a tangan, amahica {{GENDER:|kisu}} caay kalucek maydih misumad haw? pikinsa <strong>[[:$1]]</strong>.\n[[$1|thumb]]",
+       "filepageexists": "tina tangan a sapuelac kasabelih <strong>[[:$1]]</strong> izaw tuway, uyzasa ayza inayi’ pisaungay tina kalungangan a tangan.\nitini kisu misulitay a pecu’ nu lacul caay misuped tayza kya sapuelac kasabelih, kanca kisu lima-saungay mikawaway-kalumyiti kya sapuelac kasabelih.\n[[$1|thumb]]",
+       "fileexists-extension": "kalahizaan a tangan kalungangan izawtu i: [[$2|thumb]]\n* patapabaway a tangan kalungangan: <strong>[[:$1]]</strong>\n* naizaway a tangan kalungangan: <strong>[[:$2]]</strong>\npipili’ pisaungay zuma kalungangan",
+       "fileexists-thumbnail-yes": "tina tangan akayi’ ku masukepay a zunga <em>(sukep tu zunga)</em>.\n[[$1|thumb]]\npikinsa tangan <strong>[[:$1]]</strong>.\namahica saayaway zunga tatenga’ ku tina ditek, anu mahini sa amana tu patapabaw sukep tu zunga.",
+       "file-thumbnail-no": "tangan kalungangan saku <strong>$1</strong> ku angangan.\nmahiza mapasukepay a zunga <em>( sukep tu zunga )</em>.\namahica izaw kisu saayaw tabakiay a zunga, kanca patapabaw saayaway a zunga, anucaay sa pisumad nipangangan.",
+       "fileexists-forbidden": "izaw tu malecaday a kalungangan a tangan, zumasatu la’cus mitahpu.\namahica kisu apatapabaw tina tangan, pitatiku ayaway a belih zumasatu pisaungay zuma kalungangan. [[File:$1|thumb|center|$1]]",
+       "fileexists-shared-forbidden": "kapulungan tangan-sulu izawtu malecaday kalungangan a tangan.\namahica kisu apatapabawtu tina tangan, pitatiku ayaway a belih zumasatu pisaungay zuma kalungangan.\n[[File:$1|thumb|center|$1]]",
+       "fileexists-no-change": "patapabaway a tangan atu ayza baziyong a <strong>[[:$1]]</strong> tada malecad.",
+       "fileexists-duplicate-version": "patapabaway a tangan sungila’ malecad tu {{PLURAL:$2|sasutili’ay maluman baziyong}}a <strong>[[:$1]]</strong>",
        "file-exists-duplicate": "tina tangan masaliyaw isasa’ay a {{PLURAL:$1|cacay|yadah}} tangan",
+       "file-deleted-duplicate": "tadamalecad tina tangan a tangan ([[:$1]]) i nasawniay masipu tu.\nkanca kisu milucek kya tanganay a masipuay a nasulitan nazipa’an iayaw miliyaw patapabaw.",
+       "file-deleted-duplicate-notitle": "sungila’ malecad tina tanganay a tangan nasawniay masipu tuway, zumasatu satezep paazih kya satangahan.\nanu caay henay kisu miliyaw patapabaw, kanca kisu milunguc sikinli miciwsace madimut tangan a misaungayay miliyaw ciwsya.",
        "uploadwarning": "patapabaw patalaw",
        "uploadwarning-text": "pisumad isasa’ay a tangan sapuelac atu mitanam aca.",
        "savefile": "misuped tu tangan",
        "upload-scripted-pi-callback": "la’cus patapabaw yamalyilu XML-stylesheet sapisadimel mituzu’ a tangan.",
        "uploaded-script-svg": "i mapatapabaway a SVG tangan matepa kapah micengseay a kuku’-ulic aazihen-paya \"$1\".",
        "uploaded-hostile-svg": "i mapatapabaway a SVG tangan yangse aazihen-paya matepa caay kaenapay a CSS.",
+       "uploaded-event-handler-on-svg": "caay kahasa i SVG tangan setin event-handler susin <code>$1=\"$2\"</code>",
+       "uploaded-href-unsafe-target-svg": "i patapabaw tuway a SVG tangan matepa href masasiket tayza caay zahkezay a kalunasulitan:URI papatuzu'an sa ku <code>&lt;$1 $2=\"$3\"&gt;</code>",
+       "uploaded-animate-svg": "i patapabaway a SVG tangan matepa \"animate\" aazihen-paya hakay pisaungay \"from\" susin <code>&lt;$1 $2=\"$3\"&gt;</code> misumad href.",
+       "uploaded-setting-event-handler-svg": "ilabu’ nu mapatapabaway a SVG tangan matepa <code>&lt;$1 $2=\"$3\"&gt;</code>,masatezep tu ku setin event-handler susin.",
        "uploaded-setting-href-svg": "masatezep pisaungay \"set\" aazihen-paya amicunus \"href\" susin ta mama yinsu.",
+       "uploaded-wrong-setting-svg": "i mapatapabaway a SVG tangan matepa <code>&lt;set to=\"$1\"&gt;</code>, masatezep pisaungay \"set\" aazihen-paya micunus remote/data/script papatuzu'an ta amahicahica susin.",
+       "uploaded-setting-handler-svg": "nay patapabaw a SVG tangan matepa <code>$1=\"$2\"</code>, masatezep SVG pisaungay remote/data/script misetin \"handler\" susin.",
+       "uploaded-remote-url-svg": "ilabu’ nu mapatapabaway a SVG tangan matepa <code>$1=\"$2\"</code>, masatezep SVG pisaungay amahicahica tu ibaatay a URL misetin maazihan.",
        "uploaded-image-filter-svg": "ilabu’ mapatapabaway a SVG tangan matepa zunga sebseb-sakaluk pisaungay URL:<code>&lt;$1 $2=\"$3\"&gt;</code>.",
        "uploadinvalidxml": "la’cus mitingalaw matapabaway tangan a XML.",
+       "uploadjava": "kya tangan ku yamalyilu Java .class tangan a ZIP tangan.\nzayhan Java tangan hakay takud han zahkez kinsa pakazahkezan a munday, caay mahasa patapabaw Java tangan.",
        "upload-source": "saangangan a tangan",
        "sourcefilename": "saangangan tangan kalungangan:",
        "sourceurl": "saangangan URL:",
        "upload-options": "patapabaw mapiliay",
        "watchthisupload": "miazih tina tangan",
        "filewasdeleted": "nasawniay izaw tu ku malecaday kalungangan a tangan patapabaw, masiputu nazikuzan.\nkanca kisu a patapabaw tina tangan pataayaw mikinsa $1.",
+       "filename-thumb-name": "tina mahiza u cacay sukep tu zunga a satangahan. amana kapisukep tu zunga patapabaw patiku malecad u Wiki. anusa, kapisumad tu pyawti, sakaizaw nu imiatu caay kahalu u sukep tu zunga saayaway sulit.",
+       "filename-bad-prefix": "patapabaway a tangan kalungangan nu misu nu <strong>\"$1\"</strong> angangan, inayi’ ku amahicahica tu sapuelac sakaizaway a kalungangan, hina nay sueyi sasasing lunuk masanga’.\nu tangan nu misu pisaungay cacay mangaleb izaw sapuelac sakaizaway a kalungangan.",
        "upload-proto-error": "padinwaay a ketun caay katatenga’",
+       "upload-proto-error-text": "ibaat patapabaw tangan maydih pisaungay ku <code>http://</code> saca <code>ftp://</code> angangan a URL.",
        "upload-file-error": "ilabuay a mungangaw",
        "upload-file-error-text": "mitanam i sefu-kikay patizeng singa’an tu sulit sa tahkal labuay mungangaw.\npimasukazih [[Special:ListUsers/sysop|mikuwanay]].",
        "upload-misc-error": "caay kapulita patudud mungangaw",
+       "upload-misc-error-text": "napatapabaw sa izaw caay kapulita mungangaw.\npikinsa URL nu misu kapahtu haw, zumasatu kapah misuped-miala, zuma aca sa pitanam aca.\namahica izaw hanay munday, pipatakus [[Special:ListUsers/sysop|mikuwanay]].",
        "upload-too-many-redirects": "kya URL miliyaw patatuzu’ tayza sayadah zuma a puenengan",
        "upload-http-error": "HTTP mungangaw: $1",
        "upload-copy-upload-invalid-domain": "tina calay-subal(wangyi) caay mahasa kopi patapabaway a tangan.",
        "upload-dialog-button-upload": "patapabaw",
        "upload-form-label-infoform-title": "pulitaay a kalunasulitan",
        "upload-form-label-infoform-name": "kalungangan",
+       "upload-form-label-infoform-name-tooltip": "tukubic a sapuelac tu tangan pyawti, amalusapapangangan. kapah kisu pisaungay u nu misu a kamu atu nayi’ ku cacan malusapipangangan, amana kapiyamalyilu tangan mikilulay-tangan-ngangan.",
        "upload-form-label-infoform-description": "sapuelac",
        "upload-form-label-infoform-description-tooltip": "sapuyu’en ku kamu sapuelac tu nalimaan amahicahicaay tu matatungus pibuhci tu kamu i kalukawaw.\ntinaku i sasing, taneng mapatahkal ku sasakamuen i sasing, kalukawawatu kahicelaan.",
        "upload-form-label-own-work": "uyni ku nalimaan nu maku",
        "upload-form-label-infoform-categories": "kakuniza",
        "upload-form-label-infoform-date": "demiad",
        "upload-form-label-own-work-message-generic-local": "milucek tu kaku patapabaway a tangan nu maku maduduc tu ku isasa’ay {{SITENAME}} miedapay a cedang atu sapabeli tu kinli a cedang",
+       "upload-form-label-not-own-work-message-generic-local": "amahica kisu la’cus patubeli mikilul {{SITENAME}} tu amikuwanay-pasayzaay patapabaw tu tangan, kapiedeb tina a sasukamu-liwang zumasatu mitanam tu zuma a sakaluk.",
        "upload-form-label-not-own-work-local-generic-local": "kapah kisu mitanam [[Special:Upload|pataayaw tu kawaw a patapabaw kasabelih]].",
        "upload-form-label-own-work-message-generic-foreign": "matineng tu kaku patapabaw tina tangan tayza i cacay kapulungan suped-sulu, milucek tu kaku miduduc tu nu wiki a cedang atu sapabeli tu kinli a likec.",
        "upload-form-label-not-own-work-message-generic-foreign": "anu makai kisu patubeli miduduc kasasimel suped-sulu a amikuwanay-pasayzaay patapabaw tu tangan,piedeb tina sasukamu-liwang a mitanam tuzumaay a sakaluk.",
+       "upload-form-label-not-own-work-local-generic-foreign": "amahica tina tangan teneng mikilul kya kakacawan a pabeli tu kinli amikuwanay-pasayzaay patapabaw ku tangan, hakay kisu maydih kakelul mitanam pisaungay  [[Special:Upload|{{SITENAME}} a patapabaw kasabelih]]",
        "backend-fail-stream": "la’cus pabahel tu tangan \"$1\".",
        "backend-fail-backup": "la’cus mikopi tu tangan \"$1\".",
        "backend-fail-notexists": "tangan $1 inayi’.",
        "backend-fail-connect": "la’cus misasiket tayza suped-sulu zikuzan malutapiingan \"$1\"",
        "backend-fail-internal": "suped-sulu zikuzan \"$1\" izaw caay kapulitaay a mungangaw.",
        "backend-fail-contenttype": "la’cus taazihan misuped i \"$1\" a tangan lacul nikalahizaan",
+       "backend-fail-batchsize": "suped-sulu zikuzan masaungay $1 pice-tangan {{PLURAL:$2|misaungay}}; sisetyimo kelec ku $2 {{PLURAL:$2|misaungay}}.",
        "backend-fail-usable": "u nanu tungus caay kataneng saca dilyikotoling/sapilacul mahedaw, la’cus miasip saca misuliten tangan \"$1\" .",
        "filejournal-fail-dbconnect": "la’cus papatun tu zikuzan nu suped-sulu \"$1\" a nasulitan-nazipa’an  kalunasulitan-sulu.",
        "filejournal-fail-dbquery": "la’cus misabaluh suped-sulu zikuzan \"$1\" a nasulitan-nazipa’an kalunasulitan-sulu",
        "uploadstash-exception": "la’cus misuped patapabaw tayza suped-sulu ($1): \"$2\".",
        "invalid-chunk-offset": "la’cus atilad kakitizaan",
        "img-auth-accessdenied": "kakaian misuped-miala",
+       "img-auth-nopathinfo": "caay kataneng PATH_INFO aazihen-sulyang.\nmilacul kisu sefu-kikay caay patudu’ tina cesyun,\nhakay kisu pisaungay CGI ku palana’an a sefu-kikay, zumasatu caay midama img_auth sasahicaan.\npiazih tu tatenga’ay  https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
        "img-auth-notindir": "masetin patapabaway a dilyikotoling piazihan-tu-sulit inayi’ matuzu’ay nu misu a pazazan.",
        "img-auth-badtitle": "la’cus pisaungay \"$1\" patizeng kapahay a satangahan.",
        "img-auth-nologinnWL": "caay henay kisu patalabu, zumasatu \"$1\" inayi’ ilabu nu mahasa piazihan-tu-sulit.",
        "img-auth-nofile": "tangan \"$1\" inayi’.",
        "img-auth-isdir": "imahini kisu mitanam misuped-miala dilyikotoling \"$1\".\nmahasa dada’ misuped-miala tangan.",
        "img-auth-streaming": "pabahel \"$1\" henay ayza",
+       "img-auth-public": "img_auth.php a sadama tu kawaw ngiw ngay caay koniay Wiki taneng patahkal tangan, tina Wiki masetin tu koniay Wiki.\nalawhani kazahkezan a zateng, mapasatezep tuway img_auth.php.",
        "img-auth-noread": "misaungayay inayi’ tungus maasip \"$1\"",
        "http-invalid-url": "la’cus URL: $1",
        "http-invalid-scheme": "caay midama \"$1\" maketunay a URL.",
        "http-curl-error": "imahini miala URL sa mungangaw: $1",
        "http-bad-status": "miteka HTTP milunguc izaw tu ku munday: $1 $2",
        "upload-curl-error6": "la’cus misiket tu calay-zazan(wanglu) ta URL",
+       "upload-curl-error6-text": "la’cus misiket tu calay-zazan(wanglu) ta matuzu’ay nu URL.\nkapiliyaw miteka mikinsa URL u tatenga’ay tu haw, nika malucekay tu calay-kakacawan(wangcan) kapah ku pisaungay.",
        "upload-curl-error28": "patapabaw mautang",
        "upload-curl-error28-text": "calay-kakacawan(wangcan) mangasiw patukil a tukiay kelec. \npikinsa kya calay-kakacawan(wangcan) malecek saungay haw? pihanhan henay pitaneng aca.\npatahkal nizateng tisuwan kapah kisu i caay makalahay a tuki mitanam misiket tu calay-zazan(wanglu).",
        "license": "sapabeli tu kinli a cedang",
        "linkstoimage": "isasaay {{PLURAL:$1| kasabelih  misiket |saka $1 a kasabelih misiket}}katukuh tina tangan:",
        "linkstoimage-more": "mangasiw $1 {{PLURAL:$1|kasabelih masasiket}} ta tina tangan.\nisasa’ay piazihan-tu-sulit pasilsil iayaway a dada’ {{PLURAL:$1|1 masasiket|$1 masasiket}} ta tina tangan a kasabelih.\nkapah tu kisu miciwsace [[Special:WhatLinksHere/$2|leku piazihan-tu-sulit]].",
        "nolinkstoimage": "nayi’ ku kasabelih masasiket katukuh tini a tangan.",
+       "morelinkstoimage": "miciwsace masasiket tayza tina a tangan i [[Special:WhatLinksHere/$1|sayadah masasiket]].",
        "linkstoimage-redirect": "$1 (tangan miliyaw patatuzu’) $2",
        "sharedupload": "tina tangan namakay $1 satu hakay masaungay nu zuma a cwanan.",
+       "sharedupload-desc-there": "tina tangan namakay $1 tu hakay masazuma tu ku cwanan tu pisaungay.\nkapiazih tu tatenga’ay [$2 tangan  sapuelac kasabelih] matineng tatalaayaway tu cesyun.",
        "sharedupload-desc-here": "kina tangan nay $1 hakay satu pisaungay tu zuma a cwanan.\nisasaay paazih kuyniay a tangan i [$2 tangan patahkal kasabelih] a patahkalay a lacul.",
+       "sharedupload-desc-edit": "tina tangan namakay $1 zumasatu hakay masazuma ku cwanan pisaungay.\nanu maydih kisu mikawaway-kalumyiti lacul misapuelac kapah tayza i [$2 tangan sapuelac kasabelih].",
+       "sharedupload-desc-create": "tina a tangan namakay $1 atu hakay masazuma ku cwanan pisaungay.\nanu maydih kisu mikawaway-kalumyiti lacul sapuelac kapah tayza i [$2 tangan sapuelac kasabelih].",
        "filepage-nofile": "inayi’ kalungangan a tangan.",
        "filepage-nofile-link": "inayi’ kalungangan a tangan, kapah tu kisu [$1 patapabaw].",
        "uploadnewversion-linktext": "patudud tina tangan nu baluhay a baziyong",
        "upload-disallowed-here": "la’cus kisu mitahpu tuyni a tangan.",
        "filerevert": "patiku $1",
        "filerevert-legend": "patiku tu tangan",
+       "filerevert-intro": "imahini kisu patiku tangan ayza <strong>[[Media:$1|$1]]</strong> ta [$4 i $2 $3 a baziyong].",
        "filerevert-comment": "mahicaay:",
        "filerevert-defaultcomment": "patiku tuway tayza i $1 $2 ($3) a baziyong",
        "filerevert-submit": "patiku",
        "filerevert-identical": "ayzaay baziyong a tangan atu mipili’ay baziyong malecalecad.",
        "filedelete": "misipu \"$1\"",
        "filedelete-legend": "misipu tu tangan",
+       "filedelete-intro": "maydih kisu ayza misipu tangan <strong>[[Media:$1|$1]]</strong> atu zuma sacahamin nazipa’an baziyong",
        "filedelete-intro-old": "imahini amisipu kisu <strong>[[Media:$1|$1]]</strong> i [$4 $2 $3] a baziyong.",
        "filedelete-comment": "mahicaay:",
        "filedelete-submit": "masipu",
        "filedelete-success": "masipu tu <strong>$1</strong>.",
+       "filedelete-success-old": "masipu tuway <strong>[[Media:$1|$1]]</strong> i $2 $3 a baziyong.",
        "filedelete-nofile": "<strong>$1</strong> inayi’.",
        "filedelete-nofile-old": "inayi’ matepa <strong>$1</strong> izaway matuzu’ay a susinay emicen henay baziyong.",
        "filedelete-otherreason": "zumaay/nicunusay a mahicaay:",
        "filedelete-maintenance": "midiput a demiad sa tangan misipu atu patiku nanunuz pisaungay.",
        "filedelete-maintenance-title": "la’cus masipu tu tangan",
        "mimesearch": "MIME kilim",
+       "mimesearch-summary": "tina kasabelih kapah mikilim tu tangan a MIME nikalahizaan.\nsulitan a kese:lacul nikalahizaan/sailuc-nikalahizaan saca lacul nikalahizaan/*, amahica <code>image/jpeg</code>.",
        "mimetype": "MIME nikalahizaan:",
        "download": "patasasa'",
        "unwatchedpages": "caayay kasisip a kasabelih",
        "listduplicatedfiles": "misaliyaw tu tangan piazihan tu sulit",
        "listduplicatedfiles-summary": "tina piazihan tu sulit yamalyilu sabaluhay baziyong a tangan atu zuma tangan misaliyaw a piazihan tu sulit, tina piazihan tu sulit dada’ paazih itiniay a tangan",
        "unusedtemplates": "caayay kasaungay a taazihan mitudung",
+       "unusedtemplatestext": "tina kasabelih pasilsil sacahamin i {{ns:template}} isasa’ nu pangangananay a salaedan caay macaliway a zuma kasabelih a taazihan mitudung.\niayaw misipu, maydihtu mikinsa izaw haw masasiket uyniyay taazihan mitudung a zuma kasabelih.",
        "randompage": "kakibalucu’ ay a kasabelih",
        "randompage-nopages": "i {{PLURAL:$2|pangangananay a salaedan}} inayi’ amahicahica tu kasabelih: $1.",
        "randomincategory": "kakibalucu’ kakuniza a kasabelih",
        "statistics-users": "mapanganganay tu [[Special:ListUsers|misaungayay]]",
        "statistics-users-active": "saungay nu binawlan a misaungayay",
        "pageswithprop": "izaw susin a kasabelih",
+       "pageswithprop-legend": "izaway kasabelih susin a kasabelih",
        "pageswithprop-text": "tina kasabelih sapalalitemuh tu kawaw pisaungay matuzu’ay susin a kasabelih",
        "pageswithprop-prop": "susin kalungangan:",
        "pageswithprop-submit": "mileku",
        "pageswithprop-prophidden-long": "midimut mangasiw tanaya’ a susin sulyang ($1) tuway",
        "pageswithprop-prophidden-binary": "madimut tusa-taayaw-eneng susin sulyang ($1)",
        "doubleredirects": "tusaay a miliyaw patatuzu’",
+       "doubleredirectstext": "tina kasabelih pasilsil miliyaw patatuzu’ ta zumaay miliyaw patatuzu’ kasabelih a kasabelih. paytusil yamalyilu sakacacay atu sakatusa miliyaw patatuzu’ kasabelih misiket, atu sakatusa zikuzan nu miliyaw patatuzu’ a papatuzu’an, sakatusa zikuzan nu miliyaw patatuzu’ a papatuzu’an sawsawni ku \"tetenga’ay\" a patatuzu’an kasabelih, ku sakacacay tu miliyaw patatuzu’ kasabelih apayizaay a kasabelih.\n<del>misipu-sipuuay-kenis</del> dayhiw kya kasacacay a munday sapawazay tuway.",
        "double-redirect-fixed-move": "[[$1]] mapahezek tu milimad.\ntina kasabelih malunuk misaimed zumasatu mamiliyaw patatuzau’ tu [[$2]].",
        "double-redirect-fixed-maintenance": "i midiput a kawaw sa lunuk misumad tupatibabaay miliyaw patatuzu’ nay [[$1]] katukuh [[$2]].",
        "double-redirect-fixer": "miliyaw patatuzu’ misumaday",
        "uncategorizedcategories": "cayhenay kakunizaay a kakuniza",
        "uncategorizedimages": "cayhen kakunizaay a tangan",
        "uncategorizedtemplates": "sapi kakuniza taazihan mitudung",
+       "uncategorized-categories-exceptionlist": "# yamalyilu caay kakapah Special:UncategorizedCategories mapatahkalay a kakuniza piazihan-tu-sulit, cacay kakuniza cacay a silsil, a \"*\" miteka, saku zuma tatebanan nu nisulitan (yamalyilu nayi’ ku cacan) sapatangah nu silsil amisekipo. pisaungay \"#\" dayhiw buhci tu kamu.",
        "unusedcategories": "caayay kasaungay a kakuniza",
        "unusedimages": "caayay kasaungay a tangan",
        "wantedcategories": "maydihay a kakuniza",
        "wantedpages": "maydihay a kasabelih",
+       "wantedpages-summary": "isasa’ay sa ku sayadahay masasiket a inayi’ay a kasabelih, izaw a dada’ miliyaw patatuzu’masasiket a kasabelih. amahica amiala inayi’ay a miliyaw patatuzu’ kasabelih, tayza [[{{#special:BrokenRedirects}}| malepi’ay a miliyaw patatuzu’ piazihan-tu-sulit]].",
        "wantedpages-badtitle": "kyu i lecapuay a satangahan la’cus: $1",
        "wantedfiles": "maydihay a tangan",
+       "wantedfiletext-cat": "isasa’ay a tangan masaungay sa, uyzasa tangan inayi’. hekal suped-sulu a tangan kanahatu mueneng, uyzasa tina piazihan-tu-sulit apasilsil tuway. tina kakuniza pacebaay a tubeli kasacacay saka <del>sipu-kenis</del> sacuzu’. zuma sa, kasabelih sipakabit ilabu' tangan inayi’ apaazih i piazihan-tu-sulit [[:$1]].",
+       "wantedfiletext-cat-noforeign": "isasa’ay a tangan mapasaungay tu uyzasa inayi’ay. tina a dada’ silabas kakaiyan tu, kasabelih sipakabit ilabu’ nika inayi’ay a tangan mapalaylay i [[:$1]]",
+       "wantedfiletext-nocat": "isasa’ tangan masaungay sa, zumasatu tangan inayi’. hekal suped-sulu a tangan kanahatu, uyzasa tina piazihan-tu-sulit apasilsil tuway. uyniyan paceba’ay a  kasacacay apacuzu’ ku <del> sipu-kenis </del> sacuzu’.",
        "wantedtemplates": "maydihay a taazihan mitudung",
        "mostlinked": "masasiket sayadahay a kasabelih",
        "mostlinkedcategories": "masasiket sayadahay a kakuniza",
        "deadendpagestext": "isasa’ay i {{SITENAME}} a kasabelih caay masasiket katukuh zuma kasabelih.",
        "protectedpages": "madiputay a kasabelih",
        "protectedpages-indef": "paazih a cacay inayi’-kakatekuhan midiputay kasabelih",
+       "protectedpages-summary": "tina kasabelih pasilsil ayza madiputay kasabelih. maydih palalitemuh tu kawaw madiputay satangahan piazihan-tu-sulit, piazih tu tatenga’ay [[{{#special:ProtectedTitles}}|{{int:protectedtitles}}]]",
        "protectedpages-cascade": "paazih a cacay patatusulay a midiput kasabelih",
        "protectedpages-noredirect": "midimut miliyaw patatuzu’ kasabelih",
        "protectedpagesempty": "inayi’ pisaungay uyniyay  aazihen-sulyang a madiputay kasabelih ayza.",
        "protectedpages-unknown-timestamp": "caay kapulita",
        "protectedpages-unknown-performer": "caay kapulita a misaungayay",
        "protectedtitles": "madiputay a satangahan",
+       "protectedtitles-summary": "tina kasabelih pasilsil ayza madiputay satangahan. maydih palalitemuh tu kawaw madiputay a kasabelih piazihan-tu-sulit, piazih tu tatenga’ay  [[{{#special:ProtectedPages}}|{{int:protectedpages}}]].",
        "protectedtitlesempty": "inayi’ pisaungay uyniyay aazihen-sulyang a madiputay  satangahan ayza.",
        "protectedtitles-submit": "paazih tu satangahan",
        "listusers": "misaungayay a piazihan tu sulit",
        "apisandbox-alert-field": "la’cus tina pisinga’an a sulyang.",
        "apisandbox-continue": "palalid",
        "apisandbox-continue-clear": "palawpis",
+       "apisandbox-continue-help": "{{int:apisandbox-continue}} kapah [https://www.mediawiki.org/wiki/API:Query#Continuing_queries palalid] sazikuz a milunguc, {{int:apisandbox-continue-clear}} amasipu palalid mahizaay a aazihen-sulyang.",
        "apisandbox-param-limit": "musulit <kbd>max</kbd> amisaungay satabakiay a kelec.",
        "apisandbox-multivalue-all-namespaces": "$1 (sacahamin pangangananay a salaedan)",
        "apisandbox-multivalue-all-values": "$1 (sacahamin sulyang)",
        "booksources-text": "isasa’ay a piazihan tu sulit yamalyilu zuma pacakay baluhay cudad saca malumanay cudad i calay-kakacawan(wangcan) masasiket, hakay izaw ku kaydihan nu misu a cudad pataayaw matineng tatalaayaway a cesyun.",
        "booksources-invalid-isbn": "nipabeli nu misu a ISBN caay tatenga’ay; pikinsa kopiay a saangangan mungangaw haw?",
        "magiclink-tracking-rfc": "pisaungay RFC kaliwaza misasiket kasabelih",
+       "magiclink-tracking-rfc-desc": "tina kasabelih misaungay RFC kaliwaza masasiketay a kasabelih, piazih tu tatenga’ay  [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Magic_links mediawiki.org] a hicaen malimad.",
        "magiclink-tracking-pmid": "masasiketay a kasabelih nu pisaungay PMID kaliwaza tina Wiki mapasatezep tu miliyaw patizeng tu mima",
+       "magiclink-tracking-pmid-desc": "tina kasabelih misaungay PMID kaliwaza masasiketay a kasabelih, piazih tu tatenga’ay  [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Magic_links mediawiki.org] a hicaen malimad.",
        "magiclink-tracking-isbn": "misasiketay a kasabelih nu pisaungay ISBN kaliwaza",
        "specialloguserlabel": "mileku-misaungayay:",
        "speciallogtitlelabel": "pamutekan (satangahan saca {{ns:user}}: misaungayay pakatineng misaungayay):",
        "allinnamespace": "sacahamin kasabelih ($1 pangangananay a salaedan)",
        "allpagessubmit": "mileku",
        "allpagesprefix": "kilul tu saayaw nu nakamuan palalitemuh tu kawaw kasabelih:",
+       "allpagesbadtitle": "matuzu’ay i kasabelih satangahan la’cus, yamalyilu labu kamu  saca labu’ay a Wiki i saayaway sulit.\nwiza hakay yamalyilu cacay saca yadahay la’cus micukaymas i pyawti a tatebanan nu nisulitan.",
        "allpages-bad-ns": "{{SITENAME}} inayi’ \"$1\" pangangananay a salaedan.",
        "allpages-hide-redirects": "midimut miliyaw patatuzu’ kasabelih",
        "cachedspecial-viewing-cached-ttl": "imahini kisu miciwsace tina belih a saduba'ay baziyong, pabaw-sahezek izaw $1 a mautang.",
        "cachedspecial-refresh-now": "ciwsace sabaluhay a baziyong.",
        "categories": "kakuniza",
        "categories-submit": "paazih",
+       "categoriespagetext": "isasa’ay ku yamalyilu kasabelih saca myiti a{{PLURAL:$1|kakuniza}}.\n[[Special:UnusedCategories|caay pisaungay a kakuniza]] caay itini paazih。\npiazih tu tatenga’ay [[Special:WantedCategories|maydih a kakuniza]].",
        "categoriesfrom": "paazih kakuniza miteka i:",
        "deletedcontributions": "masipuay a misaungayay paanin",
        "linksearch": "ihekal masasiket mikilim",
        "linksearch-pat": "mikilim aazihen a sulit:",
        "linksearch-ns": "pangangananay a salaedan:",
        "linksearch-ok": "kilim",
+       "linksearch-text": "kapah pisaungay kapulungan-kawiza tatebanan nu nisulitan tinaku *.wikipedia.org.\nkapulungan-kawiza tatebanan nu nisulitan kanca pisaungay i sapabaway tindud calay-belih(wangyi), tinaku \"*.org\".<br />\ndama {{PLURAL:$2|tungsyun syitin}} izaw: $1 (amahica caay matuzu’ay satu pataayaw tu kawaw pisaungay http:// tungsyun syitin).",
        "linksearch-line": "$1 nay $2 masasiket",
        "linksearch-error": "kapulungan-kawiza tatebanan-nisulitan pisaungay dada’ i angangan-kikay kalungangan a lingatu.",
        "listusersfrom": "paazih misaungayay miteka nay:",
        "activeusers-noresult": "caykatepa misaungayay.",
        "activeusers-submit": "paazih misaungayay i saungay nu binawlan",
        "listgrouprights": "misaungayay luyaluy a tungus",
+       "listgrouprights-summary": "isasa’ay sa ku tina Wiki a misaungayay luyaluy piazihan-tu-sulit,atu mahizaay a misuped-miala tungus.\nkapah kisu i [[{{MediaWiki:Listgrouprights-helppage}}|pulita cesyun]] matepa mahizaay tekeday tungus a cesyun.",
+       "listgrouprights-key": "buhci tu kamu:\n* <span class=\"listgrouprights-granted\"> pabelituay a tungus </span>\n* <span class=\"listgrouprights-revoked\"> palawpesay a tungus </span>",
        "listgrouprights-group": "luyaluy",
        "listgrouprights-rights": "kinli",
        "listgrouprights-helppage": "Help:Group rights",
        "listgrouprights-namespaceprotection-namespace": "pangangananay a salaedan",
        "listgrouprights-namespaceprotection-restrictedto": "mahasa misaungayay mikawaway-kalumyiti a tungus",
        "listgrants": "pabeli tu kinli",
+       "listgrants-summary": "isasa’ay sa ku pabeli tu kinli piazihan-tu-sulit masasiket misaungayay tungus. misaungayay kapah pabeli tu kinli inyungcense pisaungay ku canghaw nu heni, uyzasa kelic nu misaungayay pabeli inyungcense a tungus. palecad, inyungcense la’cus pisaungay misaungayay inayi’ay a tungus.\nhakay kisu kapah tu i [[{{MediaWiki:Listgrouprights-helppage}}|tini]] maala teked tungus ku zumaay a kalunasulitan.",
        "listgrants-rights": "kinli",
        "trackingcategories": "mikilul nazikuzan kakuniza",
+       "trackingcategories-summary": "tina kasabelih pasilsil nay MediaWiki zwanti lunuk masanga’ sapikilul nazikuzan kasabelih a kakuniza, uyniyay kakuniza a kalungangan kapah nay pangangananay a salaedan {{ns:8}} a mahizaay sisetyimo palatuh misaimed.",
        "trackingcategories-desc": "kakuniza milisimet a tatungus",
        "restricted-displaytitle-ignored": "sekipo paazih satangah a kasabelih",
+       "restricted-displaytitle-ignored-desc": "kasabelih masekipo tuway <code><nowiki>{{DISPLAYTITLE}}</nowiki></code>, zayhan kya satangahan i kasabelih tatenga’ay a satangahan caay kalecad.",
+       "noindex-category-desc": "pangangananay a salaedan mahasa, zumasatu izaw mosu a sulit <code><nowiki>__NOINDEX__</nowiki></code> caay pasilsil nu kikay-tademaw tu kapah pikiliman tu nisulitan miasip a kasabelih.",
+       "index-category-desc": "pangangananay a salaedan mahasa atu izaw ku mosuay a sulit <code><nowiki>__INDEX__</nowiki></code> pay kikay-tademaw pasilsil kapah pikiliman tu nisulitan miasip a kasabelih.",
        "post-expand-template-inclusion-category-desc": "namicuwat taazihan mitudung tuway kya tabaki’ mangawi <code>$wgMaxArticleSize</code> hamaw layad taazihan mitudung caay malecek micuwat kasabelih.",
+       "post-expand-template-argument-category-desc": "namicuwat taazihan mitudung aazihen-sulyang, pinalu hacica tabaki mangasiw  <code>$wgMaxArticleSize</code> a kasabelih (zuma ilabu’ nu tulu kwahaw, tinaku <code>{{{Foo}}}</code>)",
+       "expensive-parserfunction-category-desc": "kasabelih pisaungay mieluc kayadah sisetyimo  katahkalan nu kalisiw a tingalaw-sakaluk hansu-sausi(tinaku <code>#ifexist</code>).\npiazih tu tatenga’ay  [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgExpensiveParserFunctionLimit Manual:$wgExpensiveParserFunctionLimit].",
        "broken-file-category-desc": "yamalyilu malepi’ a tangan masasiket a kasabelih (sipakabit ilabu tangan masasiket a tangan inayi’).",
+       "hidden-category-category-desc": "i lacul misaungay <code><nowiki>__HIDDENCAT__</nowiki></code> a kakuniza, kapah midimut pataayaw tu kawaw i kasabelih paazihay a kakuniza masasiket atilad.",
        "trackingcategories-nodesc": "inayi’ ku silaheciay a sapuelac",
        "trackingcategories-disabled": "mapasatezep kakuniza tuway",
        "mailnologin": "inayi’ pabahel puenengan",
+       "mailnologintext": "kanca kisu iayaw [[Special:UserLogin|patalabu]]\nzumasatu i [[Special:Preferences|setin tu kanamuhan]]\nmisetin cacay kapahay a imyiyo(email) puenengan kya kapah pabahel tigami tu zuma misaungayay.",
        "emailuser": "imyiyo(email) patakus tina misaungayay",
        "emailuser-title-target": "imyiyo(email) tu{{GENDER:$1|misaungayay}}",
        "emailuser-title-notarget": "imyiyo(email) patakus misaungayay",
+       "emailpagetext": "kapah kisu pisaungay isasa’ay a sapat nu sulit pabahel imyiyo(email) tu ku tina {{Gender:$1| misaungayay}}.\ni [[Special:Preferences|setin tu kanamuhan]] pasulitan a imyiyo(email) puenengan mala ku tigamiay a \"patigamiay\", sisa kya misaungayay kapah kakelul patubeli tisuwanan.",
        "defemailsubject": "{{SITENAME}} misaungayay \"$1\" patigamiay a imyiyo(email)",
        "usermaildisabled": "misaungayay imyiyo(email) mapasatezep tu",
        "usermaildisabledtext": "la’cus pabahel imyiyo(email) tayza tian Wiki zumaay a misaungayay",
        "emailccsubject": "napatigami $1 nu misa a palatuh mikilulay-cudad: $2",
        "emailsent": "mapatahkal tu i imyiyo(email)",
        "emailsenttext": "patigamitu ku imyiyo(email) palatuh nu misu.",
+       "emailuserfooter": "tina imyiyo(email) sanay $1 micaliw {{SITENAME}} a \"{{int:emailuser}}\" sasahicaan {{GENDER:$1|patigami}}{{GENDER:$1|$2}}. amahica {{GENDER:$2|kisu}} patubeli tina imyiyo(email), {{GENDER:$2|misuay}} tigami akakelul patigmi katukuh {{GENDER:$1|nuayaway a patigamiay}}, {{GENDER:$1|ciniza|ciniza|ciniza}}taneng maazih {{GENDER:$2|misuay}}imyiyo(email) puenengan.",
        "usermessage-summary": "subelid tu latuh tayza i sisetyimo.",
        "usermessage-editor": "sisetyimo palatuh",
        "watchlist": "miazihay a piazihan tu sulit",
        "watchlistanontext": "patalabu henay amiciwsace saca  misumad miazihay a piazihan-tu-sulit a kasacacay.",
        "watchnologin": "caay henay patalabu",
        "addwatch": "cunusen tu miazihay a piazihan-tu-sulit",
+       "addedwatchtext": "i [[Special:Watchlist|numisuay miazihay a piazihan-tu-sulit]] macunusen tu ku kasabelih \"[[:$1]]\" atu ku matatengil kasabelih.\nanucilacila sakay tina kasabelih atu kya masasiket sasukamu kasabelih a sumad amapasilsil itini piazihan-tu-sulit.",
+       "addedwatchtext-talk": "\"[[:$1]]\" atu mahizaay u kasabelih micunus tutayza itisuwan [[Special:Watchlist|miazihay a piazihan tu sulit]]",
        "addedwatchtext-short": "miazihay a piazihan-tu-sulit nu misu macunus tu kasabelih \"$1\".",
        "removewatch": "misipu nay misisip a piazihan-tu-sulit",
+       "removedwatchtext": "makayza [[Special:Watchlist|tisuwan miazihay a piazihan tu sulit]] misipu kasabelih \"[[:$1]]\" atu u matatengil kasabelih.",
+       "removedwatchtext-talk": "makayza tisuwan [[Special:Watchlist|miazihay a piazihan tu sulit]] misipu \"[[:$1]]\" atu mahizaay u kasabelih.",
        "removedwatchtext-short": "miazihay a piazihan-tu-sulit nu misu masipu kasabelih \"$1\" tuway.",
        "watch": "miazih",
        "watchthispage": "miazih tuyni kasabelih",
        "enotif_subject_moved": "{{SITENAME}} $2 milimad kasabelih $1",
        "enotif_subject_restored": "{{SITENAME}} $2 patiku kasabelih $1 tuway",
        "enotif_subject_changed": "{{SITENAME}} $2 masumad tuway kasabelih $1",
+       "enotif_body_intro_deleted": "{{SITENAME}} $2 masipu tu kasabelih $1 i $PAGEEDITDATE, piazihen i $3.",
+       "enotif_body_intro_created": "{{SITENAME}} a kasabelih $1 i $PAGEEDITDATE ngay misaungayay $2 {{GENDER:$2| patizeng}}, piazih ayzaay a sumad $3",
+       "enotif_body_intro_moved": "{{SITENAME}} a kasabelih $1 i $PAGEEDITDATE ngay misaungayay $2 {{GENDER:$2| milimad}}, piazih ayzaay a sumad $3.",
+       "enotif_body_intro_restored": "{{SITENAME}} a kasabelih $1 i $PAGEEDITDATE ngay misaungayay $2 {{GENDER:$2| patiku}}, piazih ayzaay a sumad $3",
+       "enotif_body_intro_changed": "{{SITENAME}} a kasabelih $1 i $PAGEEDITDATE ngay misaungayay $2 {{GENDER:$2|misumad}}, piazih ayzaay a sumad $3.",
        "enotif_anon_editor": "paceba panganganay a misaungayay $1",
        "enotif_minoredit": "payni mikilulay a mikawaway-kalumyiti",
        "deletepage": "misipu tu kasabelih",
        "confirm": "malucekaytu",
        "excontent": "lacul nu: \"$1\"",
+       "excontentauthor": "lacul ku:\"$1\", zumasatu dada’ ku cacay paaninay tu kalusasing \"[[Special:Contributions/$2|$2]]\" ([[User talk:$2|sasukamu]])",
        "exbeforeblank": "caay henay palawpes hamin a lacul ku: \"$1\"",
        "delete-confirm": "misipu \"$1\"",
        "historywarning": "<strong> patalaw:</strong> imahini amisipuay a kasabelih nu misu yamalyilu $1 {{PLURAL:$1|sumad}} nazipa’an",
        "historyaction-submit": "paazih",
+       "confirmdeletetext": "imahini kisu masipu cacay kasabelih saca zunga atu kya sacahamin nazipa’an.\npilucek matineng kisu miteka tina saungay patatuzu’ a heci, zumasatu piluceki saungay nu misu matatungus [[{{MediaWiki:Policy-url}}]] a kakilulen.",
        "actioncomplete": "pahezek tu ku saungay",
        "actionfailed": "saungay mungangaw",
        "deletedtext": "masipu tu \"$1\".\npiazih tu tatenga’ay $2 ciwsace capi demiad a masipuay a nasulitan.",
        "deletecomment": "mahicaay:",
        "deleteotherreason": "zumaay/nicunusay a mahicaay:",
        "deletereasonotherlist": "zuma a mahicaay",
+       "deletereason-dropdown": "*  maazihay tu sa masipu tu mahicaay \n** sizuma sa palatuh \n** pauning\n** maalaw nisanga’an niza tu tungus a kawaw \n** masacudaday milunguc\n** malepi’ay miliyaw patatuzau’",
        "deleteprotected": "tina kasabelih madiputay tu, la’cus kisu misipu tina kasabelih.",
+       "deleting-backlinks-warning": "<strong>patalaw:</strong>imahini kisu masipuay a kasabelih izaw [[Special:WhatLinksHere/{{FULLPAGENAME}}|zuma kasabelih]] masasiket saca nicaliwan.",
        "rollback": "panukasan mikawaway-kalumyiti",
        "rollbacklink": "panukasan",
        "rollbacklinkcount": "patiku {{PLURAL:$1|mikawaway tu kalumyiti}}",
        "cantrollback": "la’cus patiku mikawaway-kalumyiti;\ntina kasabelih a sazikuz paaninay u kinacacay a masacudaday.",
        "alreadyrolled": "la’cus patiku nay [[User:$2|$2]] ([[User talk:$2|sasukamu]]{{int:pipe-separator}}[[Special:Contributions/$2|{{int:contribslink}}]] sapihica sazikuzay cacay mikawaway-kalumyiti [[:$1]], izawtu zumaay tatemaw mikawaway-kalumyiti saca patikutu kya kasabelih.)\n\nsazikuzay a cacay mikawaway-kalumyiti kya kasabelih a misaungayay sa ku [[User:$3|$3]] ([[User talk:$3|sasukamu]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]).",
        "editcomment": "mikawaway-kalumyiti pecu’ nu lacul ku: <em>$1</em>.",
+       "revertpage": "mapatiku tuway [[Special:Contributions/$2|$2]] ([[User talk:$2|sasukamu]]) a mikawaway-kalumyiti sazikuzay nay [[User:$1|$1]] amisumad nu ayaway a baziyong",
+       "revertpage-nouser": "mapatiku tu midimut misaungayay ku mikawaway-kalumyiti malasazikuz {{GENDER:$1|[[User:$1|$1]]}} masumad nu ayaway a baziyong",
+       "rollback-success": "mapatiku tuway {{GENDER:$3|$1}} mapasanga’ay a mikawaway-kalumyiti;\nmisumad tatiku nay {{GENDER:$4|$2}} masumad nu ayaway sazikuzay a baziyong.",
        "rollback-success-notify": "mapatiku $1 nikawawan mikawaway-kalumyiti;\nmisumad patiku ta $2 masumad nu ayaway a sazikuz cacay baziyong. [$3 paazih ku masumaday]",
        "sessionfailure-title": "kasasiket mungangaw",
        "sessionfailure": "kisu patalabu kasasiketan mahiza simunday,\nsaka pataayaw-milangat kasasiketan maalaw atu madebung, tina saungay mapalawpes tuway.\npitatiku ayaway a kasabelih, miliyaw maasip kya kasabelih pitaneng aca.",
        "log-name-contentmodel": "lacul tatudungen misanga’ misumad nasulitan nazipa’an",
        "log-description-contentmodel": "tina kasabelih pasilsil tu kasabelih lacul tatudungen misanga’ sumad misulit atu pisaungay caay pataayaw tu kawaw tu lacul nu tatudungen misanga’ patizeng tu kasabelih",
        "logentry-contentmodel-new": "$1 {{GENDER:$2|masaungay tuway}} caay pataayaw tu kawaw a lacul tatudungen misanga’ \"$5\" patizeng kasabelih $3",
+       "logentry-contentmodel-change": "$1 {{GENDER:$2|masumad tuway}} kasabelih $3 u lacul tatudungen misanga’ nay \"$4\" ta \"$5\"",
        "logentry-contentmodel-change-revertlink": "patiku",
        "logentry-contentmodel-change-revert": "patiku",
        "protectlogpage": "midiput nasulitan nakawawan",
+       "protectlogtext": "isasa’ ku misumad kasabelih midiputay a piazihan-tu-sulit.\npiazih tu tatenga’ay [[Special:ProtectedPages|madiputay kasabelih piazihan-tu-sulit]] ciwsace ayzaay madiputay kasabelih.",
        "protectedarticle": "madiput tuway \"[[$1]]\"",
        "modifiedarticleprotection": "masumad tuway \"[[$1]]\" a midiput tindud",
        "unprotectedarticle": "mahulak tu \"[[$1]]\" a diput",
        "protect_expiry_old": "Expiration time is in the past.",
        "protect-unchain-permissions": "mihulak pamutek yadahay a midiput mapiliay",
        "protect-text": "kapah kisu itini miciwsace atu misumad kasabelih <strong>$1</strong> a midiput tu kasalaylay.",
+       "protect-locked-blocked": "kasabelih malangat tuway, la’cus misumad midiput kasalaylay.\nisasa’ay kasabelih <strong>$1</strong> ayzaay a setin:",
+       "protect-locked-dblock": "nasulitan-sulu maazihen, la’cus misumad midiput kasalaylay.\nzikuz u kasabelih <strong>$1</strong> ayza setin:",
+       "protect-locked-access": "numisu a canghaw inayi’ ku tungus mabalic midiput kasaliyliy.\nisasa’ay kasabelih <strong>$1</strong> ayzaay a setin:",
        "protect-default": "mahasa sacahamin misaungayay",
        "protect-fallback": "mahasa dada’ ku \"$1\" situngusay a misaungayay",
        "protect-level-autoconfirmed": "mahasa dada’ lunuk malucekay tu misaungayay",
        "protect-existing-expiry-infinity": "masetin tuway a kakatekuhan: inayi’kakatekuhan",
        "protect-otherreason": "zumaay/nicunusay a mahicaay:",
        "protect-otherreason-op": "zuma a mahicaay",
+       "protect-dropdown": "* maazihay a pidiput mahicaay \n** matabesiw tu ku pisala’cus\n** tadayadah ku tatuni’ palatuh \n** hatay tu kaizawan a mikawaway-kalumyiti ngayaw\n** takalaw ku talabu’ay i kasabelih",
        "protect-edit-reasonlist": "midiput a mahicaay nu mikawaway-kalumyiti",
+       "protect-expiry-options": "1 a tuki:1 hour,1 a demiad:1 day,1 a lipay:1 week,2 a lipay:2 weeks,1 a bulad:1 month,3 a bulad:3 months,6 a bulad:6 months,1 a mihca:1 year, haynisadikud:infinite",
        "restriction-type": "tungus:",
        "restriction-level": "kelec kasaselal:",
        "minimum-size": "makaadidi’ay a sausi",
        "undeletepage": "ciwsace atu patiku masipuay tu kasabelih",
        "undeletepagetitle": "<strong>isasa’ay mala [[:$1|$1]] masipuay tu masumad nu ayaway baziyong</strong>.",
        "viewdeletedpage": "ciwsace masipuay tu kasabelih",
+       "undeletepagetext": "isasa’ay a {{PLURAL:$1|kasabelih masipu tuway|kasabelih masipu tuwat}} uyzasa maemicen henay, kapah henay patiku.\nmaemicenay a tangan hakay paenengay a tuki milawpes.",
        "undelete-fieldset-title": "patiku misumad nu ayaway",
+       "undeleteextrahelp": "amahica apatiku sacahamin a kasabelih nazipa’an,palawpes hatizaay sacahamin mapili’ay atilad atu sapecec <strong><em>{{int:undeletebtn}}</em></strong>.\namahica apatiku matuzu’ay a kasabelih nazipa’an, pipilil’ apatikuay a sumad mapili’ay atilad atu sapecec <strong><em>{{int:undeletebtn}}</em></strong>.",
        "undeleterevisions": "$1{{PLURAL:$1|baziyong}}misipu",
+       "undeletehistory": "amahica kisu patiku kya kasabelih, sacahamin a masumaday a nazipa’an amapatiku amin.\namahica masipu tuway anu izaw ku misaungay malecaday a kalungangan patizeng baluhayay kasabelih, mapatikuay a masumaday nazipa’an atahkal iayaw nu tina nazipa’an nu kasabelih.",
        "undeleterevdel": "amahica sabaluhayay a kasabelih saca tangan masumad nu ayaway masipu tu liyad, a la’cus mileku palawpes misipuay a saungay.\nanumahiza sa kanca mipili’ hatizaay saca palawpes  midimut masipuay tu sabaluhay a sumad nu ayaway.",
        "undelete-revision": "masipu nu $3 a $1 (i $4 $5) masumad nu ayaway",
        "undeleterevision-missing": "la’cus saca mahedaw a nisumadan.\nhakay kisu pisaungay tu mungangaway misiket, saca kya amasumad nu ayaway namaemicen patiku saca masipu.",
+       "undeleterevision-duplicate-revid": "la’cus patiku {{PLURAL:$1|1 masumad nu ayaway|$1 masumad nu ayaway}}, zayhan {{PLURAL:$1|masumad nu ayaway a}} <code>rev_id</code> mapasaungay tuway.",
        "undelete-nodiff": "inayi’ nasawniay a sumad.",
        "undeletebtn": "patiku",
        "undeletelink": "ciwsace/patiku",
        "undeleteinvert": "kabelihan mipili’",
        "undeletecomment": "mahicaay:",
        "cannotundelete": "liyad saca hamin a palawpes  misipu mungangaw:\n$1",
+       "undelete-header": "kapiazih tu tatenga’ay [[Special:Log/delete|masipu nasulitan nazipa’an]] palalitemuh tu kawaw capi demiad masipuay kasabelih.",
        "undelete-search-title": "mikilim masipuay a kasabelih",
        "undelete-search-prefix": "paazih kasabelih miteka nay:",
        "undelete-search-submit": "kilim",
        "undelete-error": "palawpes masipu kasabelih  mungangaw",
        "undelete-error-short": "palawpes masipu tangan mungangaw: $1",
        "undelete-error-long": "imahini palawpes misipu tangan bahal mungangaw:\n\n$1",
+       "undelete-show-file-confirm": "pilucek kisu maydih miciwsace tu tangan \"<nowiki>$1</nowiki>\" i $2 $3 masipuay tu masumad nu ayaway?",
        "undelete-show-file-submit": "hang",
        "namespace": "pangangananay a salaedan:",
        "invert": "kabelihan mipili’",
        "block": "milangat misaungayay",
        "unblock": "mihulak malangat misaungayay",
        "blockip": "milangat {{GENDER:$1|misaungayay}}",
+       "blockiptext": "suliten isasa’ay a aazihan cudad taneng malangat uzumaay IP puenengan saca misaungayay a mikawaway-kalumyiti tungus.\nizaw dada’ saka mitena’ mipeci’, atu matatungus [[{{MediaWiki:Policy-url}}|pasayzaan saca amikuwanay-pasayzaay]] a zasatu sasa misaungay.\npiisasa’ suliten cacay masaheciay a mahicaay(tinaku: kilul ku kumu cacay mapapeci’ay a kasabelih).\nkapah kisu pisaungay [//ais.wikipedia.org/wiki/ inayi’ kakuniza subal luyou CIDR]kamu-sakilul kese milangat IP subal, satabakiay mahasa a subal IPv4 ku /$1, IPv6 ku /$2.",
        "ipaddressorusername": "IP puenengan saca misaungayay a kalungangan:",
        "ipbexpiry": "kakatekuhan:",
        "ipbreason": "mahicaay:",
+       "ipbreason-dropdown": "* sawsawni maazihay a langat mahicaay \n** suliten pacebaaay a cesyun \n** masipu kasabelih a lacul \n** payakayak hekalay a kuwangkaw masasiket\n** i kasabelih suliten inayi’ay sakaizaway a sulit \n** palawacu’ay a saungay, madebung/mibulibul tu tawan\n** patahtah kayadahay a canghaw \n** pisaungay kabidangay misaungayay a kalungangan",
        "ipb-hardblock": "amana pisaungay tina IP puenengan patalabuay a misaungayay mikawaway-kalumyiti",
        "ipbcreateaccount": "mitena’ patizeng baluhay canghaw",
        "ipbemailban": "satezep misaungayay pabahel imyiyo(email)",
        "ipb-confirm": "malucekay tu langat",
        "badipaddress": "la’cusay a IP puenengan",
        "blockipsuccesssub": "milangat malahci",
+       "blockipsuccesstext": "[[Special:Contributions/$1|$1]] malangat tuway.<br />\npiazih tu tatenga’ay [[Special:BlockList|malangat piazihan-tu-sulit]] amikinsa ayzaay a langat.",
        "ipb-blockingself": "amilangat tu kisu haw! malucek kisu amahiza haw?",
        "ipb-confirmhideuser": "amilangat kisu cacay a misaungayay zumasatu \"midimut\" kya misaungayay, apasatezep paazih sacahamin patahkal i piazihan-tu-sulit atu ibalu nu nasulitan a misaungayay a kalungangan.\ntatenga’ kisu ahatizaan haw?",
        "ipb-confirmaction": "anu malucekay hinien mahica asa kisu, kapikinsa tu isasa’ay \"{{int:ipb-confirm}}\" langat-pisinga’an.",
        "blocklog-showsuppresslog": "tina misaungayay nasawniay malangat zumasatu midimut.\nisasa’ay sa u satezep paazih kiluk amiazih tu tatenga’ay",
        "blocklogentry": "malangat [[$1]] a kakatukuhan i $2 $3",
        "reblock-logentry": "misumad [[$1]] a langat kakatukuhan katukuh $2 $3",
+       "blocklogtext": "uyniyan ku misaungayay langat atu palawpes malangat saungay a nasulitan.\ncaay pasilsil lunuk malangat a IP puenengan.\npiazih tu tatenga’ay [[Special:BlockList| malangat piazihan-tu-sulit ]] a ayza imahini kawaw a sapikawa atu milangat",
        "unblocklogentry": "mahulak malangat $1 tuway",
        "block-log-flags-anononly": "wiza dada’ paceba panganganay ku misaungayay",
        "block-log-flags-nocreate": "canghaw patizeng mapasatezep tuway",
        "ipb-otherblocks-header": "zuma {{PLURAL:$1|milangat}}",
        "unblock-hideuser": "u nanu tina misaungayay a kalungangan masetin tu midimut, la’cus kisu mihulak tina misaungayay a langat.",
        "ipb_cant_unblock": "mungangaw: inayi’ milangat ID $1, hakay mahulak tu kya langat.",
+       "ipb_blocked_as_range": "mungangaw: IP puenengan $1 caay kakelul palangat, sisa la’cus kakelul mihulak ku langat.\ntina IP puenengan i $2 malangatay a subal, kapah kisu mihulak tina subal a langat.",
        "ip_range_invalid": "la’cusay IP subal.",
        "ip_range_toolarge": "caay mahasa milangat kya taliyuk mangasiw /$1.",
        "proxyblocker": "kutay sefu-kikay milangat-kikay",
+       "proxyblockreason": "izay IP puenengan nu misu mawawah kutay sefu-kikay, sisa malangat tuway.\npitakus situngusay tu kawaw nu calay-zazan(wanglu) nu misu saca mikumi sitaneng midama, sakamuen tu heni tina mangaleb pihaceng kazahkezan a munday.",
+       "sorbsreason": "numisu a IP u puenengan i {{SITENAME}} pisaungayan  DNSBL mapala mawawah midayli  sefu-kikay",
+       "sorbs_create_account_reason": "numisu a IP puenengan i {{SITENAME}} pisaungay sa DNSBL mala mawawah kutay sefu-kikay.\nla’cus kisu panganganen ku canghaw.",
        "softblockrangesreason": "IP puenengan nu misu ($1) inayi’ mahasa paceba pangangan paanin, pipatalabu.",
        "xffblockreason": "IP puenengan nu misu pisaungay X-Forwarded-For satangahan, kisu saca pisaungay nu misu a kutay sefu-kikay malangat tuway.\nmalangatay a mahicaay ku:$1",
+       "cant-see-hidden-user": "maydih kisu milangatay a misaungayay malangat tuway zumasatu midimutay tuway.\ninayi’ kisu pidimut misaungayay tungus, la’cus kisu miciwsace saca mikawaway-kalumyiti sa misaungayay langat setyitase.",
        "ipbblocked": "izay kisu malangat, sisa la’cus milangat saca mihulak malangatay a zuma  misaungayay",
        "ipbnounblockself": "caay mahasa mihulak malangat tu kisu.",
        "lockdb": "pamutek tu sulu nu kalunasulitan",
        "unlockdb": "mihulak azihen nasulitan-sulu",
+       "lockdbtext": "pamutek nasulitan-sulu apapatezep sacahamin misaungayay mikawaway-kalumyiti kasabelih, misumad setin tu kanamuhan, mikawaway-kalumyiti miazihay a piazihan-tu-sulit atu zuma maydih misumad tu nasulitan-sulu a saungay.\npiluceki amahica kisu haw? zumasatau imahini midiput tu kawaw pahezek sa mihulak nasulitan-sulu a pamutek.",
+       "unlockdbtext": "mihulak pamutek nasulitan-sulu amasasa sacahamin misaungayay mikawaway-kalumyiti kasabelih, misumad setin tu kanamuhan, mikawaway-kalumyiti miazihay a piazihan-tu-sulit atu zuma maydih misumaday nasulitan-sulu a saungay.\npilucek kisu amahini kisu haw?",
        "lockconfirm": "hang, maydih kaku pamutek kalunasulitan-sulu.",
        "unlockconfirm": "hang, maydih kaku mihulak pamutek sulu nu nasulitan",
        "lockbtn": "pamutek tu sulu nu kalunasulitan",
        "lockdbsuccesssub": "malahci pamutek sulu nu nasulitan tuway",
        "unlockdbsuccesssub": "misipu pamutek tu sulu nu kalunasulitan tuway",
        "lockdbsuccesstext": "mamutek tu ku nasulitan-sulu. <br />\namana kapawan anu mahemin midiput pahezek [[Special:UnlockDB| mihulak pamutek ]] nasulitan-sulu.",
+       "lockfilenotwritable": "inayi’ tungus suliten nasulitan-sulu pamutek tu tangan.\ncalay-belih(wangyi) sefu-kikay maydih tu tangan a suliten tungus u azihen atu mihulak pamutek nasulitan-sulu.",
        "databaselocked": "pamutek tuway ku kalunasulitan-sulu",
        "databasenotlocked": "caay pamutek henay ku kalunasulitan-sulu",
        "lockedbyandtime": "(nay {{GENDER:$1|$1}} i $2 a $3)",
        "move-page": "milimad $1",
        "move-page-legend": "milimad kasabelih",
        "movepagetext": "isasa’ay a aazihan cudad kapah sapihica miliyaw pangangan cacay kasabelih, atu milimad mikabu kya kasabelih a sacahamin nazipa’an nasulitan ta izaw baluhay kalungangan a kasabelih.\nmaluman pyawti a kasabelih amiliyaw patatuzu’ kasabelih, payiza pisaungay baluhay pyawti a kasabelih.\npikinsa izaw haw ku [[Special:DoubleRedirects|hatusa miliyaw patatuzu’]] saca [[Special:BrokenRedirects|malepi’ay a miliyaw patatuzu’]] maydih misumad.\nizaw kisu sikining ngay masasiket palalid payiza tatenga’ay a kakitizaan.\n\npiazihen, amahica baluhayay a kasabelih kalungangan masaungay tuway, kyu tina kasabelih <strong> caay </strong> malimad tu zuma kakitizaan, anu caay baluhay kalungangan miliyaw patatuzu’ kasabelih zumasatu inayi’ amahicahica  mikawaway-kalumyiti nazipa’an.\nmahiza sakamu, kapah kisu milimad mungangaw ta zuma kalungangan a kasabelih patiku ta saayaway kalungangan,uyzasa amana mitahpu amahicahica tu ayzaay kasabelih.\n\n<strong> azihen: </strong>\ntina saungay sakay manamuhay kasabelih hakay tadamaan atu talibahalay a sumad;\niayaw nu saungay kaluceki matineng kisu haw hakay kya teluc nay milimad.",
+       "movepagetalktext": "amahica mipili’ tina atilad, mahizaay a sasukamu kasabelih amalunuk atu tina belih maladay milimad ta baluhay kakitizaan, ihekal sibaluhay kalungangan izawtu cacay muenengay sasukamu kasabelih.\nuyniyay a pulita, amahica izaw siceyo kanca kisu lima-saungay milimad saca mikabu tu izaway a kasabelih.",
+       "moveuserpage-warning": "<strong> patalaw:</strong>imahini kisu milimad misaungayay a kasabelih, piazihen dada’ misaungayay kasabelih amasumad kalungangan, zumasatu <em>caay</em> miliyaw pangangan misaungayay.",
+       "movecategorypage-warning": "<strong> patalaw:</strong>imahini kisu milimad kakuniza kasabelih。piazihen tina saungay dada’ milimad kasabelih,ilabu’ nu maluman kakuniza a kasabelih <em>caay</em> milimad ta baluhay kakuniza.",
+       "movenologintext": "kanca kisu ku mapangangan tuway a misaungayay atu [[Special:UserLogin|patalabu]] kya taneng milimad tu kasabelih",
        "movenotallowed": "inayi’ tungus milimad tu tangan.",
        "movenotallowedfile": "inayi’ tungus milimad tu tangan.",
        "cant-move-user-page": "inayi’ tungus kisu milimad misaungayay kasabelih (caay yamalyilu sailuc-kasabelih)",
        "exporttext": "kapah kisu patahkal matuzu’ay kasabelih saca kayadah belih a sulit atu mikawaway-kalumyiti nazipa’an, pisaungay XML kese a tabu.\nuyniyay tangan kapah pacumud ta zuma pisaungay  MediaWiki a Wiki, micaliw [[Special:Import|pacumud kasabelih]].\n\napatahkal kasabelih, isasa’ nu sulit atilad misulit kasabelih pyawti, cacay pyawti pisaungay cacay tusil, zumasatu pipili’ apatahkal ayzaay a sumad yamalyilu haw sacahamin nazipa’an masumad nu ayaway nasulitan, saca patahkal dada’ ayzaay a sumad atu sazikuzay mikawaway-kalumyiti a cesyun。\n\ni sulit atilad kapah tu kisu pisaungay masasiket, tinaku:[[{{#Special:Export}}/{{MediaWiki:Mainpage}}]] dayhiw patahkal kasabelih \"[[{{MediaWiki:Mainpage}}]]\"",
        "exportall": "patahkal sacahamin  kasabelih",
        "exportcuronly": "yamalyilu dada’ ayzaay a sumad nu ayaway, caay yamalyilu lekuay a sumad nu nazipa’an",
+       "exportnohistory": "<strong>piazihen:</strong>zayhan matalaw tu sefu-kikay silahciay munday, mapasatezep tu patahkal ku kasabelih a sacahamin nazipa’an misilit.",
        "exportlistauthors": "yamalyilu paykasabelih lekuay a piazihan-tu-sulit nu paaninay",
        "export-submit": "patahkal",
        "export-addcattext": "pisaungay kakuniza cunusen kasabelih:",
        "allmessagesname": "kalungangan",
        "allmessagesdefault": "pataayaw tu kawaw palatuh  a sulit",
        "allmessagescurrent": "ayzaay palatuh a sulit",
+       "allmessagestext": "tina kakitizaan pasilsil sacahamin i MediaWiki pangangananay a salaedanay a sisetyimo palatuh.\namahica kisu maydih mikileh mikuwanay a MediaWiki sanuniyazu’en, piazih tu tatenga’ay [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation MediaWiki sanuniyazu’en] atu [https://translatewiki.net translatewiki.net].",
        "allmessages-filter-legend": "kilim",
        "allmessages-filter": "kilul misumad zasatu mikilim:",
        "allmessages-filter-unmodified": "caay henay misumad",
        "thumbnail_image-failure-limit": "capi demiad paazih tina sukep tu zunga pinapian tuway mungangaw ($1 saca yadah ), pisawsawni teneng aca.",
        "import": "pacumuden kasabelih",
        "importinterwiki": "nay zuma wiki pacumud",
+       "import-interwiki-text": "pipili’ cacay Wiki atu kasabelih satangahan saka miteka pacumud.\nnaamahiza amisulit masumad nu ayaway demiad atu mikawaway-kalumyitiay a kalungangan.\nsacahaminay nay milakuit Wiki pacumuday a saungay amasulit i [[Special:Log/import|pacumud nasulitan nazipa’an]].",
        "import-interwiki-sourcewiki": "saangangan Wiki:",
        "import-interwiki-sourcepage": "saangangan kasabelih:",
        "import-interwiki-history": "kopi tina kasabelih sacahamin nazipa’an masumad nu ayaway",
        "import-nonewrevisions": "inayi’ pacumud amahicahica tu masumad nu ayaway (masumad nu ayaway izaw tuway, saca zayhan mungangaw sekipo)",
        "xml-error-string": "$1 i tusil $2, langat $3 ($4 wyiyincu$4): $5",
        "import-upload": "patapabaw XML kalunasulitan",
+       "import-token-mismatch": "kasasiketan kalunasulitan mahedaw.\n\nhakay kisu mapakatahkal. <strong> pilucek kisu i patalabu setyitase henay haw? atu pitaneng aca </strong>.\namahica la’cus henay misaungay, pitanam [[Special:UserLogout|katahkal]] patalabu aca,zumasatu pikinsa saazihay-sakaluk nu misu mahasa tina kakacawan pisaungay cookie.",
        "import-invalid-interwiki": "amana pacumud nay matuzu’ay a Wiki.",
        "import-error-edit": "inayi’ tungus kisu mikawaway-kalumyiti kasabelih \"$1\", la’cus micumud",
        "import-error-create": "inayi’ tungus kisu patizeng kasabelih \"$1\", la’cus micumud.",
        "import-error-interwiki": "kasabelih \"$1\" kalungangan maliwan hizantu mala hekalay masasiket (interwiki) pisaungay, la’cus pacumud.",
        "import-error-special": "kasabelih \"$1\" tungusay nu caay mahasa kasabelih a sazumaay azihen pangangananay a salaedan, la’cus  pacumud.",
        "import-error-invalid": "kasabelih \"$1\" pacumud tina Wiki a kalungangan la’cus, la’cus pacumud.",
+       "import-error-unserialize": "kasabelih \"$1\" sumad $2 la’cus pabelih-salaylay. tina masumad nu ayaway pisaungay $3 lacul tatudungen misanga’ i $4 a sasakawawen pasalaylayan.",
+       "import-error-bad-location": "tina a Wiki a kasabelih \"$1\" caay pidama pisaungay tu lacul tatudungen misanga’ $3, masumad nu ayaway $2 la’cus misuped tayza i kasabelih.",
        "import-options-wrong": "{{PLURAL:$2|mapili’ay}} mungangaw: <nowiki>$1</nowiki>",
        "import-rootpage-invalid": "matuzu’ay a limit-kasabelih satangahan la’cus.",
        "import-rootpage-nosubpage": "pangangananay a salaedann \"$1\" a lamit kasabelih caay mahasa sailuc-kasabelih",
        "creditspage": "kasabelih kasakumi nu misayingaay",
        "nocredits": "tina kasabelih inayi’ kasakumi nu misayingaay cesyun.",
        "spamprotectiontitle": "misapili’ a cengse nu babakahen a sulit",
+       "spamprotectiontext": "misulitan a sulit lacul nu misu mapasatezepan misuped tu tatuni’ palatuh misebsebay a cengse, hakay zayhan misu a lacul yamalyilu tu malangat ku hekal masasiket.",
        "spamprotectionmatch": "isasa’ay a lacul mateka’ babakahen a sulit sebseb cengse:$1",
        "spambot_username": "misadimel MediaWiki babakahen a sulit",
        "spam_reverting": "patiku tayza caay yamalyilu $1 masasiket a sabaluhay masumad nu ayaway",
        "filedelete-archive-read-only": "calay-belih(wangyi) sefu-kikay inayi’ malangat-misupet dilyikotoling \"$1\" suliten a tungus.",
        "previousdiff": "← malumanay a mikawaway tu kalumyiti",
        "nextdiff": "baluhayay mikawaway tu kalumyit →",
+       "mediawarning": "<strong>patalaw:</strong> tina tangan nikalahizaan hakay yamalyilu padetengan la’cusay kodo.\namahica mikawaw hakay u numisu a sisetyimo sakalepi’an.",
        "imagemaxsize": "zunga pinalu hacica tabaki kelec:<br /><em>(saungay i tangan sapuelac kasabelih)</em>",
        "thumbsize": "sukep tu zunga hacica-tabaki:",
        "widthheightpage": "$1 × $2, $3 {{PLURAL:$3|ku kasabelih}}",
        "file-info-size-pages": "$1 × $2 syangsu, hacica ku tabaki nu tangan:$3,MIME kakuniza: $4, $5 {{PLURAL:$5|ku kasabelih}}",
        "file-nohires": "inay ku sangaleb takalaway a katingalaw, kapah tu nipabeli.",
        "svg-long-desc": "SVG tangan, maazihay hacica ku tabaki $1 × $2  syangsu, tangan hacica ku tabaki: $3",
-       "svg-long-desc-animated": "SVG tangan, maazihay hacica ku tabaki $1 × $2 syangsu, tangan hacica ku tabaki: $3",
+       "svg-long-desc-animated": "SVG kulit-iga tangan, maazihay hacica tabaki:$1 × $2 syangsu, tangan pinalu hacica tabaki: $3",
        "svg-long-error": "la’cusay a SVG tangan: $1",
        "show-big-image": "saayaway a tangan",
        "show-big-image-preview": "pataayaway miazih hacica ku tabaki: $1.",
        "file-info-gif-frames": "$1 {{PLURAL:$1|kulit sapat}}",
        "file-info-png-frames": "$1 {{PLURAL:$1|kulit sapat}}",
        "file-no-thumb-animation": "<strong>azihen: kyu sitaneng mikelec, tina tangan sukep tu zunga nayi’ ku laheci nu tunghwa</strong>",
+       "file-no-thumb-animation-gif": "<strong>piazihen: nay sitaneng kiyu makelec, tina nikalahizaan takalaw katingalaw GIF zunga inayi’ kananuwangay a teluc.</strong>",
        "newimages": "baluhay tangan a sulu nu zunga",
        "newimages-summary": "uyni sazumaay a kasabelih paazih sabaluhay patapabaw a tangan",
        "newimages-legend": "kilim",
        "saturday-at": "sakaenem a demiad nu lipay i $1",
        "sunday-at": "pilipayan i $1",
        "yesterday-at": "nacila i $1",
+       "bad_image_list": "pikilulen isasa’ay a kese:\n\npiazihan-tu-sulit kasacacay dada’ kapah (ku * angangan). paytusil a sakacacay masasiket kanca ku caay kakapahay a tangan misiket. malecad a tusil ihekal ku sakacacay a misiket, u nikasumaday amin, piazihan-tu-sulit, ku tangan kitizaan a kasabelih.",
        "metadata": "pulita tu kalunasulitan",
        "metadata-help": "uyni tangan labuay amin yamalyilu zuma cesyun, uyni a cesyun akay nay suewyi  sasasing asaca sapisekyin i tapang asaca suwyihwa saayaw katukuh pahzekan a nakawawan mapacunusay. anu nay tangan saayaway setyitase masumadtu, hatizaay pulita kalunasulitan akay la’cus mileku mabetil a misumad tu tangan.",
        "metadata-expand": "paazih pulita kalunasulitan",
        "namespacesall": "hamin",
        "monthsall": "hamin",
        "confirmemail": "milucekay tu imyiyo(email) puengan",
+       "confirmemail_noemail": "caay henay kisu kaw numisu [[Special:Preferences|setin tu kanamuhan]] ilabu’ misetin cacay kapahay a imyiyo(email) puenengan.",
+       "confirmemail_text": "{{SITENAME}} maydih kisu i pisaungay iayaw nu tigami sasahicaan sawantan imyiyo(email) puenengan nu misu.\nsapecec isasa’ay a pipenecan kapah pabahel imyiyo(email) cacay sapilucekay a tigami. kya tigami yamalyilu cacay silsil kodo masasiket;\ni saazihay-sakaluk nu misu pacumud tina masasiket sapilucek imyiyo(email) puenengan nu misu ku kapahay.",
+       "confirmemail_pending": "malucekay-kodo macaliw tu ku imyiyo(email) pabahel tisuwan, amahica kisu patizeng tuway ku canghaw nu misu nasawni, hakay sawsawni kya milayap.\namahica inayi’ malayap, piliyaw sapilunguc ku malucekay-kodo aca.",
        "confirmemail_send": "imyiyo(email) malucekay-kodo",
        "confirmemail_sent": "malucekay mapatigami tuway.",
+       "confirmemail_oncreate": "malucekay-kodo mapabahel katukuh imyiyo(email) puenengan nu misu.\npatalabu saungay cayka maydih pisaungay tina kodo, uyzasa anu izaw ku palana’an sasahicaan a imyiyo(email) miwawah i Wiki maydih pabeli tina kodo iayaw.",
+       "confirmemail_sendfailed": "{{SITENAME}} la’cus mapabahel ku numisu malucekay a tigami, kapikinsa tu imyiyo(email) puenengan izaw hakiya ku  la’cusay tatebanan nu nisulitan.\n\npabahelay pacubelis: $1",
        "confirmemail_invalid": "la’cus malucekay-kodo.\nkya kodo hakay mangasiw tuway.",
        "confirmemail_needlogin": "$1 pilucek tu misuay a imyiyo(email) puenengan nu misu.",
+       "confirmemail_success": "numisu a imyiyo(email) malucekay tu. kapah kisu ayza [[Special:UserLogin|patalabu]] misaungay tina a calay-kakacawan(wangcan).",
        "confirmemail_loggedin": "malucekay tu misuay imyiyo(email) puengengan.",
        "confirmemail_subject": "{{SITENAME}} imyiyo(email) puenengan malucekay tuway",
+       "confirmemail_body": "caay kapulita a tademaw (hakay kisu i, namakay IP puenengan $1) i {{SITENAME}} canghaw \"$2\" a imyiyo(email) puenengan misetin itini.\n\npiluceki tina canghaw tungusay numisuay,zumasatu pisaungay saazihay-sakaluk miwawah sasa masasiket amiwawah i {{SITENAME}} a imyiyo(email) sasahicaan:\n\n$3\n\namahica kisu *caay* pangangan tina canghaw,\npiwawah sasa masasiket palawpes imyiyo(email) milucek:\n\n$5\n\ntina lucekay-kodo amangasiw i $4.",
+       "confirmemail_body_changed": "caay kapulita a tademaw (hakay kisu i, namakay IP puenengan $1) a i {{SITENAME}} canghaw \"$2\" a imyiyo(email) puenengan misetin itini.\n\npiluceki tina canghaw tungusay numisuay, zumasatu pisaungay saazihay-sakaluk miwawah sasa masasiket amiwawah i {{SITENAME}} a imyiyo(email)  sasahicaan:\n\n$3\n\namahica kisu *caay* pangangan tina canghaw,\npiwawah sasa masasiket palawpes imyiyo(email) milucek:\n\n$5\n\ntina lucekay-kodo amangasiw i $4.",
+       "confirmemail_body_set": "caay kapulita a tademaw (hakay kisu i, namakay IP puenengan $1) a i {{SITENAME}} canghaw \"$2\" a imyiyo(email) puenengan misetin itini.\n\npiluceki tina canghaw tungusay numisuay, zumasatu pisaungay saazihay-sakaluk miwawah sasa masasiket amiwawah i {{SITENAME}} a imyiyo(email) sasahicaan:\n\n$3\n\namahica kisu *caay* pangangan tina canghaw,\npiwawah sasa masasiket palawpes imyiyo(email) milucek:\n\n$5\n\ntina lucekay-kodo amangasiw i $4.",
        "confirmemail_invalidated": "palawpes tu imyiyo(email) puenengan palucekay",
        "invalidateemail": "palawpes imyiyo(email) malucekay tuway",
        "notificationemail_subject_changed": "{{SITENAME}} panganganay a imyiyo(email) puenengan masumad tuway",
+       "notificationemail_body_changed": "namakay IP puenengan $1 a tademaw (hakay kisu i), i {{SITENAME}} misumad ku canghaw $2 a imyiyo(email) puenengan tu \"$3\".\n\namahica caay kisu mihica, pisiketen calay-kakacawan(wangcan) a mikuwanay",
+       "notificationemail_body_removed": "namakay IP puenengan $1 a tademaw (hakay kisu i) , i {{SITENAME}} masipu tu ku canghaw $2 a imyiyo(email) puenengan.\n\namahica caay kisu mihica, pisiketen calay-kakacawan(wangcan) a mikuwanay.",
        "scarytranscludedisabled": "[Interwiki miliyaw kodo caay ka saungayen]",
        "scarytranscludefailed": "[taazihan-mitudung $1 maasip  mungangaw]",
        "scarytranscludefailed-httpstatus": "[taazihan-mitudung $1 maasip mungangaw: HTTP $2]",
        "scarytranscludetoolong": "[URL tada tanayu’]",
        "deletedwhileediting": "<strong> patalaw:</strong>tinakasabelih i kisuwan miteka mikawaway-kalumyiti nazikuzan a masipu!",
+       "confirmrecreate": "naamahiza mikawaway-kalumyiti kisu sa, misaungayay [[User:$1|$1]] ([[User talk:$1|sasukamu]]) masipu tuway tina kasabelih, mahicaay ku:\n: <em>$2</em>\npilucek kisu tatenga’ maydih miliyaw patizeng tina kasabelih.",
        "recreate": "miliyaw miteka patizeng",
        "confirm-purge-title": "misipu tina kasabelih",
        "confirm_purge_button": "malucekay",
        "autosumm-new": "napatizeng tu kasabelih, lacul ku \"$1\"",
        "autosumm-newblank": "patizeng nayi’ ku cacan a kasabelih",
        "lag-warn-normal": "tina piazihan tu sulit hakay caay paazih tu macapiay demiad $1 {{PLURAL:$1| widi}}labu’ay a sumad.",
+       "lag-warn-high": "nanu nasulitan-sulu patukil mautang, tina piazihan tu sulit hakay caayay paazih capi demiad $1 {{PLURAL:$1|widi}}ilabu’ay misumad.",
        "watchlistedit-normal-title": "miazihay a piazihan tu sulit nu mikawaway-kalumyiti",
        "watchlistedit-normal-legend": "nay miazihay a piazihan-tu-sulit misipu satangahan",
+       "watchlistedit-normal-explain": "sasa’ay paazih i miazihay a piazihan-tu-sulit a satangahan nu mmisu.\namisipu satangahan, pipili’ kya satangahan itepalay a mapili’ay atilad zumasatu sapecec \"{{int:Watchlistedit-normal-submit}}\".\nkapah tu kisu [[Special:EditWatchlist/raw|mikawaway-kalumyiti saayaway miazihay a piazihan-tu-sulit]].",
        "watchlistedit-normal-submit": "misipu satangahan",
        "watchlistedit-normal-done": "masipu tuway nay miazihay a piazihan-tu-sulit nu misu {{PLURAL:$1|$1}} satangahan",
        "watchlistedit-raw-title": "mikawaway-kalumyiti saayaway misisip a piazihan-tu-sulit",
+       "watchlistedit-raw-explain": "isasa’ paazih i numisuay a miazihay a piazihan-tu-sulit a satangahan,taneng kisu micaliw ku ikawaway-kalumyiti picunusen atu misipu piazihan-tu-sulit kasacacay, cacay satangahan cacay ku tusil. \npahezek tu ku mikawaway-kalumyiti, pisapecec \"{{int:Watchlistedit-raw-submit}}\".\nkapah tu kisu [[Special:EditWatchlist|pisaungay tatungus sakawaw nu kalumyiti]]",
        "watchlistedit-raw-submit": "misabaluhay misisip tu piazihan-tu-sulit",
        "watchlistedit-raw-done": "misabaluh tuway miazihay a piazihan-tu-sulit nu misu.",
        "watchlistedit-raw-added": "macunus tu {{PLURAL:$1|atangahan}}",
        "watchlisttools-raw": "mikawaway-kalumyiti saayaway misisip a piazihan-tu-sulit",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1| sasukamu ]])",
        "timezone-local": "itizaay",
+       "duplicate-defaultsort": "<strong> patalaw:</strong> pataayaw tu kawaw tu kasalaylay pecec \"$2\" matineng mitahpu nasawniay pataayaw tu kawaw nu kasalaylay pecec \"$1\".",
+       "duplicate-displaytitle": "<strong>patalaw:</strong> paazih satangahan \"$2\" mitahpu ayaway paazih satangahan \"$1\".",
+       "restricted-displaytitle": "<strong>patalaw:</strong> anu paazih tu pyawti atu kasabelih tatenga’ay pyawti caay kalecad, masekipo tu paazih pyawti \"$1\".",
        "invalid-indicator-name": "<strong> mungangaw:</strong> kasabelih setyitase micuzu’ay tu kawaw a <code>name</code> susin amana inayi’",
        "version": "baziyong",
        "version-extensions": "malacul tu sacunusan a sakaluk",
        "version-license-not-found": "caay katepa tina sacunusay a pulita sapabeli tu kinli a cedang cesyun.",
        "version-credits-title": "$1 a kasakumi nu paaninay",
        "version-credits-not-found": "caay katepa tina sacunusay a pulita kasakumi nu misayingaay  cesyun",
+       "version-poweredby-credits": "tina Wiki nay <strong>[https://www.mediawiki.org/ MediaWiki]</strong> patizeng, sizakec ku nisanga’an niza tu tungus a kawaw © 2001-$1 $2.",
        "version-poweredby-others": "zuma",
        "version-poweredby-translators": "translatewiki.net mibelihay",
+       "version-credits-summary": "mikukay kami isasa’ay a tadeamw ku [[Special:Version|MediaWiki]] a paanin",
        "version-license-info": "MediaWiki ku paybalucu’ zwanti; kapah kisu sausi paybalucu’ zwanti kikingkay patahkalay a GNU hina kapulungan sapabeli tu kinli a cedang kakilulen, amiliyaw patiyak atu / saca amisumad tina cengse; namahicahica kisu sausi ku tina sapabeli tu kinli a cedang a sakatusa baziyong saca (kapah kisu mipili’ nay kisu) izikuzay a amahicahica baziyong.\n\ntina cengse patiyakay a patusukan sa ku maydih kapah nipabeli saedap, uyzasa caay mitelung amahicahica sikinin mihusiw; zumasatu caay milimek sakay matatungus-pacakayay saca uzumaay sasahicaan a matatungusay zasatu sikinin. kahica nu kawaw piazihan GNU hina kapulungan pabeli tu kinli.\n\nkanca kisu maladay tina cengse milayap [{{SERVER}}{{SCRIPTPATH}}/COPYING GNU hina kapulungan sapabeli tu kinli a cedang a mikilulay-cudad]; amahica inayi’, pipatigami patakus paybalucu’ zwanti kikingkay, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, saca [//www.gnu.org/licenses/old-licenses/gpl-2.0.html ipabaw nu calay miasip].",
        "version-software": "malacul tu ku zwanti",
        "version-software-product": "nasang’ay a tuutuud",
        "fileduplicatesearch-result-n": "tina tangan \"$1\" izaw {{PLURAL:$2|1 malecad misaliyaw|$2 malecad misaliyaw}}.",
        "fileduplicatesearch-noresults": "inayi’ ku kalungangan nu \"$1\" a tangan.",
        "specialpages": "sazumaay a kasabelih",
+       "specialpages-note-restricted": "* habutud sazumaay a kasabelih.\n* <span class=\"mw-specialpagerestricted\"> mapakelecay a sazumaay a kasabelih. </span>",
        "specialpages-group-maintenance": "aazihen a cudad nu midiputay",
        "specialpages-group-other": "sazumaay cidek a kasabelih",
        "specialpages-group-login": "patalabu / panganganen ku canghaw",
        "specialpages-group-developer": "saayaway miteka a misakakawaway sakaluk",
        "blankpage": "salengacay a kasabelih",
        "intentionallyblankpage": "kasabelih padetengan a miliwan.",
+       "external_image_whitelist": "#amana misumad tina tusil a sulit <pre>\n#piisasa’ suliten tatungusay pakatineng a zateng (maydih dada’ // ilaeday a lacul)\n# amakinsa ihekalay masasiketay a zunga matatungus  uyniyay sakacucek tu haw?\n# matatungus sakacucek misiket apaazih nay zunga,anu caay haw paazih masasiket dada’ \n#nay # angangan a tusil apahica ku buhci tu kamu \n#tina sakacucek caay pasawantanen tabakiay saca adidi’ay a sulit\n#pisulit i pabaw tina tusil ku sacahamin tatungusay pakatineng a zateng,amana misumad tina tusil a sulit </pre>",
        "tags": "kapah misumad tu tazihan a paya",
        "tag-filter": "[[Special:Tags|aazihen paya]] kilim:",
        "tag-filter-submit": "kilim",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|aazihen paya’}}]]: $2)",
        "tag-mw-contentmodelchange": "lacul tatudungen misanga’  misumad",
+       "tag-mw-contentmodelchange-description": "mikawaway-kalumyiti  [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel misumad kasabelih a lacul tatudungen misanga’].",
        "tags-title": "aazihen a paya",
        "tags-tag": "aazihen a paya  kalungangan",
        "tags-display-header": "ilabu nu misumad nasulitan-nazipa’an paazih sasakawawen",
        "tags-create-reason": "mahicaay:",
        "tags-create-submit": "patizeng",
        "tags-create-no-name": "manakanca kisu matuzu’ay cacay aazihen a paya kalungangan.",
+       "tags-create-invalid-chars": "aazihen-paya kalungangan la’cus yamalyilu widi-bacu (<code>,</code>) saca u cielis-kenis (<code>/</code>).",
        "tags-create-invalid-title-chars": "aazihen-paya kalungangan caay yamalyilu la’cus misaungayay kasabelih satangahan a tatebanan-nisulitan",
        "tags-create-already-exists": "aazihen a paya \"$1\" izaw tu.",
        "tags-create-warnings-above": "mitanam patizeng aazihen-paya \"$1\" tatukian mahica isasa’ {{PLURAL:$2|patalaw}}:",
        "tags-delete-title": "misipu tu aazihen a paya",
        "tags-delete-explanation-initial": "imahini amisipu kisu tu aazihen-paya \"$1\" nay kalunasulitan-sulu.",
        "tags-delete-explanation-in-use": "aazihen-paya amakay ayzaay imahini pisaungay a {{PLURAL:$2|$2 masumad nu ayaway saca nazipa’annasulitan nazipa’an kasacacay|$2 sumad nu ayaway saca nazipa’an kasacacay}} misipu.",
+       "tags-delete-explanation-warning": "tina saungay ku <strong>la’cus patikuay a</strong> zumasatu <strong>la’cus palawpesay a</strong>, kanahatu ku nasulitan-sulu a mikuwanay la’cus tu. piluceki kisu maydih misipuay a satangahan.",
+       "tags-delete-explanation-active": "<strong>aazihen-paya \"$1\" misaungay henay, la’cus misipu.</strong> anu pahanhan aazihen-paya misaungay, tayza micaedung kya aazihen-paya a kasabelih, zumastau itiza pasatezep kya aazihen-paya.",
        "tags-delete-reason": "mahicaay:",
        "tags-delete-submit": "la’cus palawpes misipu tina aazihen a paya",
        "tags-delete-not-allowed": "la’cus misipu nay sacunus misaheciay a aazihen-paya, anu... kya sacunus mahasa tuway",
        "tags-delete-not-found": "aazihen a paya \"$1\" inayi’.",
+       "tags-delete-too-many-uses": "aazihen-paya \"$1\" macaedung ta $2 makatusatusa {{PLURAL:$2|masumad nu ayaway}}, tina dayhiwtu aazihen-paya a la’cus masipu.",
        "tags-delete-warnings-after-delete": "aazihen-paya \"$1\" masipu tuway, nika matahkal isasa’ {{PLURAL:$2|patalaw}}",
        "tags-delete-no-permission": "inayi’ tungus masipu aazihen a paya kisu.",
        "tags-activate-title": "miteka aazihen a paya",
        "htmlform-datetime-placeholder": "YYYY-MM-DD HH:MM:SS",
        "htmlform-date-invalid": "matuzu’ kisu a sulyang caay kakapah ku taazihan a demiad, pitanam misaungay YYYY-MM-DD kese",
        "htmlform-time-invalid": "matuzu’ay a sulyang nu misu caay kacacay kapah taazihan a tuki, pitanam misaungay HH:MM:SS kese",
+       "htmlform-datetime-invalid": "matuzu’ay u sulyang nu misu caay ka cacay kapah tu u taazihan a demiad atu tatukian, kapitanam misaungay YYYY-MM-DD HH:MM:SS kese",
        "htmlform-date-toolow": "matuzu’ay a sulyang nu misu mahasaay a demiad i ayaw nu $1.",
        "htmlform-date-toohigh": "matuzu’ay a sulyang i zikuz nu mahasaay a demiad $1.",
        "htmlform-time-toolow": "namatuzu’ kisu a sulyang i ayaw nu saayaw mahasa a tuki $1.",
        "logentry-delete-revision": "ilabu nu $1 {{GENDER:$2|masumad tuway}} $3 {{PLURAL:$5|1 nazipa’an|$5 nasulitan nazipa’an}} a maazihay:$4",
        "logentry-delete-revision-legacy": "$1 {{GENDER:$2|masumad tuway}} kasabelih $3 masumad nu ayaway a maazihay",
        "logentry-suppress-delete": "$1 {{GENDER:$2|misatezep paazih tuway}} kasabelih $3",
+       "logentry-suppress-event": "$1 {{GENDER:$2|malimekan misumad}} $3 {{PLURAL:$5|1 nazipa’an|$5 nasulitan nazipa’an}} a maazihay: $4",
+       "logentry-suppress-revision": "$1 {{GENDER:$2|malimekan misumad}} kasabelih $3 {{PLURAL:$5|masumad nu ayaway|$5 masumad nu ayaway}} a maazihay:$4",
        "logentry-suppress-event-legacy": "$1 {{GENDER:$2|milimekan misumad}} $3 nasulitan nazipa’an a maazihay",
        "logentry-suppress-revision-legacy": "$1 {{GENDER:$2|masumad tu}} kasabelih $3 labu’ masumad nu ayaway a maazihay",
        "revdelete-content-hid": "madimut tu ku lacul",
        "revdelete-uname-unhid": "palawpes midimut misaungayay a kalungangan",
        "revdelete-restricted": "caedung mikuwanay a kelec tuway",
        "revdelete-unrestricted": "masipu tu ku mikuwanay a kelec",
+       "logentry-block-block": "$1 {{GENDER:$2|malangat tu}} {{GENDER:$4|$3}} kakatekuhan u $5 $6",
        "logentry-block-unblock": "$1 {{GENDER:$2|mahulaktu ku langat}} {{GENDER:$4|$3}}",
+       "logentry-suppress-block": "$1 {{GENDER:$2|malangat tu}} {{GENDER:$4|$3}} kakatekuhan u $5 $6",
+       "logentry-suppress-reblock": "$1 {{GENDER:$2|masumad tuway}} {{GENDER:$4|$3}} a milangatay a setin kakatekuhan sa ku $5 $6",
        "logentry-import-upload": "$1 nay tangan patapabaw {{GENDER:$2|pacumud}} $3",
        "logentry-import-upload-details": "$1 pisaungay tangan patapabaw tuway {{GENDER:$2|pacumud}} $3 ($4 {{PLURAL:$4| misumad nu ayaway}})",
        "logentry-import-interwiki": "$1 namakay zuma wiki {{GENDER:$2|pacumud}} $3",
+       "logentry-import-interwiki-details": "$1 namakayza $5 {{GENDER:$2|pacumud}} $3 ($4 {{PLURAL:$4|masumad nu ayaway}})",
        "logentry-merge-merge": "$1 pala $3 {{GENDER:$2|mikabu}} ta $4 (masumad nu ayaway baziyong ta $5)",
        "logentry-move-move": "$1 {{GENDER:$2|milimad tuway}} kasabelih $3 katukuh $4",
        "logentry-move-move-noredirect": "$1 {{GENDER:$2|malimad tuway}} kasabelih $3 katukuh $4, caay pisubelid miliyaw patatuzau’",
        "logentry-move-move_redir": "$1 malimad kasabelih $3 tu $4 zumasatu mitahpu nuayaway miliyaw patatuzau’",
+       "logentry-move-move_redir-noredirect": "$1 {{GENDER:$2|milimad tuway}} kasabelih $3 mitahpu miliyaw patatuzau’ kasabelih ta $4, caay piliwan miliyaw patatuzau’ kasabelih",
        "logentry-patrol-patrol": "$1 {{GENDER:$2|mapasilusi}} kasabelih $3 a sumad $4 ku mapatayza tu mikibi",
        "logentry-patrol-patrol-auto": "$1 malunuk tu {{GENDER:$2| silusi }} kasabelih $3 sumad $4 apatayza mikibi tu.",
        "logentry-newusers-newusers": "{{GENDER:$2|patizeng}} misaungayay canghaw tuway $1",
        "logentry-upload-overwrite": "$1 {{GENDER:$2|patapabaw tuway}} baluhay baziyong a $3",
        "logentry-upload-revert": "$1 {{GENDER:$2|masayacay tuway}} $3",
        "log-name-managetags": "tazihan-paya kuwan tu nasulitan",
+       "log-description-managetags": "tina kasabelih pasilsil mahizaay [[Special:Tags|aazihen-paya]] a kuwan kawaw kasacacay. i nazipa’an dada’ yamalyilu nay mikuwanay lima-saungay saka saungay; napatizeng nu Wiki zwanti saca masipu a aazihen-paya kasacacay, caay masulit itiniay a nasulitan nazipa’an",
        "logentry-managetags-create": "$1 {{GENDER:$2|mapatizeng yuway}} aazihen-paya \"$4\"",
+       "logentry-managetags-delete": "$1 {{GENDER:$2|masipu tuway}} aazihen-paya \"$4\" (malaheci tu nay $5 {{PLURAL:$5|masumad nu ayaway saca nasulitan nazipa’an}} misipu)",
        "logentry-managetags-activate": "$1 {{GENDER:$2|misaungay tuway}} aazihen-paya \"$4\" pabeli tu misaungayay atu kikay-tademaw pisaungay",
        "logentry-managetags-deactivate": "$1 {{GENDER:$2|mapasatezep tuway}} aazihen-paya \"$4\" pabeli misaungayay atu kikay-tademaw pisaungay",
        "log-name-tag": "aazihen a paya nasulitan nazipa’an",
+       "logentry-tag-update-add-revision": "$1 {{GENDER:$2|macunus}} tina {{PLURAL:$7|tazihan-paya}} $6 ta baziyong $4 nu belih $3",
        "logentry-tag-update-remove-revision": "$1 {{GENDER:$2|masipu tuway}} {{PLURAL:$9|aazihen-paya}} $8 nay kasabelih $3  sumad $4.",
        "logentry-tag-update-remove-logentry": "$1 {{GENDER:$2|masipu tuway}} {{PLURAL:$9|aazihen-paya}} $8 nay nasulitan nazipa’an kasacacay $3 a sumad $5.",
+       "logentry-tag-update-revision": "$1 {{GENDER:$2|misabaluh tuway}} aazihen-paya i kasabelih $3 a sumad $4 ({{PLURAL:$7|micunus}} $6; {{PLURAL:$9|misipu}} $8).",
+       "logentry-tag-update-logentry": "$1 {{GENDER:$2|misabaluh tuway}} aazihen-paya i kasabelih $3 a nasulitan nazipa’an kasacacay $5 ({{PLURAL:$7|micunus}} $6; {{PLURAL:$9|misipu}} $8)",
        "rightsnone": "(nayi’)",
        "rightslogentry-temporary-group": "$1 (nanunuz, katukuh $2)",
        "feedback-adding": "imahini micunus nabalucu’ hwidubaku tayza kasabelih...",
        "feedback-back": "tatiku",
        "feedback-bugcheck": "kapah! kinsaen nabalucu’an nu misu mapasilsil tu [$1 matinengay a munday] haw?",
        "feedback-bugnew": "makinsa tu kaku. patubeli baluhay a munday",
+       "feedback-bugornote": "amahica kisu amisilut pulita sapuelac cacay sitanengay a munday, pitayza [$1 patubeli tu kawaw].\nsaca kapah kisu pisaungay isasa’ay a dayumay aazihan cudad patubeli tu kawaw, misaungayay a kalungangan nu misu atu buhci tu kamu amacunusen tayza i \"[$3 $2]\" kasabelih.",
        "feedback-cancel": "palawpes",
        "feedback-close": "malahecitu",
        "feedback-external-bug-report-button": "munday nu patubeli",
        "feedback-dialog-title": "patahkal tu kamu mihwidubaku",
+       "feedback-dialog-intro": "kapah kisu misaungay isasa’ay a kadayuman aazihan cudad pabahel tu nabalucu’an hwidubaku nu misu. u nabalucu’an nu misu a misaungay tu nu misu misaungayay a kalungangan cunusen tayza i kasabelih \"$1\".",
        "feedback-error1": "mungangaw: la’cus sapulita API maminay a heci",
        "feedback-error2": "mungangaw: mikawaway-kalumyiti mungangaw",
        "feedback-error3": "mungangaw: API inayi’ patukil",
        "feedback-message": "palatuh:",
        "feedback-subject": "satangahan:",
        "feedback-submit": "patayzaan",
+       "feedback-terms": "matineng kaku u numaku a pisaungay makutay nu cesyun yamalyilu leku tu saazihsay-sakaluk atu kawaw sisetyimo baziyong cesyun, zumasatu u cesyun atu nabalucu’an nu hwidubakumapalung patahkal kasasimel.",
        "feedback-termsofuse": "patubeli kaku kilulen mamaala a cedang nipabeli nabalucu’an hwidubaku.",
        "feedback-thanks": "kukay! nu misu a nabalucu’an hwidubaku mapatiyak tu kasabelih \"[$2 $1]\"",
        "feedback-thanks-title": "kukay tisuwanan!",
        "limitreport-expansiondepth": "satakalaw micuwat ilabu",
        "limitreport-expensivefunctioncount": "sayadah-eluc tingalaw-sakaluk hansu kinapina",
        "expandtemplates": "micuwat tu taazihan a mitudung",
+       "expand_templates_intro": "tina sazumaay a kasabelih amicuwat ku sulit taazihan mitudung,kapah yamalyilu dama tingalaw-sakaluk kamu-sakilul, tinaku <code><nowiki>{{</nowiki>#language:…}}</code> atu nikasumad-asip tinaku <code><nowiki>{{</nowiki>CURRENTDAY}}</code>.\ntatenag’ sa, sahetu ilabu’ nu tusa-kwahaw a lacul amicuwat amin.",
        "expand_templates_title": "ulic satangahan, pabeli {{FULLPAGENAME}} atu zuma pisaungay:",
        "expand_templates_input": "suliten:",
        "expand_templates_output": "heci",
        "expand_templates_generate_xml": "paazih XML tingalaw-kilang",
        "expand_templates_generate_rawhtml": "paazih saayaway a HTML",
        "expand_templates_preview": "pataayaway miazih",
+       "expand_templates_preview_fail_html": "<em>zayhan kasasiketan a kalunasulitan mahedaw zumasatu {{SITENAME}} mawawah tu paazih saayaway HTML sasahicaan,saka pataayaw-milangat JavaScript madebung madimut tu pataayaway miazih a lacul.</em>\n\n<strong>amahica kisu ayzaay a pataayaway miazih saungay inayi’ ku zuma a pakayzaan, pitaneng aca.</strong>\namahica la’cus henay, pitanam [[Special:UserLogout| katahkal ]]atu patalabu aca.",
+       "expand_templates_preview_fail_html_anon": "<em>zayhan kisu caay henay patalabu atu {{SITENAME}} mawawah paazih saayaway HTML sasahicaan, apataayaw-milangat JavaScript madebung madimut tu pataayaway miazih lacul.</em>\n\n<strong>amahica kisu ayzaay a pataayaway miazih saungay inayi’ ku zumaay a pakayzaan, zikuz nu [[Special:UserLogin|patalabu]] pitaneng aca.</strong>",
        "expand_templates_input_missing": "maydih kisu pabeli hatizaay a sulit nasulitan nu misu.",
        "pagelanguage": "misumad kasabelih a kamu",
        "pagelang-language": "kamu",
        "default-skin-not-found": "ayah! kisu i <code dir=\"ltr\">$wgDefaultSkin</code> misetinay a Wiki  pataayaw tu kawaw nuhekalan <code>$1</code>  la’cus pisaungay.\n\nnilacul laylay nu misu kanca yamalyilu isasa’ay a {{PLURAL:$4| nuhekalan}}. piazih tu tatenga’ay  [https://www.mediawiki.org/wiki/Manual:Skin_configuration misaungay a cudad: nuhekalan a setin] kya maala hicaen {{PLURAL:$4|miwawah nuhekalan zumasatu misetin pataayaw tu kawaw sulyang}}a cesyun .\n\n$2\n\n; amahica kisu nasawni milacul tuway MediaWiki:\n: hakay kisu ku pisaungay git saca kakilul micaliw yuensma-kodo pisaungay zuma sakaluk milacul, u malecekay tina pulita。pitanam milacul  [https://www.mediawiki.org/wiki/Category:All_skins mediawiki.org a nuhekalan dilyikotoling] a liyad nuhekalan pisaungay isasa’ay a sasakawawen:\n:* patasasa’ [https://www.mediawiki.org/wiki/Special:MyLanguage/Download tarball milacul cengse], kya cengse yamalyilu yadahay nuhekalan atu sacunus. kapah kisu kopi atu mizepit i <code>skins/</code>dilyikotoling. \n:* nay [https://www.mediawiki.org/wiki/Special:SkinDistributor mediawiki.org] patasasa’ tekeday a nuhekalan tarball.\n:* [https://www.mediawiki.org/wiki/Download_from_Git#Using_Git_to_download_MediaWiki_skins pisaungay Git patasasa’ nuhekalan].\n: amahica kisu ku MediaWiki a saayaway miteka a misakakawaway,mahizaay kanca caay lawilawnumisuay a git suped-sulu.\n\n; amahica kisu nasawni pacakat MediaWiki:\n: MediaWiki 1.24 atu sasutili’ay baluhayay a baziyong caaytu lunuk miwawah malacul tu nuhekalan (piazih tu tatenga’ay  [https://www.mediawiki.org/wiki/Manual:Skin_autodiscovery misaungay a cuad: nuhekalan lunuk mikilim]). kapah kisu isasa’ {{PLURAL:$5|silsil}}pazepit i <code>LocalSettings.php</code> ngay miwawah {{PLURAL:$5| sacahamin }} ayzasa malacul tuway a {{PLURAL:$5|nuhekalan}}:\n\n<pre dir=\"ltr\">$3</pre>\n\n; amahica kisu nasawni misumad <code>LocalSettings.php</code>:\n: pisaliyaw milucek kisu misulit a nuhekalan  kalungangan mungangaw haw?",
        "default-skin-not-found-no-skins": "ayah! kisu i <code>$wgDefaultSkin</code> misetinay a Wiki pataayaw tu kawaw nuhekalan <code>$1</code> la’cus pisaungay.\n\ncaay milacul kisu amahicahica a nuhekalan.\n\n; amahica kisu nasawni milacul tuway saca pacakat tuway MediaWiki:\n: hakay kisu ku pisaungay git saca kakilul micaliw yuensma-kodo pisaungay zuma sakaluk milacul, u malecekay tina pulita. MediaWiki 1.24 saca sasutili’ay baluhayay a baziyong i angangan suped-sulu caay yamalyilu amahicahica a nuhekalan. pitanam milacul [https://www.mediawiki.org/wiki/Category:All_skins mediawiki.org a nuhekalan dilyikotoling] a liyad nuhekalan pisaungay isasa’ay a sasakawawen:\n:* patasasa’ [https://www.mediawiki.org/wiki/Special:MyLanguage/Download tarball milacul cengse], kya cengse yamalyilu yadahay nuhekalan atu sacunus. kapah kisu kopi atu mizepit i <code>skins/</code>dilyikotoling.\n:* nay [https://www.mediawiki.org/wiki/Special:SkinDistributor mediawiki.org] patasasa’ tekeday a nuhekalan  tarball.\n:* [https://www.mediawiki.org/wiki/Download_from_Git#Using_Git_to_download_MediaWiki_skins  pisaungay Git patasasa’ nuhekalan].\n: amahica kisu ku MediaWiki a saayaway miteka a misakakawaway, mahizaay kanca caay lawilawnumisuay a git suped-sulu。piazih tu tatenga’ay  [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Skin_configuration misaungay a cudad: nuhekalan setin] ngay maala hicaen miwawah  nuhekalan zumasatu misetin pataayaw tu kawaw sulyang a cesyun.",
        "default-skin-not-found-row-enabled": "* <code>$1</code> / $2 (mawawah tu)",
+       "default-skin-not-found-row-disabled": "* <code>$1</code> / $2 (<strong>mapasatezep tu</strong>)",
        "mediastatistics": "myiti sausi cesyun",
        "mediastatistics-summary": "patapabawan tu ku tangan nikalahizaan u sausi ku cesyun, tina aazihen cudad dada’ sausi tangan sefu-kikay baluhay baziyong, caay yamalyilu maluman atu masipuay tu baziyong.",
        "mediastatistics-nbytes": "{{PLURAL:$1|$1 wyiyincu}} ($2; $3%)",
        "authmanager-authn-not-in-progress": "zencen caay henay miteka saca kasasiketan kalunasulitan mahedaw tu, piliyaw miteka aca.",
        "authmanager-authn-no-primary": "nipabeliay a pincen la’cus mizencen.",
        "authmanager-authn-no-local-user": "nipabeliay a pincen inayi’ mahizaay amahicahica i tina wiki a misaungayay.",
+       "authmanager-authn-no-local-user-link": "nipabeli a pincen kapahtu uyzasa inayi’ masasiket amahicahica itini wiki a misaungayay. pisaungay zuma  sasakawawen patalabu, saca patizeng baluhay misaungayay, a izaw kisu ku mapili’ay kapah masasiket nasawniay a pincen nu misu tayza baluhay canghaw.",
        "authmanager-authn-autocreate-failed": "lunuk patizeng itiniay a canghaw mungangaw: $1",
        "authmanager-change-not-supported": "nipabeliay a pincen la’cus misumad, izay la’cus pisaungay henay.",
        "authmanager-create-disabled": "maedeb canghaw lunuk patizeng tuway.",
        "authmanager-provider-password": "Password-based zencen",
        "authmanager-provider-password-domain": "Password- atu domain-based zencen",
        "authmanager-provider-temporarypassword": "nanunuz a mima",
+       "authprovider-confirmlink-message": "kilul patalabu mitanam kinapina nu misu i capi demiad, isasa’ay a canghaw kapah masasiket katukuh wiki canghaw nu misu. masasiket canghaw kapah kisu pisaungay uyniyay canghaw patalabu. pipili’ maydih nu misu cacay masasiketay a canghaw.",
        "authprovider-confirmlink-request-label": "manakanca masasiket tu canghaw",
        "authprovider-confirmlink-success-line": "$1: masasiket tuway",
        "authprovider-confirmlink-failed": "canghaw masasiket caay kahamin malahci: $1",
index ac26e83..89cd878 100644 (file)
        "nosuchusershort": "لا يوجد مستخدم باسم $1\".\nتأكد من إملاء الاسم.",
        "nouserspecified": "يجب عليك تحديد اسم مستخدم.",
        "login-userblocked": "هذا المستخدم ممنوع. لا يسمح بالولوج.",
-       "wrongpassword": "كلمة السر التي أدخلتها غير صحيحة.\nمن فضلك حاول مرة أخرى.",
+       "wrongpassword": "اسم المستخدم أو كلمة السر التي أدخلتها غير صحيحة.\nمن فضلك حاول مرة أخرى.",
        "wrongpasswordempty": "كلمة السر المدخلة كانت فارغة.\nمن فضلك حاول مرة أخرى.",
        "passwordtooshort": "يجب أن تتكون كلمة السر على الأقل من {{PLURAL:$1|حرف واحد|حرفين|$1 حروف|$1 حرفا|$1 حرف}}.",
        "passwordtoolong": "كلمات السر لا يجب أن تكون أطول من  {{PLURAL:$1|1 حرف|$1 حروف}}.",
        "rcfilters-savedqueries-apply-and-setdefault-label": "إنشاء مرشح افتراضي",
        "rcfilters-savedqueries-cancel-label": "ألغ",
        "rcfilters-savedqueries-add-new-title": "احفظ إعدادات المرشحات الحالية",
-       "rcfilters-savedqueries-already-saved": "المرشحات محفوظة بالفعل",
+       "rcfilters-savedqueries-already-saved": "هذه المرشحات محفوظة بالفعل. غير إعداداتك لإنشاء مرشح محفوظ جديد.",
        "rcfilters-restore-default-filters": "استرجاع المرشحات الافتراضية",
        "rcfilters-clear-all-filters": "مسح كل المرشحات",
        "rcfilters-show-new-changes": "عرض أحدث التغييرات",
index d9fa9b0..9abf0be 100644 (file)
        "recentchanges-label-plusminus": "صفحه‌نین اؤلچوسو بایت میقداری ایله تعیین ائدیلیر",
        "recentchanges-legend-heading": "<strong>قیسالتمالار:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (بیرده [[Special:NewPages|یئنی صفحه‌لرین لیستینه]] باخین)",
+       "rcfilters-other-review-tools": "داها یوخلاما آلتلری",
+       "rcfilters-savedqueries-defaultlabel": "ذخیره اوْلونموش فیلترلر",
        "rcnotefrom": "آشاغی داکی دَییشیک لرده <strong>$3, $4</strong> (دن <strong>$1</strong> {{PLURAL:$5|چان گوستریلیب|چان گوستریلیب دیر}}).",
        "rclistfrom": "$3 $2 واختیندان باشلایاراق یئنی دییشیکلری گؤستر",
        "rcshowhideminor": "کیچیک دَییشیکلری $1",
index 309e431..2e19771 100644 (file)
        "nosuchusershort": "Удзельніка зь іменем «$1» не існуе. Праверце напісаньне.",
        "nouserspecified": "Вы мусіце пазначыць імя ўдзельніка.",
        "login-userblocked": "{{GENDER:$1|Гэты ўдзельнік заблякаваны|Гэтая ўдзельніца заблякаваная}}. Уваход у сыстэму забаронены.",
-       "wrongpassword": "Уведзены няслушны пароль. Калі ласка, паспрабуйце яшчэ раз.",
+       "wrongpassword": "Уведзенае няслушнае імя ўдзельніка ці пароль.\nКалі ласка, паспрабуйце яшчэ раз.",
        "wrongpasswordempty": "Быў уведзены пусты пароль. Калі ласка, паспрабуйце яшчэ раз.",
        "passwordtooshort": "Паролі павінны ўтрымліваць ня менш за $1 {{PLURAL:$1|сымбаль|сымбалі|сымбаляў}}.",
        "passwordtoolong": "Паролі ня могуць быць даўжэй за $1 {{PLURAL:$1|сымбаль|сымбалі|сымбаляў}}.",
        "email-blacklist-label": "Забараніць гэтым удзельнікам дасылаць мне лісты электроннай поштай:",
        "prefs-searchoptions": "Пошук",
        "prefs-namespaces": "Прасторы назваў",
-       "default": "па Ð·Ð¼Ð¾Ñ\9eÑ\87ванÑ\8cнÑ\96",
+       "default": "па змоўчаньні",
        "prefs-files": "Файлы",
        "prefs-custom-css": "Індывідуальны CSS",
        "prefs-custom-js": "Індывідуальны JS",
        "rcfilters-savedqueries-apply-and-setdefault-label": "Стварыць фільтар па змоўчаньні",
        "rcfilters-savedqueries-cancel-label": "Адмяніць",
        "rcfilters-savedqueries-add-new-title": "Захаваць цяперашнія налады фільтру",
-       "rcfilters-savedqueries-already-saved": "Гэтыя фільтры ўжо захаваныя",
+       "rcfilters-savedqueries-already-saved": "Гэтыя фільтры ўжо захаваныя. Зьмяніце вашыя налады, каб стварыць новы захаваны фільтар.",
        "rcfilters-restore-default-filters": "Аднавіць фільтры па змоўчаньні",
        "rcfilters-clear-all-filters": "Ачысьціць усе фільтры",
        "rcfilters-show-new-changes": "Праглядзець найноўшыя зьмены",
        "uploadstash-exception": "Не магу захаваць загрузку ў сховішчы ($1): «$2».",
        "uploadstash-bad-path": "Шлях не існуе.",
        "uploadstash-bad-path-invalid": "Шлях не зьяўляецца слушным.",
+       "uploadstash-bad-path-unknown-type": "Невядомы тып «$1».",
+       "uploadstash-bad-path-unrecognized-thumb-name": "Невядомая назва мініятуры.",
+       "uploadstash-bad-path-no-handler": "Ня знойдзены апрацоўнік для mime-тыпу $1 файлу $2.",
+       "uploadstash-bad-path-bad-format": "Ключ «$1» мае няслушны фармат.",
+       "uploadstash-file-not-found": "Ключ «$1» ня знойдзены ў схованцы.",
+       "uploadstash-file-not-found-no-thumb": "Не атрымалася здабыць мініятуру.",
+       "uploadstash-file-not-found-no-local-path": "Няма лякальнага шляху да маштабаванага элемэнту.",
+       "uploadstash-file-not-found-no-object": "Не атрымалася стварыць лякальны аб’ект файлу для мініятуры.",
+       "uploadstash-file-not-found-no-remote-thumb": "Памылка атрыманьня мініятуры: $1\nURL = $2",
        "invalid-chunk-offset": "Няслушнае зрушэньне фрагмэнту",
        "img-auth-accessdenied": "Доступ забаронены",
        "img-auth-nopathinfo": "Адсутнічае PATH_INFO.\nВаш сэрвэр не ўстаноўлены на пропуск гэтай інфармацыі.\nМагчма, ён працуе праз CGI і не падтрымлівае img_auth.\nГлядзіце https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
index 6c39836..03c7ece 100644 (file)
        "nosuchusershort": "\"$1\" নামের কোন ব্যবহারকারী নেই। নামের বানান পরীক্ষা করুন।",
        "nouserspecified": "আপনাকে অবশ্যই ব্যবহারকারী নাম নির্দিষ্ট করতে হবে।",
        "login-userblocked": "এই ব্যবহারকারীকে বাধা দেওয়া হয়েছে। প্রবেশ সম্ভব নয়।",
-       "wrongpassword": "à¦\86পনি à¦­à§\81ল à¦ªà¦¾à¦¸à¦\93য়ারà§\8dড à¦¬à§\8dযবহার à¦\95রà§\87à¦\9bà§\87ন। অনুগ্রহ করে আবার চেষ্টা করুন।",
+       "wrongpassword": "পà§\8dরদতà§\8dত à¦¬à§\8dযবহারà¦\95ারà§\80র à¦¨à¦¾à¦® à¦¬à¦¾ à¦ªà¦¾à¦¸à¦\93য়ারà§\8dড à¦­à§\81ল। অনুগ্রহ করে আবার চেষ্টা করুন।",
        "wrongpasswordempty": "পাসওয়ার্ড প্রবেশের ঘরটি খালি ছিল। দয়া করে আবার চেষ্টা করুন।",
        "passwordtooshort": "পাসওয়ার্ড কমপক্ষে {{PLURAL:$1|১ অক্ষরের|$1 অক্ষরের}} হতে হবে।",
        "passwordtoolong": "পাসওয়ার্ড {{PLURAL:$1|১|$1}} অক্ষরের চেয়ে দীর্ঘ হতে পারবে না।",
        "prefs-email": "ইমেইলের পছন্দগুলি",
        "prefs-rendering": "অবয়ব",
        "saveprefs": "সংরক্ষণ",
-       "restoreprefs": "সà¦\95ল à¦ªà§\82রà§\8dবনিরà§\8dধারিত à¦¸à§\87à¦\9fিà¦\82 à¦«à¦¿à¦°à¦¿à¦¯à¦¼à§\87 à¦\86নà§\8b (সকল অংশে)",
+       "restoreprefs": "সà¦\95ল à¦ªà§\82রà§\8dবনিরà§\8dধারিত à¦¸à§\87à¦\9fিà¦\82 à¦«à¦¿à¦°à¦¿à¦¯à¦¼à§\87 à¦\86নà§\81ন (সকল অংশে)",
        "prefs-editing": "সম্পাদনা",
        "searchresultshead": "অনুসন্ধান",
        "stub-threshold": "অসম্পূর্ণ নিবন্ধের সংযোগ ফরম্যাটিং-এর জন্য এরকম নিবন্ধের আকারের প্রান্তসীমা ($1):",
        "action-applychangetags": "আপনার পরিবর্তনগুলোর সাথে ট্যাগ সংযোজন করুন",
        "action-changetags": "নির্দিষ্ট সংস্করণ এবং লগ ভুক্তিগুলিতে যথেচ্ছভাবে ট্যাগ সংযোজন ও অপসারণ করা",
        "action-deletechangetags": "ডাটাবেজ থেকে ট্যাগ অপসরণ করার",
-       "action-purge": "এই পাতা হালনাগাদ করার",
+       "action-purge": "এই পাতাটি শোধন করুন",
        "nchanges": "$1টি {{PLURAL:$1|পরিবর্তন}}",
        "enhancedrc-since-last-visit": "{{PLURAL:$1|সর্বশেষ প্রদর্শনের পর}} $1টি",
        "enhancedrc-history": "ইতিহাস",
index 99f31db..ffa4bc0 100644 (file)
        "nosuchusershort": "No hi ha cap usuari anomenat «$1». Comproveu que ho hàgiu escrit correctament.",
        "nouserspecified": "Heu d'especificar un nom d'usuari.",
        "login-userblocked": "Aquest usuari està blocat. Inici de sessió no permès.",
-       "wrongpassword": "La contrasenya que heu introduït és incorrecta. Torneu-ho a provar.",
+       "wrongpassword": "El nom d'usuari o la contrasenya que heu introduït són incorrectes. Torneu-ho a provar.",
        "wrongpasswordempty": "La contrasenya que s'ha introduït estava en blanc. Torneu-ho a provar.",
        "passwordtooshort": "La contrasenya ha de tenir un mínim {{PLURAL:$1|d'un caràcter|de $1 caràcters}}.",
        "passwordtoolong": "La contrasenya ha de tenir un màxim {{PLURAL:$1|d'un caràcter|de $1 caràcters}}.",
        "timezoneregion-indian": "Oceà Índic",
        "timezoneregion-pacific": "Oceà Pacífic",
        "allowemail": "Permet que altres usuaris m'enviïn missatges per correu electrònic",
+       "email-blacklist-label": "Prohibeix a aquests usuaris que m'enviïn correus electrònics:",
        "prefs-searchoptions": "Cerca",
        "prefs-namespaces": "Espais de noms",
        "default": "per defecte",
        "prefs-editor": "Edició",
        "prefs-preview": "Previsualització",
        "prefs-advancedrc": "Opcions avançades",
+       "prefs-opt-out": "Renuncia a les millores",
        "prefs-advancedrendering": "Opcions avançades",
        "prefs-advancedsearchoptions": "Opcions avançades",
        "prefs-advancedwatchlist": "Opcions avançades",
        "rcfilters-grouping-title": "Agrupació",
        "rcfilters-activefilters": "Filtres actius",
        "rcfilters-advancedfilters": "Filtres avançats",
+       "rcfilters-limit-title": "Canvis a mostrar",
        "rcfilters-limit-shownum": "Mostra {{PLURAL:$1|el darrer canvi| els darrers $1 canvis}}",
        "rcfilters-days-title": "Darrers dies",
        "rcfilters-hours-title": "Hores recents",
        "rcfilters-restore-default-filters": "Restaura els filtres per defecte",
        "rcfilters-clear-all-filters": "Esborra tots els filtres",
        "rcfilters-show-new-changes": "Mostra els nous canvis",
-       "rcfilters-search-placeholder": "Filtra els canvis recents (navegueu o comenceu a escriure)",
+       "rcfilters-search-placeholder": "Filtra els canvis recents (utilitzeu el menú o cerqueu el nom del filtre)",
        "rcfilters-invalid-filter": "Filtre no vàlid",
        "rcfilters-empty-filter": "No hi ha cap filtre actiu. Es mostren totes les contribucions.",
        "rcfilters-filterlist-title": "Filtres",
        "rcfilters-filter-logactions-description": "Accions administratives, creacions de comptes, eliminacions de pàgines, càrregues…",
        "rcfilters-filtergroup-lastRevision": "Darreres revisions",
        "rcfilters-filter-lastrevision-label": "Darrera revisió",
-       "rcfilters-filter-lastrevision-description": "El canvi més recent a una pàgina.",
-       "rcfilters-filter-previousrevision-label": "Revisions anteriors",
+       "rcfilters-filter-lastrevision-description": "Només el canvi més recent a una pàgina.",
+       "rcfilters-filter-previousrevision-label": "No la darrera revisió",
        "rcfilters-filter-previousrevision-description": "Tots els canvis que no són «la darrera revisió».",
        "rcfilters-filter-excluded": "Exclòs",
        "rcfilters-exclude-button-off": "Exclou els seleccionats",
        "uploaded-script-svg": "S’ha trobat l’element programable «$1» al fitxer SVG carregat.",
        "uploaded-hostile-svg": "S’ha trobat codi CSS no segur a l’element d’estil del fitxer SVG carregat.",
        "uploaded-event-handler-on-svg": "No es permet establir els atributs de gestió d’esdeveniments <code>$1=\"$2\"</code> en fitxers SVG.",
-       "uploaded-href-attribute-svg": "Els atributs href en fitxers SVG només tenen permès enllaçar a destinacions http:// o https://, s'ha trobat <code>&lt;$1 $2=\"$3\"&gt;</code>.",
+       "uploaded-href-attribute-svg": "Els elements <a> només poden enllaçar (href) amb objectius «data:» (fitxer incrustat), «http://», «https://» o de fragment («#», «same-document»). Proveu d'incrustar les imatges en exportar el vostre SVG. S'ha trobat <code>&lt;$1 $2=\"$3\"&gt;</code>.",
        "uploaded-href-unsafe-target-svg": "S’ha trobat un element «href» amb dades no segures: destinació URI <code>&lt;$1 $2=\"$3\"&gt;</code> en el fitxer SVG carregat.",
        "uploaded-animate-svg": "S'ha trobat l'etiqueta «animate» que pot estar canviant l'href mitjançant l'atribut <code>&lt;$1 $2=\"$3\"&gt;</code> en el fitxer SVG carregat.",
        "uploaded-setting-event-handler-svg": "La configuració d'atributs per la gestió d'esdeveniments està bloquejada. S'ha trobat <code>&lt;$1 $2=\"$3\"&gt;</code> al fitxer SVG pujat.",
        "listfiles_size": "Mida (octets)",
        "listfiles_description": "Descripció",
        "listfiles_count": "Versions",
-       "listfiles-show-all": "Inclou versions antigues de les imatges",
+       "listfiles-show-all": "Inclou versions antigues dels fitxers",
        "listfiles-latestversion": "Versió actual",
        "listfiles-latestversion-yes": "Sí",
        "listfiles-latestversion-no": "No",
        "pageswithprop-text": "Aquesta pàgina llista les pàgines que utilitzen una propietat de pàgina en particular.",
        "pageswithprop-prop": "Nom de la propietat:",
        "pageswithprop-reverse": "Ordena en invers",
+       "pageswithprop-sortbyvalue": "Ordena pel valor de la propietat",
        "pageswithprop-submit": "Vés",
        "pageswithprop-prophidden-long": "valor de propietat text llarg ocult ($1)",
        "pageswithprop-prophidden-binary": "valor de propietat binària oculta ($1)",
        "enotif_lastdiff": "Per a visualitzar aquest canvi, consulteu $1",
        "enotif_anon_editor": "usuari anònim $1",
        "enotif_body": "Benvolgut/uda $WATCHINGUSERNAME,\n\n$PAGEINTRO $NEWPAGE\n\nResum de l'editor: $PAGESUMMARY $PAGEMINOREDIT\n\nContacteu amb l'editor:\ncorreu: $PAGEEDITOR_EMAIL\nwiki: $PAGEEDITOR_WIKI\n\nNo rebreu més notificacions en cas de més activitat a menys que visiteu aquesta pàgina havent iniciat sessió.\nTambé podeu canviar el mode de notificació de les pàgines que vigileu en la vostra llista de seguiment.\n\nEl servei de notificacions del projecte {{SITENAME}}\n\n--\nPer a canviar les opcions de notificació per correu electrònic aneu a\n{{canonicalurl:{{#special:Preferences}}}}\n\nPer a canviar les opcions de la vostra llista de seguiment aneu a\n{{canonicalurl:{{#special:EditWatchlist}}}}\n\nPer eliminar la pàgina de la vostra llista de seguiment aneu a\n$UNWATCHURL\n\nSuggeriments i ajuda:\n$HELPPAGE",
+       "enotif_minoredit": "Aquesta és una modificació menor",
        "created": "creada",
        "changed": "modificada",
        "deletepage": "Elimina la pàgina",
        "undelete-search-title": "Cerca de pàgines esborrades",
        "undelete-search-box": "Cerca pàgines esborrades",
        "undelete-search-prefix": "Mostra pàgines que comencin:",
+       "undelete-search-full": "Mostra títols de pàgines que continguin:",
        "undelete-search-submit": "Cerca",
        "undelete-no-results": "Amb aquest criteri de cerca, no s'ha trobat cap pàgina a l'arxiu de supressions",
        "undelete-filename-mismatch": "No es pot revertir l'eliminació de la revisió de fitxer amb marca horària $1: no coincideix el nom de fitxer",
        "delete_and_move_text": "La pàgina de destinació, «[[:$1]]», ja existeix.\nVoleu eliminar-la per a fer lloc al trasllat?",
        "delete_and_move_confirm": "Sí, esborra la pàgina",
        "delete_and_move_reason": "S'ha eliminat per a permetre el reanomenament de \" [[$1]] \"",
-       "selfmove": "Els títols d'origen i de destinació coincideixen: no és possible de reanomenar una pàgina a si mateixa.",
+       "selfmove": "El títol és el mateix;\nno es pot reanomenar una pàgina a si mateixa.",
        "immobile-source-namespace": "No es poden moure les pàgines de l’espai de noms «$1»",
        "immobile-target-namespace": "No es poden moure pàgines cap a l'espai de noms \"$1\"",
        "immobile-target-namespace-iw": "No es poden moure pàgines a l'enllaç interwiki",
        "import-nonewrevisions": "No s'ha importat cap revisió (ja hi eren abans o s'han omès a causa d'errors).",
        "xml-error-string": "$1 a la línia $2, columna $3 (byte $4): $5",
        "import-upload": "Carrega dades XML",
-       "import-token-mismatch": "Pèrdua de dades de sessió.\n\nPotser heu finalitzat la sessió. <strong>Comproveu si encara teniu la sessió iniciada i torneu-ho a intentar</strong>.\nSi encara no funciona, proveu de [[Special:UserLogout|finalitzar la sessió]] i inicieu-la de nou, comprovant que el vostre navegador permeti les galetes per a aquest lloc.",
+       "import-token-mismatch": "Pèrdua de dades de sessió.\n\nPotser heu finalitzat la sessió. <strong>Comproveu si encara teniu la sessió iniciada i torneu-ho a intentar</strong>.\nSi encara no funciona, proveu de [[Special:UserLogout|finalitzar la sessió]] i iniciar-la de nou, comprovant que el vostre navegador permeti les galetes per a aquest lloc.",
        "import-invalid-interwiki": "No es pot importar des del wiki especificat.",
        "import-error-edit": "La pàgina «$1» no s'ha importat perquè no teniu permís per modificar-la.",
        "import-error-create": "La pàgina «$1» no s'ha importat perquè no teniu permís per crear-la.",
        "compare-title-not-exists": "El títol que heu especificat no existeix.",
        "compare-revision-not-exists": "La revisió que heu especificat no existeix.",
        "diff-form": "Diferències",
+       "permanentlink-revid": "ID de la revisó",
+       "permanentlink-submit": "Vés a la revisió",
        "dberr-problems": "Ho sentim. Aquest lloc web està experimentant dificultats tècniques.",
        "dberr-again": "Intenteu esperar uns minuts i tornar a carregar.",
        "dberr-info": "(No es pot accedir a la base de dades: $1)",
index 4a1f7a7..2ab3b83 100644 (file)
        "thursday": "拜四",
        "friday": "拜五",
        "saturday": "拜六",
-       "sun": "禮拜",
-       "mon": "拜一",
-       "tue": "拜二",
-       "wed": "拜三",
-       "thu": "拜四",
-       "fri": "拜五",
-       "sat": "拜六",
+       "sun": "Lā̤ buái",
+       "mon": "Buái ék",
+       "tue": "Bái-nê",
+       "wed": "Buái săng",
+       "thu": "Buái sé",
+       "fri": "Buái ngô",
+       "sat": "Buái lĕ̤k",
        "january": "Ék-nguŏk",
        "february": "Nê-nguŏk",
        "march": "Săng-nguŏk",
        "cancel": "取消",
        "moredotdotdot": "固価...",
        "morenotlisted": "茲萆單單𣍐完整",
-       "mypage": "頁面",
+       "mypage": "Nguāi gì hiĕk-miêng",
        "mytalk": "我其討論",
        "anontalk": "攀講",
        "navigation": "Dô̤-hòng",
        "editsection": "siŭ-gāi",
        "editold": "修改",
        "viewsourceold": "看源代碼",
-       "editlink": "修改",
-       "viewsourcelink": "看源代碼",
+       "editlink": "siŭ-gāi",
+       "viewsourcelink": "Káng nguòng-dâi-mā",
        "editsectionhint": "修改段落:$1",
        "toc": "目錄",
        "showtoc": "顯示",
        "red-link-title": "$1(mò̤ ciā hiĕk)",
        "sort-descending": "降序排序",
        "sort-ascending": "陞序排序",
-       "nstab-main": "頁面",
+       "nstab-main": "Ùng-ciŏng",
        "nstab-user": "用戶頁",
        "nstab-media": "媒體頁",
-       "nstab-special": "特殊頁",
+       "nstab-special": "Dĕk-sṳ̀-hiĕk",
        "nstab-project": "項目頁",
        "nstab-image": "文件",
        "nstab-mediawiki": "消息",
index 1b6d15c..09e47ff 100644 (file)
        "rcfilters-filter-watchlist-watched-description": "Хьан тергаме могӀамехь болу хийцамаш.",
        "rcfilters-filter-watchlist-watchednew-label": "Тергаме могӀаман керла хийцамаш",
        "rcfilters-filter-watchlist-watchednew-description": "Хьан тергаме могӀаман юкъара хьуна гина боцу хийцамаш.",
-       "rcfilters-filter-watchlist-notwatched-label": "ТеÑ\80гаме Ð¼Ð¾Ð³Ó\80аман Ñ\8eкÑ\8aахь яц",
-       "rcfilters-filter-watchlist-notwatched-description": "Ð\95Ñ\80Ñ\80иге, Ñ\85Ñ\8cан Ñ\82еÑ\80гаме Ð¼Ð¾Ð³Ó\80аман Ñ\8eкÑ\8aаÑ\85Ñ\8c ÐµÑ\80Ñ\88 Ñ\86а Ð³Ð¾Ð¹Ñ\82Ñ\83.",
+       "rcfilters-filter-watchlist-notwatched-label": "ТеÑ\80гаме Ð¼Ð¾Ð³Ó\80амехь яц",
+       "rcfilters-filter-watchlist-notwatched-description": "Ð\95Ñ\80Ñ\80иге, Ñ\85Ñ\8cан Ñ\82еÑ\80гаме Ð¼Ð¾Ð³Ó\80амеÑ\85Ñ\8c ÐµÑ\80Ñ\88 Ñ\8eкÑ\8aаÑ\80айоÑ\85Ñ\83Ñ\88.",
        "rcfilters-filtergroup-changetype": "Хийцамийн тайпа",
        "rcfilters-filter-pageedits-label": "АгӀонан нисдарш",
        "rcfilters-filter-pageedits-description": "Дийцарийн а, категорийн а чулацаман дина нисдарш...",
        "historywarning": "<strong>Тергам бе:</strong> Хьо дӀаяккха гӀертачу агӀона, нисдарийн истори ю, $1 {{PLURAL:$1|верси}} йолуш:",
        "historyaction-submit": "Гайта",
        "confirmdeletetext": "Хьо гӀерта агӀо я файл дӀаяккха '''дехар до''', дӀаяккхале хьалха хьажа [[{{MediaWiki:Policy-url}}|кхуза]].",
-       "actioncomplete": "Ð\94еÑ\88деÑ\80г Ðºхочушдина",
-       "actionfailed": "Кхочушъ дина дац",
+       "actioncomplete": "Ð\9aхочушдина",
+       "actionfailed": "Кхочушъ цадина",
        "deletedtext": "«$1» дӀаяьккхина яра.\nХьажа. $2 хьажарна оцу тӀаьхьара дӀаяхначара могӀаме.",
        "dellogpage": "ДӀадаьхнарш долу тéптар",
        "dellogpagetext": "Лахахь гойтуш ю тӀаьххьара дӀаяьхнарш.",
        "undelete": "ДӀаяьхна агӀонашка хьажар",
        "undeletepage": "ДӀаяьхна агӀонашка хьажар а, меттахӀоттор а",
        "undeletepagetitle": "'''Лахахь гайтина хӀокху [[:$1]] агӀона дӀаяхина версеш'''.",
-       "viewdeletedpage": "Ð\94Ó\80аÑ\8fÑ\8cÑ\85на Ð¹Ð¾Ð»Ñ\83 Ð°Ð³Ó\80онаÑ\88ка Ñ\85Ñ\8cажаÑ\80",
+       "viewdeletedpage": "ДӀаяьхна агӀонашка хьажар",
        "undelete-fieldset-title": "МеттахӀоттае версеш",
        "undeleteextrahelp": "Ерриге агӀонан истори меттахӀоттая массо а билгалонаш еса а йити '''«{{int:undeletebtn}}»''' тӀетаӀае.\nЦхӀайолу агӀонан версеш меттахӀоттая хьалха меттахӀоттош йолу версеш билгалъяьхна тӀетагӀе '''«{{int:undeletebtn}}»'''.",
        "undeleterevisions": "$1 {{PLURAL:$1|верси}} архив чу {{PLURAL:$1|йиллина}}",
        "pageinfo-robot-index": "Магийна",
        "pageinfo-robot-noindex": "Магийна дац",
        "pageinfo-watchers": "Хьоьжучера дукхалла",
+       "pageinfo-visiting-watchers": "АгӀона тидамбеш болу а, хийцамаш гуш болу а декъашхой",
        "pageinfo-few-watchers": "{{PLURAL:$1|ТӀаьхьадогӀучерал}} $1 кӀезиг",
        "pageinfo-redirects-name": "ХӀокху агӀон тӀе йолу дӀассахьажорийн дукхалла",
        "pageinfo-subpages-name": "ХӀокху агӀона бухара агӀонаш",
        "pageinfo-subpages-value": "$1 ($2 {{PLURAL:$2|цӀе хийцар|цӀе хийцарш}}; $3 {{PLURAL:$3|гуттар хуьлург|гуттар хуьлурш}})",
-       "pageinfo-firstuser": "АгӀо кхуллург",
+       "pageinfo-firstuser": "АгӀо кхоьллинарг",
        "pageinfo-firsttime": "АгӀо кхоьллина терахь",
        "pageinfo-lastuser": "ТӀеххьара тадар дийнарг",
        "pageinfo-lasttime": "ТӀеххьара нисдар дина терахь",
        "compare-invalid-title": "Ахьа язйина йолу цӀе ца магайо.",
        "compare-title-not-exists": "Иштта цӀе яц.",
        "compare-revision-not-exists": "Иштта версеш яц.",
+       "diff-form": "Башхаллаш",
+       "diff-form-oldid": "Версин шира идентификатор (тӀехь дац)",
+       "diff-form-revid": "Башхаллаш йолу версин идентификатор",
+       "diff-form-submit": "Схьагайта башхаллаш",
        "dberr-problems": "Бехк ма бил! ХӀокху сайтехь техникан халонаш хила.",
        "dberr-again": "Хьажа карла йаккха агlо массех минот йаьлча.",
        "dberr-info": "(аьтто ца хили зӀе хӀотта серверца бухара хаамашца: $1)",
index ff67d24..55bd308 100644 (file)
@@ -19,7 +19,8 @@
                        "Pirehelokan",
                        "Diyariq",
                        "Sarchia",
-                       "Lost Whispers"
+                       "Lost Whispers",
+                       "Épine"
                ]
        },
        "tog-underline": "ھێڵ ھێنان بەژێر بەستەرەکان:",
        "nosuchusershort": "بەکارھێنەرێک بە ناوی «$1»ەوە نیە.\nبە نووسراوەکەتدا بچۆرەوە.",
        "nouserspecified": "دەبێت ناوێکی بەکارھێنەری دەستنیشان بکەیت.",
        "login-userblocked": "ئەم بەکارهێنەرە بڵۆک کراوە. چوونەژوورەوە ڕێگەپێنەدراوە.",
-       "wrongpassword": "تێپەڕوشەی ھەڵە.\nتکایە دووبارە تێبکۆشە.",
+       "wrongpassword": "ناوی بەکارھێنەر یان تێپەڕوشە ھەڵەیە.\nتکایە دووبارە ھەوڵبدەرەوە.",
        "wrongpasswordempty": "تێپەڕەوشەی لێدراو بەتاڵبوو.\nتکایە هەوڵ بدەوە.",
        "passwordtooshort": "تێپەڕوشەکەت لانی کەم دەبێ {{PLURAL:$1|١ پیت|$1 پیت}} بێت.",
        "passwordtoolong": "تێپەڕ وشەکان ناتوانرێت لە {{PLURAL:$1|١ کارەکتەر|$1 کارەکتەر}} درێژتر بێت.",
        "timezoneregion-indian": "ئوقیانووسی ھیند",
        "timezoneregion-pacific": "ئۆقیانووسی ئارام",
        "allowemail": "ڕێگە بدە بە بەکارھێنەرانی تر کە ئیمەیلم بۆ بنێرن",
+       "email-blacklist-label": "ڕێگری لە ناردنی پۆستی ئەلیکترۆنی لە لایەن ئەم بەکارھێنەرانە بکە:",
        "prefs-searchoptions": "گەڕان",
        "prefs-namespaces": "بۆشایی ناوەکان",
        "default": "بنچینەیی",
        "rcfilters-filter-newpages-label": "دروستکردنی پەڕەکان",
        "rcfilters-filter-categorization-label": "گۆڕانکاری پۆلەکان",
        "rcfilters-filter-logactions-label": "کردارە لۆگییەکان",
-       "rcfilters-view-advanced-filters-label": "پاڵوێنە پێشکەوتووەکان",
        "rcnotefrom": "ژێرەوە {{PLURAL:$5|گۆڕانکارییەکەیە|گۆڕانکارییەکانە}} لە <strong>$3، $4</strong>ەوە (ھەتا <strong>$1</strong> نیشان دراوە).",
        "rclistfrom": "گۆڕانکارییە نوێکان نیشان بدە بە دەستپێکردن لە $3 $2",
        "rcshowhideminor": "دەستکارییە بچووکەکان $1",
        "logentry-delete-delete": "$1 پەڕەی $3ی {{GENDER:$2|سڕییەوە}}",
        "logentry-delete-delete_redir": "$1 {{GENDER:$2|ڕەوانەکەری}} $3 سڕیەوە",
        "logentry-delete-restore": "$1 پەڕەی $3ی {{GENDER:$2|ھێنایەوە}} ($4)",
+       "restore-count-revisions": "{{PLURAL:$1|$1 پێداچوونەوە}} هێنرایەوە",
        "logentry-delete-revision": "$1 دەرکەوتنی {{PLURAL:$5|پێداچوونەوەیەکی|$5 پێداچوونەوەی}} پەڕەی $3ی {{GENDER:$2|گۆڕیی}}: $4",
        "logentry-suppress-delete": "$1 پەڕەی $3 {{GENDER:$2|بەرگری کرد}}.",
        "revdelete-content-hid": "ناوەرۆک شاردراوە",
index fdcb972..d9aea2e 100644 (file)
        "logentry-move-move_redir-noredirect": "$1 адлы къулланыджы $3 саифесининъ адыны ёнетме узеринден янъы бир ёнетме къалдырмайып $4 деп {{GENDER:$2|денъиштирди}}",
        "searchsuggest-search": "Къыдыр",
        "searchsuggest-containing": "ичинде бу олгъан...",
+       "variantname-crh": "Lat./Кир.",
+       "variantname-crh-latn": "Latin",
+       "variantname-crh-cyrl": "Кирил",
        "pagelang-language": "Тиль"
 }
index 75d187f..016dfe5 100644 (file)
        "logentry-move-move_redir-noredirect": "$1 adlı qullanıcı $3 saifesiniñ adını yönetme üzerinden yañı bir yönetme qaldırmayıp $4 dep {{GENDER:$2|deñiştirdi}}",
        "searchsuggest-search": "Qıdır",
        "searchsuggest-containing": "içinde bu olğan...",
+       "variantname-crh": "Lat./Кир.",
+       "variantname-crh-latn": "Latin",
+       "variantname-crh-cyrl": "Кирил",
        "pagelang-language": "Til"
 }
index cddd35e..c682fc9 100644 (file)
        "nosuchusershort": "Neexistuje uživatel se jménem „$1“. Zkontrolujte zápis.",
        "nouserspecified": "Musíte zadat uživatelské jméno.",
        "login-userblocked": "{{GENDER:$1|Tento uživatel je zablokován|Tato uživatelka je zablokována}}. Přihlášení není dovoleno.",
-       "wrongpassword": "Bylo zadáno nesprávné heslo.\nZkuste to znovu.",
+       "wrongpassword": "Bylo zadáno nesprávné uživatelské jméno nebo heslo.\nZkuste to znovu.",
        "wrongpasswordempty": "Bylo zadáno prázdné heslo. Zkuste to znovu.",
        "passwordtooshort": "Heslo musí být dlouhé nejméně $1 {{PLURAL:$1|znak|znaky|znaků}}.",
        "passwordtoolong": "Hesla nemohou být delší než {{PLURAL:$1|1 znak|$1 znaky|$1 znaků}}.",
        "rcfilters-savedqueries-apply-and-setdefault-label": "Vytvořit výchozí filtr",
        "rcfilters-savedqueries-cancel-label": "Zrušit",
        "rcfilters-savedqueries-add-new-title": "Uložit současné nastavení filtrů",
+       "rcfilters-savedqueries-already-saved": "Tyto filtry jsou již uloženy. K uložení nového je třeba změnit jejich nastavení.",
        "rcfilters-restore-default-filters": "Obnovit výchozí filtry",
        "rcfilters-clear-all-filters": "Zrušit všechny filtry",
        "rcfilters-show-new-changes": "Zobrazit nejnovější změny",
        "show": "ukázat",
        "minoreditletter": "m",
        "newpageletter": "N",
-       "boteditletter": "b",
+       "boteditletter": "r",
        "number_of_watching_users_pageview": "[$1 {{PLURAL:$1|sledující uživatel|sledující uživatelé|sledujících uživatelů}}]",
        "rc_categories": "Omezit na kategorie (oddělené „|“):",
        "rc_categories_any": "Jakákoli z vybraných",
        "uploadstash-refresh": "Aktualizovat seznam souborů",
        "uploadstash-thumbnail": "zobrazit náhled",
        "uploadstash-exception": "Načtený soubor se nepodařilo uložit do skrýše ($1): „$2“.",
+       "uploadstash-bad-path": "Cesta neexistuje.",
+       "uploadstash-bad-path-invalid": "Cesta není platná.",
+       "uploadstash-bad-path-unknown-type": "Neznámý typ „$1“.",
+       "uploadstash-file-not-found-no-thumb": "Nepodařilo se získat náhled.",
+       "uploadstash-file-not-found-no-remote-thumb": "Načtení náhledu se nepodařilo: $1\nURL = $2",
+       "uploadstash-file-too-large": "Nelze poskytnout soubor větší než $1 bajtů.",
+       "uploadstash-not-logged-in": "Není přihlášen žádný uživatel, soubory musí patřit uživatelům.",
+       "uploadstash-wrong-owner": "Tento soubor ($1) nepatří aktuálnímu uživateli.",
+       "uploadstash-no-such-key": "Uvedený klíč ($1) neexistuje, nelze odebrat.",
+       "uploadstash-zero-length": "Soubor má nulovou délku.",
        "invalid-chunk-offset": "Neplatný posun bloku",
        "img-auth-accessdenied": "Přístup odepřen",
        "img-auth-nopathinfo": "Chybí PATH_INFO.\nVáš server není nastaven tak, aby tuto informaci poskytoval.\nMožná funguje pomocí CGI a img_auth na něm nemůže fungovat.\nVizte https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
index 8275d2c..7c3544c 100644 (file)
        "tog-numberheadings": "Überschriften automatisch nummerieren",
        "tog-showtoolbar": "Bearbeiten-Werkzeugleiste anzeigen",
        "tog-editondblclick": "Seiten mit Doppelklick bearbeiten",
-       "tog-editsectiononrightclick": "Einzelne Abschnitte per Rechtsklick bearbeiten",
+       "tog-editsectiononrightclick": "Einzelne Abschnitte per Rechtsklick auf die Überschrift bearbeiten",
        "tog-watchcreations": "Selbst erstellte Seiten und hochgeladene Dateien automatisch beobachten",
        "tog-watchdefault": "Selbst geänderte Seiten und Dateien automatisch beobachten",
        "tog-watchmoves": "Selbst verschobene Seiten und Dateien automatisch beobachten",
        "nosuchusershort": "Der Benutzername „$1“ ist nicht vorhanden. Bitte überprüfe die Schreibweise.",
        "nouserspecified": "Bitte gib einen Benutzernamen an.",
        "login-userblocked": "{{GENDER:$1|Dieser Benutzer|Diese Benutzerin}} ist gesperrt. Die Anmeldung ist nicht erlaubt.",
-       "wrongpassword": "Das Passwort ist falsch. Bitte versuche es erneut.",
+       "wrongpassword": "Der Benutzername oder das Passwort ist falsch. Bitte versuche es erneut.",
        "wrongpasswordempty": "Es wurde kein Passwort eingegeben. Bitte versuche es erneut.",
        "passwordtooshort": "Passwörter müssen mindestens {{PLURAL:$1|1 Zeichen|$1 Zeichen}} lang sein.",
        "passwordtoolong": "Passwörter können nicht länger als {{PLURAL:$1|ein|$1}} Zeichen sein.",
        "rcfilters-savedqueries-apply-and-setdefault-label": "Standardfilter erstellen",
        "rcfilters-savedqueries-cancel-label": "Abbrechen",
        "rcfilters-savedqueries-add-new-title": "Aktuelle Filtereinstellungen speichern",
-       "rcfilters-savedqueries-already-saved": "Diese Filter sind bereits gespeichert",
+       "rcfilters-savedqueries-already-saved": "Diese Filter sind bereits gespeichert. Ändere deine Einstellungen, um einen neuen Gespeicherten Filter zu erstellen.",
        "rcfilters-restore-default-filters": "Standardfilter wiederherstellen",
        "rcfilters-clear-all-filters": "Alle Filter löschen",
        "rcfilters-show-new-changes": "Neueste Änderungen ansehen",
        "movesubpagetalktext": "Die dazugehörige Diskussionsseite hat {{PLURAL:$1|eine Unterseite, die unten angezeigt wird|$1 Unterseiten, die unten angezeigt werden}}.",
        "movenosubpage": "Diese Seite hat keine Unterseiten.",
        "movereason": "Grund:",
-       "revertmove": "zurück verschieben",
+       "revertmove": "zurückverschieben",
        "delete_and_move_text": "Die Seite „[[:$1]]“ existiert bereits.\nMöchtest du diese löschen, um die Seite verschieben zu können?",
        "delete_and_move_confirm": "Ja, Seite löschen",
        "delete_and_move_reason": "Gelöscht, um Platz für die Verschiebung von „[[$1]]“ zu machen",
index 341c800..14bf37f 100644 (file)
        "categoryviewer-pagedlinks": "($1) ($2)",
        "about": "Heqa",
        "article": "Wesiqe",
-       "newwindow": "(Teqaya newi de abena)",
+       "newwindow": "(pençerey newey de beno a)",
        "cancel": "Annuler",
        "moredotdotdot": "Vêşi...",
        "morenotlisted": "Na lista qay kemi ya.",
        "currentevents-url": "Project:Hediseyê rocaney",
        "disclaimers": "Redê mesuliyeti",
        "disclaimerpage": "Project:Redê mesulêtê pêro",
-       "edithelp": "Pastiyer vurnayış",
+       "edithelp": "Peştdariya vurnayışi",
        "helppage-top-gethelp": "Peşti",
        "mainpage": "Perra Seri",
        "mainpage-description": "Perra seri",
        "viewsourcelink": "çımey bıvêne",
        "editsectionhint": "Leteyo ke bıvuriyo: $1",
        "toc": "Zerreki",
-       "showtoc": "bımotne",
+       "showtoc": "bımocne",
        "hidetoc": "bınımne",
        "collapsible-collapse": "Teng kı",
        "collapsible-expand": "Hera ke",
        "publishchanges": "Vırnayışan qeyd ke",
        "preview": "Verqayt",
        "showpreview": "Verasayışi bımocne",
-       "showdiff": "Vurnayışan bımotne",
+       "showdiff": "Vurnayışan bımocne",
        "anoneditwarning": "<strong>İqaz:</strong> Şıma be hesabê xo nêkewtê cı. \nAdresê şımayê IP tarixê vırnayışê na pele de do qeyd bo. Eke şıma <strong>[$1 cıkewê]</strong> ya zi <strong>[$2 hesab vırazê]</strong>, vurnayışê şıma be zewbina kare ra nameyê şıma rê bar beno.",
        "anonpreviewwarning": "\"Şıma be hesabê xo nêkewtê cı. Eke qeyd kerê, adresê şımaê IP tarixê vırnayışê na pele de do qeyd bo.\"",
        "missingsummary": "'''DİQET:''' Şıma jû xulasa nênuşte.\nEke şıma \"$1\" reyna bıtıknê, vırnayışê şıma bê xulasa qeyd beno.",
        "rev-deleted-diff-view": "Jew timarkerdışê ena versiyon '''wedariyayo''.\nÎdarekarî şenê ena versiyon bivîne; belki tiya de [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} wedarnayişî] de teferruat esto.",
        "rev-suppressed-diff-view": "Jew timarkerdışê ena versiyon '''Ploxneyış'' biyo.\nÎdarekarî eşkeno ena dif bivîne; belki tiya de [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} ploxnayış] de teferruat esto.",
        "rev-delundel": "bımotne/bınımne",
-       "rev-showdeleted": "bımotne",
+       "rev-showdeleted": "bımocne",
        "revisiondelete": "Çımraviyarnayışan bestere/peyser biya",
        "revdelete-nooldid-title": "Çımraviyarnayışo waşte nêvêreno",
        "revdelete-nooldid-text": "Şıma vıraştışê nê fonksiyoni rê ya yew çımraviyarnayışo waşte diyar nêkerdo, çımraviyarnayışo diyarkerde çıniyo, ya ki şıma wazenê ke çımraviyarnayışê nıkayêni bınımnê.",
        "recentchanges-legend-heading": "<strong>Kıtabekê vırnayışê peyêni:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} Şıma şenê ([[Special:NewPages|Lista peranê  newan]] zi bıvênê)",
        "recentchanges-legend-plusminus": "''(±123)''",
-       "recentchanges-submit": "Bımotne",
+       "recentchanges-submit": "Bımocne",
        "rcfilters-savedqueries-new-name-label": "Name",
        "rcfilters-savedqueries-cancel-label": "Bıtexelne",
        "rcnotefrom": "Cêr de <strong>$2</strong> ra nata {{PLURAL:$5|vurnayışiyê}} asenê (tewr vêşi <strong>$1</strong> asenê) <strong>$3, $4</strong>",
        "rcshowhidebots-show": "Bımotne",
        "rcshowhidebots-hide": "Bınımne",
        "rcshowhideliu": "karberê qeydbiyay $1",
-       "rcshowhideliu-show": "Bımotne",
+       "rcshowhideliu-show": "Bımocne",
        "rcshowhideliu-hide": "Bınımne",
        "rcshowhideanons": "$1 karberê bênamey",
        "rcshowhideanons-show": "Bımotne",
        "rcshowhideanons-hide": "Bınımne",
        "rcshowhidepatr": "$1 vurnayışê ke dewriya geyrayê",
-       "rcshowhidepatr-show": "Bımotne",
+       "rcshowhidepatr-show": "Bımocne",
        "rcshowhidepatr-hide": "Bınımne",
        "rcshowhidemine": "vırnayışê mı $1",
        "rcshowhidemine-show": "Bımotne",
        "mostrevisions": "Pelan ke tewr zaf revizyonî biyê.",
        "prefixindex": "Verbenda pelli heme",
        "prefixindex-namespace": "Peleyê Veroleyıni ($1 cay nami)",
-       "prefixindex-submit": "Bımotne",
+       "prefixindex-submit": "Bımocne",
        "prefixindex-strip": "Listeya réz bıyayışi",
        "shortpages": "Pelê kılmi",
        "longpages": "Pelê dergi",
        "cachedspecial-viewing-cached-ts": "Na pela raşt niya, şımayê enewke versiyonê verhafızada na pela vinenê.",
        "cachedspecial-refresh-now": "Peyêni bıvin.",
        "categories": "Kategoriy",
-       "categories-submit": "bımotne",
+       "categories-submit": "Bımocne",
        "categoriespagetext": "{{PLURAL:$1|Kategoriya cêrene|Kategoriyanê cêrênan}} de peli ya zi medya estê.\n[[Special:UnusedCategories|Kategoriyê ke nêxebetiyenê]] tiya de nêmocniyayê.\n[[Special:WantedCategories|Kategoriyanê waşteyeyan]] de zi bıvêne.",
        "categoriesfrom": "Kategoriyê ke ney ra sergendênê bımotne:",
        "deletedcontributions": "İştırakê karberi esterdi",
index 5fc6cc6..52595ad 100644 (file)
@@ -37,7 +37,7 @@
        "tog-shownumberswatching": "Fà vèder al nómer ed j utèint che gh'àn la pàgina sòta uservasiòun",
        "tog-oldsig": "La tó fîrma 'd adèsa",
        "tog-fancysig": "Trâta la fîrma cme wikitèst (sèinsa colegamèint avtomâtich)",
-       "tog-uselivepreview": "Permèt la funsiòun \"Live preview\" (guêrda préma 'd salvêr in dirèta)",
+       "tog-uselivepreview": "Fa vèder al pàgini sèinsa carghêri",
        "tog-forceeditsummary": "Dmânda s'l'è vèira che al câmp argumèint l' é vōd",
        "tog-watchlisthideown": "Lōga al mé mudéfichi int i  tgnû 'd ôc specêl",
        "tog-watchlisthidebots": "Lōga al mudéfichi di bot int i tgnû 'd ôc specêl",
@@ -55,7 +55,6 @@
        "underline-never": "Mài",
        "underline-default": "Mantî al j impustasiòun dal navigadōr o 'd la skin",
        "editfont-style": "Stîl dal carâter int la caşèla 'd mudéfica:",
-       "editfont-default": "Pre-stabilî dal navigadōr",
        "editfont-monospace": "Carâter a larghésa fésa",
        "editfont-sansserif": "Carâter sans-serif",
        "editfont-serif": "Carâter serif",
        "anontalk": "Discusiòun",
        "navigation": "Navigasiòun",
        "and": "&#32;e",
-       "qbfind": "Câta",
-       "qbbrowse": "Sfòja",
-       "qbedit": "Mudéfica",
-       "qbpageoptions": "Siēlti 'd la pàgina",
-       "qbmyoptions": "Al mē pàgini",
        "faq": "Dmândi fâti",
-       "faqpage": "Project:Dmândi fâti despès",
        "actions": "Asiòun",
        "namespaces": "Spâsi di nòm",
        "variants": "Mudéfichi",
        "edit-local": "Mudéfica la spiegasiòun lochêla",
        "create": "Invèinta",
        "create-local": "Zûnta spiegasiòun lochêla",
-       "editthispage": "Mudéfica cla pàgina ché",
-       "create-this-page": "Fà cla pàgina ché",
        "delete": "Scanşéla",
-       "deletethispage": "Scanşéla cla pàgina ché",
-       "undeletethispage": "Fà 'l recóper ed cla pàgina ché",
        "undelete_short": "Recóper ed {{PLURAL:$1|'na versiòun|$1 versiòun}}",
        "viewdeleted_short": "Guèrda {{PLURAL:$1|'na mudéficha scanşlêda|$1 mudéfichi scanşlêdi}}",
        "protect": "Prutēz",
        "protect_change": "Câmbia",
-       "protectthispage": "Prutēz cla pàgina ché",
        "unprotect": "Câmbia la prutesiòun",
-       "unprotectthispage": "Câmbia la prutesiòun per cla pàgina ché",
        "newpage": "Pàgina nōva",
-       "talkpage": "Pàgina 'd discusiòun",
        "talkpagelinktext": "Discusiòun",
        "specialpage": "Pàgina specêla",
        "personaltools": "Strumèint persunêl",
-       "articlepage": "Guêrda la pàgina",
        "talk": "Discusiòun",
        "views": "Vîşiti",
        "toolbox": "Strumèint",
-       "userpage": "Guêrda la pàgina utèint",
-       "projectpage": "Guêrda la pàgina dal prugèt",
        "imagepage": "Guêrda la pàgina dal file",
        "mediawikipage": "Guêrda al mesâg",
        "templatepage": "Guêrda 'l mudèl",
        "explainconflict": "Un êter utèint l'à salvê 'na nōva versiòun ed la pàgina mèinter t'ēr adrē fêr dal mudéfichi. Int la caşèla 'd mudéfica ché 'd sōver a gh'é al tèst ed la pàgina che adès l'é in lénia, acsé cme l'é stêda salvêda da cl'êter utèint. La versiòun cun al tō mudéfichi invēci l'é int la caşèla dal mudéfichi ché sòta. S' ét vō cunfermêri, ét dēv purtêr al tō mudéfichi int al tèst che gh'é bèle (caşèla ché 'd sōver). Se té schés al ptòun '$1', a gnirà salvê '''sōl''' al tèst dèinter a la caşèla 'd mudéfica ché 'd sōver.",
        "yourtext": "Al tó tèst",
        "storedversion": "La versiòun in memôria",
-       "nonunicodebrowser": "'''Atèinti: a s'é drē druvêr un navigadōr ch' al và mìa d' acôrdi cun i carâter ''Unicode''. Per permèter la mudéfica dal pàgini sèinsa fêr d' incunveniĵnt, int la caşèla ed mudéfica i carâter mia ASCII a vînen fât vèder cme côdis eşadecimêl.'''",
        "editingold": "<strong>Atèinti: a s'é drē mudifichêr 'na versiòun mìa arnuvêda 'd la pàgina.</strong> \nS'es pèinsa ed salvêrla, tót i cambiamèint fât dōp cla mudéfica ché andrân pêrs.",
        "yourdiff": "Diferèinsi",
        "copyrightwarning": "Per piaşèir tîn cûnt che tót al colaborasiòun a {{SITENAME}} a vînen cunsidrêdi publichêdi sòta la licèinsa $2 (per i particulêr guêrda $1). S' an 't vō mìa che i tō tèst a pôsen èser cambiê e turnê a publichêr da tót sèinsa lémit, an publichêri mìa ché.<br /> In pió, se 't  i póblich ché, a 't dichiâr, sòta la tó responsabilitê, che còl ch' è stê scrét a 't l'ê scrét té personalmèint opór l'é ste cupiê da documèint sèinsa ch' al sìa quacê da nisûn dirét 'd autōr. <strong> Ché insém an pubblichêr mìa materiêl quacê da dirét 'd autōr sèinsa autorişâsiòun! </strong>",
        "block": "Blôca l'utèint",
        "unblock": "Şblôcä l'utèint",
        "blockip": "Blôcä {{GENDER:$1|utèint}}",
-       "blockip-legend": "Blôcä l'utèint",
        "ipboptions": "2 ōri:2 hours,1 dé:1 day,3 dé:3 days,1 stmâna:1 week,2 stâni:2 weeks,1 mèiş:1 month,3 mèiş:3 months,6 mèiş:6 months,1 ân:1 year,infinito:infinite",
        "ipb-unblock-addr": "Şblôcä $1",
        "unblockip": "Şblôcä l'utèint",
index 61ca0b1..17cdcb1 100644 (file)
        "rcfilters-savedqueries-apply-and-setdefault-label": "Δημιουργία προεπιλεγμένου φίλτρου",
        "rcfilters-savedqueries-cancel-label": "Ακύρωση",
        "rcfilters-savedqueries-add-new-title": "Αποθήκευση τρεχουσών ρυθμίσεων φίλτρων",
+       "rcfilters-savedqueries-already-saved": "Αυτά τα φίλτρα έχουν ήδη αποθηκευτεί. Αλλάξετε τις παραμέτρους για να δημιουργήσετε ένα νέο Αποθηκευμένο Φίλτρο.",
        "rcfilters-restore-default-filters": "Επαναφορά προεπιλεγμένων φίλτρων",
        "rcfilters-clear-all-filters": "Εκκαθάριση όλων των φίλτρων",
        "rcfilters-show-new-changes": "Προβολή νεότερων αλλαγών",
        "uploadstash-bad-path": "Δεν υπάρχει τρόπος",
        "uploadstash-bad-path-invalid": "Ο τρόπος δεν είναι έγκυρος.",
        "uploadstash-bad-path-unknown-type": "Άγνωστος τύπος ''$1''.",
+       "uploadstash-bad-path-unrecognized-thumb-name": "Nom feuilleté non classifié.",
+       "uploadstash-bad-path-no-handler": "Δεν βρέθηκε χειριστής για την αντιγραφή του φακέλλου",
        "uploadstash-bad-path-bad-format": "Ο κωδικός ''$1' δεν έχει τον κατάλληλο τύπο.",
+       "uploadstash-file-not-found": "Le code ''$1'' ñ'a pas été trouvé en planque",
        "uploadstash-file-not-found-no-thumb": "Δεν μπόρεσε να αποκτήσει το ''thumbnail''.",
+       "uploadstash-file-not-found-no-local-path": "Δεν υπάρχει τρόπος τοπικά για το κλιμακούμενο αντικείμενο",
        "uploadstash-file-not-found-no-object": "Δεν μπόρεσε να δημιουργήσει τοπικό φάκελλο ''thumbnail''.",
        "uploadstash-file-not-found-no-remote-thumb": "Η προσκόμιση του ''thumnail'' απέτυχε:$1\nURL=$2",
        "uploadstash-file-not-found-missing-content-type": "Λείπει τίτλος περιεχομένου",
index b463d95..52ac447 100644 (file)
        "nosuchusershort": "There is no user by the name \"$1\".\nCheck your spelling.",
        "nouserspecified": "You have to specify a username.",
        "login-userblocked": "This user is blocked. Login not allowed.",
-       "wrongpassword": "Incorrect password entered.\nPlease try again.",
+       "wrongpassword": "Incorrect username or password entered.\nPlease try again.",
        "wrongpasswordempty": "Password entered was blank.\nPlease try again.",
        "passwordtooshort": "Passwords must be at least {{PLURAL:$1|1 character|$1 characters}}.",
        "passwordtoolong": "Passwords cannot be longer than {{PLURAL:$1|1 character|$1 characters}}.",
        "diff-multi-sameuser": "({{PLURAL:$1|One intermediate revision|$1 intermediate revisions}} by the same user not shown)",
        "diff-multi-otherusers": "({{PLURAL:$1|One intermediate revision|$1 intermediate revisions}} by {{PLURAL:$2|one other user|$2 users}} not shown)",
        "diff-multi-manyusers": "({{PLURAL:$1|One intermediate revision|$1 intermediate revisions}} by more than $2 {{PLURAL:$2|user|users}} not shown)",
+       "diff-paragraph-moved-tonew": "Paragraph was moved. Click to jump to new location.",
+       "diff-paragraph-moved-toold": "Paragraph was moved. Click to jump to old location.",
        "difference-missing-revision": "{{PLURAL:$2|One revision|$2 revisions}} of this difference ($1) {{PLURAL:$2|was|were}} not found.\n\nThis is usually caused by following an outdated diff link to a page that has been deleted.\nDetails can be found in the [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} deletion log].",
        "search-summary": "",
        "searchresults": "Search results",
        "rcfilters-savedqueries-apply-and-setdefault-label": "Create default filter",
        "rcfilters-savedqueries-cancel-label": "Cancel",
        "rcfilters-savedqueries-add-new-title": "Save current filter settings",
-       "rcfilters-savedqueries-already-saved": "These filters are already saved",
+       "rcfilters-savedqueries-already-saved": "These filters are already saved. Change your settings to create a new Saved Filter.",
        "rcfilters-restore-default-filters": "Restore default filters",
        "rcfilters-clear-all-filters": "Clear all filters",
        "rcfilters-show-new-changes": "View newest changes",
        "magiclink-tracking-pmid-desc": "This page uses PMID magic links. See [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Magic_links mediawiki.org] on how to migrate.",
        "magiclink-tracking-isbn": "Pages using ISBN magic links",
        "magiclink-tracking-isbn-desc": "This page uses ISBN magic links. See [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Magic_links mediawiki.org] on how to migrate.",
-       "rfcurl": "//tools.ietf.org/html/rfc$1",
+       "rfcurl": "https://tools.ietf.org/html/rfc$1",
        "pubmedurl": "//www.ncbi.nlm.nih.gov/pubmed/$1?dopt=Abstract",
        "specialloguserlabel": "Performer:",
        "speciallogtitlelabel": "Target (title or {{ns:user}}:username for user):",
        "variantname-uz": "uz",
        "variantname-uz-latn": "uz-Latn",
        "variantname-uz-cyrl": "uz-Cyrl",
+       "variantname-crh": "crh",
+       "variantname-crh-latn": "crh-Latn",
+       "variantname-crh-cyrl": "crh-Cyrl",
        "metadata": "Metadata",
        "metadata-help": "This file contains additional information, probably added from the digital camera or scanner used to create or digitize it.\nIf the file has been modified from its original state, some details may not fully reflect the modified file.",
        "metadata-expand": "Show extended details",
index 092d1ec..74c2317 100644 (file)
        "underline-never": "Neniam",
        "underline-default": "Pravaloro laŭ foliumilo",
        "editfont-style": "Tipara stilo de redakta tekstujo",
-       "editfont-default": "Retumila defaŭlto",
        "editfont-monospace": "Egallarĝa tiparo",
        "editfont-sansserif": "Senserifa tiparo",
        "editfont-serif": "Serifa tiparo",
        "explainconflict": "Iu alia ŝanĝis la paĝon post kiam vi ekredaktis.\nLa supra tekstujo enhavas la aktualan tekston de la artikolo.\nViaj ŝanĝoj estas en la malsupra tekstujo.\nVi devas mem kunfandi viajn ŝanĝojn kaj la jaman tekston.\n'''Nur''' la teksto en la supra tekstujo estos konservita kiam vi alklakos \"$1\".",
        "yourtext": "Via teksto",
        "storedversion": "Registrita versio",
-       "nonunicodebrowser": "'''ATENTU: Via foliumilo ne eltenas unikodon, bonvolu ŝanĝi ĝin antaŭ ol redakti artikolon.'''",
        "editingold": "'''AVERTO: Vi nun redaktas malnovan version de tiu ĉi artikolo.\nSe vi konservos vian redakton, ĉiuj ŝanĝoj faritaj post tiu versio perdiĝos.'''",
+       "unicode-support-fail": "Ŝajnas ke via krozilo ne subtenas Unikodon. Tiu subteno estas necesa por redakti paĝojn, tial via redakto ne estis konservita.",
        "yourdiff": "Malsamoj",
        "copyrightwarning": "Bonvolu noti, ke ĉiu kontribuaĵo al la {{SITENAME}} estu rigardata kiel eldonita laŭ $2 (vidu je $1). Se vi volas, ke via verkaĵo ne estu redaktota senkompate kaj disvastigota laŭvole, ne alklaku \"Konservi\".<br />\nVi ankaŭ ĵuras, ke vi mem verkis la tekston, aŭ ke vi kopiis ĝin el fonto senkopirajta.\n'''NE UZU AŬTORRAJTE PROTEKTATAJN VERKOJN SENPERMESE!'''",
        "copyrightwarning2": "Bonvolu noti ke ĉiuj kontribuoj al {{SITENAME}} povas esti reredaktitaj, ŝanĝitaj aŭ forigitaj de aliaj kontribuantoj. Se vi ne deziras, ke viaj verkoj estu senkompate reredaktitaj, ne publikigu ilin ĉi tie.<br />\nVi ankaŭ promesu al ni ke vi verkis tion mem aŭ kopiis el publika domajno aŭ simila libera fonto (vidu $1 por detaloj).\n'''NE PROPONU KOPIRAJTITAJN VERKOJN SEN PERMESO!'''",
        "readonlywarning": "'''AVERTO: La datumbazo estas ŝlosita por teknika laboro, do vi ne povas konservi viajn redaktojn ĉi-momente.'''\nVi eble volus elkopii kaj alglui vian tekston al tekstdosiero kaj konservi ĝin por posta uzo.\n\nLa administranto kiu ŝlosis ĝin donis ĉi tiun eksplikaĵon: $1",
        "protectedpagewarning": "'''Averto: Ĉi tiu paĝo estas ŝlosita kontraŭ redaktado krom de administrantoj.'''\nJen la lasta protokolero provizita por via referenco:",
        "semiprotectedpagewarning": "'''Notu:''' Ĉi tiu paĝo estas ŝlosita tiel ke nur ensalutintaj uzantoj povas redakti ĝin.\nJen la lasta protokolero por via referenco:",
-       "cascadeprotectedwarning": "<strong>Averto:</strong> Ĉi tiu paĝo estas ŝlosita, tiel ke nur uzantoj kun administrantaj privilegioj povas redakti ĝin, ĉar ĝi estas inkludita en la {{PLURAL:$1|sekvan kaskade protektitan paĝon|sekvajn kaskade protektitajn paĝojn}}:",
+       "cascadeprotectedwarning": "<strong>Averto:</strong> Ĉi tiu paĝo estas ŝlosita, tiel ke nur uzantoj kun administraj privilegioj povas redakti ĝin, ĉar ĝi estas inkludita en la {{PLURAL:$1|sekvan kaskade protektitan paĝon|sekvajn kaskade protektitajn paĝojn}}:",
        "titleprotectedwarning": "'''Averto: Ĉi tiu paĝo estis ŝlosita tial nur [[Special:ListGroupRights|specifaj rajtoj]] estas bezonaj por krei ĝin.'''\nJen la lasta protokolero por via referenco:",
        "templatesused": "{{PLURAL:$1|Ŝablono uzata|Ŝablonoj uzataj}} en ĉi tiu paĝo:",
        "templatesusedpreview": "{{PLURAL:$1|Ŝablono uzata|Ŝablonoj uzataj}} en ĉi tiu antaŭrigardo:",
        "post-expand-template-argument-category": "Paĝoj enhavantaj forlasitajn argumentojn de ŝablonoj",
        "parser-template-loop-warning": "Rekursiva ŝablono estis trovita: [[$1]]",
        "template-loop-category": "Paĝoj kun ŝablonaj iteracioj",
+       "template-loop-category-desc": "Ĉi tiu paĝo enhavas ŝablonan ciklon, t. e. ŝablono kiu rikure vokas sin mem.",
+       "template-loop-warning": "<strong>Averto:</strong> Ĉi tiu paĝo vokas [[:$1]], kaŭzante ŝablonan ciklon (senfinan rikuran vokadon).",
        "parser-template-recursion-depth-warning": "Ŝablona profundeco transpasis limon ($1)",
        "language-converter-depth-warning": "Profundo de lingvo-konvertilo preterpasis limon ($1)",
        "node-count-exceeded-category": "Paĝoj kie la nombro da nodoj estas preterpasita",
        "search-interwiki-caption": "Kunprojektoj",
        "search-interwiki-default": "Rezultoj de $1:",
        "search-interwiki-more": "(plu)",
+       "search-interwiki-more-results": "pliaj rezultoj",
        "search-relatedarticle": "Relataj",
        "searchrelated": "rilataj",
        "searchall": "ĉiuj",
        "timezoneregion-indian": "Hinda Oceano",
        "timezoneregion-pacific": "Pacifiko",
        "allowemail": "Ebligi akceptadon de retmesaĝoj de aliaj uzantoj",
+       "email-blacklist-label": "Malpermesu al jenaj uzantoj mesaĝi al mi:",
        "prefs-searchoptions": "Serĉi",
        "prefs-namespaces": "Nomspacoj",
        "default": "defaŭlte",
        "youremail": "Retadreso:",
        "username": "{{GENDER:$1|Uzantnomo}}:",
        "prefs-memberingroups": "{{GENDER:$2|Ano}} de {{PLURAL:$1|grupo|grupoj}}:",
+       "group-membership-link-with-expiry": "$1 (ĝis $2)",
        "prefs-registration": "Tempo de registrado:",
        "yourrealname": "Vera nomo:",
        "yourlanguage": "Lingvo",
        "saveusergroups": "Konservi grupojn de {{GENDER:$1|uzantoj}}",
        "userrights-groupsmember": "Membro de:",
        "userrights-groupsmember-auto": "Implica membro de:",
-       "userrights-groups-help": "Vi povas modifi la grupojn kiun ĉi uzanto enestas.\n* Markita markbutono signifas ke la uzanto estas en tiu grupo.\n* Nemarkita markbutono signifas ke la uzanto ne estas in tiu grupo.\n* Steleto (*) signifas ke vi ne povas forigi la grupon post vi aldonis ĝin, aŭ male.",
+       "userrights-groups-help": "Vi povas modifi la grupojn en kiuj ĉi tiu uzanto estas.\n* Markita markbutono (kesto) signifas ke la uzanto estas en tiu grupo.\n* Nemarkita markbutono (kesto) signifas ke la uzanto ne estas en tiu grupo.\n* Steleto (*) signifas ke vi ne povas forigi la grupon post ke vi aldonis ĝin, aŭ male.",
        "userrights-reason": "Kialo:",
        "userrights-no-interwiki": "Vi ne rajtas redakti uzanto-rajtojn en aliaj vikioj.",
        "userrights-nodatabase": "Datumbazo $1 ne ekzistas aŭ ne estas loka.",
        "userrights-changeable-col": "Grupoj kiujn vi povas ŝanĝi",
        "userrights-unchangeable-col": "Grupoj kiujn vi ne povas ŝanĝi",
+       "userrights-expiry-current": "Eksvalidiĝas je $1",
+       "userrights-expiry-none": "Ne eksvalidiĝas",
+       "userrights-expiry": "Eksvalidiĝos:",
+       "userrights-expiry-othertime": "Alia tempo:",
+       "userrights-expiry-options": "1 tago:1 tago,1 semajno:1 semajno,1 monato:1 monato,3 monatoj:3 monatoj,6 monatoj:6 monatoj,1 jaro:1 jaro",
+       "userrights-invalid-expiry": "La eksvalidiĝa tempo por la grupo „$1“ estas nevalida.",
+       "userrights-expiry-in-past": "La eksvalidiĝas tempo por la grupo „$1“ jam pasis.",
        "userrights-conflict": "Konflikto ĉe la ŝanĝo de uzantorajtoj! Bonvolu kontroli kaj konfirmi viajn ŝanĝojn.",
        "group": "Grupo:",
        "group-user": "Uzantoj",
        "recentchanges-legend": "Opcioj pri lastaj ŝanĝoj",
        "recentchanges-summary": "Per ĉi tiu paĝo vi povas sekvi la plej lastajn ŝanĝojn en la {{SITENAME}}.",
        "recentchanges-noresult": "En la donita tempo ne estis ŝanĝoj, kiuj konformas al la kriterioj.",
+       "recentchanges-timeout": "Ĉi tiu serĉo transiris sian tempolimon. Vi eble provu malsamajn serĉajn parametrojn.",
        "recentchanges-feed-description": "Sekvi la plej lastatempajn ŝanĝojn al la vikio en ĉi tiu fonto.",
        "recentchanges-label-newpage": "Ĉi tiu redakto kreis novan paĝon",
        "recentchanges-label-minor": "Ĉi tiu estas eta redakto",
        "recentchanges-legend-heading": "<strong>Klarigo:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (vidu ankaŭ [[Special:NewPages|liston de novaj paĝoj]])",
        "recentchanges-submit": "Montri",
+       "rcfilters-tag-remove": "Forigi „$1“",
+       "rcfilters-legend-heading": "<strong>Listo de mallongigoj:</strong>",
+       "rcfilters-other-review-tools": "Aliaj reviziaj iloj",
+       "rcfilters-group-results-by-page": "Grupigi rezultojn laŭ paĝoj",
+       "rcfilters-grouping-title": "Grupigo",
        "rcfilters-activefilters": "Aktivaj filtriloj",
+       "rcfilters-advancedfilters": "Altnivelaj filtriloj",
+       "rcfilters-limit-title": "Ŝanĝoj montrotaj",
+       "rcfilters-limit-shownum": "Montri la {{PLURAL:$1|lastan ŝanĝon|lastajn $1 ŝanĝojn}}",
+       "rcfilters-days-title": "Ĵusaj tagoj",
+       "rcfilters-hours-title": "Ĵusaj horoj",
+       "rcfilters-days-show-days": "$1 {{PLURAL:$1|tago|tagoj}}",
+       "rcfilters-days-show-hours": "$1 {{PLURAL:$1|horo|horoj}}",
+       "rcfilters-highlighted-filters-list": "Emfazita: $1",
+       "rcfilters-quickfilters": "Konservitaj filtriloj",
+       "rcfilters-quickfilters-placeholder-title": "Ankoraŭ neniuj filtriloj estas konservitaj",
+       "rcfilters-savedqueries-defaultlabel": "Konservitaj filtriloj",
+       "rcfilters-savedqueries-rename": "Alinomi",
+       "rcfilters-savedqueries-remove": "Forigi",
+       "rcfilters-savedqueries-new-name-label": "Nomo",
+       "rcfilters-savedqueries-new-name-placeholder": "Priskribas la celon de la filtrilo",
+       "rcfilters-savedqueries-apply-label": "Krei filtrilon",
+       "rcfilters-savedqueries-cancel-label": "Nuligi",
        "rcfilters-restore-default-filters": "Restarigi defaŭltajn filtrilojn",
        "rcfilters-clear-all-filters": "Nuligi ĉiujn filtrilojn",
+       "rcfilters-show-new-changes": "Vidi la plej novajn ŝanĝojn",
        "rcfilters-search-placeholder": "Filtri lastajn ŝanĝojn (vi povas elekti aŭ ekskribi)",
        "rcfilters-invalid-filter": "Nevalida filtrilo",
        "rcfilters-empty-filter": "Ekzistas neniuj aktivaj filtriloj. Ĉiuj kontribuaĵoj estas montritaj.",
        "rcfilters-filterlist-title": "Filtriloj",
+       "rcfilters-filterlist-whatsthis": "Kiel funkcias ĉi tiuj?",
+       "rcfilters-filterlist-feedbacklink": "Diru al ni kion vi opinias pri tiuj (novaj) filtraj iloj.",
+       "rcfilters-highlightbutton-title": "Emfazi rezultojn",
+       "rcfilters-highlightmenu-title": "Elekti koloron",
        "rcfilters-filterlist-noresults": "Neniuj filtriloj troviĝis",
        "rcfilters-filtergroup-authorship": "Redakta aŭtoreco",
        "rcfilters-filter-editsbyself-label": "Viaj redaktoj",
-       "rcfilters-filter-editsbyself-description": "Viaj redaktoj.",
+       "rcfilters-filter-editsbyself-description": "Viaj kontribuoj.",
        "rcfilters-filter-editsbyother-label": "Redaktoj de aliuloj",
        "rcfilters-filter-editsbyother-description": "Redaktoj kreitaj far aliaj uzantoj (krom vi).",
        "rcfilters-filtergroup-userExpLevel": "Spertonivelo (nur por registritaj uzantoj)",
        "block": "Forbari uzanton",
        "unblock": "Malforbari uzanton",
        "blockip": "Forbari {{GENDER:$1|uzanton}}",
-       "blockip-legend": "Forbari uzanton",
        "blockiptext": "Uzu la sube formularon por forbari skribpermison de specifa uzantnomo aŭ IP-adreso. Tiu endus ''nur'' por eviti vandalismon, kaj laŭe la [[{{MediaWiki:Policy-url}}|politiko]].\nKlarigu la precizan kialon sube (ekzemple, citu paĝojn, kiuj estis vandaligitaj).\nVi povas forbari IP-adresan intervalon per la  [https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing CIDR]a sintakso; la plej ampleksa intervalo estas /$1 por IPv4 kaj /$2 por IPv6.",
        "ipaddressorusername": "IP-adreso aŭ salutnomo:",
        "ipbexpiry": "Blokdaŭro",
index 391fbbe..b5f65a1 100644 (file)
        "anonpreviewwarning": "<em>No has iniciado sesión. Al guardar los cambios se almacenará tu dirección IP en el historial de edición de esta página.</em>",
        "missingsummary": "<strong>Atención:</strong> no has escrito un resumen de edición.\nSi haces clic de nuevo en «$1» tu edición se grabará sin él.",
        "selfredirect": "<strong>Advertencia:</strong> estás redirigiendo esta página a sí misma.\nPuede que hayas especificado erróneamente el destino de la redirección, o quizá estés editando la página equivocada. En cualquier caso, si haces clic de nuevo en \"$1\", se creará la redirección.",
-       "missingcommenttext": "Escribe un comentario a continuación.",
+       "missingcommenttext": "Escribe un comentario.",
        "missingcommentheader": "<strong>Atención:</strong> no has escrito un asunto para este comentario.\nSi haces clic nuevamente en \"$1\" tu edición se grabará sin él.",
        "summary-preview": "Previsualización del resumen de edición:",
        "subject-preview": "Previsualización del asunto:",
        "rcfilters-savedqueries-apply-and-setdefault-label": "Crear filtro predeterminado",
        "rcfilters-savedqueries-cancel-label": "Cancelar",
        "rcfilters-savedqueries-add-new-title": "Guardar ajustes de filtro actuales",
-       "rcfilters-savedqueries-already-saved": "Estos filtros son ja almacenados",
+       "rcfilters-savedqueries-already-saved": "Ya se guardaron estos filtros. Modifica tu configuración para crear un filtro guardado nuevo.",
        "rcfilters-restore-default-filters": "Restaurar filtros predeterminados",
        "rcfilters-clear-all-filters": "Borrar todos los filtros",
        "rcfilters-show-new-changes": "Ver cambios más recientes",
        "rcfilters-filter-user-experience-level-unregistered-label": "No registrados",
        "rcfilters-filter-user-experience-level-unregistered-description": "Editores no conectados.",
        "rcfilters-filter-user-experience-level-newcomer-label": "Recién llegados",
-       "rcfilters-filter-user-experience-level-newcomer-description": "Usuarios registrados con menos de 10 ediciones y 4 días de actividad.",
+       "rcfilters-filter-user-experience-level-newcomer-description": "Usuarios registrados con menos de diez ediciones o cuatro días de actividad.",
        "rcfilters-filter-user-experience-level-learner-label": "Aprendices",
        "rcfilters-filter-user-experience-level-learner-description": "Editores registrados cuya experiencia se ubica entre \"Recién Llegados\" y \"Usuarios experimentados\".",
        "rcfilters-filter-user-experience-level-experienced-label": "Usuarios experimentados",
index ce275c8..a59a059 100644 (file)
        "readonly": "Andmebaas lukustatud",
        "enterlockreason": "Sisesta lukustamise põhjus ning juurdepääsu taastamise ligikaudne aeg",
        "readonlytext": "Andmebaas on praegu lukustatud. Uusi sissekandeid ja muid muudatusi ei saa teha. Tõenäoliselt toimub andmebaasi plaanipärane hooldus, mille järel tavaline olukord taastub.\nSüsteemiadministraator, kes andmebaasi lukustas, andis järgmise selgituse: $1",
-       "missing-article": "Andmebaas ei leidnud küsitud lehekülje \"$1\" $2 teksti.\n\nPõhjuseks võib olla võrdlus- või ajaloolink kustutatud leheküljele.\n\nKui tegemist ei ole nimetatud olukorraga, võib tegu olla ka süsteemi veaga.\nSellisel juhul tuleks teavitada [[Special:ListUsers/sysop|administraatorit]], edastades talle ka käesoleva lehe internetiaadressi.",
+       "missing-article": "Andmebaasist ei leidnud päritud lehekülje \"$1\" $2 teksti.\n\nPõhjuseks võib olla võrdlus- või ajaloolink kustutatud leheküljele.\n\nKui asi ei ole selles, võib tegu olla süsteemi veaga.\nPalun teata sellest [[Special:ListUsers/sysop|administraatorile]], edastades ka lehekülje internetiaadressi.",
        "missingarticle-rev": "(redaktsioon: $1)",
        "missingarticle-diff": "(redaktsioonid: $1, $2)",
        "readonly_lag": "Andmebaas on automaatselt lukustatud, seniks kuni sekundaarsed andmebaasiserverid on primaarserveriga samal järjel.",
        "password-login-forbidden": "Selle kasutajanime ja parooli kasutamine on keelatud.",
        "mailmypassword": "Lähtesta parool",
        "passwordremindertitle": "{{SITENAME}} – ajutine parool",
-       "passwordremindertext": "Keegi IP-aadressiga $1, tõenäoliselt sa ise, palus, et talle saadetaks {{GRAMMAR:elative|{{SITENAME}}}} uus parool ($4). Kasutaja \"$2\" ajutiseks paroolis seati \"$3\". Kui soovid tõepoolest uut parooli, pead sisse logima ja uue parooli valima. Ajutine parool aegub {{PLURAL:$5|ühe päeva|$5 päeva}} pärast.\n\nKui uut parooli palus keegi teine või sulle meenus vana parool ja sa ei soovi seda enam muuta, võid käesolevat teadet eirata ning jätkata endise parooli kasutamist.",
+       "passwordremindertext": "Keegi IP-aadressiga $1, tõenäoliselt sa ise, palus, et talle\nsaadetaks {{GRAMMAR:elative|{{SITENAME}}}} uus parool ($4).\nKasutaja \"$2\" ajutiseks paroolis seati \"$3\". Kui soovid tõepoolest\nuut parooli, pead sisse logima ja uue parooli valima.\nAjutine parool aegub {{PLURAL:$5|ühe|$5}} päeva pärast.\n\nKui uut parooli palus keegi teine või sulle meenus vana parool\nja sa ei soovi seda enam muuta, võid seda teadet eirata ning\njätkata senise parooli kasutamist.",
        "noemail": "Kasutaja $1 e-posti aadressi meil kahjuks pole.",
        "noemailcreate": "Pead sisestama korrektse e-posti aadressi",
        "passwordsent": "Uus parool on saadetud kasutaja $1 registreeritud e-postiaadressil.\nPärast parooli saamist logige palun sisse.",
        "accountcreated": "Konto loodud",
        "accountcreatedtext": "Kasutaja [[{{ns:User}}:$1|$1]] ([[{{ns:User talk}}:$1|talk]]) konto on loodud.",
        "createaccount-title": "{{GRAMMAR:illative|{{SITENAME}}}} konto loomine",
-       "createaccount-text": "Keegi on loonud {{GRAMMAR:illative|{{SITENAME}}}} ($4) sinu e-posti aadressile vastava kasutajatunnuse \"$2\". Parooliks seati \"$3\". Logi sisse ja muuda oma parool.\n\nKui kasutajakonto loomine on eksitus, võid käesolevat sõnumit lihtsalt eirata.",
+       "createaccount-text": "Keegi on loonud {{GRAMMAR:illative|{{SITENAME}}}} ($4) sinu e-posti aadressile vastava kasutajatunnuse \"$2\". Parooliks seati \"$3\". Peaksid sisse logima ja parooli muutma.\n\nKui kasutajakonto loomine oli eksitus, võid seda sõnumit lihtsalt eirata.",
        "login-throttled": "Oled lühikese aja jooksul proovinud liiga palju kordi sisse logida.\nPalun oota $1, enne kui uuesti proovid.",
        "login-abort-generic": "Sisselogimine ebaõnnestus – Katkestatud",
        "login-migrated-generic": "Sinu konto on migreeritud ja sinu kasutajanime pole enam selles vikis.",
        "anonpreviewwarning": "''Sa pole sisse logitud. Selle lehe redigeerimislogisse salvestatakse su IP-aadress.''",
        "missingsummary": "'''Meeldetuletus:''' Sa ei ole lisanud muudatuse resümeed.\nKui vajutad uuesti salvestamise nupule, salvestatakse muudatus ilma resümeeta.",
        "selfredirect": "<strong>Hoiatus:</strong> Suunad selle lehekülje iseeneda juurde.\nVõimalik, et oled määranud ümbersuunamise jaoks vale sihtleheküljeks või redigeerid vale lehekülge.\nÜmbersuunamine luuakse sellest hoolimata, kui klõpsad uuesti \"$1\".",
-       "missingcommenttext": "Palun sisesta siit allapoole kommentaar.",
+       "missingcommenttext": "Palun sisesta kommentaar.",
        "missingcommentheader": "<strong>Meeldetuletus:</strong> Sa pole kirjutanud kommentaarile teemat.\nKui klõpsad uuesti \"$1\", salvestatakse su kommentaar ilma teemata.",
        "summary-preview": "Resümee eelvaade:",
        "subject-preview": "Resümee eelvaade:",
        "newarticle": "(Uus)",
        "newarticletext": "Lehekülge, kuhu link sind suunas, pole veel.\nEt lehekülg luua, alusta allolevas kastis kirjutamist (lisateave [$1 juhendist]).\nKui sattusid siia kogemata, klõpsa brauseri ''tagasi''-nupule.",
        "anontalkpagetext": "----''See on anonüümse kasutaja arutelulehekülg. See kasutaja pole kontot loonud või ei kasuta seda. Sellepärast tuleb meil kasutaja tuvastamiseks kasutada tema IP-aadressi. Sellist IP-aadressi võib kasutada mitu kasutajat. Kui oled osutatud IP-aadressi kasutaja ning leiad, et siinsed kommentaarid ei puutu kuidagi sinusse, [[Special:CreateAccount|loo palun kasutajakonto]] või [[Special:UserLogin|logi sisse]], et sind edaspidi teiste anonüümsete kasutajatega segi ei aetaks.''",
-       "noarticletext": "Käesoleval leheküljel hetkel teksti ei ole.\nVõid [[Special:Search/{{PAGENAME}}|otsida pealkirjaks olevat fraasi]] teistelt lehtedelt,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} uurida asjassepuutuvaid logisid] või [{{fullurl:{{FULLPAGENAME}}|action=edit}} puuduva lehekülje ise luua]</span>.",
+       "noarticletext": "Siin leheküljel puudub praegu tekst.\nSaad [[Special:Search/{{PAGENAME}}|otsida pealkirjateksti]] teistelt lehekülgedelt,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} uurida asjassepuutuvaid logisid]\nvõi [{{fullurl:{{FULLPAGENAME}}|action=edit}} puuduva lehekülje luua]</span>.",
        "noarticletext-nopermission": "Sellel leheküljel pole praegu teksti.\nSaad [[Special:Search/{{PAGENAME}}|otsida selle lehekülje pealkirja]] teistelt lehekülgedelt või <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} otsida seonduvatest logidest]</span>, aga sul pole õigust seda lehekülge alustada.",
        "missing-revision": "Lehekülje \"{{FULLPAGENAME}}\" redaktsiooni $1 pole.\n\nHarilikult tähendab see seda, et sind siia juhatanud link on vananenud ja siin asunud lehekülg on kustutatud.\nÜksikasjad leiad [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} kustutamislogist].",
        "userpage-userdoesnotexist": "Kasutajakontot \"<nowiki>$1</nowiki>\" pole olemas.\nPalun mõtle järele, kas soovid seda lehte luua või muuta.",
        "recentchanges-legend": "Viimaste muudatuste seaded",
        "recentchanges-summary": "Jälgi sellel leheküljel viimaseid muudatusi.",
        "recentchanges-noresult": "Selles ajavahemikus pole tehtud neile kriteeriumitele vastavaid muudatusi.",
+       "recentchanges-timeout": "See otsing aegus. Võid proovida teisi otsiparameetreid.",
+       "recentchanges-network": "Tehnilise tõrke tõttu ei õnnestunud tulemusi laadida. Palun proovi lehekülge värskendada.",
        "recentchanges-feed-description": "Jälgi vikisse tehtud viimaseid muudatusi.",
        "recentchanges-label-newpage": "Uus lehekülg",
        "recentchanges-label-minor": "Pisiparandus",
        "rcfilters-days-show-hours": "$1 {{PLURAL:$1|tund|tundi}}",
        "rcfilters-highlighted-filters-list": "Esile tõstetud: $1",
        "rcfilters-quickfilters": "Salvestatud filtrid",
-       "rcfilters-quickfilters-placeholder-title": "Linke pole veel salvestatud",
+       "rcfilters-quickfilters-placeholder-title": "Filtreid pole veel salvestatud",
        "rcfilters-quickfilters-placeholder-description": "Et filtri sätted salvestada ja et neid hiljem uuesti kasutada, klõpsa alloleva aktiivsete filtrite loendi juures järjehoidjaikooni.",
        "rcfilters-savedqueries-defaultlabel": "Salvestatud filtrid",
        "rcfilters-savedqueries-rename": "Nimeta ümber",
        "rcfilters-savedqueries-apply-and-setdefault-label": "Koosta vaikefilter",
        "rcfilters-savedqueries-cancel-label": "Loobu",
        "rcfilters-savedqueries-add-new-title": "Salvesta filtri praegused sätted",
+       "rcfilters-savedqueries-already-saved": "Need filtrid on juba salvestatud. Muuda sätteid, et koostada uus salvestatud filter.",
        "rcfilters-restore-default-filters": "Taasta vaikefiltrid",
        "rcfilters-clear-all-filters": "Eemalda kõik filtrid",
        "rcfilters-show-new-changes": "Vaata uusimaid muudatusi",
-       "rcfilters-search-placeholder": "Filtri viimaseid muudatusi (sirvi või alusta tippimist)",
+       "rcfilters-search-placeholder": "Filtri muudatusi (kasuta menüüd või otsi filtri nime)",
        "rcfilters-invalid-filter": "Vigane filter",
        "rcfilters-empty-filter": "Aktiivsed filtrid puuduvad. Näidatakse kogu kaastööd.",
        "rcfilters-filterlist-title": "Filtrid",
        "rcfilters-tag-prefix-namespace-inverted": "<strong>:mitte</strong> $1",
        "rcfilters-exclude-button-off": "Jäta valitud välja",
        "rcfilters-exclude-button-on": "Valitud välja jäetud",
-       "rcfilters-view-advanced-filters-label": "Täpsemad filtrid",
        "rcfilters-view-tags": "Märgistatud muudatused",
        "rcfilters-view-namespaces-tooltip": "Filtri tulemusi nimeruumide lõikes",
        "rcfilters-view-tags-tooltip": "Filtri tulemusi muudatusmärgiste lõikes",
        "rcfilters-view-return-to-default-tooltip": "Naase filtri peamenüüsse",
+       "rcfilters-view-tags-help-icon-tooltip": "Uuri veel märgistatud muudatuste kohta",
        "rcfilters-liveupdates-button": "Uuendused reaalajas",
        "rcfilters-liveupdates-button-title-on": "Lülita reaalajas uuendamine välja",
        "rcfilters-liveupdates-button-title-off": "Näita uusi muudatusi kohe nende tegemise järel",
        "uploaded-script-svg": "Üleslaaditud SVG-failist leiti skriptitav element \"$1\".",
        "uploaded-hostile-svg": "Üleslaaditud SVG-faili laadielemendist leiti ebaturvaline CSS.",
        "uploaded-event-handler-on-svg": "Sündmuse halduse atribuutide <code>$1=\"$2\"</code> seadmine pole SVG-failis lubatud.",
-       "uploaded-href-attribute-svg": "SVG-failis on lubatud href-atribuudiga viidata ainult sihtkohta skeemiga http:// või https://. Leiti <code>&lt;$1 $2=\"$3\"&gt;</code>.",
+       "uploaded-href-attribute-svg": "Element <a> saab href-atribuudi väärtuses linkida ainult sihtobjektile data: (manusfail), http:// või https:// või fragmendile (#, sama-dokument).  Teistes elementides, nagu <image>, on lubatud ainult data: ja fragment. Proovi SVG-faili eksportimisel faile manustada. Leiti <code>&lt;$1 $2=\"$3\"&gt;</code>.",
        "uploaded-href-unsafe-target-svg": "Üleslaaditud SVG-failist leiti href, mis viitab ebaturvalistele andmetele: URI sihtkoht <code>&lt;$1 $2=\"$3\"&gt;</code>.",
        "uploaded-animate-svg": "Üleslaaditud SVG-failist leiti silt \"animate\", mis võib href-i muuta, kasutades from-atribuuti <code>&lt;$1 $2=\"$3\"&gt;</code>.",
        "uploaded-setting-event-handler-svg": "Sündmuse halduse atribuutide seadmine on keelatud, üleslaaditud SVG-failist leiti <code>&lt;$1 $2=\"$3\"&gt;</code>.",
        "uploadstash-refresh": "Värskenda faililoendit",
        "uploadstash-thumbnail": "vaata pisipilti",
        "uploadstash-exception": "Üleslaaditavat faili ei õnnestunud peithoidlas talletada ($1): \"$2\".",
+       "uploadstash-bad-path": "Teed pole olemas.",
+       "uploadstash-bad-path-invalid": "Tee pole sobiv.",
+       "uploadstash-bad-path-unknown-type": "Tundmatu tüüp \"$1\".",
+       "uploadstash-bad-path-unrecognized-thumb-name": "Tundmatu pisipildi nimi.",
+       "uploadstash-bad-path-no-handler": "Faili $2 MIME tüübile $1 ei leitud töötlejat.",
+       "uploadstash-bad-path-bad-format": "Võti \"$1\" pole sobivas vormingus.",
+       "uploadstash-file-not-found": "Peithoidlas ei leidu võtit \"$1\".",
+       "uploadstash-file-not-found-no-thumb": "Pisipilti ei õnnestu hankida.",
+       "uploadstash-file-not-found-no-local-path": "Mastaabitud elemendi kohalikku teed ei leitud.",
+       "uploadstash-file-not-found-no-object": "Pisipildi kohalikku failiobjekti ei õnnestunud luua.",
+       "uploadstash-file-not-found-no-remote-thumb": "Pisipilti ei õnnestunud hankida: $1\nURL = $2",
+       "uploadstash-file-not-found-missing-content-type": "Sisutüübi päis puudub.",
+       "uploadstash-file-not-found-not-exists": "Ei õnnestunud leida teed või faili ennast.",
+       "uploadstash-file-too-large": "$1 baidist suuremat faili ei saa töödelda.",
+       "uploadstash-not-logged-in": "Ükski kasutaja pole sisse logitud, fail peab kuuluma kasutajatele.",
+       "uploadstash-wrong-owner": "See fail ($1) ei kuulu praegusele kasutajale.",
+       "uploadstash-no-such-key": "Puudub selline võti ($1), ei saa eemaldada.",
+       "uploadstash-no-extension": "Faililaiend puudub.",
+       "uploadstash-zero-length": "Faili suurus on tühiväärtusega.",
        "invalid-chunk-offset": "Tüki vigane nihe",
        "img-auth-accessdenied": "Juurdepääs keelatud",
        "img-auth-nopathinfo": "PATH_INFO puudub.\nSinu server pole seadistatud seda teavet edastama.\nSee võib olla CGI-põhine ja ei toeta img_auth-i.\nVaata lehekülge https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
        "apihelp": "API abi",
        "apihelp-no-such-module": "Moodulit \"$1\" ei leitud.",
        "apisandbox": "API liivakast",
+       "apisandbox-jsonly": "API liivakasti kasutamine nõuab JavaScripti.",
        "apisandbox-api-disabled": "API on selles võrgukohas keelatud.",
        "apisandbox-intro": "Kasuta seda lehekülge <strong>MediaWiki API</strong> katsetamiseks.\nÜksikasjad API kasutamise kohta leiad [[mw:API:Main page|API dokumentatsioonist]]. Näide: [https://www.mediawiki.org/wiki/API#A_simple_example esilehe sisu hankimine]. Vali toiming, et näha veel näiteid.\n\nPane tähele, et kuigi siin on liivakast, võivad siin leheküljel tehtud toimingud vikit muuta.",
+       "apisandbox-fullscreen": "Laienda paneel",
+       "apisandbox-fullscreen-tooltip": "Laienda liivakastipaneel brauseriakna suuruseks",
+       "apisandbox-unfullscreen": "Näita lehekülge",
+       "apisandbox-unfullscreen-tooltip": "Vähenda liivakastipaneeli, nii et MediaWiki navigeerimislingid on nähtaval",
        "apisandbox-submit": "Tee päring",
        "apisandbox-reset": "Puhasta",
+       "apisandbox-retry": "Proovi uuesti",
+       "apisandbox-loading": "API mooduli \"$1\" teabe laadimine...",
+       "apisandbox-load-error": "API mooduli \"$1\" teabe laadimisel esines tõrge: $2",
+       "apisandbox-no-parameters": "Sellel API moodulil pole parameetreid.",
+       "apisandbox-helpurls": "Abilingid",
        "apisandbox-examples": "Näited",
+       "apisandbox-dynamic-parameters": "Lisaparameetrid",
+       "apisandbox-dynamic-parameters-add-label": "Lisa parameeter:",
+       "apisandbox-dynamic-parameters-add-placeholder": "Parameetri nimi",
+       "apisandbox-dynamic-error-exists": "Parameeter nimega \"$1\" on juba olemas.",
+       "apisandbox-deprecated-parameters": "Vananenud parameetrid",
+       "apisandbox-fetch-token": "Hangi luba automaatselt",
+       "apisandbox-submit-invalid-fields-title": "Mõned väljad on vigased",
+       "apisandbox-submit-invalid-fields-message": "Palun paranda märgitud väljad ja proovi uuesti.",
        "apisandbox-results": "Tulemused",
+       "apisandbox-sending-request": "API päringu saatmine...",
+       "apisandbox-loading-results": "API tulemuste laekumine...",
+       "apisandbox-results-error": "API päringu vastuse laadimisel esines tõrge: $1.",
+       "apisandbox-results-login-suppressed": "See päring tehti välja logitud kasutajaga, mida saab kasutada selleks, et hiilida mööda brauseri sama päritolu turvafunktsioonist. Pane tähele, et sellise päringuga ei töötle API liivakast automaatset luba õigesti. Palun sisesta luba käsitsi.",
+       "apisandbox-request-selectformat-label": "Näita päringu andmeid nii:",
+       "apisandbox-request-format-url-label": "URL-päringusõne",
        "apisandbox-request-url-label": "Päringu URL:",
+       "apisandbox-request-json-label": "Päringu JSON:",
        "apisandbox-request-time": "Päringuaeg: {{PLURAL:$1|$1 ms}}",
+       "apisandbox-results-fixtoken": "Paranda luba ja saada uuesti",
+       "apisandbox-results-fixtoken-fail": "$1-luba ei õnnestunud hankida.",
+       "apisandbox-alert-page": "Osa välju sellel leheküljel pole sobivad.",
+       "apisandbox-alert-field": "Selle välja väärtus pole sobiv.",
+       "apisandbox-continue": "Jätka",
+       "apisandbox-continue-clear": "Tühjenda",
+       "apisandbox-continue-help": "\"{{int:apisandbox-continue}}\" [https://www.mediawiki.org/wiki/API:Query#Continuing_queries jätkab] viimase päringuga; \"{{int:apisandbox-continue-clear}}\" tühjendab jätkuparameetrid.",
+       "apisandbox-param-limit": "Sisesta <kbd>max</kbd>, et kasutada ülemmäära.",
+       "apisandbox-multivalue-all-namespaces": "$1 (kõik nimeruumid)",
+       "apisandbox-multivalue-all-values": "$1 (kõik väärtused)",
        "booksources": "Raamatuotsimine",
        "booksources-search-legend": "Raamatuotsimine",
        "booksources-search": "Otsi",
        "protect-expiring": "aegumistähtaeg $1 (UTC)",
        "protect-expiring-local": "aegumistähtaeg $1",
        "protect-expiry-indefinite": "tähtajatu",
-       "protect-cascade": "Kaitse lehekülgi, mis on lülitatud käesoleva lehekülje koosseisu (kaskaadkaitse)",
+       "protect-cascade": "Kaitse lehekülgi, mis on siinse lehekülje koosseisus (kaskaadkaitse)",
        "protect-cantedit": "Sa ei saa lehekülje kaitsetaset muuta, sest sul puudub lehekülje redigeerimise õigus.",
        "protect-othertime": "Muu aeg:",
        "protect-othertime-op": "muu aeg",
        "ipb_blocked_as_range": "Tõrge: IP-aadressi $1 pole eraldi blokeeritud ja blokeeringut ei saa eemaldada.\nSee kuulub aga blokeeritud IP-vahemikku $2, mille blokeeringut saab eemaldada.",
        "ip_range_invalid": "Vigane IP-vahemik.",
        "ip_range_toolarge": "Suuremad aadressiblokid kui /$1 pole lubatud.",
+       "ip_range_exceeded": "See IP-aadressivahemik ületab maksimumvahemikku. Lubatud vahemik: /$1.",
+       "ip_range_toolow": "IP-aadressivahemikud on sisuliselt keelatud.",
        "proxyblocker": "Proksiblokeerija",
        "proxyblockreason": "Sinu IP-aadress on blokeeritud, sest see on avatud proksi. Palun võta ühendust oma internetiteenuse pakkujaga või tehnilise toega ja teata neile sellest probleemist.",
        "sorbsreason": "Sinu IP-aadress on {{GRAMMAR:genitive|{{SITENAME}}}} kasutatavas DNS-põhises mustas nimekirjas märgitud kui avatud proksi.",
index 2873c01..7fafe29 100644 (file)
        "recentchanges-summary": "Orrialde honetan ikus ditzakezu wiki honetan egindako azken aldaketak.",
        "recentchanges-noresult": "Ez da egon aldaketarik emandako tartean irizpide hau betetzen dutenik.",
        "recentchanges-timeout": "Bilaketa honek denbora muga gainditu du. Agian beste parametro batzuekin bilatu nahi duzu.",
+       "recentchanges-network": "Errore tekniko baten ondorioz, ez da emaitzarik kargatu. Saiatu orria freskatzen.",
        "recentchanges-feed-description": "Sindikazio honetan wikian eginiko azkeneko aldaketak jarrai daitezke.",
        "recentchanges-label-newpage": "Aldaketa honek orri berri bat sortu du",
        "recentchanges-label-minor": "Aldaketa hau txikia da",
        "rcfilters-savedqueries-apply-and-setdefault-label": "Iragazkia sortu",
        "rcfilters-savedqueries-cancel-label": "Utzi",
        "rcfilters-savedqueries-add-new-title": "Gorde oraingo iragazki ezarpenak",
+       "rcfilters-savedqueries-already-saved": "Iragazki hauek dagoeneko gorde dira. Aldatu ezarpenak Gordetako Iragazki berri bat sortzeko.",
        "rcfilters-restore-default-filters": "Leheneratu iragazki lehenetsiak",
        "rcfilters-clear-all-filters": "Iragazki guztiak garbitu",
        "rcfilters-show-new-changes": "Aldaketa berrienak ikusi",
-       "rcfilters-search-placeholder": "Azken aldaketak iragazi (arakatu ala idazten hasi)",
+       "rcfilters-search-placeholder": "Aldaketak iragazi (menua erabili edo bilatu iragazkiaren izena)",
        "rcfilters-invalid-filter": "Balio ez duen iragazkia",
        "rcfilters-empty-filter": "Filtro aktiborik ez dago. Ekarpen guztiak erakusten.",
        "rcfilters-filterlist-title": "Iragazkiak",
        "uploadstash-refresh": "Fitxategien zerrenda eguneratu",
        "uploadstash-thumbnail": "Koadro txikia ikusi",
        "uploadstash-exception": "Igoera ezin izan da ($1) biltegian gorde: \"$2\".",
+       "uploadstash-bad-path": "Bidea ez da existitzen.",
+       "uploadstash-bad-path-invalid": "Bideak ez du balio.",
+       "uploadstash-bad-path-unknown-type": "Mota ezezaguna \"$1\".",
+       "uploadstash-file-not-found-missing-content-type": "Eduki-motako goiburua falta da.",
+       "uploadstash-file-not-found-not-exists": "Ezin da bidea aurkitu, edo ez da fitxategi arrunta.",
+       "uploadstash-file-too-large": "Ezin da $1 byte baino handiagoa den fitxategia zerbitzatu.",
+       "uploadstash-not-logged-in": "Erabiltzaileak ez du saioa hasi, fitxategiak erabiltzailearenak izan behar dira.",
+       "uploadstash-wrong-owner": "($1) Fitxategia ez da uneko erabiltzailearena.",
+       "uploadstash-no-such-key": "($1) gakorik ez dago, ezin da ezabatu.",
+       "uploadstash-no-extension": "Luzapena nulua da.",
+       "uploadstash-zero-length": "Fitxategiaren luzeera zero da.",
        "invalid-chunk-offset": "Desplazamendu zati baliogabea",
        "img-auth-accessdenied": "Sarbide ukatua",
        "img-auth-nopathinfo": "PATH_INFO falta da.\nZure zerbitzaria ez dago informazio hau pasatzeko konfiguratuta.\nCGI-oinarriduna izan daiteke, img_auth onartzen ez duena.\nIkusi https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
index 4df5481..a0dc4c3 100644 (file)
@@ -61,7 +61,8 @@
                        "AzorAhai",
                        "Yoosef Pooranvary",
                        "DEXi",
-                       "Obzord"
+                       "Obzord",
+                       "Alp Er Tunqa"
                ]
        },
        "tog-underline": "خط کشیدن زیر پیوندها:",
        "rcfilters-savedqueries-apply-and-setdefault-label": "ایجاد پالایه پیش‌فرض",
        "rcfilters-savedqueries-cancel-label": "لغو",
        "rcfilters-savedqueries-add-new-title": "ذخیره تنظیمات کنونی پالایه",
+       "rcfilters-savedqueries-already-saved": "این پالایه‌ها اکنون ذخیره شده‌اند",
        "rcfilters-restore-default-filters": "بازگردانی پالایه‌های پیش‌فرض",
        "rcfilters-clear-all-filters": "پاک‌کردن تمام پالایه‌ها",
        "rcfilters-show-new-changes": "دیدن جدیدترین تغییرات",
        "uploadstash-refresh": "تازه کردن فهرست پرونده‌ها",
        "uploadstash-thumbnail": "نمایش بندانگشتی",
        "uploadstash-exception": "ناتوان از ذخیره کردن بارگذاری در نهانگاه ($1): ''$2''.",
+       "uploadstash-bad-path": "مسر وجود ندارد.",
+       "uploadstash-bad-path-invalid": "مسیر معتبر نیست.",
+       "uploadstash-bad-path-unknown-type": "گونهٔ ناشناختهٔ \"$1\".",
+       "uploadstash-bad-path-unrecognized-thumb-name": "بندانگشتی نامعلوم.",
+       "uploadstash-bad-path-no-handler": "برای نمایش $1 پروندهٔ $2 اجراکننده‌ای یافت نشد.",
+       "uploadstash-bad-path-bad-format": "کلید «$1» ساختار درستی ندارد.",
+       "uploadstash-file-not-found": "کلید «$1» در انبار یافت نشد.",
+       "uploadstash-file-not-found-no-thumb": "امکان گرفتن بندانگشتی نیست.",
+       "uploadstash-file-not-found-no-local-path": "مسیر محلی برای آیتم مقایس‌شده وجود ندارد.",
+       "uploadstash-file-not-found-no-object": "امکان ساخت شیء پروندهٔ محلی برای بندانگشتی وجود ندارد.",
+       "uploadstash-file-not-found-no-remote-thumb": "دریافت بندانگشتی با مشکل مواجه شد:$1\nنشانی = $2",
+       "uploadstash-file-not-found-missing-content-type": "سرآیند نوع-محتوی یافت نشد.",
+       "uploadstash-file-not-found-not-exists": "امکان یافتن مسیر نیست، یا فایل ساده نیست.",
+       "uploadstash-file-too-large": "امکان ذخیرهٔ پرونده بیش از $1 بایت نیست.",
+       "uploadstash-not-logged-in": "هیچ کاربری به سامانه وارد نشده‌است، پرونده باید متعلق به یک کاربر باشد.",
+       "uploadstash-wrong-owner": "این پرونده ($1) به این کاربر تعلق ندارد.",
+       "uploadstash-no-such-key": "امکان حذف کلید ($1) نیست.",
+       "uploadstash-no-extension": "افزونه نیست.",
+       "uploadstash-zero-length": "اندازهٔ پرونده صفر است.",
        "invalid-chunk-offset": "جابجایی نامعتبر قطعه",
        "img-auth-accessdenied": "منع دسترسی",
        "img-auth-nopathinfo": "PATH_INFO موجود نیست.\nسرور شما برای ردکردن این مقدار تنظیم نشده‌است.\nممکن است مبتنی بر سی‌جی‌آی باشد و از img_auth پشتیبانی نکند.\nhttps://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization را ببینید.",
index 2d66603..ee86a94 100644 (file)
        "fileduplicatesearch-noresults": "Tiedostoa nimeltä ”$1” ei löytynyt.",
        "specialpages": "Toimintosivut",
        "specialpages-note-top": "Merkkien selitys",
+       "specialpages-note-restricted": "* Tavalliset toimintosivut.\n* <span class=\"mw-specialpagerestricted\">Rajoitetut toimintosivut.</span>",
        "specialpages-group-maintenance": "Sivujen huoltaminen",
        "specialpages-group-other": "Muut",
        "specialpages-group-login": "Sisäänkirjautuminen ja tunnusten luonti",
        "compare-title-not-exists": "Määrittämääsi sivua ei ole.",
        "compare-revision-not-exists": "Määrittämääsi versiota ei ole.",
        "diff-form": "Eroavaisuudet",
+       "diff-form-oldid": "Vanhan sivuversion tunnistenumero (vapaaehtoinen tieto)",
+       "diff-form-revid": "Eroavan sivuversion tunnistenumero",
        "diff-form-submit": "Näytä muutokset",
        "permanentlink": "Pysyvä linkki",
        "permanentlink-revid": "Versiotunniste",
+       "permanentlink-submit": "Mene sivuversioon",
        "dberr-problems": "Tällä sivustolla on teknisiä ongelmia.",
        "dberr-again": "Odota hetki ja lataa sivu uudelleen.",
        "dberr-info": "(Tietokantaan ei saada yhteyttä: $1)",
        "gotointerwiki-external": "Olet lähdössä {{GRAMMAR:elative|{{SITENAME}}}} toiselle sivustolle [[$2]].\n\n'''[$1 Jatka osoitteeseen $1]'''",
        "undelete-cantedit": "Et voi palauttaa tätä sivua, koska sinulla ei ole oikeutta muokata tätä sivua.",
        "undelete-cantcreate": "Et voi palauttaa tätä sivua, koska tällä nimellä ei ole olemassaolevaa sivua eikä sinulla ole oikeutta luoda tätä sivua.",
+       "pagedata-title": "Sivudata",
        "pagedata-bad-title": "Virheellinen otsikko: $1."
 }
index d15abb3..8892be0 100644 (file)
                        "DePlusJean",
                        "Pierpao",
                        "Vexthedorito",
-                       "Djiboun"
+                       "Djiboun",
+                       "Pols12"
                ]
        },
        "tog-underline": "Soulignement des liens :",
        "tog-watchuploads": "Ajouter les nouveaux fichiers que j’importe à ma liste de suivi",
        "tog-watchrollback": "Ajouter à ma liste de suivi les pages sur lesquelles j’ai effectué une révocation",
        "tog-minordefault": "Marquer toutes mes modifications comme étant mineures par défaut",
-       "tog-previewontop": "Afficher la prévisualisation au dessus de la zone d’édition",
+       "tog-previewontop": "Afficher la prévisualisation au-dessus de la zone de modification",
        "tog-previewonfirst": "Afficher la prévisualisation lors de la première modification",
        "tog-enotifwatchlistpages": "M’avertir par courriel lorsqu’une page ou un fichier de ma liste de suivi est modifié",
        "tog-enotifusertalkpages": "M’avertir par courriel lorsque ma page de discussion est modifiée",
        "underline-always": "Toujours",
        "underline-never": "Jamais",
        "underline-default": "Valeur par défaut du thème ou du navigateur",
-       "editfont-style": "Style de police de la zone d’édition :",
+       "editfont-style": "Style de police de la zone de modification :",
        "editfont-monospace": "Police à chasse fixe",
        "editfont-sansserif": "Police sans-serif",
        "editfont-serif": "Police serif",
        "nosuchusershort": "Il n’y a pas de contributeur avec le nom « $1 ».\nVeuillez vérifier l’orthographe.",
        "nouserspecified": "Vous devez saisir un nom d’utilisateur.",
        "login-userblocked": "{{GENDER:$1|Cet utilisateur|Cette utilisatrice}} est bloqué{{GENDER:$1||e}}. La connexion n’est pas autorisée.",
-       "wrongpassword": "Le mot de passe est incorrect.\nVeuillez essayer à nouveau.",
+       "wrongpassword": "Le nom d’utilisateur ou le mot de passe est incorrect.\nVeuillez essayer à nouveau.",
        "wrongpasswordempty": "Vous n’avez entré aucun mot de passe.\nVeuillez essayer à nouveau.",
        "passwordtooshort": "Votre mot de passe doit contenir au moins $1 caractère{{PLURAL:$1||s}}.",
        "passwordtoolong": "Les mots de passe ne peuvent pas dépasser $1 caractère{{PLURAL:$1||s}}.",
        "rcfilters-savedqueries-apply-and-setdefault-label": "Créer le filtre par défaut",
        "rcfilters-savedqueries-cancel-label": "Annuler",
        "rcfilters-savedqueries-add-new-title": "Sauvegarder la configuration du filtre courant",
-       "rcfilters-savedqueries-already-saved": "Ces filtres sont déjà enregistrés",
+       "rcfilters-savedqueries-already-saved": "Ces filtres sont déjà enregistrés. Modifiez vos paramètres pour créer un nouveau Filtre enregistré.",
        "rcfilters-restore-default-filters": "Rétablir les filtres par défaut",
        "rcfilters-clear-all-filters": "Effacer tous les filtres",
        "rcfilters-show-new-changes": "Afficher les modifications les plus récentes",
        "uploadbtn": "Importer le fichier",
        "reuploaddesc": "Annuler l'importation et retourner au formulaire d'import",
        "upload-tryagain": "Envoyer la description du fichier modifiée",
-       "upload-tryagain-nostash": "Soumettre un fichier téléchargé de nouveau avec une description modifiée",
+       "upload-tryagain-nostash": "Soumettre un fichier téléversé de nouveau avec une description modifiée",
        "uploadnologin": "Non connecté",
        "uploadnologintext": "Vous devez vous $1 pour importer des fichiers.",
        "upload_directory_missing": "Le répertoire d’import de fichier ($1) est introuvable et n’a pas pu être créé par le serveur web.",
        "file-deleted-duplicate-notitle": "Un fichier identique à ce fichier a déjà été supprimé ainsi que le titre. \nVous devriez demander à quelqu'un la possibilité de vérifier le journal de ce fichier supprimé afin d'examiner la situation  avant de l'importer à nouveau.",
        "uploadwarning": "Attention !",
        "uploadwarning-text": "Modifiez la description du fichier et essayez de nouveau.",
-       "uploadwarning-text-nostash": "Veuillez télécharger de nouveau le fichier, modifier la description ci-dessous et réessayer.",
+       "uploadwarning-text-nostash": "Veuillez téléverser de nouveau le fichier, modifier la description ci-dessous et réessayer.",
        "savefile": "Sauvegarder le fichier",
        "uploaddisabled": "Désolé, l’import de fichiers est désactivé.",
        "copyuploaddisabled": "Import de fichier par URL désactivé.",
        "uploadstash-file-too-large": "Impossible de fournir un fichier plus grand que $1 octets.",
        "uploadstash-not-logged-in": "Aucun utilisateur n’est connecté, les fichiers doivent appartenir à des utilisateurs.",
        "uploadstash-wrong-owner": "Ce fichier ($1) n’appartient pas à l’utilisateur courant.",
-       "uploadstash-no-such-key": "Aucune clé ($1), impossible de supprimer.",
+       "uploadstash-no-such-key": "Clé introuvable ($1), impossible de supprimer.",
        "uploadstash-no-extension": "L’extension est nulle.",
        "uploadstash-zero-length": "La taille du fichier est zéro.",
        "invalid-chunk-offset": "Offset de segment non valide",
        "autoredircomment": "Page redirigée vers [[$1]]",
        "autosumm-new": "Page créée avec « $1 »",
        "autosumm-newblank": "Page vide créée",
-       "size-bytes": "$1&nbsp;o",
+       "size-bytes": "$1 {{PLURAL:$1|octet|octets}}",
        "size-kilobytes": "$1&nbsp;Kio",
        "size-megabytes": "$1&nbsp;Mio",
        "size-gigabytes": "$1&nbsp;Gio",
        "version-poweredby-others": "autres",
        "version-poweredby-translators": "traducteurs de translatewiki.net",
        "version-credits-summary": "Nous tenons à remercier les personnes suivantes pour leur contribution à  [[Special:Version|MediaWiki]].",
-       "version-license-info": "MediaWiki est un logiciel libre, vous pouvez le redistribuer ou le modifier selon les termes de la Licence Publique Générale GNU telle que publiée par la Free Software Foundation ; soit la version 2 de la Licence, ou (à votre choix) toute version ultérieure.\n\nMediaWiki est distribué dans l'espoir qu'il sera utile, mais SANS AUCUNE GARANTIE, sans même la garantie implicite de COMMERCIALISATION ou D'ADAPTATION À UN USAGE PARTICULIER. Voir la Licence Publique Générale GNU pour plus de détails.\n\nVous devriez avoir reçu [{{SERVER}}{{SCRIPTPATH}}/COPYING une copie de la Licence Publique Générale GNU] avec ce programme, sinon, écrivez à la Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, États-Unis ou [//www.gnu.org/licenses/old-licenses/gpl-2.0.html lisez-la en ligne].",
+       "version-license-info": "MediaWiki est un logiciel gratuit, vous pouvez le redistribuer ou le modifier selon les termes de la Licence Publique Générale GNU telle que publiée par la Free Software Foundation ; soit la version 2 de la Licence, ou (à votre choix) toute version ultérieure.\n\nMediaWiki est distribué dans l’espoir qu'il sera utile, mais SANS AUCUNE GARANTIE, sans même la garantie implicite de COMMERCIALISABILITÉ ou D’ADÉQUATION À UN USAGE PARTICULIER. Voir la Licence Publique Générale GNU pour plus de détails.\n\nVous devriez avoir reçu [{{SERVER}}{{SCRIPTPATH}}/COPYING une copie de la Licence Publique Générale GNU] avec ce programme, sinon, écrivez à la Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, États-Unis ou [//www.gnu.org/licenses/old-licenses/gpl-2.0.html lisez-la en ligne].",
        "version-software": "Logiciels installés",
        "version-software-product": "Produit",
        "version-software-version": "Version",
        "feedback-close": "Terminé",
        "feedback-external-bug-report-button": "Signaler un bogue technique",
        "feedback-dialog-title": "Soumettre un commentaire",
-       "feedback-dialog-intro": "Vous pouvez utiliser le simple formulaire ci-dessous pour faire parvenir vos commentaires. Votre commentaire sera ajouté à la page « $1 », ainsi que votre nom d’utilisateur.",
+       "feedback-dialog-intro": "Vous pouvez utiliser le formulaire simple ci-dessous pour faire parvenir votre commentaire. Il sera ajouté à la page « $1 », avec votre nom d’utilisateur.",
        "feedback-error1": "Erreur : résultat de l'API non reconnu",
        "feedback-error2": "Erreur : la modification a échoué",
        "feedback-error3": "Erreur : aucune réponse de l'API",
        "default-skin-not-found-row-disabled": "* <code>$1</code> / $2 (<strong>désactivé</strong>)",
        "mediastatistics": "Statistiques sur les médias",
        "mediastatistics-summary": "Statistiques sur les types de fichiers téléversés. Elles ne prennent en compte que la version la plus récente des fichiers. Les versions anciennes ou supprimées sont exclues.",
+       "mediastatistics-nfiles": "$1 ($2 %)",
        "mediastatistics-nbytes": "{{PLURAL:$1|$1 octet|$1 octets}} ($2 ; $3%)",
        "mediastatistics-bytespertype": "Taille totale de fichiers pour cette section : {{PLURAL:$1|$1 octet|$1 octets}} ($2 ; $3%).",
        "mediastatistics-allbytes": "Taille totale pour tous les fichiers : {{PLURAL:$1|$1 octet|$1 octets}} ($2).",
        "authprovider-confirmlink-message": "D’après vos dernières tentatives de connexion, les comptes suivants peuvent être liés à votre compte wiki. Les lier vous permettra de se connecter via ces comptes. Veuillez sélectionner lesquels doivent être liés.",
        "authprovider-confirmlink-request-label": "Comptes qui doivent être liés",
        "authprovider-confirmlink-success-line": "$1 : Liés avec succès.",
+       "authprovider-confirmlink-failed-line": "$1 : $2",
        "authprovider-confirmlink-failed": "La liaison du compte n’a pas bien réussi : $1",
        "authprovider-confirmlink-ok-help": "Continuer après l’affichage des messages d’échec de liaison.",
        "authprovider-resetpass-skip-label": "Sauter",
        "unlinkaccounts-success": "Le compte a été dissocié.",
        "authenticationdatachange-ignored": "Les modifications de données d’authentification n’ont pas été gérées. Peut-être aucun fournisseur n’a-t-il été configuré ?",
        "userjsispublic": "Veuillez noter : les sous-pages JavaScript ne doivent pas contenir de données confidentielles parce qu’elles sont visibles des autres utilisateurs.",
-       "usercssispublic": "Veuillez noter: les sous-pages CSS ne doivent pas contenir de données confidentielles parce qu'elles sont visibles des autres utilisateurs.",
+       "usercssispublic": "Veuillez noter : les sous-pages CSS ne doivent pas contenir de données confidentielles parce qu’elles sont visibles des autres utilisateurs.",
        "restrictionsfield-badip": "Adresse IP ou plage non valide : $1",
        "restrictionsfield-label": "Plages IP autorisées :",
        "restrictionsfield-help": "Une adresse IP ou une plage CIDR par ligne. Pour tout activer, utiliser <pre>0.0.0.0/0\n::/0</pre>",
index d03fe22..e956dcc 100644 (file)
        "bad_image_list": "Is é seo a leanas an formáid:\n\nNíl ach míreanna liosta amháin (línte ag tosú le *) san áireamh.\nIs riachtanach gur nasc do dhrochchomhad é an chéad nasc ar líne.\nIs eisceachtaí iad na naisc eile ar an líne céanna, .i. leathanaigh gur féidir an comhad a bheith orthu go hinlíne.",
        "metadata": "Meiteasonraí",
        "metadata-help": "Tá breis eolais sa comhad seo, curtha, is dócha, as ceamara digiteach ná scanóir a chruthaigh ná a digitigh é.\nMá tá an comhad mionathraithe as an bunleagan, b'fhéidir nach mbeidh ceann de na sonraí fágtha sa comhad atá athruithe.",
-       "metadata-expand": "Taispeáin sonraí síneadh",
+       "metadata-expand": "Taispeáin sonraí sínte",
        "metadata-collapse": "Folaigh sonraí síneadh",
        "metadata-fields": "Beidh na meiteasonraí EXIF seo a leanas dá dtaispeáint ar an leathanach íomhá nuair atá an clár meiteasonraí ceilte.\nBeidh na cinn eile ceilte de réir réamhshocraithe.\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",
        "exif-imagewidth": "Leithead",
index 51286cd..e8ae8e4 100644 (file)
        "rcfilters-savedqueries-apply-and-setdefault-label": "Crear filtro por defecto",
        "rcfilters-savedqueries-cancel-label": "Cancelar",
        "rcfilters-savedqueries-add-new-title": "Gardar a configuración do filtro actual",
+       "rcfilters-savedqueries-already-saved": "Estes filtro xa están gardados",
        "rcfilters-restore-default-filters": "Restaurar os filtros por defecto",
        "rcfilters-clear-all-filters": "Borrar todos os filtros",
        "rcfilters-show-new-changes": "Mostrar os cambios máis recentes",
index afe6cb6..493e033 100644 (file)
        "imagelinks": "Failicho vapor",
        "linkstoimage": "{{PLURAL:$1|Hem pan|$1 Him panam}} hea failik {{PLURAL:$1|zoddtta|zoddttat}}",
        "nolinkstoimage": "Hea failik zoddpi panam nant",
-       "sharedupload-desc-here": "Hi fail $1, hachi ani dusreo projectanim haka uzar korunk zata.\nHachem [$2 failichem vivron panan] asleli vivron khala dilea:",
+       "sharedupload-desc-here": "Hi fail $1, hachi ani dusrea prokolpanim haka uzar korunk zata.\nHachem [$2 failichem vivron panan] asleli vivron khala dilea:",
        "upload-disallowed-here": "Tu hea faili voir borounk xokonai",
        "filedelete-otherreason": "Dusrem/aniki karon:",
        "randompage": "Khoincheim adlem modlem pan",
index 71e1c0d..319dc4d 100644 (file)
        "unwatchthispage": "નીરીક્ષણ બંધ કરો",
        "notanarticle": "માહિતી વિનાનું પાનું",
        "notvisiblerev": "અન્ય સભ્ય દ્વારા થયેલું સંપાદન ભૂંસી નખાયું છે.",
-       "watchlist-details": "ચર્ચાનાં પાનાં અલગથી ન ગણતા {{PLURAL:$1|$1 પાનું|$1 પાનાં}} ધ્યાનસૂચીમાં છે.",
+       "watchlist-details": "{{PLURAL:$1|$1 પાનું|$1 પાનાઓ}} (અને ચર્ચા પાનાઓ) તમારી ધ્યાનસૂચિમાં છે.",
        "wlheader-enotif": "ઈમેલ સૂચના પદ્ધતિ સક્રીય કરાઈ.",
        "wlheader-showupdated": "તમારી છેલ્લી મુલાકાત પછી બદલાયેલાં પાના  '''ઘાટા''' અક્ષરો વડે દર્શાવ્યાં છે.",
        "wlnote": "નીચે $3, $4 વાગ્યા સુધીના છેલ્લા {{PLURAL:$2|એક કલાક|<strong>$2</strong> કલાક}}માં થયેલા {{PLURAL:$1|ફેરફાર|<strong>$1</strong> ફેરફારો }} દર્શાવ્યા છે.",
index 2472d85..963dab0 100644 (file)
        "nosuchusershort": "אין משתמש בשם \"$1\".\nנא לוודא שהאיות נכון.",
        "nouserspecified": "יש לציין שם משתמש.",
        "login-userblocked": "משתמש זה חסום. אינכם מורשים להיכנס לחשבון.",
-       "wrongpassword": "×\94ס×\99ס×\9e×\94 ×©×\94×\96נת ×©×\92×\95×\99×\94.\nנא לנסות שוב.",
+       "wrongpassword": "ש×\9d ×\94×\9eשת×\9eש ×\90×\95 ×\94ס×\99ס×\9e×\94 ×©×\94×\96נת ×©×\92×\95×\99×\99×\9d.\nנא לנסות שוב.",
        "wrongpasswordempty": "הסיסמה שהזנת ריקה.\nנא לנסות שוב.",
        "passwordtooshort": "סיסמאות חייבות להיות באורך {{PLURAL:$1|תו אחד|$1 תווים}} לפחות.",
        "passwordtoolong": "סיסמאות אינן יכולות להיות ארוכות {{PLURAL:$1|מתו אחד|מ־$1 תווים}}.",
        "rcfilters-savedqueries-apply-and-setdefault-label": "יצירת מסנן התחלתי",
        "rcfilters-savedqueries-cancel-label": "ביטול",
        "rcfilters-savedqueries-add-new-title": "שמירת הגדרות המסננים הנוכחיות",
-       "rcfilters-savedqueries-already-saved": "מסננים אלה כבר נשמרו",
+       "rcfilters-savedqueries-already-saved": "מסננים אלה כבר נשמרו. באפשרותך לשנות את ההגדרות שלך כדי ליצור ולשמור מסנן חדש.",
        "rcfilters-restore-default-filters": "שחזור למסנני ברירת המחדל",
        "rcfilters-clear-all-filters": "מחיקת כל המסננים",
        "rcfilters-show-new-changes": "הצגת השינויים החדשים ביותר",
index 9ab3efa..696cdf8 100644 (file)
        "enotif_lastvisited": "Pogledaj $1 za promjene od zadnjeg posjeta.",
        "enotif_lastdiff": "Pogledajte $1 kako biste mogli vidjeti tu izmjenu.",
        "enotif_anon_editor": "neprijavljeni suradnik $1",
-       "enotif_body": "Poštovani $WATCHINGUSERNAME,\n\n$PAGEINTRO $NEWPAGE\n\nSažetak urednika: $PAGESUMMARY $PAGEMINOREDIT\n\nMožete kontaktirati suradnika koji je posljednji uređivao stranicu:\nmail: $PAGEEDITOR_EMAIL\nwiki: $PAGEEDITOR_WIKI\n\nDo Vašeg ponovnog posjeta stranici nećete dobivati nove obavijesti. Postavke za izvješćivanje možete vratiti na prvotno zadane za sve praćene stranice svog popisa praćenja.\n\nVaš sustav izvješćivanja {{SITENAME}}.\n\n--\nZa promjene postavki izvješćivanja putem e-pošte, posjetite\n{{canonicalurl:{{#special:Preferences}}}}\n\nZa promjene svog popisa praćenja, posjetite\n{{canonicalurl:{{#special:EditWatchlist}}}}\n\nZa brisanje stranica iz svog popisa praćenja, posjetite\n$UNWATCHURL\n\nZa povratne informacije i pomoć posjetite:\n$HELPPAGE",
+       "enotif_body": "Poštovani $WATCHINGUSERNAME,\n\n$PAGEINTRO $NEWPAGE\n\nSažetak uređivača: $PAGESUMMARY $PAGEMINOREDIT\n\nMožete kontaktirati suradnika koji je posljednji uređivao stranicu:\nmail: $PAGEEDITOR_EMAIL\nwiki: $PAGEEDITOR_WIKI\n\nDo Vašega ponovnog posjeta stranici ne ćete dobivati nove obavijesti. Postavke za izvješćivanje možete vratiti na prvobitno zadane za sve praćene stranice Vašega popisa praćenja.\n\nVaš sustav izvješćivanja {{SITENAME}}.\n\n--\nZa promjene postavki izvješćivanja putem e-pošte, posjetite\n{{canonicalurl:{{#special:Preferences}}}}\n\nZa promjene svog popisa praćenja, posjetite\n{{canonicalurl:{{#special:EditWatchlist}}}}\n\nZa brisanje stranica iz svog popisa praćenja, posjetite\n$UNWATCHURL\n\nZa povratne informacije i pomoć posjetite:\n$HELPPAGE",
        "enotif_minoredit": "Ovo je sitnije uređivanje",
        "created": "stvorio",
        "changed": "promijenio",
index 6584a3d..7744632 100644 (file)
        "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-search-placeholder": "Friss változtatások szűrése (böngéssz vagy kezdj el gépelni)",
+       "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ó.",
        "rcfilters-filterlist-title": "Szűrők",
index a615909..b82a237 100644 (file)
        "metadata": "Մետատվյալներ",
        "metadata-help": "Նիշքը պարունակում է ընդարձակ տվյալները, հավանաբար ավելացված թվային լուսանկարչական ապարատի կամ սկաների կողմից, որոնք օգտագործվել են նկարը ստեղծելու կամ թվայնացնելու համար։\nԵթե նիշքը ձևափոխվել է ստեղծումից ի վեր, ապա որոշ տվյալները կարող են չհամապատասխանել ձևափոխված նիշքին։",
        "metadata-expand": "Ցուց տալ ընդարձակ տվյալները",
-       "metadata-collapse": "Ô¹Õ¡Ö\84Ö\81Õ¶Õ¥Õ¬ Õ¨Õ¶Õ¤Õ¡Ö\80Õ±Õ¡Õ¯ Õ¿Õ¾ÕµÕ¬Õ¡ները",
+       "metadata-collapse": "Ô¹Õ¡Ö\84Ö\81Õ¶Õ¥Õ¬ Õ¬Ö\80Õ¡Ö\81Õ¸Ö\82Ö\81Õ«Õ¹ Õ¿Õ¾ÕµÕ¡Õ¬ները",
        "metadata-fields": "EXIF մետատվյալների այն դաշտերը, որոնք նշված ենք այս ուղերձի մեջ, կցուցադրվեն պատկերի էջում, երբ մետատվյալների աղյուսակը ծալված է։ Այլ տվյալները լռությամբ կթաքցվեն։\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",
        "exif-imagewidth": "Լայնք",
        "exif-imagelength": "Բարձրություն",
index 08414e7..dc29549 100644 (file)
@@ -56,7 +56,8 @@
                        "Rachmat04",
                        "Arifpedia",
                        "Uchup19",
-                       "Archd"
+                       "Archd",
+                       "Empu"
                ]
        },
        "tog-underline": "Garis bawahi pranala:",
        "nosuchusershort": "Tidak ada pengguna dengan nama \"$1\".\nSilakan periksa kembali ejaan Anda.",
        "nouserspecified": "Anda harus memasukkan nama pengguna.",
        "login-userblocked": "Pengguna ini diblokir. Tidak diizinkan/diperbolehkan untuk masuk log.",
-       "wrongpassword": "Kata sandi yang Anda masukkan salah. Silakan coba lagi.",
+       "wrongpassword": "Kata sandi yang Anda masukkan salah.\nSilakan coba lagi.",
        "wrongpasswordempty": "Anda tidak memasukkan kata sandi. Silakan coba lagi.",
        "passwordtooshort": "Kata sandi paling tidak harus terdiri dari {{PLURAL:$1|1 karakter|$1 karakter}}.",
        "passwordtoolong": "Passwords tidak boleh lebih dari {{PLURAL:$1|1 karakter|$1 karakter}}.",
        "recentchanges-summary": "Temukan perubahan terbaru dalam wiki di halaman ini.<br />\n;Keterangan:(<span style=\"color:blue;\">beda</span>) perubahan, (<span style=\"color:blue;\">versi</span>) sejarah revisi, '''B''' halaman baru, '''b''' suntingan bot, '''k''' suntingan kecil, <span class=\"unpatrolled\">!</span> perubahan belum dipatroli,<br /><span style=\"color:green;\">'''(+ ''bita'')'''</span> isi konten bertambah, <span style=\"color:red;\">(- ''bita'')</span> isi konten berkurang, (← Ringkasan otomatis), (→ <span style=\"color:grey;\">Suntingan bagian</span>)",
        "recentchanges-noresult": "Tidak ada perubahan dalam rentang waktu ini yang cocok dengan kriteria.",
        "recentchanges-timeout": "Waktu pencarian ini telah habis. Anda mungkin ingin mencoba parameter pencarian lain.",
+       "recentchanges-network": "Selama terjadi kesalahan teknis, tidak ada hasil yang bisa dimuat. Silakan coba untuk menyegarkan kembali halaman.",
        "recentchanges-feed-description": "Temukan perubahan terbaru dalam wiki di umpan ini.",
        "recentchanges-label-newpage": "Suntingan ini membuat halaman baru",
        "recentchanges-label-minor": "Ini adalah suntingan kecil",
        "uploadstash-refresh": "Segarkan daftar berkas.",
        "uploadstash-thumbnail": "lihat miniatur",
        "uploadstash-exception": "Tidak dapat menyimpan unggahan dalam simpanan ($1): \"$2\".",
+       "uploadstash-bad-path": "Jalur tidak tersedia.",
+       "uploadstash-bad-path-invalid": "Jalur tidak valid.",
+       "uploadstash-bad-path-unknown-type": "Tipe tidak diketahui \"$1\".",
        "invalid-chunk-offset": "Ofset potongan tidak valid",
        "img-auth-accessdenied": "Akses ditolak",
        "img-auth-nopathinfo": "PATH_INFO hilang.\nServer Anda tidak diatur untuk melewatkan informasi ini.\nServer tersebut mungkin berbasis CGI dan tidak dapat mendukung img_auth.\nLihat https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
        "sp-contributions-newonly": "Hanya tampilkan suntingan yang berupa pembuatan halaman",
        "sp-contributions-hideminor": "Sembunyikan suntingan kecil",
        "sp-contributions-submit": "Cari",
+       "sp-contributions-outofrange": "Tidak bisa menampilkan hasil. Alamat IP yang diminta melebihi batas CIDR /$1",
        "whatlinkshere": "Pranala balik",
        "whatlinkshere-title": "Halaman yang memiliki pranala ke \"$1\"",
        "whatlinkshere-page": "Halaman:",
        "ipb_blocked_as_range": "Kesalahan: IP $1 tidak diblokir secara langsung dan tidak dapat dilepaskan. IP $1 diblok sebagai bagian dari pemblokiran kelompok IP $2, yang dapat dilepaskan.",
        "ip_range_invalid": "Blok IP tidak sah.",
        "ip_range_toolarge": "Rentang blok lebih besar dari /$1 tidak diperbolehkan.",
+       "ip_range_exceeded": "Jangkauan alamat IP melampaui batas maksimum. Jangakaun yang diperbolehkan: /$1",
        "ip_range_toolow": "Rentang IP secara efektif tidak diizinkan.",
        "proxyblocker": "Pemblokir proxy",
        "proxyblockreason": "Alamat IP Anda telah diblokir karena alamat IP Anda adalah ''proxy'' terbuka. Silakan hubungi penyedia jasa internet Anda atau dukungan teknis dan beritahukan mereka masalah keamanan serius ini.",
index 4b6a868..14f84c0 100644 (file)
        "nosuchusershort": "Non è registrato alcun utente di nome \"$1\". Verificare il nome inserito.",
        "nouserspecified": "È necessario specificare un nome utente.",
        "login-userblocked": "Questa utenza è bloccata. Non è possibile effettuare il login.",
-       "wrongpassword": "La password inserita non è corretta. Riprovare.",
+       "wrongpassword": "Nome utente o password inserita non corretta.\nRiprova.",
        "wrongpasswordempty": "Non è stata inserita alcuna password. Riprovare.",
        "passwordtooshort": "Le password devono contenere almeno {{PLURAL:$1|1 carattere|$1 caratteri}}.",
        "passwordtoolong": "La password non può contenere più di {{PLURAL:$1|1 carattere|$1 caratteri}}.",
        "rcfilters-savedqueries-apply-and-setdefault-label": "Crea filtro predefinito",
        "rcfilters-savedqueries-cancel-label": "Annulla",
        "rcfilters-savedqueries-add-new-title": "Salva le impostazioni attuali del filtro",
-       "rcfilters-savedqueries-already-saved": "Questi filtri sono già salvati",
+       "rcfilters-savedqueries-already-saved": "Questi filtri sono già salvati. Modifica le impostazioni per creare un nuovo filtro salvato.",
        "rcfilters-restore-default-filters": "Ripristina i filtri predefiniti",
        "rcfilters-clear-all-filters": "Pulisci tutti i filtri",
        "rcfilters-show-new-changes": "Visualizza le modifiche più recenti",
index cb1ff60..5141158 100644 (file)
@@ -86,7 +86,8 @@
                        "Translatealcd",
                        "Delim",
                        "Hinaloe",
-                       "Phantomize"
+                       "Phantomize",
+                       "Suzukaze-c"
                ]
        },
        "tog-underline": "リンクの下線:",
        "prefs-editor": "エディター",
        "prefs-preview": "プレビュー",
        "prefs-advancedrc": "詳細の設定",
+       "prefs-opt-out": "改善の使用を断る",
        "prefs-advancedrendering": "詳細の設定",
        "prefs-advancedsearchoptions": "詳細設定",
        "prefs-advancedwatchlist": "詳細の設定",
        "rcfilters-liveupdates-button-title-off": "新しい編集を即座に読み表示する",
        "rcfilters-watchlist-markseen-button": "すべての変更を訪問済みにする",
        "rcfilters-watchlist-edit-watchlist-button": "ウォッチリストを編集",
+       "rcfilters-preference-label": "最近の更新の改善版を隠す",
        "rcnotefrom": "以下は<strong>$3 $4</strong>以降の{{PLURAL:$5|更新です}} (最大 <strong>$1</strong> 件)。",
        "rclistfromreset": "日時指定をリセット",
        "rclistfrom": "$3の$2以降の更新を表示する",
        "newimages-hidepatrolled": "巡回済みのアップロードを隠す",
        "newimages-mediatype": "メディアの種類:",
        "noimages": "表示できるものがありません。",
-       "gallery-slideshow-toggle": "ã\83\88ã\82°ã\83«ã\82µã\83 ã\83\8dã\82¤ã\83«",
+       "gallery-slideshow-toggle": "ã\82µã\83 ã\83\8dã\82¤ã\83«ã\82\92å\88\87ã\82\8aæ\9b¿ã\81\88ã\82\8b",
        "ilsubmit": "検索",
        "bydate": "日付順",
        "sp-newimages-showfrom": "$1の$2以降の新しいファイルを表示",
index 7cae851..18aaa92 100644 (file)
        "welcomecreation-msg": "Akun panjenengan wis kacipta. Aja lali nata konfigurasi [[Special:Preferences|preferensi {{SITENAME}}]] panjenengan.",
        "yourname": "Jeneng panganggo:",
        "userlogin-yourname": "Jeneng panganggo",
-       "userlogin-yourname-ph": "Isinen jeneng panganggoné panjenengan",
+       "userlogin-yourname-ph": "Isi jeneng-panganggo panjenengan",
        "createacct-another-username-ph": "Isi jeneng panganggo",
        "yourpassword": "Tembung wadi:",
        "userlogin-yourpassword": "Tembung wadi",
        "createacct-yourpassword-ph": "Isi tembung wadi",
        "yourpasswordagain": "Tik manèh tembung wadiné:",
        "createacct-yourpasswordagain": "Konfirmasi tembung wadi",
-       "createacct-yourpasswordagain-ph": "Lebokaké manèh tembung wadiné",
+       "createacct-yourpasswordagain-ph": "Isi manèh tembung wadi mau",
        "userlogin-remembermypassword": "Gawé supaya panggah mlebu log",
        "userlogin-signwithsecure": "Nganggo koneksi aman",
        "cannotlogin-title": "Ora bisa mlebu log",
        "userlogin-createanother": "Gawé akun liya",
        "createacct-emailrequired": "Alamat layang-èl",
        "createacct-emailoptional": "Alamat layang-èl (manasuka)",
-       "createacct-email-ph": "Isinen layang-èlé panjenengan",
+       "createacct-email-ph": "Isi layang-èl panjenengan",
        "createacct-another-email-ph": "Isi alamat layang-èl",
-       "createaccountmail": "Anggonen tembung wadi sembarang sauntara lan kirimen iku menyang alamat layang-èl sing dikarepaké",
+       "createaccountmail": "Nganggo tembung wadi sauntara sing dikirimaké menyang alamat layang-èl",
        "createacct-realname": "Jeneng asli (manasuka)",
        "createacct-reason": "Alesan",
        "createacct-reason-ph": "Alesané panjenengan nggawé akun liya",
-       "createacct-submit": "Gawé akuné panjenengan",
+       "createacct-submit": "Gawé akun panjenengan",
        "createacct-another-submit": "Gawé akun",
        "createacct-continue-submit": "Banjuraké gawé akun",
        "createacct-another-continue-submit": "Banjuraké gawé akun",
        "nouserspecified": "Panjenengan kudu milih jeneng panganggo.",
        "login-userblocked": "Panganggo iki pinalangan. Ora kena mbelu.",
        "wrongpassword": "Tembung wadi sing diisèkaké salah.\nMangga jajalen manèh.",
-       "wrongpasswordempty": "Panjenengan ora milih tembung sandhi. Mangga dicoba manèh.",
+       "wrongpasswordempty": "Tembung wadi kosong.\nJajalen manèh.",
        "passwordtooshort": "Tembung sesinglon paling sethithik cacahé {{PLURAL:$1|1 aksara|$1 aksara}}.",
        "passwordtoolong": "Tembung wadi ora kena munjuli {{PLURAL:$1|1 pralambang|$1 pralambang}}.",
        "passwordtoopopular": "Tembung wadi sing wis kaprah ora kena dianggo. Mangga pilih tembung wadi liya sing mbédani.",
        "password-login-forbidden": "Panganggoning jeneng panganggo lan tembung wadi iki dilarang.",
        "mailmypassword": "Balèni gawé tembung wadi",
        "passwordremindertitle": "Tembung wadi sauntara kanggo {{SITENAME}}",
-       "passwordremindertext": "Ana wong (mbokmanawa panjenengan dhéwé, saka alamat IP $1) nyuwun supaya dikirimi tembung sandhi anyar kanggo {{SITENAME}} ($4). Tembung sandi sawetara kanggo panganggo \"$2\" wis digawé lan saiki \"$3\". Yèn panjenengan pancèn nggayuh iki, mangga énggal mlebu log lan ngganti tembung sandi saiki.\nTembung sandi sawetara mau bakal kadaluwarsa ing {{PLURAL:$5|sadina|$5 dina}}.\nYèn wong liya sing nglakoni panyuwunan iki, utawa panjenengan éling tembung sandi panjenengan, lan ora kepéngin ngowahi, panjenengan ora usah nggubris pesen iki lan bisa tetep nganggo tembung sandi lawas.",
+       "passwordremindertext": "Ana wong (mbokmanawa panjenengan dhéwé, saka alamat IP $1) nyuwun supaya dikirimi tembung wadi anyar kanggo {{SITENAME}} ($4). Tembung wadi sawetara kanggo panganggo \"$2\" wis digawé lan saiki \"$3\". Yèn panjenengan pancèn nggayuh iki, mangga énggal mlebu log lan ngganti tembung wadi saiki.\nTembung wadi sawetara mau bakal kadaluwarsa ing {{PLURAL:$5|sadina|$5 dina}}.\nYèn wong liya sing nglakoni panyuwunan iki, utawa panjenengan éling tembung wadi panjenengan, lan ora kepéngin ngowahi, panjenengan ora usah nggubris pesen iki lan bisa tetep nganggo tembung wadi lawas.",
        "noemail": "Ora ana alamat layang-èl sing kacathet tumrap ing panganggo \"$1\".",
        "noemailcreate": "Panjenengan kudu maringi alamat e-mail sing absah",
        "passwordsent": "Tembung sandi anyar wis dikirim menyang alamat layang èlèktronik tumrap \"$1\". \nMangga mlebu log manèh sawisé panjenengan nampa iku.",
        "blocked-mailpassword": "Alamat IP-né panjenengan diblokir saka mbesut. Kanggo ngéndhani tumindak salah-guna, ora diparengaké nganggo pamulihan tembung wadi saka alamat IP iki.",
        "eauthentsent": "Layang-èl konfirmasi wis dikirim menyang alamat layang-èl sing diisèkaké. Sadurungé ana layang-èl liyané sing dikirim menyang akun iku, panjenengan kudu nuruti arahan ana ing layang-èl iku saperlu ngonfirmasi yèn akun iku pancèn duwèké panjenengan.",
-       "throttled-mailpassword": "Layang kanggo mbalèkaké tembung sandhi wis dikirim sasuwené ing {{PLURAL:$1|jam|$1 jam}}.\nKanggo nyegah ananing tumindhak culika, namung sak layang kanggo mbalèkaké tembung sandhi sing bakal dikirim sasuwéné ing {{PLURAL:$1|jam|$1 jam}}.",
+       "throttled-mailpassword": "Layang kanggo mbalèkaké tembung wadi wis dikirim sasuwené ing {{PLURAL:$1|jam|$1 jam}}.\nKanggo nyegah ananing tumindhak culika, namung sak layang kanggo mbalèkaké tembung wadi sing bakal dikirim sasuwéné ing {{PLURAL:$1|jam|$1 jam}}.",
        "mailerror": "Masalah pangirim layang: $1",
        "acct_creation_throttle_hit": "Para neneka menyang wiki iki sing nganggo alamat IP-né panjenengan wis gawé {{PLURAL:$1|akun cacah 1|akun cacah $1}} sajeroné $2 pungkasan, sing cacahé nyandhak cacah maksimum sing diidinaké.\nTemahané, para neneka sing nganggo alamat IP iki ora bisa gawé akun manèh sauntara iki.",
        "emailauthenticated": "Alamat layang-èlé panjenengan wis dikonfirmasi ing tanggal $2 pukul $3.",
        "resetpass_header": "Ganti tembung wadining akun",
        "oldpassword": "Tembung wadi lawas:",
        "newpassword": "Tembung wadi anyar:",
-       "retypenew": "Tik manèh tembung wadi anyaré:",
+       "retypenew": "Isi manèh tembung wadi anyaré:",
        "resetpass_submit": "Setèl tembung wadi lan mlebu log",
        "changepassword-success": "Tembung wadiné panjenengan kasil diowah!",
        "changepassword-throttled": "Panjenengan wis kakèhan njajal mlebu log.\nTulung nunggu dhisik $1 sadurungé njajal manèh.",
        "resetpass-temp-password": "Tembung wadi sauntara:",
        "resetpass-abort-generic": "Ngowahi tembung wadi kawurungaké déning èkstènsi.",
        "passwordreset": "Balèni gawé tembung wadi",
-       "passwordreset-text-one": "Lengkapana formulir iki kanggo nampa tembung sandhi sementara lewat layang elektronik.",
-       "passwordreset-text-many": "{{PLURAL:$1|Isinen salah sijine kotak ing ngisor iki kanggo nampa tembung sandhi sementara lewat layang elektronik.}}",
+       "passwordreset-text-one": "Isi formulir iki kanthi jangkep kanggo nampa tembung wadi sauntara lumantar layang-èl.",
+       "passwordreset-text-many": "{{PLURAL:$1|Isi salah siji babagan ing ngisor iki supaya bisa nampa tembung wadi sauntara lumantar layang-èl.}}",
        "passwordreset-disabled": "Setèl ulang tembung wadi dipatèni ing wiki iki.",
        "passwordreset-emaildisabled": "Fitur layang elektronik wis dipateni ing wiki iki.",
        "passwordreset-username": "Jeneng panganggo:",
        "passwordreset-email": "Alamat layang-èl:",
        "passwordreset-emailtitle": "Rerincèné akun ing {{SITENAME}}",
        "passwordreset-emailtext-ip": "Ana wong (bokmanawa panjenengan, saka alamat IP $1) nyuwun tembung wadiné panjenengan disetèl ulang mungguh ing {{SITENAME}} ($4). {{PLURAL:$3|Akun}} ing ngisor iki ana gayutané karo layang-èl iki:\n\n$2\n\n{{PLURAL:$3|Tembung wadi sauntara iki}} bakal kadaluwarsa sawisé {{PLURAL:$5|sadina|$5 dina}}.\nPanjenengan kudu mlebu log lan milih tembung wadi anyar saiki. Yèn wong liya sing nyuwun iki, utawa yèn panjenengan pranyata wis kèlingan tembung wadiné panjenengan sing lawas banjur panjenengan ora nedya ngganti, panjenengan bisa nglirwakaké layang iki lan mbanjuraké nganggo tembung wadiné panjenengan sing lawas.",
-       "passwordreset-emailtext-user": "Panganggo $1 seka {{SITENAME}} njaluk ganti tembung sandhiné Sampéyan ana ing {{SITENAME}} ($4). {{PLURAL:$3|Rèkèning|Rèkèning-rèkèning}} ngisor iki magepokan karo padunungané layang èlèktronik iki:\n\n$2\n\n{{PLURAL:$3|Tembung sandhi sawetara iki}} bakal kedaluwarsa ing {{PLURAL:$5|sak dina|$5 dina}}.\nSampéyan kudu mlebu log lan milih siji tembung sandhi anyar saiki. Yèn wong liya sing njaluk iki, utawa yèn Sampéyan jebul wis kèlingan tembung sandhiné sing lawas saéngga ora ana niyat kanggo ngganti, Sampéyan bisa ngejaraké wara-wara iki lan bacutaké nganggo tembung sandhiné lawas Sampéyan.",
+       "passwordreset-emailtext-user": "Panganggo $1 saka {{SITENAME}} nyuwun ganti tembung wadi panjenengan ana ing {{SITENAME}} ($4). {{PLURAL:$3|Akun|Akun-akun}} ngisor iki magepokan karo alamat layang-èl iki:\n\n$2\n\n{{PLURAL:$3|Tembung-wadi-sauntara}} iki bakal kedaluwarsa ing {{PLURAL:$5|sak dina|$5 dina}}.\npanjenengan kudu mlebu log lan milih siji tembung wadi anyar saiki. Yèn wong liya sing njaluk iki, utawa yèn panjenengan jebul wis kèlingan tembung wadi sing lawas saéngga ora ana niyat kanggo ngganti, panjenengan bisa ngejaraké wara-wara iki lan bacutaké nganggo tembung wadi lawas panjenengan.",
        "passwordreset-emailelement": "Jeneng panganggo: \n$1\n\nTembung wadi sauntara: \n$2",
        "passwordreset-emailsentemail": "Yèn layang èlèktronik iki nggayut akuning sampéyan, layang kanggo salin tembung wadi bakal dikirim.",
        "passwordreset-emailsentusername": "Manawa ana alamat layang-èl sing ana gayutané karo jeneng panganggo iki, layang-èl kanggo nyetèl ulang tembung wadi bakal dikirim.",
        "changeemail": "Owah utawa busak alamat layang-èl",
-       "changeemail-header": "Isinen formulir iki saperlu salin alamat layang-èlé panjenengan. Manawa panjenengan péngin ngilangi gegayutané alamat layang-èl saka akuné panjenengan, kosongaké waé babagan layang-èl anyar nalika ngirim formuliré.",
+       "changeemail-header": "Isi formulir iki saperlu salin alamat layang-èl panjenengan. Manawa panjenengan péngin ngilangi gegayutané alamat layang-èl saka akuné panjenengan, kosongaké waé babagan layang-èl anyar nalika ngirim formuliré.",
        "changeemail-no-info": "Sampéyan kudu mlebu log kanggo ngaksès kaca iki langsung.",
        "changeemail-oldemail": "Alamat layang-èl saiki:",
        "changeemail-newemail": "Alamat layang-èl anyar:",
        "changeemail-none": "(ora ana)",
        "changeemail-password": "Sandi {{SITENAME}} panjenengan:",
        "changeemail-submit": "Ganti layang-èl",
-       "changeemail-nochange": "Mangga isinen mawa alamat layang-èl sing anyar tur béda.",
+       "changeemail-nochange": "Mangga isi mawa alamat layang-èl sing anyar tur béda.",
        "resettokens": "Reset token",
        "resettokens-text": "Anda dapat me-reset Token yang memungkinkan akses ke data pribadi tertentu yang terkait dengan akun Anda di sini.\n\nAnda harus melakukannya jika Anda secara tidak sengaja berbagi dengan seseorang atau jika akun Anda telah disusupi.",
        "resettokens-no-tokens": "Ora ana token sing bisa direset.",
        "anonpreviewwarning": "<em>Panjenengan durung mlebu log. Yèn disimpen, alamat IP panjenengan bakal kacathet ing sajarah besutan kaca iki.</em>",
        "missingsummary": "<strong>Pangéling-éling:</strong> Panjenengan ora ngisèni ringkesané besutan.\nManawa panjenengan mencèt \"$1\" manèh, besutané panjengan bakal kasimpen tanpa katerangan.",
        "selfredirect": "<strong>Pepéling:</strong> Panjenengan ngalih kaca iki menyang kaca iki dhéwé.\nPanjenengan mungkin salah wènèh paraning alihan utawa salah mbesut kaca.\nYèn panjenengan ngeklik \"$1\" manèh, kaca alihan bakal digawé.",
-       "missingcommenttext": "Mangga isi tanggepan ing ngisor iki.",
+       "missingcommenttext": "Mangga awèh tanggepan.",
        "missingcommentheader": "'''Pangéling:''' Sampéyan durung nyadhiyakaké judhul/jejer kanggo tanggepan iki.\nYèn Sampéyan klik \"$1\" manèh, suntingan Sampéyan bakal kasimpen tanpa kuwi.",
        "summary-preview": "Pratuduh ringkesan besutan:",
        "subject-preview": "Pratuduh jejer:",
        "searchall": "kabèh",
        "showingresults": "Ing ngisor iki dituduhaké {{PLURAL:$1|'''1''' kasil|'''$1''' kasil}}, wiwitané saking #<strong>$2</strong>.",
        "showingresultsinrange": "Nuduhaké nganti {{PLURAL:$1|<strong>1</strong> kasil|<strong>$1</strong> kasil}} sajeroning penthangan #<strong>$2</strong> tekan #<strong>$3</strong>.",
-       "search-showingresults": "{{PLURAL:$4|Asil <strong>$1</strong> dari <strong>$3</strong>|Asil <strong>$1 - $2</strong> saking <strong>$3</strong>}}",
+       "search-showingresults": "{{PLURAL:$4|Asil <strong>$1</strong> saka <strong>$3</strong>|Asil <strong>$1 – $2</strong> saka <strong>$3</strong>}}",
        "search-nonefound": "Ora ana kasil sing cocog karo pitakonan (''query'').",
        "search-nonefound-thiswiki": "Ora ana kasil sing jumbuh karo panjalukan ing situs iki.",
        "powersearch-legend": "Panggolèkan sabanjuré (''advance search'')",
        "prefs-help-gender": "Setèlané pilalan iki sipaté manasuka.\nPiranti alusé nganggo ajiné saperlu nyeluk lan nyebut panjenengan tumraping liyan sarana tembung gèndher sing patut sacara paramasastra.\nKaterangan iki bakal kanton marang umum.",
        "email": "Layang-èl",
        "prefs-help-realname": "Jeneng asli ora kudu diisi.\nYèn diisi, jeneng asliné panjenengan bakal kanggo atribusi awit karyané panjenengan.",
-       "prefs-help-email": "Alamat layang èlèktronik sipaté mung pilihan, nanging dibutuhaké kanggo nyetèl ulang tembung sandhi yèn Sampéyan lali.",
+       "prefs-help-email": "Alamat layang-èl sipaté mung pilihan, nanging dibutuhaké kanggo nyetèl ulang tembung wadi yèn panjenengan lali.",
        "prefs-help-email-others": "Sampéyan uga bisa milih kanggo ngidinaké wong liya ngubungi Sampéyan liwat layang èlèktronik sing ana ing kaca panganggo utawa kaca guneman.\nAlamat layang èlèktronik Sampéyan ora dituduhaké nalika wong liya ngubungi Sampéyan.",
        "prefs-help-email-required": "Butuh alamat layang-èl.",
        "prefs-info": "Katerangan pokok",
        "rcfilters-filter-lastrevision-description": "Mung owahan paling anyar marang kacané.",
        "rcfilters-filter-previousrevision-label": "Dudu révisi pungkasan",
        "rcfilters-filter-previousrevision-description": "Kabèh owahan sing dudu \"révisi pungkasan\".",
-       "rcfilters-view-advanced-filters-label": "Saringan lanjutan",
        "rcfilters-view-tags": "Besutan sing tinengeran",
        "rcfilters-view-namespaces-tooltip": "Saring kasilé miturut mandala-arané",
        "rcfilters-view-tags-tooltip": "Saring kasilé nganggo tengering besutan",
        "rcshowhidecategorization-show": "Tuduhaké",
        "rcshowhidecategorization-hide": "Dhelikaké",
        "rclinks": "Tuduhaké $1 owahan pungkasan ing dalem $2 dina pungkasan.",
-       "diff": "béd",
-       "hist": "saj",
+       "diff": "bé",
+       "hist": "sa",
        "hide": "Dhelikaké",
        "show": "Tuduhaké",
        "minoreditletter": "c",
        "emailuser-title-notarget": "Kirimi panganggo layang-èl",
        "emailpagetext": "Panjenengan bisa migunakaké formulir ing ngisor kanggo ngirim layang-e marang {{GENDER:$1|panganggo}} iki.\nAlamat layang-e sing panjenengan lebokaké ing [[Special:Preferences|préferèsi panjenengan]] bakal dadi alamat \"Saka\" jroning layang-e kasebut, mula panampa layang-e bakal bisa mbalesi langsung menyang panjenengan.",
        "defemailsubject": "{{SITENAME}} layang èlèktronik saka panganggo \"$1\"",
-       "usermaildisabled": "Layang-èlé panganggo dipatèni",
+       "usermaildisabled": "Layang-èl panganggo dipatèni",
        "usermaildisabledtext": "Sampéyan ora bisa ngirim layang èlèktronik nèng panganggo liya nèng wiki iki",
        "noemailtitle": "Ora ana alamat layang-èl",
        "noemailtext": "Panganggo iki ora mènèhi alamat layang-e sing absah.",
        "emailccme": "Kirimana aku salinan pesenku.",
        "emailccsubject": "Salinan pesen panjenengan kanggo $1: $2",
        "emailsent": "Layang-èl dikirim",
-       "emailsenttext": "Layang-èlé panjenengan wis dikirim.",
+       "emailsenttext": "Layang-èl panjenengan wis dikirim.",
        "emailuserfooter": "Layang-e iki dikirimaké déning $1 marang $2 migunakaké fungsi \"Layangpanganggo\" ing {{SITENAME}}.",
        "usermessage-summary": "Tinggalaké layang sistem.",
        "usermessage-editor": "Pawartaning layang sistem",
        "deletecomment": "Alesan:",
        "deleteotherreason": "Alesan liya utawa tambahan:",
        "deletereasonotherlist": "Alesan liya",
-       "deletereason-dropdown": "*Alesan pambusakan\n** Spam\n** Vandalisme\n** Nglanggar hak cipta\n** Disuwun sing nulis\n** Pangalihan rusak",
+       "deletereason-dropdown": "*Alesan pambusakan\n** Spam\n** Vandhalisme\n** Terakan hak cipta\n** Panyuwun sing nulis\n** Alihan rusak",
        "delete-edit-reasonlist": "Besut alesané pambusak",
        "delete-toobig": "Kaca iki darbé sajarah besutan sing dawa, punjul $1 {{PLURAL:$1|owahan}}.\nMbusak kaca sing kaya mangkono wis ora diidinaké kanggo njagani supaya ora ana sing rusak ing {{SITENAME}}.",
        "delete-warning-toobig": "Kaca iki duwé sajarah besut sing dawa, punjul $1 {{PLURAL:$1|révisi}}.\nMbusak kaca iki bisa ngrusak lakuné basis dhata ing {{SITENAME}};\nkudu diayahi kanthi ngati-ati.",
        "movenosubpage": "Kaca iki ora duwé anak-kaca.",
        "movereason": "Alesan:",
        "revertmove": "balèkaké",
-       "delete_and_move_text": "Kaca jujugan \"[[:$1]]\" wis ana.\nApa sampéyan kersa mbusak iku supaya kacané bisa dilih?",
+       "delete_and_move_text": "Kaca paran \"[[:$1]]\" wis ana.\nApa panjenengan péngin mbusak iku supaya kacané bisa dilih?",
        "delete_and_move_confirm": "Ya, busak kaca iku.",
        "delete_and_move_reason": "Dibusak kanggo sarana ngalihaké saka \"[[$1]]\"",
        "selfmove": "Sesirah sumber lan tujuan padha;\nora bisa ngalih nyang tujuan sing padha.",
        "logentry-newusers-newusers": "Akun panganggo $1 {{GENDER:$2|digawé}}",
        "logentry-newusers-create": "Akun panganggo $1 {{GENDER:$2|digawé}}",
        "logentry-newusers-create2": "Akun panganggo $3 {{GENDER:$2|digawé}} déning $1",
-       "logentry-newusers-byemail": "Akun panganggo $3 {{GENDER:$2|digawé}} déning $1 lan tembung sandhine dikirim lewat layang elektronik",
+       "logentry-newusers-byemail": "Akun panganggo $3 {{GENDER:$2|digawé}} déning $1 lan tembung wadi dikirim lumantar layang-èl",
        "logentry-newusers-autocreate": "Akun panganggo $1 otomatis {{GENDER:$2|digawé}}",
        "logentry-protect-move_prot": "$1 {{GENDER:$2|ngalih}} setèlan rereksan saka $4 dadi $3",
        "logentry-protect-unprotect": "$1 {{GENDER:$2|njabud}} payomané $3",
        "authmanager-authn-not-in-progress": "Otèntifikasi ora lumaku utawa dhata sèsiné ilang. Mangga ambali kanthi miwiti saka pisanan.",
        "authmanager-authn-autocreate-failed": "Gawéan otomatis akun lokal wurung: $1",
        "authmanager-create-disabled": "Gawéan akun dipatèni.",
-       "authmanager-create-from-login": "Saperlu nggawé akuné panjenengan, mangga isinen babagané.",
+       "authmanager-create-from-login": "Saperlu nggawé akun, panjenengan mangga ngisi babagan-babagan ing ngisor iki.",
        "authmanager-authplugin-setpass-bad-domain": "Dhomain ora trep.",
        "authmanager-autocreate-noperm": "Gawéan akun otomatis ora diidinaké.",
        "authmanager-autocreate-exception": "Gawéan akun otomatis sawetara dipatèni amarga masalah sadurungé.",
index 9121f8a..1d5f64f 100644 (file)
        "nosuchusershort": "이름이 \"$1\"인 사용자는 없습니다.\n철자가 맞는지 확인하세요.",
        "nouserspecified": "사용자 계정 이름을 입력하지 않았습니다.",
        "login-userblocked": "이 사용자는 차단되었습니다. 로그인할 수 없습니다.",
-       "wrongpassword": "ì\9e\85ë ¥í\95\9c ë¹\84ë°\80ë²\88í\98¸ê°\80 ì\9e\98못ë\90\98ì\97\88ì\8aµë\8b\88ë\8b¤.\në\8b¤ì\8b\9c ì\8b\9cë\8f\84í\95\98ì\84¸ì\9a\94.",
+       "wrongpassword": "ì\9e\98못ë\90\9c ì\82¬ì\9a©ì\9e\90 ì\9d´ë¦\84 ë\98\90ë\8a\94 ë¹\84ë°\80ë²\88í\98¸ê°\80 ì\9e\85ë ¥ë\90\98ì\97\88ì\8aµë\8b\88ë\8b¤.\në\8b¤ì\8b\9c ì\8b\9cë\8f\84í\95´ ì£¼ì\8b­ì\8b\9cì\98¤.",
        "wrongpasswordempty": "비밀번호를 입력하지 않았습니다.\n다시 시도하세요.",
        "passwordtooshort": "비밀번호는 {{PLURAL:$1|$1 글자}} 이상이어야 합니다.",
        "passwordtoolong": "비밀번호는 {{PLURAL:$1|1자|$1자}}보다 길어서는 안 됩니다.",
        "rcfilters-savedqueries-apply-and-setdefault-label": "기본 필터 만들기",
        "rcfilters-savedqueries-cancel-label": "취소",
        "rcfilters-savedqueries-add-new-title": "현재의 필터 설정 저장",
-       "rcfilters-savedqueries-already-saved": "이 필터는 이미 저장되어 있습니다",
+       "rcfilters-savedqueries-already-saved": "이 필터는 이미 저장되어 있습니다. 새로운 저장된 필터를 만들려면 설정을 변경하십시오.",
        "rcfilters-restore-default-filters": "기본 필터 복구",
        "rcfilters-clear-all-filters": "필터 모두 지우기",
        "rcfilters-show-new-changes": "최신 변경사항 보기",
index 1f945f3..159d808 100644 (file)
        "mytalk": "Diskusyón",
        "anontalk": "Diskusyón para este adresso de IP",
        "navigation": "Navigación",
-       "and": "&#32;i",
+       "and": "&#32;y",
        "faq": "DDS",
        "actions": "Aksiones",
        "namespaces": "Espacios de nombres",
        "otherlanguages": "En otras linguas",
        "redirectedfrom": "(Redirijado de $1)",
        "redirectpagesub": "Hoja redirigida",
-       "lastmodifiedat": "Esta hoja fue trocada por la vez dalcavo en el $1, a las $2 la ora.",
+       "lastmodifiedat": "Esta hoja la vez dalcavo se trocó enel $1, a las $2 la ora.",
        "viewcount": "Este pajina fue vijitado {{PLURAL:$1|una vez|$1 vezes}}.",
        "protectedpage": "Hoja guardada",
        "jumpto": "Saltar a:",
        "tooltip-pt-login": "Te consejamos de entrar a tu cuento, portanto no sos obligado",
        "tooltip-pt-logout": "Sal de tu cuento",
        "tooltip-pt-createaccount": "Te consejamos de avrir un cuento y hazer entrada allá, portanto no sos obligado",
-       "tooltip-ca-talk": "Diskusyón encima del artíkolo",
-       "tooltip-ca-edit": "Puedes trocar esta hoja. Ma te rogamos para que eches una ojada (previsteo) antes de enrejistrarla.",
+       "tooltip-ca-talk": "Diskusyón encima del contènido desta hoja",
+       "tooltip-ca-edit": "Troca esta hoja",
        "tooltip-ca-addsection": "Ajusta un kapítolo muevo",
        "tooltip-ca-viewsource": "Esta hoja está guadrada.\nPuedes ver su manadero",
        "tooltip-ca-history": "Enderechamientos passados desta hoja",
        "tooltip-n-portal": "Encima del projeto, lo que se puede hazer y ande se topa las cosas",
        "tooltip-n-currentevents": "Jhaberes de oy día en ancho",
        "tooltip-n-recentchanges": "La lista de los trocamientos dalcavo enel viki",
-       "tooltip-n-randompage": "Carga una hoja por azardo",
+       "tooltip-n-randompage": "Avre una hoja por azardo",
        "tooltip-n-help": "Para saver mas y tomar ayudo",
-       "tooltip-t-whatlinkshere": "Una lista de todas las hojas del viki que tienen atamientos a esta hoja",
+       "tooltip-t-whatlinkshere": "La lista de todas las hojas del viki que tienen atamientos a esta hoja",
        "tooltip-t-recentchangeslinked": "Los trocamientos dalcavo en las hojas atadas a la ésta",
        "tooltip-feed-rss": "Sindicación RSS de esta hoja",
        "tooltip-feed-atom": "Canal Atomo parâ esta hoja",
        "tooltip-t-emailuser": "A este usuario, mándale una letra electrόnica (ímey)",
        "tooltip-t-upload": "Suve dosyas",
        "tooltip-t-specialpages": "La lista de todas las hojas especiales",
-       "tooltip-t-print": "La forma apropiada parâ imprimir esta hoja",
+       "tooltip-t-print": "La vista desta hoja apropiada para imprimir",
        "tooltip-t-permalink": "Atamiento permanente (fikso) a este enderechamiento de la hoja",
        "tooltip-ca-nstab-main": "Ve el artíkolo",
        "tooltip-ca-nstab-user": "Ver la hoja del usador",
        "feedback-cancel": "Anular",
        "feedback-message": "Messaje",
        "feedback-subject": "Sujeto",
-       "searchsuggest-search": "Busxca en {{SITENAME}}",
+       "searchsuggest-search": "Buxca en {{SITENAME}}",
        "duration-seconds": "$1{{PLURAL:$1|segundo|segundos}}",
        "duration-minutes": "$1{{PLURAL:$1|minuto|minutos}}",
        "duration-hours": "$1{{PLURAL:$1|ora|oras}}",
index ad5a415..0da01f8 100644 (file)
        "uploadstash-thumbnail": "Miniaturbild weisen",
        "uploadstash-bad-path": "Wee (path) gëtt et net.",
        "uploadstash-bad-path-unknown-type": "Onbekannten Typ \"$1\".",
+       "uploadstash-file-not-found-no-thumb": "D'Miniaturbild konnt net erofgeluede ginn.",
        "uploadstash-no-extension": "Erweiderung ass eidel (null).",
        "uploadstash-zero-length": "Fichier huet d'Gréisst null.",
        "img-auth-accessdenied": "Zougang refuséiert",
index 581e409..137a1bc 100644 (file)
@@ -15,7 +15,7 @@
        },
        "tog-underline": "خط کیشائن ژێر پیوندەل:",
        "tog-hideminor": "آشاردن دەسکاریەل گؤجەر  إژ گؤەڕیال(تغییرات) ایسە(اخیر)",
-       "tog-hidepatrolled": "دسکاریۀل گه دیار بینۀ ئژ فئرست-رزگ تغییرات اخیر بشارا",
+       "tog-hidepatrolled": "ویرایش‌های گشت‌خورده از فهرست تغییرات اخیر پنهان شود",
        "tog-newpageshidepatrolled": "وڵگۀل گه دیار بینۀ ئژ فئرست-رزگ ولگۀل تازۀ بشارا",
        "tog-hidecategorization": "فهرست بالا سی ئی صفحه",
        "tog-extendwatchlist": " کؤل رزگ-فئرست الؤن(آلشت)کریال-تغیرات نیشان دۀ،نۀ هر تنیا دؤمائنۀل",
        "tog-minordefault": "کؤڵ دسکاری بیۀل به عنؤان پئش فرض عڵامت بۀرن",
        "tog-previewontop": "پیش نمایش وهِ رئ جعبۀ نمایش نیشؤن به",
        "tog-previewonfirst": "پێش دئین وە اوەڵێن دەسکاری نیشۆن دە",
-       "tog-enotifwatchlistpages": "ئÛ\80ر Ù\88ÚµÚ¯Û\80 Û\8cا Ù¾Ø±Ø¤Ù\86دئÙ\87 Ø¦Ú\98 Ù\81ئرست-رزگ Ù¾Û\8câ\80\8cÚ¯Û\8cرÛ\8câ\80\8cÛ\80Ù\84Ù\85 Ø¯Ø³Ú©Ø§Ø±Û\8c Ø¨Û\8c Ù\86اÙ\85Ù\87 Ø¦Û\80را Ù\85Ù\87 Ú©Ù\90Ù\84Ù\91 Ú©Û\80",
-       "tog-enotifusertalkpages": "Ù\87Û\80Ù\86ئگÙ\87\88ختئ Ú¯Ù\87 Ø¦Û\80 Ù\88ÚµÚ¯Û\80 Ú¯Ù¾ Ú©Ø§Ø¨Ø±Û\8cÙ\85 ØªØºÛ\8cÛ\8cر Ú©Ø±Û\8cا Ù\86اÙ\85Ù\87 Ø¦Û\80را Ù\85Ù\87 Ú©Ù\90Ù\84 Ú©Û\80",
-       "tog-enotifminoredits": "ئÛ\80ر ØªØºÛ\8cÛ\8cرÛ\80Ù\84-آڵؤÙ\86Û\80Ù\84(Ø¢Ù\84شتÛ\80Ù\84)گؤجÛ\80رÛ\8cجÛ\8c Ø¦Û\80ر Ù\88ÚµÚ¯Û\80Ù\84 Ø¤ Ù¾Ø±Ø¤Ù\86دÛ\80Ù\84Ù\85 Ú©Ø±Û\8cا Ù\86اÙ\85Ù\87 Ø¦Û\80را Ù\85Ù\87 Ú©Ù\90Ù\84 Ú©Û\80",
+       "tog-enotifwatchlistpages": "ئÛ\95Ú¯Û\95ر Ù¾Û\95Ú\95Û\95Û\8e Û\8cا Ú¤Û\95ÚµÚ¯Û\95Û\8e Ø¦Û\95Ú\98 Ù¾Û\95Û\8eÚ¯Û\8cرÛ\8cÛ\95Ù\84Ù\85 Ú¯Ù\88Ù\88Û\95Ú\95Û\8cا(تغÛ\8cرÛ\8cاÙ\81ت)ئÛ\8cÙ\85Û\95Û\8cÙ\84Û\8e Ø¦Û\95Ú\95اÙ\86Ù\85 Ú©Ù\84 Ú©Û\95",
+       "tog-enotifusertalkpages": "Ù\87Û\95Ù\86Û\8e(Ú¤Û\95Ù\82تÛ\8e)Ù¾Û\95Ú\95Û\95Û\8e Ø¦Û\95Ú\98 Ú¤Û\95ÚµÚ¯Û\95 Ú¯Û\95Ù¾(Ù\82سÛ\95)Ú¯Ù\88Ù\88Û\95Ú\95Û\8cا Ø¦Û\8cÙ\85Û\95Ù\84Û\8e Ø¦Û\95Ú\95اÙ\86Ù\85 Ú©Ù\84 Ú©Û\95",
+       "tog-enotifminoredits": "ئÛ\95Ú\95ا Ú¯Ù\88Ù\88Û\95Ú\95اÙ\86Ù\86Û\95Ù\84(تغÛ\8cرات)Ú¯Ù\88Ù\88جÛ\95ر Ø¦Û\95 Ú¤Û\95ÚµÚ¯Û\95Ù\84/Ù¾Û\95Ú\95Û\95Ù\84 Ø¦Û\8cÙ\85Û\95Û\8cÙ\84Û\8e Ø¦Û\95Ú\95اÙ\86Ù\85 Ú©Ù\84 Ú©Û\95",
        "tog-enotifrevealaddr": "نیشانی ایمیل مه ئۀر ایمیل‌ل حاوواڵ رۀسن نیشؤن دۀ",
        "tog-shownumberswatching": "گلۀ شؤماری-شؤمار کاربۀل پی‌گیر نیشان دۀ",
        "tog-oldsig": ":امضاێ موجود ایوه",
        "tog-fancysig": "(امضا چؤی ویکی‌متن بوو(بدون پئؤن خودکار نیائن",
-       "tog-uselivepreview": "استفاده از پیش‌نمایش زنده",
+       "tog-uselivepreview": "پێش سەیرکەر بدون گرەک(نیاز)ڤە بروز رسانی ڤەڵگە",
        "tog-forceeditsummary": "هۀنئ گه-وختئ که خؤلاصۀ دسکاریم نَنیؤیسائۀ خۀؤۀ رم کۀ",
        "tog-watchlisthideown": "دسکاریۀل ووژم ئژ فئرست سئرکردن بشارآ",
        "tog-watchlisthidebots": "دسکاریۀل ربات ئژ فئرست سئرکردن بشآرا",
        "tog-watchlisthideminor": "دسکاریۀل گؤجۀر ئژ فئرست سئرکردن بشارآ",
        "tog-watchlisthideliu": "دەسکاری کاربرەل إنۆم هەتێ سیستم وە لیست پیگیریەل بشارآ",
        "tog-watchlistreloadautomatically": "Reload the watchlist automatically whenever a filter is changed (JavaScript required)",
+       "tog-watchlistunwatchlinks": "افزودن پیوندهای مستقیم خروج از پی‌گیری به فهرست پی‌گیری (جاواسکریپت ممکن است نیاز شود)",
        "tog-watchlisthideanons": "دةسکاری کاربرةل ناشنا ئة لیست نمائش بشارآ",
        "tog-watchlisthidepatrolled": "دسکاریۀل گشت خورده-سئرکریا ئژ فئرست سئرکردن بشآرا",
        "tog-watchlisthidecategorization": "نهفتن رده‌بندی صفحه‌ها",
@@ -60,7 +61,6 @@
        "underline-never": "هؤیچ وخت",
        "underline-default": "پوسته یا مِنِی کەر پیش‌فرض",
        "editfont-style": ":شئؤۀ قلم جعبهٔ دسکاری",
-       "editfont-default": "پیشفرض مِنِی کەر",
        "editfont-monospace": "قلم وە فاصلۀ ثابت",
        "editfont-sansserif": "قلم بئ گوشۀ",
        "editfont-serif": "قلم گوشۀ دار",
        "index-category": "صفحه‌های نمایه‌شده",
        "noindex-category": "صفحه‌های نمایه‌نشده",
        "broken-file-category": "صفحه‌های دارای پیوند خراب به پرونده",
+       "categoryviewer-pagedlinks": "($1) ($2)",
        "about": "دۀربارۀ",
        "article": "وەڵگە نۆم چێنە",
        "newwindow": "(واز کردن ئۀر دۀروۀچۀ جدید)",
        "faq": "پرسش‌های متداول",
        "actions": "کارۀل",
        "namespaces": "فضای نامۀل",
-       "variants": "قصۀ کِرۀل",
+       "variants": "گەپ دێەل(قسەکرەل)",
        "navigation-heading": "منوی ناوبری",
        "errorpagetitle": "خطا",
        "returnto": "بازگشت به $1",
        "redirectedfrom": "(تغییرمسیر إژ $1)",
        "redirectpagesub": "وةڵگة تغییرمسیر",
        "redirectto": ":تغییر مسیر به",
-       "lastmodifiedat": ".اێ وەڵگەآخرین گِل وە $1 سات $2 گؤەڕیائە(تغییریافته)",
+       "lastmodifiedat": "ئێ ڤەڵگەئاخرێن گِل ڤە $1 سات $2 گووەڕیائە(تغییریافته)",
        "viewcount": "إژ ئئ وةڵگة  {{PLURAL:$1|یإ گِل|$1$1چةن گِل}} بازدید بیة.",
        "protectedpage": "وەڵگە پڵۆم بیە",
        "jumpto": ":وازآ کردن/پریدن وۀ",
        "versionrequired": "نسخهٔ $1 از نرم‌افزار مدیاویکی لازم است",
        "versionrequiredtext": "برای دیدن این صفحه به نسخهٔ $1 از نرم‌افزار مدیاویکی نیاز دارید.\nبه [[Special:Version|این صفحه]] مراجعه کنید.",
        "ok": "خوو/ باشد",
+       "pagetitle": "$1 - {{SITENAME}}",
+       "pagetitle-view-mainpage": "{{SITENAME}}",
        "retrievedfrom": "إژ \"$1\" گیریائۀ",
        "youhavenewmessages": "{{PLURAL:$3|درین}}$1$2",
        "youhavenewmessagesfromusers": "{{PLURAL:$4|هؤمة}} $1 د {{PLURAL:$3|کاربةرێ تِر|$3 کاربةر}}دِرین($2).",
        "youhavenewmessagesmulti": ".پیغامةل جدیدی ئة $1 درین",
        "editsection": "دةسکاری",
        "editold": "دةسکاری",
-       "viewsourceold": "سئرکردÙ\86 Ø¨Ù\90Ù\86Ú\86Û\80Ú©/Ù\85Û\80Ù\86بÛ\80ع",
+       "viewsourceold": "بÙ\86Ú\86Û\95Ú©(Ù\85Ù\86بع) Ø¨Û\8aÙ\86(سÛ\95Û\8cرکÛ\95)",
        "editlink": "دەسکاری",
-       "viewsourcelink": "سئرکردÙ\86 Ø¨Ù\90Ù\86Ú\86Û\80Ú©/Ù\85Û\80Ù\86بÛ\80ع",
+       "viewsourcelink": "بÙ\86Ú\86Û\95Ú©(Ù\85Ù\86بع) Ø¨Û\8aÙ\86(سÛ\95Û\8cرکÛ\95)",
        "editsectionhint": "دۀسکاری بۀخش: $1",
        "toc": "محتویات",
        "showtoc": "نیشان دائن",
        "perfcached": "داده‌های زیر از حافظهٔ نهانی فراخوانی شده‌اند و ممکن است کاملاً به‌روز نباشند. حداکثر {{PLURAL:$1|یک نتیجه| $1 نتیجه}} در حافظهٔ نهانی قابل دسترس است.",
        "perfcachedts": "داده‌های زیر از حافظهٔ نهانی فراخوانی شده‌اند و آخرین بار در $1 به‌روزرسانی شدند. حداکثر {{PLURAL:$4|یک نتیجه|$4 نتیجه}} در حافظهٔ نهانی قابل دسترس است.",
        "querypage-no-updates": "امکان به‌روزرسانی این صفحه فعلاً غیرفعال شده‌است.\nاطلاعات این صفحه ممکن است به‌روز نباشد.",
-       "viewsource": "سئرکردÙ\86 Ø¨Ù\90Ù\86Ú\86Û\80Ú©/Ù\85Û\80Ù\86بÛ\80ع",
+       "viewsource": "بÙ\86Ú\86Û\95Ú©(Ù\85Ù\86بع) Ø¨Û\8aÙ\86(سÛ\95Û\8cرکÛ\95)",
        "viewsource-title": "نمایش بِنچةک ئةرا $1",
        "actionthrottled": "جلوی عمل شما گرفته شد",
        "actionthrottledtext": "به منظور جلوگیری از انتشار خرابکاری، اجازه ندارید که چنین عملی را بیش از چند بار در یک مدت زمان کوتاه انجام بدهید.\nلطفاً پس از چند دقیقه دوباره تلاش کنید.",
        "yourdomainname": ":دامنهٔ شما",
        "password-change-forbidden": ".شما نمی‌توانید گذرواژه‌ها را در این ویکی تغییر دهید",
        "externaldberror": "خطایی در ارتباط با پایگاه داده رخ داده است یا اینکه شما اجازهٔ به‌روزرسانی حساب خارجی خود را ندارید.",
-       "login": "إ نۆم هەتن سیستم",
+       "login": "ڤە نۆم هەتن",
        "login-security": "وژت معرفی‌که",
        "nav-login-createaccount": " إ نؤم هةتن سیستم/ حساوو کاربةری سازین",
        "logout": "دەرچێن|خروج",
        "badretype": "گذرواژةلێ گإ نۆیساتة چؤِی یةک نیِن",
        "usernameinprogress": ". دِرێ حساوو دؤرسة مةکإ . خواهشا صبر کةن",
        "userexists": ".نام کاربةری‌ گإ واردت کردئة قبلاً استفاده بیة\n.خواهشا نامێ تر استفادة کةن",
-       "loginerror": "خطای إ نام هةتن سیستم",
+       "loginerror": "نادوەرستی ڤە نۆم هەتن",
        "createacct-error": "خطای  حساوو کاربةری سازین",
        "createaccounterror": "نمآوو ئئ حساووة بِسازین: $1",
        "nocookiesnew": "حساوو کاربةری سازیا، اما هؤمة أ سیستم نهةتینة/نهاتینة.\n{{SITENAME}} برای ورود کاربران به سامانه از کوکی استفاده می‌کند.\nشما کوکی‌ها را از کار انداخته‌اید.\nلطفاً کوکی‌ها را به کار بیندازید، و سپس با نام کاربری و گذرواژهٔ جدیدتان به سامانه وارد شوید.",
        "suspicious-userlogout": "درخواست هؤمة ئةرا  دةرچئن إژ سیستم  رد بیة زیرا به نظر می‌رسد که این  .درخواست توسط یک مرورگر معیوب یا پروکسی میانگیر کل/ارسال بیة",
        "createacct-another-realname-tip": "نام راسکانی/واقعی دڵ بخواهیة.\nاگر آن را وارد کنید هنگام ارجاع به آثارتان و انتساب آن‌ها به شما از نام واقعی‌تان استفاده خواهد شد.",
        "pt-login": "إنۆم هەتِن.",
-       "pt-login-button": "إ نۆم هەتن سیستم",
+       "pt-login-button": "ڤە نۆم هەتن",
        "pt-login-continue-button": "ادامه سی ورود سیستم",
        "pt-createaccount": "حساووئ أرا ووژتان بِسازِن",
        "pt-userlogout": "دەرچێن|خروج",
        "resetpass-no-info": "برای دسترسی مستقیم به این صفحه شما باید به سامانه وارد شده باشید.",
        "resetpass-submit-loggedin": "تغییردائن رمز",
        "resetpass-submit-cancel": "ئآهووسانن/لغو",
-       "resetpass-wrong-oldpass": "گذرÙ\88اÚ\98Ù\87Ù\94 Ù\85Ù\88Ù\82ت Û\8cا Ø§Ø®Û\8cر Ù\86اÙ\85عتبر.\nÙ\85Ù\85Ú©Ù\86 Ø§Ø³Øª Ú©Ù\87 Ø´Ù\85ا Ù\87Ù\85Û\8cÙ\86Ú© Ú¯Ø°Ø±Ù\88اÚ\98Ù\87â\80\8cتاÙ\86 Ø±Ø§ Ø¨Ø§ Ù\85Ù\88Ù\81Ù\82Û\8cت ØªØºÛ\8cÛ\8cر Ø¯Ø§Ø¯Ù\87 Ø¨Ø§Ø´Û\8cد Û\8cا Ø¯Ø±Ø®Ù\88است Û\8cÚ© Ú¯Ø°Ø±Ù\88اÚ\98Ù\87Ù\94 Ù\85Ù\88Ù\82ت ØªØ§Ø²Ù\87 Ú©Ø±Ø¯Ù\87 Ø¨Ø§Ø´Û\8cد.",
+       "resetpass-wrong-oldpass": "گذرواژهٔ موقت یا اخیر نامعتبر.\nممکن است که شما همینک گذرواژه‌تان را تغییر داده باشید یا درخواست یک گذرواژهٔ موقت تازه کرده باشید.",
        "resetpass-recycled": "لطفاً رمز عبور خود را به چیز دیگری غیر از رمز عبور فعلی تنظیم کنید.",
        "resetpass-temp-emailed": "شما با یک کد ایمیل شدهٔ موقت وارد شده‌اید.\nبرای پایان ورود، شما باید رمز عبور جدیدی اینجا وارد کنید:",
        "resetpass-temp-password": ":رمز عبور موقت",
        "passwordreset-emailsentusername": "اگر نشانی پست الکترونیکی مرتبطی موجود باشد، یک نامه برای بازنشانی گذرواژه به آن ارسال خواهد شد.",
        "passwordreset-nocaller": "زنگ مجبور نییه به تأمین کردن",
        "passwordreset-nosuchcaller": "زنگ موجود نییه: $1",
+       "passwordreset-ignored": "به بازنشانی گذرواژه پرداخته نشد. آیا ممکن است که هيچ مهياکننده‌ای برای این کار تنظيم نشده باشد؟",
        "passwordreset-invalidemail": "آدرس ایمیل نامعتبره",
+       "passwordreset-nodata": "یک نام کاربری و یا یک آدرس ايميل، هيچکدام ارائه نشده",
        "changeemail": "تغییر یا حذف نشانی ایمیل",
        "changeemail-header": "برای تغییر ایمیلتان این فرم را کامل کنید. برای حذف ایملیتان کافی است بخش ایمیل را خالی رها کنید و فرم را ارسال کنید.",
        "changeemail-no-info": ".برای دسترسی مستقیم به این صفحه شما باید به سیستم وارد شده باشید",
        "headline_tip": "عنوان سطح ۲",
        "nowiki_sample": "متن قالب‌بندی‌نشده اینجا وارد شود",
        "nowiki_tip": "نادیده‌گرفتن قالب‌بندی ویکی",
+       "image_sample": "نموونە.jpg",
        "image_tip": "تصویر داخل متن",
+       "media_sample": "نموونە.ogg",
        "media_tip": "پیوند پرونده",
        "sig_tip": "امضای هومۀ و برچسب زۀمان",
        "hr_tip": " )خط افقی(از آن کم استفاده کنید",
        "preview": "پیش‌نمایش",
        "showpreview": "پیش‌نمایش",
        "showdiff": "گؤەڕیال(تغییرات) بۆین",
-       "blankarticle": "<strong>هوشدار:</strong> هۆمە وەڵگەتۆن سازیە پەتیە(حالیە).\nأڕ «$1» دۆِ گِل کلیک کِین ، وەڵگە بێ  نۆم جِک(محتوا) مەسازێ.",
+       "blankarticle": "<strong>ئاگاتۆ داشتوو:</strong> هۆمە ڤەڵگەتۆن سازیە پەتیە(خالیە).\nئەڕ «$1» دۆِ گِل کلیک کِین ، ڤەڵگە بێ  نۆمجِک(محتوا) مەسازێ.",
        "anoneditwarning": "<strong>هشدار:</strong> شما وارد نشده‌اید. نشانی آی‌پی شما برای عموم قابل مشاهده خواهد بود اگر هر تغییری ایجاد کنید. اگر <strong>[$1 وارد شوید]</strong> یا <strong>[$2 یک حساب کاربری بسازید]</strong>، ویرایش‌هایتان به نام کاربری‌تان نسبت داده خواهد شد، همراه با مزایای دیگر.",
        "anonpreviewwarning": "''شما به سامانه وارد نشده‌اید. ذخیره کردن باعث می‌شود که نشانی آی‌پی شما در تاریخچهٔ این صفحه ثبت گردد.''",
        "missingsummary": "'''یادآوری:''' شما خلاصهٔ ویرایش ننوشته‌اید.\nاگر دوباره دکمهٔ «$1» را فشار دهید ویرایش شما بدون آن ذخیره خواهد شد.",
        "updated": "(تازة سازی بیة)",
        "note": "'''نکته:'''",
        "previewnote": "'''به یاد داشته باشید که این فقط پیش‌نمایش است.'''\nتغییرات شما هنوز ذخیره نشده‌است!",
-       "continue-editing": "بچۆإ بةخش دةکاری",
+       "continue-editing": "بچۆ ئەڕا بەش دەسکاریکردن",
        "previewconflict": "این پیش‌نمایش منعکس‌کنندهٔ متن ناحیهٔ ویرایش متن بالایی است، به شکلی که اگر متن را ذخیره کنید نمایش خواهد یافت.",
        "session_fail_preview": "'''شرمنده! به علت از دست رفتن اطلاعات نشست کاربری نمی‌توانیم ویرایش شما را پردازش کنیم.'''\nلطفاً دوباره سعی کنید.\nاگر دوباره به همین پیام برخوردید از سامانه [[Special:UserLogout|خارج شوید]] و دوباره وارد شوید.",
        "session_fail_preview_html": "'''متأسفانه امکان ثبت ویرایش شما به خاطر از دست رفتن اطلاعات نشست کاربری وجود ندارد.'''\n\n''با توجه به این که در {{SITENAME}} امکان درج اچ‌تی‌ام‌ال خام فعال است، پیش‌نمایش صفحه پنهان شده تا امکان حملات مبتنی بر جاوااسکریپت وجود نداشته باشد.''\n\n'''اگر مطمئن هستید که این پیش‌نمایش یک ویرایش مجاز است، آن را تکرار کنید.'''\nاگر تکرار پیش‌نمایش نتیجه نداد، از سامانه [[Special:UserLogout|خارج شوید]] و دوباره وارد شوید.",
        "explainconflict": "از وقتی ویرایش این صفحه را آغاز کرده‌اید شخص دیگری آن را تغییر داده است.\nناحیهٔ متنی بالایی شامل متن صفحه به شکل کنونی آن است.\nتغییرات شما در ناحیهٔ متنی پایینی نشان داده شده‌است.\nشما باید تغییراتتان را با متن کنونی ترکیب کنید.\nبا فشردن دکمهٔ «$1» <strong>فقط</strong> متن ناحیهٔ متنی بالایی ذخیره خواهد شد.",
        "yourtext": "متن شما",
        "storedversion": "نسخهٔ ذخیره شده",
-       "nonunicodebrowser": "'''هشدار: مرورگر شما با استانداردهای یونیکد سازگار نیست.'''\nراه حلی به کار گرفته شده تا شما بتوانید صفحات را با امنیت ویرایش کنید: کاراکترهای غیر ASCII به صورت کدهایی در مبنای شانزده به شما نشان داده می‌شوند.",
        "editingold": "'''هشدار: شما در حال ویرایش نسخه‌ای قدیمی از این صفحه هستید.'''\nاگر ذخیره‌اش کنید، هر تغییری که پس از این نسخه انجام شده‌است از بین خواهد رفت.",
        "yourdiff": "تفاوت‌ها",
        "copyrightwarning": "لطفاً توجه داشته‌باشید که همهٔ مشارکت‌ها در {{SITENAME}} منتشرشده تحت $2 در نظر گرفته‌می‌شوند (برای جزئیات بیش‌تر $1 را ببینید).\nاگر نمی‌خواهید نوشته‌هایتان بی‌رحمانه ویرایش و توزیع شوند؛ بنابراین، آنها را اینجا ارائه نکنید.<br />\nشما همچنین به ما تعهد می‌کنید که خودتان این را نوشته‌اید یا آن را از یک منبع با مالکیت عمومی یا مشابه آزاد آن برداشته‌اید (برای جزئیات بیش‌تر $1 را ببینید).\n<strong>کارهای دارای حق تکثیر را بدون اجازه ارائه نکنید!</strong>",
        "yourgender": "ترجیح می‌دهید چگونه توصیف شوید؟",
        "gender-unknown": "هنگام ذکر شما، نرم‌افزار تا جای ممکن از کلمات خنثی از نظر جنسیت استفاده خواهد",
        "gender-male": "پیا",
-       "gender-female": "ژن",
+       "gender-female": "ئافرەت(ژەن)",
        "prefs-help-gender": "انجام این تنظیم اختیاری است.\nنرم‌افزار از این مقدار برای اشارهٔ صحیح به جنسیت و ذکر شما برای دیگران با استفاده از دستور زبان درست استفاده می‌کند.\nاین اطلاعات عمومی خواهند بود.",
        "email": "ایمیل",
        "prefs-help-realname": "نام واقعی اختیاری است.\nاگر وارد شده است هنگام ارجاع به آثارتان و انتساب آن‌ها به شما ممکن است از نام واقعی‌تان استفاده شود.",
        "userrights-changeable-col": "گروه‌هایی که می‌توانید تغییر دهید",
        "userrights-unchangeable-col": "گروه‌هایی که نمی‌توانید تغییر دهید",
        "userrights-conflict": "تعارض دسترسی‌های کاربری! لطفاً بررسی کنید و تغییرات را تأیید کنید.",
-       "group": "گروه:",
+       "group": "داکووکە(گروو):",
        "group-user": "کاربۀر",
        "group-autoconfirmed": "کاربران تأییدشدهٔ خودکار",
        "group-bot": "ربات‌ها",
        "rc_categories": "محدود به این رده‌ها (رده‌ها را با «|» جدا کنید):",
        "rc_categories_any": "هر کدام از منتخب‌ها",
        "rc-change-size-new": " $1دؤما تۀقیر دائن{{PLURAL:$1|بایت|بایتل}}",
-       "newsectionsummary": "/* $1 */ بةخش جدید",
+       "newsectionsummary": "/* $1 */ بەخش نوو",
        "rc-enhanced-expand": "نمایش جزئیات",
        "rc-enhanced-hide": "نهفتن جزئیات",
        "rc-old-title": "ایجادشده با عنوان اصلی «$1»",
        "recentchangeslinked-toolbox": "تغییرۀ مرتبط",
        "recentchangeslinked-title": "تغییرات مرتبط با $1",
        "recentchangeslinked-summary": "در زیر فهرستی از تغییرات اخیر صفحه‌های پیوند داده شده از این صفحه (یا اعضای رده مورد نظر) را می‌بینید.\nصفحه‌هایی که در [[Special:Watchlist|فهرست پی‌گیری‌هایتان]] باشند به صورت '''پررنگ''' نشان داده می‌شوند.",
-       "recentchangeslinked-page": ":نام وةڵگة",
+       "recentchangeslinked-page": "نۆم ڤەڵگە:",
        "recentchangeslinked-to": "نمایش تغییرات صفحه‌هایی که به صفحهٔ داده‌شده پیوند دارند",
        "recentchanges-page-added-to-category": "[[:$1]] اضاف بیە ڕِزگ",
        "recentchanges-page-added-to-category-bundled": "[[:$1]] و {{PLURAL:$2|یک صفحه|$2 صفحه}}ٔ دیگر به رده اضافه شدند",
        "fileuploadsummary": "خلاصه:",
        "filereuploadsummary": "تغییرات پرونده:",
        "filestatus": "وضعیت حق تکثیر:",
-       "filesource": ":بِنچۀک/مۀنبۀع",
+       "filesource": "بنچەک(منبع)",
        "ignorewarning": "چشم‌پوشی از هشدار و ذخیرهٔ پرونده.",
        "ignorewarnings": "چشم‌پوشی از همهٔ هشدارها",
        "minlength1": "نام پرونده دست کم باید یک حرف باشد.",
        "listgrouprights": "اختیارات گروه‌های کاربری",
        "listgrouprights-summary": "فهرست زیر شامل گروه‌های کاربری تعریف شده در این ویکی و اختیارات داده شده به آن‌ها است.\nاطلاعات بیشتر در مورد هر یک از اختیارات را در [[{{MediaWiki:Listgrouprights-helppage}}]] بیابید.",
        "listgrouprights-key": "* <span class=\"listgrouprights-granted\">اختیارات داده‌شده</span>\n* <span class=\"listgrouprights-revoked\">اختیارات گرفته‌شده</span>",
-       "listgrouprights-group": "گروه",
+       "listgrouprights-group": "داکووکە(گروو)",
        "listgrouprights-rights": "دسترسی‌ها",
        "listgrouprights-helppage": "Help:دسترسی‌های گروهی",
        "listgrouprights-members": "(فهرست اعضا)",
        "blanknamespace": "(سەر/اصلی)",
        "contributions": "هؤمکاریۀل{{GENDER:$1|کاربۀر}}",
        "contributions-title": "مشارکت‌های کاربری $1",
-       "mycontris": "هؤمکاری کِرۀل",
-       "anoncontribs": "هؤمکاری کِرۀل",
+       "mycontris": "بەشاکرەل(هام بێرەل)",
+       "anoncontribs": "بەشاکرەل(هام بێرەل)",
        "contribsub2": "برای {{GENDER:$3|$1}} ($2)",
        "contributions-userdoesnotexist": "حساب کاربری «$1» ثبت نشده‌است.",
        "nocontribs": "هیچ تغییری با این مشخصات یافت نشد.",
        "block": "بستن کاربر.",
        "unblock": "بازکردن کاربر",
        "blockip": "بستن {{GENDER:$1|کاربر}}",
-       "blockip-legend": "بستن کاربر",
        "blockiptext": "از فرم زیر برای بستن دسترسی ویرایش یک نشانی آی‌پی یا نام کاربری مشخص استفاده کنید.\nاین کار فقط فقط باید برای جلوگیری از خرابکاری و بر اساس [[{{MediaWiki:Policy-url}}|سیاست قطع دسترسی]] انجام شود.\nدلیل مشخص این کار را در زیر ذکر کنید (مثلاً با ذکر صفحه‌های به‌خصوصی که مورد خرابکاری واقع شده‌اند).",
        "ipaddressorusername": "نشانی آی‌پی یا نام کاربری:",
        "ipbexpiry": "زمان سرآمدن:",
        "exif-sublocationdest": "بخش شهر نمایش داده شده",
        "exif-objectname": "عنوان کوتاه",
        "exif-specialinstructions": "دستورالعمل‌های ویژه",
-       "exif-headline": "سةر Ù\88ةڵگة",
+       "exif-headline": "عÙ\86Ù\88اÙ\86",
        "exif-credit": "صاحب امتیاز/ارائه کننده",
-       "exif-source": "بÙ\90Ù\86Ú\86Û\80Ú©/Ù\85Û\80Ù\86بÛ\80ع",
+       "exif-source": "بÙ\86Ú\86Û\95Ú©(Ù\85Ù\86بع)",
        "exif-editstatus": "وضعیت تحریریه تصویر",
        "exif-urgency": "فوریت/هڵةپڵة",
        "exif-fixtureidentifier": "نام ستون نشریه",
        "exif-gpsdirection-m": "جهت مغناطیسی",
        "exif-ycbcrpositioning-1": "وسط‌چین‌شده",
        "exif-ycbcrpositioning-2": "اشتراکی/هام بةشی",
-       "exif-dc-contributor": "هؤمکاری کِرۀل",
+       "exif-dc-contributor": "بەشاکرەل(هام بێرەل)",
        "exif-dc-coverage": "محدوده مکانی و یا زمانی رسانه",
        "exif-dc-date": "تاریخ(ها)",
        "exif-dc-publisher": "بۀشا کۀر-ناشر",
        "tags-tag": "نام برچسب",
        "tags-display-header": "نمایش در فهرست‌های تغییرات",
        "tags-description-header": "توضیح کامل معنی",
-       "tags-source-header": "بÙ\90Ù\86Ú\86Û\80Ú©/Ù\85Û\80Ù\86بÛ\80ع",
+       "tags-source-header": "بÙ\86Ú\86Û\95Ú©(Ù\85Ù\86بع)",
        "tags-active-header": "فعال(کارکةر)؟",
        "tags-hitcount-header": "تغییرهای برچسب‌دار",
        "tags-actions-header": "کارۀل",
index 48cf1d5..09d616d 100644 (file)
        "changeemail-none": "(nav)",
        "changeemail-password": "Jūsu {{SITENAME}} parole:",
        "changeemail-submit": "Mainīt e-pastu",
+       "changeemail-nochange": "Lūdzu, ievadi atšķirīgu jauno e-pasta adresi.",
        "resettokens-tokens": "Marķieri:",
        "resettokens-token-label": "$1 (šībrīža vērtība: $2)",
        "bold_sample": "Teksts treknrakstā",
        "accmailtext": "Nejauši ģenerēta parole lietotājam [[User talk:$1|$1]] tika nosūtīta uz $2.\n\nŠī konta paroli pēc ielogošanās varēs nomainīt ''[[Special:ChangePassword|šeit]]''.",
        "newarticle": "(Jauns raksts)",
        "newarticletext": "Šajā projektā vēl nav lapas ar šādu nosaukumu.\nLai izveidotu lapu, sāc rakstīt teksta logā apakšā (par teksta formatēšanu un sīkākai informācija skatīt [$1 palīdzības lapu]).\nJa tu šeit nonāci kļūdas pēc, vienkārši uzspied <strong>back</strong> pogu pārlūkprogrammā.",
-       "anontalkpagetext": "----''Šī ir diskusiju lapa anonīmam dalībniekam, kurš vēl nav kļuvis par reģistrētu dalībnieku vai arī neizmanto savu dalībnieka vārdu. Tādēļ mums ir jāizmanto skaitliskā IP adrese, lai viņu identificētu.\nŠāda IP adrese var būt vairākiem dalībniekiem.\nJa tu esi anonīms dalībnieks un uzskati, ka tev ir adresēti neatbilstoši komentāri, lūdzu, [[Special:CreateAccount|kļūsti par dalībnieku]] vai arī [[Special:UserLogin|izmanto jau izveidotu dalībnieka vārdu]], lai izvairītos no turpmākām neskaidrībām un tu netiktu sajaukts ar citiem anonīmiem dalībniekiem.''",
+       "anontalkpagetext": "----\n<em>Šī ir anonīma dalībnieka, kurš vēl nav izveidojis lietotāja kontu vai to nelieto, diskusiju lapa.</em>\nTādēļ mums ir jāizmanto IP adrese, lai viņu identificētu.\nŠāda IP adrese var būt vairākiem dalībniekiem.\nJa tu esi anonīms dalībnieks un uzskati, ka tev ir adresēti neatbilstoši komentāri, lūdzu, [[Special:CreateAccount|izveido kontu]] vai [[Special:UserLogin|pieslēdzies]], lai izvairītos no turpmākām neskaidrībām un tu netiktu sajaukts ar citiem anonīmiem dalībniekiem.",
        "noarticletext": "Šajā lapā šobrīd nav nekāda teksta, tu vari [[Special:Search/{{PAGENAME}}|meklēt citās lapās pēc šīs lapas nosaukuma]], <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} meklēt saistītos reģistru ierakstos] vai arī [{{fullurl:{{FULLPAGENAME}}|action=edit}} sākt rediģēt šo lapu]</span>.",
        "noarticletext-nopermission": "Šajā lapā pašlaik nav nekāda teksta.\nTu vari [[Special:Search/{{PAGENAME}}|meklēt šīs lapas nosaukumu]] citās lapās,\nvai <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} meklēt saistītus reģistru ierakstus]</span>, bet jums nav atļauja izveidot šo lapu.",
        "userpage-userdoesnotexist": "Lietotājs \"<nowiki>$1</nowiki>\" nav reģistrēts.\nLūdzu, pārliecinies vai vēlies izveidot/izmainīt šo lapu.",
        "shown-title": "Parādīt $1 {{PLURAL:$1|rezultātus|rezultātu|rezultātus}} vienā lapā",
        "viewprevnext": "Skatīt ($1 {{int:pipe-separator}} $2) ($3 vienā lapā).",
        "searchmenu-exists": "'''Šajā projektā ir raksts ar nosaukumu \"[[:$1]]\"'''",
-       "searchmenu-new": "'''Izveido rakstu \"[[:$1]]\" šajā projektā!'''",
+       "searchmenu-new": "<strong>Izveido lapu \"[[:$1]]\" šajā projektā!</strong> {{PLURAL:$2|0=|Apskati arī meklēšanā atrasto lapu.|Apskati arī meklēšanā atrastos rezultātus.}}",
        "searchprofile-articles": "Rakstos",
        "searchprofile-images": "Multivides failos",
        "searchprofile-everything": "Visur",
        "prefs-editor": "Redaktors",
        "prefs-preview": "Priekšskatījums",
        "prefs-advancedrc": "Papildu iespējas",
+       "prefs-opt-out": "Atteikties no uzlabojumiem",
        "prefs-advancedrendering": "Papildu iespējas",
        "prefs-advancedsearchoptions": "Papildu iespējas",
        "prefs-advancedwatchlist": "Papildu iespējas",
        "action-userrights-interwiki": "mainīt dalībnieku tiesības citās Vikipēdijās",
        "action-siteadmin": "bloķēt vai atbloķēt datubāzi",
        "action-sendemail": "sūtīt e-pastus",
+       "action-editmyoptions": "labot savas izvēles",
        "action-deletechangetags": "dzēst iezīmes no datubāzes",
        "nchanges": "$1 {{PLURAL:$1|izmaiņas|izmaiņa|izmaiņas}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|kopš pēdējā apmeklējuma}}",
        "uploaddisabledtext": "Failu augšupielāde ir atslēgta.",
        "php-uploaddisabledtext": "Failu augšupielāde ir atslēgta PHP.\nLūdzu, pārbaudi file_uploads uzstādījumu.",
        "uploadscripted": "Šis fails satur HTML vai skriptu kodu, kuru, interneta pārlūks, var kļūdas pēc, mēģināt interpretēt (ar potenciāli sliktām sekām).",
+       "upload-scripted-dtd": "Nevar augšupielādēt SVG failus, kuri satur nestandarta DTD deklarāciju.",
        "uploadinvalidxml": "Nevarēja apstrādāt augšupielādētā faila XML saturu.",
        "uploadvirus": "Šis fails satur vīrusu! Sīkāk: $1",
        "uploadjava": "Fails ir ZIP fails, kas satur Java .class failu.\nJava failu augšupielāde nav atļauta, jo tas var radīt iespējas apiet drošības ierobežojumus.",
        "listfiles-delete": "dzēst",
        "listfiles-summary": "Šajā īpašajā lapā ir redzami visi augšupielādētie faili.",
        "listfiles_search_for": "Meklēt failu pēc vārda:",
+       "listfiles-userdoesnotexist": "Dalībnieks \"$1\" nav reģistrēts.",
        "imgfile": "fails",
        "listfiles": "Attēlu uzskaitījums",
        "listfiles_thumb": "Sīktēls",
        "apisandbox-dynamic-parameters-add-placeholder": "Parametra nosaukums",
        "apisandbox-deprecated-parameters": "Novecojuši parametri",
        "apisandbox-results": "Rezultāti",
+       "apisandbox-sending-request": "Sūta API pieprasījumu...",
+       "apisandbox-loading-results": "Saņem API rezultātus...",
+       "apisandbox-request-selectformat-label": "Rādīt pieprasījuma datus kā:",
        "apisandbox-request-format-url-label": "URL vaicājuma teksts",
        "apisandbox-request-url-label": "Pieprasījuma URL:",
        "apisandbox-request-json-label": "Pieprasījuma JSON:",
        "notvisiblerev": "Cita lietotāja pēdējā versija ir izdzēsta",
        "watchlist-details": "(Tu uzraugi $1 {{PLURAL:$1|lapu|lapas}}, neieskaitot diskusiju lapas.)",
        "wlheader-enotif": "E-pasta paziņojumi ir ieslēgti.",
-       "wlheader-showupdated": "Lapas, kuras ir tikušas izmainītas, kopš tu tās pēdējoreiz apskatījies, te rādās ar '''pustrekniem''' burtiem",
+       "wlheader-showupdated": "Lapas, kas ir tikušas izmainītas, kopš pēdējoreiz skatījies tās, tiek rādītas <strong>trekninātā</strong> rakstā.",
        "wlshowlast": "Rādīt pēdējās $1 stundas $2 dienas",
        "watchlist-hide": "Slēpt",
        "watchlist-submit": "Rādīt",
        "protectexpiry": "Beidzas:",
        "protect_expiry_invalid": "Beigu termiņš ir nederīgs.",
        "protect_expiry_old": "Beigu termiņs ir pagātnē.",
+       "protect-unchain-permissions": "Pieslēgt papildu aizsargāšanas iespējas",
        "protect-text": "Šeit var apskatīt un izmainīt lapas <strong>$1</strong> aizsardzības līmeni.",
        "protect-locked-access": "Jūsu kontam nav atļaujas mainīt lapas aizsardzības pakāpi.\nPašreizējie lapas '''$1''' iestatījumi ir:",
        "protect-cascadeon": "Šī lapa pašlaik ir aizsargāta, jo tā ir iekļauta {{PLURAL:$1|šajās lapās|šajā lapā|šajās lapās}} (mainot šīs lapas aizsardzības līmeni aizsardzība netiks noņemta):",
        "protect-default": "Atļaut visiem lietotājiem",
        "protect-fallback": "Atļaut tikai lietotājiem ar \"$1\" atļauju",
-       "protect-level-autoconfirmed": "Atļaut tikai autoapstiprinātiem lietotājiem",
+       "protect-level-autoconfirmed": "Atļaut tikai pašpārbaudītajiem",
        "protect-level-sysop": "Atļaut tikai administratoriem",
        "protect-summary-cascade": "kaskāde",
        "protect-expiring": "līdz $1 (UTC)",
        "protect-expiring-local": "beidzas $1",
        "protect-expiry-indefinite": "bezgalīgs",
-       "protect-cascade": "Aizsargāt šajā lapā iekļautās lapas (veidnes) ''(cascading protection)''",
+       "protect-cascade": "Aizsargāt šajā lapā iekļautās lapas un veidnes (kaskādes aizsardzība)",
        "protect-cantedit": "Tu nevari izmainīt šīs lapas aizsardzības līmeņus, tāpēc, ka tur nevari izmainīt šo lapu.",
        "protect-othertime": "Cits laiks:",
        "protect-othertime-op": "cits laiks",
        "undelete-show-file-submit": "Jā",
        "namespace": "Vārdtelpa:",
        "invert": "Izvēlēties pretēji",
+       "tooltip-invert": "Atzīmē šo rūtiņu, lai paslēptu izmaiņas lapās izvēlētajā vārdtelpā (un saistītajā vārdtelpā, ja tā atzīmēts)",
        "namespace_association": "Saistītā vārdtelpa",
        "tooltip-namespace_association": "Atzīmē šo rūtiņu, lai iekļautu diskusijas vai temata vārdtelpu, kas saistīta ar izvēlēto vārdtelpu",
        "blanknamespace": "(Pamatlapa)",
        "pageinfo-article-id": "Lapas ID",
        "pageinfo-language": "Lappuses satura valoda",
        "pageinfo-content-model": "Lapas satura modelis",
+       "pageinfo-content-model-change": "mainīt",
        "pageinfo-robot-policy": "Indeksācija ar robotiem",
        "pageinfo-robot-index": "Atļauta",
        "pageinfo-robot-noindex": "Aizliegta",
        "pageinfo-watchers": "Lapas uzraudzītāju skaits",
+       "pageinfo-visiting-watchers": "Lapas uzraudzītāju skaits, kuri apskatījuši pēdējos labojumus",
        "pageinfo-few-watchers": "Mazāk kā $1 {{PLURAL:$1|uzraudzītāju|uzraudzītājs|uzraudzītāju}}",
        "pageinfo-redirects-name": "Pāradresāciju skaits uz šo lapu",
        "pageinfo-subpages-name": "Šīs lapas apakšlapas",
        "pageinfo-edits": "Kopējais izmaiņu skaits",
        "pageinfo-authors": "Kopējais atsevišķu autoru skaits",
        "pageinfo-recent-edits": "Izmaiņu skaits (pēdējās $1)",
+       "pageinfo-recent-authors": "Neseno lapas labotāju skaits",
        "pageinfo-magic-words": "{{PLURAL:$1|Maģiskie vārdi|Maģiskais vārds|Maģiskie vārdi}} ($1)",
        "pageinfo-hidden-categories": "{{PLURAL:$1|Slēptas kategorijas|Slēpta kategorija|Slēptas kategorijas}} ($1)",
        "pageinfo-templates": "{{PLURAL:$1|Iekļautās veidnes|Iekļautā veidne|Iekļautās veidnes}} ($1)",
        "exif-disclaimer": "Atruna",
        "exif-contentwarning": "Brīdinājums par saturu",
        "exif-giffilecomment": "GIF faila komentārs",
+       "exif-subjectnewscode": "Temata kods",
+       "exif-scenecode": "IPTC ainas kods",
        "exif-event": "Attēlotais notikums",
        "exif-organisationinimage": "Attēlotā organizācija",
        "exif-personinimage": "Attēlotā persona",
        "tag-filter": "[[Special:Tags|Iezīmju]] filtrs:",
        "tag-filter-submit": "Filtrs",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Iezīmes|Iezīme|Iezīmes}}]]: $2)",
+       "tag-mw-contentmodelchange": "satura modeļa izmaiņa",
        "tags-title": "Iezīmes",
        "tags-intro": "Šajā lapā uzskaitītas iezīmes, ar kurām programmatūra var atzīmēt labojumus, un to nozīme.",
        "tags-tag": "Iezīmes nosaukums",
        "compare-invalid-title": "Norādītais nosaukums nav derīgs.",
        "compare-title-not-exists": "Norādītais nosaukums neeksistē.",
        "compare-revision-not-exists": "Norādītā versija neeksistē.",
+       "diff-form": "Atšķirības",
        "dberr-problems": "Atvainojiet!\nŠai vietnei ir radušās tehniskas problēmas.",
        "dberr-again": "Uzgaidiet dažas minūtes un pārlādējiet šo lapu.",
        "dberr-info": "(Nevar piekļūt datubāzei: $1)",
index 2186a3b..aa348f5 100644 (file)
@@ -30,7 +30,7 @@
        "tog-watchuploads": "Sogna file anyar sing nyong unggah nang daptar sawangane nyong",
        "tog-watchrollback": "Tambahna kaca sing wis tek rollback maring daftar pangawasane inyong",
        "tog-minordefault": "Otomatis nandani kabeh suntingan dadi suntingan cilik",
-       "tog-previewontop": "Tidokna pratayang sedurunge kotak sunting",
+       "tog-previewontop": "Tidhokna pratayang sedurunge kotak sunting",
        "tog-previewonfirst": "Tidokna pratayang nang suntingan sing pertama",
        "tog-enotifwatchlistpages": "Kirimna imel maring inyong angger kaca awa berkas utsing mlebu daptar pangawasanne inyong diowaih",
        "tog-enotifusertalkpages": "Kirimna imel maring inyong angger kaca dhiskusine inyong owah",
        "createacct-yourpasswordagain-ph": "Lebokna tembung sandhi maning",
        "userlogin-remembermypassword": "Jorna ben Inyong tetep mlebu log",
        "userlogin-signwithsecure": "Gunakna koneksi aman",
+       "cannotlogin-title": "Ora bisa mlebu log",
+       "cannotlogin-text": "Ora mungkin mlebu log.",
        "cannotloginnow-title": "Ora teyeng mlebu siki",
+       "cannotcreateaccount-title": "Ora bisa gawé akun",
        "yourdomainname": "Domain Rika:",
        "password-change-forbidden": "Rika ora teyeng ngowaih tembung sandhi nang wiki kiye.",
        "externaldberror": "Ana kesalahan otentikasi basis data utawa Rika ora olih nglakokna pemutakhiran maring akun eksternale Rika.",
        "user-mail-no-body": "Njajal ngirim imel sing kosong urawa isine sithik thok.",
        "changepassword": "Ganti tembung sandhi",
        "resetpass_announce": "Kanggo ngrampungna gole mlebu log, rika kudu nglebokna tembung sandhi anyar.",
+       "resetpass_text": "<!-- Tambah teks neng kéné -->",
        "resetpass_header": "Ganti tembung sandhine akun",
        "oldpassword": "Tembung sandi lawas:",
        "newpassword": "Tembung sandi anyar:",
        "resetpass_submit": "Nata tembung sandhi lan mlebu log",
        "changepassword-success": "Sandhiné Rika uwis diganti!",
        "changepassword-throttled": "Rika wis kakehan gole njajal mlebu log.\nTulung ngenteni $1 sedurunge njajal maning.",
+       "botpasswords-label-create": "Gawé",
+       "botpasswords-label-update": "Nganyari",
+       "botpasswords-label-cancel": "Batalna",
+       "botpasswords-label-delete": "Busek",
        "resetpass_forbidden": "Tembung sandhi ora teyeng diganti",
        "resetpass-no-info": "Rika kudu mlebu log kanggo ngakses kaca kiye sacara langsung.",
        "resetpass-submit-loggedin": "Ganti tembung sandhi",
        "headline_tip": "Subbagian tingkat 1",
        "nowiki_sample": "Lebokna teks sing ora bakal diformat nang kene",
        "nowiki_tip": "Aja nganggo format wiki",
+       "image_sample": "Conto.jpg",
        "image_tip": "Ngaweh berkas",
+       "media_sample": "Conto.ogg",
        "media_tip": "Pranala berkas media",
        "sig_tip": "Tapak astane Rika nganggo tandha wektu",
        "hr_tip": "Garis horisontal",
        "minoredit": "Kiye suntingan cilik",
        "watchthis": "Awasi kaca kiyé",
        "savearticle": "Terbitna Kaca",
+       "publishpage": "Pacak kacané",
        "preview": "Pra tayang",
        "showpreview": "Pra tayang",
        "showdiff": "Ndeleng bedané",
        "editingsection": "Nyunting $1 (bagiyan)",
        "editingcomment": "Nyunting $1 (bagéyan anyar)",
        "editconflict": "Konflik panyuntingan: $1",
+       "yourtext": "Teks-é rika",
        "protectedpagewarning": "'''Pénget:  Kaca kiye wis dikunci dadi mung panganggo sing nduwé hak aksès pangurus baé sing teyeng nyunting.'''\nEntri cathetan pungkasan disadiakna nang ngisor kanggo referensi:",
        "semiprotectedpagewarning": "'''Cathetan:''' Kaca kiye lagi pinuju direksa, dadi mung panganggo kadaftar sing teyeng nyunting.\nEntri cathetan pungkasan disadiakna nang ngisor kanggo referensi:",
        "templatesused": "{{PLURAL:$1|Cithakan|Cithakan}} sing dienggo nang kaca kiye:",
        "rcshowhideliu": "$1 panganggo sing mlebu log",
        "rcshowhideanons": "$1 panganggo anonim",
        "rcshowhidepatr": "$1 suntingan sing dipatroli",
-       "rcshowhidemine": "$1 suntingane inyong",
+       "rcshowhidemine": "$1 suntingané inyong",
        "rclinks": "Tidokna $1 owahan pungkasan nang $2 dina pungkasan kiye",
        "diff": "bédane",
        "hist": "versi",
index 257ad66..8cd54d1 100644 (file)
        "showdiff": "बदल दाखवा",
        "blankarticle": "<strong>ईशारा:</strong>आपण तयार करीत असलेले पान कोरे आहे.जर आपण पुन्हा \"$1\" टिचकले तर,कोणताही आशय/मजकूर नसलेले पान तयार होईल.",
        "anoneditwarning": "<strong>इशारा:</strong> तुम्ही विकिपीडियाचे सदस्य म्हणून सनोंद-प्रवेश (लॉग-इन) केलेले नाही.आपण काही संपादन केले तर, तुमचा अंकपत्ता (आयपी) सार्वजनिक रित्या दृष्य होईल. जर आपण <strong>[$1 सनोंद प्रवेश केला]</strong> किंवा <strong>[$2 खाते उघडले]</strong>,तर आपण केलेली संपादने ही आपल्या नांवाशी संलग्न होतील, त्याशिवाय याचे इतरही फायदे आहेत.",
-       "anonpreviewwarning": "\"'''सावधान:''' à¤¤à¥\81मà¥\8dहà¥\80 à¤µà¤¿à¤\95िपà¥\80डियाà¤\9aà¥\87 à¤¸à¤¦à¤¸à¥\8dय à¤®à¥\8dहणà¥\82न à¤¸à¤¨à¥\8bà¤\82द-पà¥\8dरवà¥\87श (लà¥\89à¤\97-à¤\87न) à¤\95à¥\87लà¥\87ला à¤¨à¤¾à¤¹à¥\80. à¤¯à¤¾ à¤ªà¤¾à¤¨à¤¾à¤\9aà¥\8dया à¤¸à¤\82पादन à¤\87तिहासात à¤¤à¥\81मà¤\9aा à¤\85à¤\82à¤\95पतà¥\8dता (à¤\86य.पà¥\80. à¥²ड्रेस) नोंदला जाईल.\"",
+       "anonpreviewwarning": "\"'''सावधान:''' à¤¤à¥\81मà¥\8dहà¥\80 à¤µà¤¿à¤\95िपà¥\80डियाà¤\9aà¥\87 à¤¸à¤¦à¤¸à¥\8dय à¤®à¥\8dहणà¥\82न à¤¸à¤¨à¥\8bà¤\82द-पà¥\8dरवà¥\87श (लà¥\89à¤\97-à¤\87न) à¤\95à¥\87लà¥\87ला à¤¨à¤¾à¤¹à¥\80. à¤¯à¤¾ à¤ªà¤¾à¤¨à¤¾à¤\9aà¥\8dया à¤¸à¤\82पादन à¤\87तिहासात à¤¤à¥\81मà¤\9aा à¤\85à¤\82à¤\95पतà¥\8dता (à¤\86य.पà¥\80. à¤\85à¥\85ड्रेस) नोंदला जाईल.\"",
        "missingsummary": "'''आठवण:''' आपण संपादन सारांश पुरवलेला नाही.आपण 'जतन करा' वर पुन्हा टिचकी मारली तर, ते त्याशिवायच जतन होईल.",
        "selfredirect": "<strong>ईशारा:</strong>आपण या पानास, त्याच पानावर पुनर्निर्देशित करीता आहात.\nआपण पुनर्निर्देशनासाठी चूकिचे लक्ष्य नमूद केले आहे किंवा आपण चूकिच्या पानाचे संपादन करीत आहात.\nजर आपण पुन्हा \"$1\" टिचकले तर, कसेहीकरुन ते पुनर्निर्देशन तयार होईल.",
        "missingcommenttext": "कृपया खाली प्रतिक्रिया भरा.",
        "action-rollback": "या आधीच्या सदस्याने नुकतेच संपादन केलेले एखादे विशिष्ट पानाचे बदल लवकर पूर्वस्थितीत न्या",
        "action-import": "दुसऱ्या विकीवरुन पाने आयात करा",
        "action-importupload": "अपभारीत संचिकेतून पाने आयात करा",
-       "action-patrol": "à¤\87तराà¤\82à¤\9aà¥\80 संपादनांवर 'पहारा दिला' म्हणून खूण करा",
+       "action-patrol": "à¤\87तराà¤\82à¤\9aà¥\8dया संपादनांवर 'पहारा दिला' म्हणून खूण करा",
        "action-autopatrol": "आपल्या संपादनांवर पहारा दिल्याची खूण करा",
        "action-unwatchedpages": "पहारा न दिलेल्या पानांची यादी पहा",
        "action-mergehistory": "पानाचा इतिहास विलीन करा",
        "timezone-local": "स्थानिक",
        "duplicate-defaultsort": "'''ताकिद:''' डिफॉल्ट सॉर्ट की \"$2\" ओवर्राइड्स अर्लीयर डिफॉल्ट सॉर्ट की \"$1\".",
        "version": "आवृत्ती",
-       "version-extensions": "सà¥\8dथापित à¤µà¤¿à¤¸à¥\8dतार",
-       "version-skins": "à¤\87à¤\82सà¥\8dà¤\9fà¥\89ल à¤\95à¥\87लà¥\8dया à¤\97à¥\87लà¥\87लà¥\8dया à¤¤à¥\8dवà¤\9aा",
+       "version-extensions": "यà¥\87थà¥\87 à¤¸à¥\8dथापलà¥\87लà¥\80 à¤µà¤¿à¤¸à¥\8dतारà¤\95à¥\87",
+       "version-skins": "सà¥\8dथापित à¤¤à¥\8dवà¤\9aा (à¤\87à¤\82सà¥\8dà¤\9fà¥\89लà¥\8dड à¤¸à¥\8dà¤\95िनà¥\8dस)",
        "version-specialpages": "विशेष पाने",
-       "version-parserhooks": "पृथकक अंकुश",
+       "version-parserhooks": "पृथकक अंकुश (पार्सर हूक्स)",
        "version-variables": "चल",
        "version-antispam": "उत्पात प्रतिबंधन",
+       "version-api": "एपीआय (API)",
        "version-other": "इतर",
        "version-mediahandlers": "मिडिया हँडलर",
        "version-hooks": "अंकुश",
-       "version-parser-extensiontags": "पृथकक विस्तारीत खूणा",
-       "version-parser-function-hooks": "पृथकक कार्य अंकुश",
+       "version-parser-extensiontags": "पृथकक विस्तारीत खूणा (पार्सर एक्स्टेंशन टॅग्ज)",
+       "version-parser-function-hooks": "पृथकक कार्य अंकुश (पार्सर फंक्शन हूक्स)",
        "version-hook-name": "अंकुश नाव",
        "version-hook-subscribedby": "वर्गणीदार",
        "version-version": "($1)",
        "version-poweredby-translators": "ट्रांसलेटविकि.नेट वरील भाषांतरकार",
        "version-credits-summary": "आम्ही खालील व्यक्तींना, [[Special:Version|मिडियाविकि]]वर त्यांनी दिलेल्या योगदानामुळे, मान्यता देऊ ईच्छितो.",
        "version-license-info": "मिडियाविकि हे  मुक्त संगणक प्रणाली विकि पॅकेज आहे.Free Software Foundation प्रकाशित  GNU General Public परवान्याच्या अटीस अनुसरून तुम्ही त्यात बदल आणि/अथवा त्याचे  पुर्नवितरण  करू शकता.\n\nमिडियाविकि  संगणक प्रणाली उपयुक्त ठरेल या आशेने वितरित केली जात असली तरी;कोणत्याही वितरणास अथवा विशिष्ट उद्देशाकरिता योग्यतेची अगदी कोणतीही अप्रत्यक्ष अथवा उपलक्षित   अथवा  निहित अशा अथवा कोणत्याही प्रकारच्या केवळ  कोणत्याही प्राश्वासनाशिवायच (WITHOUT ANY WARRANTY) उपलब्ध आहे.अधिक माहिती करिता   GNU General Public License पहावे.\n\nतुम्हाला या प्रणाली सोबत [{{SERVER}}{{SCRIPTPATH}}/COPYING  GNU General Public License परवान्याची प्रत] मिळालेली असावयास हवी, तसे नसेल तर,[//www.gnu.org/licenses/old-licenses/gpl-2.0.html  येथे ऑनलाईन वाचा] किंवा the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA ला लिहा.",
-       "version-software": "स्थापित संगणक प्रणाली (Installed software)",
+       "version-software": "स्थापित संगणक प्रणाली (इंस्टॉल्ड सॉफ्टवेअर)",
        "version-software-product": "उत्पादन",
        "version-software-version": "आवृत्ती",
-       "version-entrypoints": "à¤\86त à¤¯à¥\87णारà¥\80 à¤¯à¥\82॰à¤\86र॰à¤\8fल",
+       "version-entrypoints": "पà¥\8dरवà¥\87श-बिà¤\82दà¥\82 à¤¯à¥\82.à¤\86र.à¤\8fल.",
        "version-entrypoints-header-entrypoint": "आत येण्याचा मार्ग",
        "version-entrypoints-header-url": "यू॰आर॰एल",
        "version-libraries-library": "ग्रंथालय",
index 1d155bc..ed13a7b 100644 (file)
        "cannotloginnow-title": "Niet mogelijk om aan te melden",
        "cannotloginnow-text": "Aanmelden is niet mogelijk bij het gebruik van $1.",
        "cannotcreateaccount-title": "Kan geen accounts aanmaken",
-       "cannotcreateaccount-text": "Direct aanmaken van een gebruiker is niet ingeschakeld op deze wiki.",
+       "cannotcreateaccount-text": "Direct aanmaken van een account is niet ingeschakeld op deze wiki.",
        "yourdomainname": "Uw domein:",
        "password-change-forbidden": "U kunt uw wachtwoord niet wijzigen in deze wiki.",
-       "externaldberror": "Er is een fout opgetreden bij het aanmelden bij de database of u hebt geen toestemming uw externe gebruiker bij te werken.",
+       "externaldberror": "Er is een fout opgetreden bij het aanmelden bij de database of u hebt geen toestemming om uw externe account bij te werken.",
        "login": "Aanmelden",
        "login-security": "Uw identiteit controleren",
        "nav-login-createaccount": "Aanmelden / registreren",
        "userlogin-helplink2": "Hulp bij aanmelden",
        "userlogin-loggedin": "U bent al aangemeld als {{GENDER:$1|$1}}.\nGebruik het onderstaande formulier om aan te melden als een andere gebruiker.",
        "userlogin-reauth": "U moet opnieuw inloggen om te bevestigen dat u {{GENDER:$1|$1}} bent.",
-       "userlogin-createanother": "Een andere account registreren",
+       "userlogin-createanother": "Een ander account aanmaken",
        "createacct-emailrequired": "E-mailadres",
        "createacct-emailoptional": "E-mailadres (optioneel)",
        "createacct-email-ph": "Geef uw e-mailadres op",
        "createacct-another-email-ph": "Geef een e-mailadres op",
        "createaccountmail": "Gebruik een tijdelijk willekeurig wachtwoord en stuur het naar het opgegeven e-mailadres",
-       "createaccountmail-help": "Kan worden gebruikt voor het aanmaken van een gebruiker voor een andere persoon zonder het wachtwoord te leren.",
+       "createaccountmail-help": "Kan worden gebruikt voor het aanmaken van een account voor een andere persoon zonder het wachtwoord te leren.",
        "createacct-realname": "Echte naam (optioneel)",
        "createacct-reason": "Reden",
-       "createacct-reason-ph": "Waarom u een andere account aanmaakt",
+       "createacct-reason-ph": "Waarom u een ander account aanmaakt",
        "createacct-reason-help": "Weergegeven bericht in het logbestand van aangemaakte gebruikers",
        "createacct-submit": "Account aanmaken",
        "createacct-another-submit": "Account aanmaken",
        "createacct-benefit-body2": "pagina{{PLURAL:$1||'s}}",
        "createacct-benefit-body3": "recente bijdrager{{PLURAL:$1||s}}",
        "badretype": "De ingevoerde wachtwoorden verschillen van elkaar.",
-       "usernameinprogress": "Het aanmaken van een gebruiker met die naam is al bezig.\nEven geduld alstublieft.",
+       "usernameinprogress": "Het aanmaken van een account met die naam is al bezig.\nEven geduld alstublieft.",
        "userexists": "De gekozen gebruikersnaam is al in gebruik.\nKies een andere naam.",
        "loginerror": "Aanmeldfout",
        "createacct-error": "Fout tijdens aanmaken account",
-       "createaccounterror": "Het was niet mogelijk de account aan te maken: $1",
-       "nocookiesnew": "De gebruiker is geregistreerd, maar niet aangemeld.\n{{SITENAME}} gebruikt cookies voor het aanmelden van gebruikers.\nSchakel die in en meld daarna aan met uw nieuwe gebruikersnaam en wachtwoord.",
+       "createaccounterror": "Het was niet mogelijk het account aan te maken: $1",
+       "nocookiesnew": "Het gebruikersaccount is aangemaakt, maar u bent niet aangemeld.\n{{SITENAME}} gebruikt cookies voor het aanmelden van gebruikers.\nSchakel die in en meld daarna aan met uw nieuwe gebruikersnaam en wachtwoord.",
        "nocookieslogin": "{{SITENAME}} gebruikt cookies voor het aanmelden van gebruikers.\nCookies zijn uitgeschakeld in uw browser.\nSchakel deze optie in en probeer het opnieuw.",
-       "nocookiesfornew": "De gebruiker is niet gemaakt omdat de bron niet bevestigd kon worden.\nZorg ervoor dat u cookies hebt ingeschakeld, herlaad deze pagina en probeer het opnieuw.",
-       "createacct-loginerror": "De gebruiker is succesvol aangemaakt, maar u kon niet automatisch worden aangemeld. Ga naar [[Special:UserLogin|handmatig aanmelden]].",
+       "nocookiesfornew": "Het gebruikersaccount is niet aangemaakt, omdat de bron niet bevestigd kon worden.\nZorg ervoor dat u cookies hebt ingeschakeld, herlaad deze pagina en probeer het opnieuw.",
+       "createacct-loginerror": "Het account is succesvol aangemaakt, maar u kon niet automatisch worden aangemeld. Ga naar [[Special:UserLogin|handmatig aanmelden]].",
        "noname": "U hebt geen geldige gebruikersnaam opgegeven.",
        "loginsuccesstitle": "Aangemeld",
        "loginsuccess": "<strong>U bent nu aangemeld bij {{SITENAME}} als \"$1\".</strong>",
-       "nosuchuser": "De gebruiker \"$1\" bestaat niet.\nGebruikersnamen zijn hoofdlettergevoelig.\nControleer de schrijfwijze of [[Special:CreateAccount|maak een nieuw gebruiker aan]].",
+       "nosuchuser": "De gebruiker \"$1\" bestaat niet.\nGebruikersnamen zijn hoofdlettergevoelig.\nControleer de schrijfwijze of [[Special:CreateAccount|maak een nieuw account aan]].",
        "nosuchusershort": "De gebruiker \"$1\" bestaat niet.\nControleer de schrijfwijze.",
        "nouserspecified": "Geef een gebruikersnaam op.",
        "login-userblocked": "Deze gebruiker is geblokkeerd.\nAanmelden is niet mogelijk.",
-       "wrongpassword": "Het opgegeven wachtwoord is onjuist.\nProbeer het opnieuw.",
+       "wrongpassword": "Onjuiste gebruikersnaam of wachtwoord ingevoerd.\nProbeer het opnieuw.",
        "wrongpasswordempty": "Het opgegeven wachtwoord was leeg.\nProbeer het opnieuw.",
        "passwordtooshort": "Wachtwoorden moeten uit minstens {{PLURAL:$1|$1 teken|$1 tekens}} bestaan.",
        "passwordtoolong": "Wachtwoorden kunnen niet langer zijn dan {{PLURAL:$1|één teken|$1 tekens}}.",
        "password-login-forbidden": "Het gebruik van deze gebruikersnaam met dit wachtwoord is niet toegestaan.",
        "mailmypassword": "Nieuw wachtwoord e-mailen",
        "passwordremindertitle": "Nieuw tijdelijk wachtwoord voor {{SITENAME}}",
-       "passwordremindertext": "Iemand, waarschijnlijk u, heeft vanaf IP-adres $1 een verzoek\ngedaan tot het toezenden van een nieuw wachtwoord voor {{SITENAME}}\n($4). Er is een tijdelijk wachtwoord aangemaakt voor gebruiker \"$2\":\n\"$3\". Als dat uw bedoeling was, meld u dan nu aan en kies een nieuw\nwachtwoord.\nUw tijdelijke wachtwoord vervalt over {{PLURAL:$5|$5 dag|$5 dagen}}.\n\nAls iemand anders dan u dit verzoek heeft gedaan of als u zich inmiddels het\nwachtwoord herinnert en het niet langer wilt wijzigen, negeer dit bericht\ndan en blijf uw bestaande wachtwoord gebruiken.",
+       "passwordremindertext": "Iemand, waarschijnlijk u, heeft vanaf IP-adres $1 een verzoek\ngedaan tot het toezenden van een nieuw wachtwoord voor {{SITENAME}}\n($4). Er is een tijdelijk wachtwoord aangemaakt voor gebruiker \"$2\":\n\"$3\". Als dat uw bedoeling was, meld u dan nu aan en kies een nieuw\nwachtwoord.\nUw tijdelijke wachtwoord vervalt over {{PLURAL:$5|één dag|$5 dagen}}.\n\nAls iemand anders dan u dit verzoek heeft gedaan, of als u zich het\nwachtwoord inmiddels herinnert en het niet langer wilt wijzigen, negeer\ndit bericht dan en blijf uw oude wachtwoord gebruiken.",
        "noemail": "Er is geen e-mailadres bekend voor gebruiker \"$1\".",
        "noemailcreate": "U moet een geldig e-mailadres opgeven",
        "passwordsent": "Het wachtwoord is verzonden naar het e-mailadres voor \"$1\".\nMeld u aan nadat u het hebt ontvangen.",
        "blocked-mailpassword": "Uw IP-adres is geblokkeerd voor het maken van wijzigingen. Om misbruik te voorkomen is het niet mogelijk om een nieuw wachtwoord aan te vragen.",
-       "eauthentsent": "Er is ter bevestiging een e-mail naar het opgegeven e-mailadres gezonden.\nVolg de aanwijzingen in de e-mail om aan te geven dat het uw e-mailadres is.\nTot die tijd worden er geen e-mails naar het e-mailadres gezonden.",
+       "eauthentsent": "Er is ter bevestiging een e-mail naar het opgegeven e-mailadres gezonden.\nVolg de aanwijzingen in de e-mail om te bevestigen dat het uw account is.\nTot die tijd wordt er geen andere e-mail naar het account gezonden.",
        "throttled-mailpassword": "In {{PLURAL:$1|het laatste uur|de laatste $1 uur}} is al een wachtwoordherinnering verzonden.\nOm misbruik te voorkomen wordt er slechts één wachtwoordherinnering per {{PLURAL:$1|uur|$1 uur}} verzonden.",
        "mailerror": "Fout bij het verzenden van e-mail: $1",
        "acct_creation_throttle_hit": "Bezoekers van deze wiki met hetzelfde IP-adres als u hebben de afgelopen $2 al {{PLURAL:$1|1 gebruiker|$1 gebruikers}} geregistreerd, wat het maximale toegestane aantal is voor deze periode.\nDaarom kunt u vanaf uw IP-adres op dit moment geen nieuwe gebruikers registreren.",
        "accountcreated": "Account aangemaakt",
        "accountcreatedtext": "Het gebruikersaccount [[{{ns:User}}:$1|$1]] ([[{{ns:User talk}}:$1|overleg]]) is aangemaakt.",
        "createaccount-title": "Gebruikers registreren voor {{SITENAME}}",
-       "createaccount-text": "Iemand heeft een gebruiker op {{SITENAME}} ($4) aangemaakt met de naam \"$2\" en uw e-mailadres.\nHet wachtwoord voor \"$2\" is \"$3\".\nMeld u aan en wijzig uw wachtwoord.\n\nNegeer dit bericht als deze gebruiker zonder uw medeweten is aangemaakt.",
+       "createaccount-text": "Iemand heeft een account voor uw e-mailadres op {{SITENAME}} ($4) aangemaakt genaamd \"$2\", met wachtwoord \"$3\".\nMeld u aan en wijzig uw wachtwoord.\n\nU kunt dit bericht negeren als dit account zonder uw medeweten is aangemaakt.",
        "login-throttled": "U heeft recentelijk te veel mislukte aanmeldpogingen gedaan.\nWacht alstublieft $1 voordat u het opnieuw probeert.",
        "login-abort-generic": "Uw aanmelding is mislukt - Afgebroken",
        "login-migrated-generic": "Uw gebruikersnaam is hernoemd, en uw gebruikersnaam bestaat niet langer op deze wiki.",
        "passwordreset-domain": "Domein:",
        "passwordreset-email": "E-mailadres:",
        "passwordreset-emailtitle": "Accountgegevens op {{SITENAME}}",
-       "passwordreset-emailtext-ip": "Iemand, waarschijnlijk u, heeft vanaf het IP-adres $1 een aanvraag gedaan om uw wachtwoord voor {{SITENAME}} ($4) opnieuw in te stellen. De volgende {{PLURAL:$3|gebruiker is|gebruikers zijn}} gekoppeld aan dit e-mailadres:\n\n$2\n\n{{PLURAL:$3|Dit tijdelijke wachtwoord vervalt|Deze tijdelijke wachtwoorden vervallen}} over {{PLURAL:$5|een dag|$5 dagen}}. Meld u aan en wijzig het wachtwoord nu. Als u dit verzoek niet zelf heeft gedaan, of als u het oorspronkelijke wachtwoord nog kent en het niet wilt wijzigen, negeer dit bericht dan en blijf uw oude wachtwoord gebruiken.",
-       "passwordreset-emailtext-user": "Gebruiker $1 op de site {{SITENAME}} heeft een aanvraag gedaan om uw wachtwoord voor {{SITENAME}} ($4) opnieuw in te stellen. De volgende {{PLURAL:$3|gebruiker is|gebruikers zijn}} gekoppeld aan dit e-mailadres:\n\n$2\n\n{{PLURAL:$3|Dit tijdelijke wachtwoord vervalt|Deze tijdelijke wachtwoorden vervallen}} over {{PLURAL:$5|een dag|$5 dagen}}.\nMeld u aan en wijzig het wachtwoord nu. Als u dit verzoek niet zelf heeft gedaan, of als u het oorspronkelijke wachtwoord nog kent en het niet wilt wijzigen, negeer dit bericht dan en blijf uw oude wachtwoord gebruiken.",
+       "passwordreset-emailtext-ip": "Iemand (waarschijnlijk u, vanaf IP-adres $1) heeft een aanvraag gedaan om uw wachtwoord voor {{SITENAME}} ($4) opnieuw in te stellen. {{PLURAL:$3|Het volgende gebruikersaccount is|De volgende gebruikersaccounts zijn}} gekoppeld aan dit e-mailadres:\n\n$2\n\n{{PLURAL:$3|Dit tijdelijke wachtwoord vervalt|Deze tijdelijke wachtwoorden vervallen}} over {{PLURAL:$5|een dag|$5 dagen}}. Meld u aan en wijzig het wachtwoord nu. Als u dit verzoek niet zelf heeft gedaan, of als u het oorspronkelijke wachtwoord nog kent en het niet wilt wijzigen, negeer dit bericht dan en blijf uw oude wachtwoord gebruiken.",
+       "passwordreset-emailtext-user": "Gebruiker $1 op {{SITENAME}} heeft een aanvraag gedaan om uw wachtwoord voor {{SITENAME}} ($4) opnieuw in te stellen. {{PLURAL:$3|Het volgende gebruikersaccount is|De volgende gebruikersaccounts zijn}} gekoppeld aan dit e-mailadres:\n\n$2\n\n{{PLURAL:$3|Dit tijdelijke wachtwoord vervalt|Deze tijdelijke wachtwoorden vervallen}} over {{PLURAL:$5|een dag|$5 dagen}}.\nMeld u aan en wijzig het wachtwoord nu. Als u dit verzoek niet zelf heeft gedaan, of als u het oorspronkelijke wachtwoord nog kent en het niet wilt wijzigen, negeer dit bericht dan en blijf uw oude wachtwoord gebruiken.",
        "passwordreset-emailelement": "Gebruikersnaam: \n$1\n\nTijdelijk wachtwoord: \n$2",
        "passwordreset-emailsentemail": "Als dit e-mailadres aan uw account gekoppeld is, dan wordt er een e-mail verzonden om uw wachtwoord opnieuw in te stellen.",
        "passwordreset-emailsentusername": "Als er een e-mailadres geregistreerd is voor die gebruikersnaam, dan wordt er een e-mail verzonden om uw wachtwoord opnieuw in te stellen.",
        "showpreview": "Bewerking ter controle bekijken",
        "showdiff": "Wijzigingen bekijken",
        "blankarticle": "<strong>Waarschuwing:</strong> de pagina die u wilt aanmaken is leeg.\nAls u opnieuw op \"$1\" klikt, wordt de pagina aangemaakt zonder enige inhoud.",
-       "anoneditwarning": "<strong>Waarschuwing:</strong> u bent niet aangemeld.\nUw IP-adres wordt opgeslagen als u wijzigingen op deze pagina maakt. Wanneer u <strong>[$1 aanmeldt]</strong> of <strong>[$2 een gebruiker aanmaakt]</strong> verschijnen uw bewerkingen onder uw gebruikersnaam, naast andere voordelen.",
+       "anoneditwarning": "<strong>Waarschuwing:</strong> U bent niet aangemeld.\nUw IP-adres zal voor iedereen zichtbaar zijn als u wijzigingen op deze pagina maakt. Wanneer u <strong>[$1 zich aanmeldt]</strong> of <strong>[$2 een account aanmaakt]</strong>, verschijnen uw bewerkingen onder uw gebruikersnaam, naast andere voordelen.",
        "anonpreviewwarning": "''U bent niet aangemeld.''\n''Door uw bewerking op te slaan wordt uw IP-adres opgeslagen in de paginageschiedenis.''",
        "missingsummary": "'''Let op:''' u hebt geen bewerkingssamenvatting opgegeven.\nAls u nogmaals op \"$1\" klikt wordt de bewerking zonder samenvatting opgeslagen.",
        "selfredirect": "<strong>Waarschuwing:</strong> U heeft een doorverwijzing gemaakt naar deze pagina. Mogelijk heeft u de verkeerde bestemming voor de doorverwijzing gebruikt, of bewerkt u de verkeerde pagina. Door nogmaals op \"$1\" te klikken word de doorverwijzing alsnog aangemaakt.",
        "subject-preview": "Voorvertoning van het onderwerp:",
        "previewerrortext": "Er is een fout opgetreden tijdens het weergeven van uw wijzigingen.",
        "blockedtitle": "Gebruiker is geblokkeerd",
-       "blockedtext": "'''Uw gebruiker of IP-adres is geblokkeerd.'''\n\nDe blokkade is uitgevoerd door $1.\nDe opgegeven reden is ''$2''.\n\n* Aanvang blokkade: $8\n* Einde blokkade: $6\n* Bedoeld te blokkeren: $7\n\nU kunt contact opnemen met $1 of een andere [[{{MediaWiki:Grouppage-sysop}}|beheerder]] om de blokkade te bespreken.\nU kunt geen gebruik maken van de functie \"Deze gebruiker e-mailen\", tenzij u een geldig e-mailadres hebt opgegeven in uw [[Special:Preferences|voorkeuren]] en het gebruik van deze functie niet geblokkeerd is.\nUw huidige IP-adres is $3 en het blokkadenummer is #$5.\nVermeld alle bovenstaande gegevens als u ergens op deze blokkade reageert.",
-       "autoblockedtext": "Uw IP-adres is automatisch geblokkeerd, omdat het is gebruikt door een andere gebruiker, die is geblokkeerd door $1.\nDe opgegeven reden is:\n\n:''$2''\n\n* Aanvang blokkade: $8\n* Einde blokkade: $6\n* Bedoeld te blokkeren: $7\n\nU kunt deze blokkade bespreken met $1 of een andere [[{{MediaWiki:Grouppage-sysop}}|beheerder]].\n\nU kunt geen gebruik maken van de functie \"Deze gebruiker e-mailen\", tenzij u een geldig e-mailadres hebt opgegeven in uw [[Special:Preferences|voorkeuren]] en het gebruik van deze functie niet is geblokkeerd.\n\nUw huidige IP-adres is $3 en het blokkadenummer is #$5.\nVermeld alle bovenstaande gegevens als u ergens op deze blokkade reageert.",
-       "systemblockedtext": "Uw gebruikersnaam of IP-adres is automatisch geblokkeerd door MediaWiki.\nDe opgegeven reden is:\n\n:<em>$2</em>\n\n* Aanvang blokkade: $8\n* Einde blokkade: $6\n* Bedoeld te blokkeren: $7\n\nUw huidige IP-adres is $3.\nVermeld alle bovenstaande gegevens in een query die u maakt.",
+       "blockedtext": "'''Uw gebruikersaccount of IP-adres is geblokkeerd.'''\n\nDe blokkade is uitgevoerd door $1.\nDe opgegeven reden is ''$2''.\n\n* Aanvang blokkade: $8\n* Einde blokkade: $6\n* Bedoeld te blokkeren: $7\n\nU kunt contact opnemen met $1 of een andere [[{{MediaWiki:Grouppage-sysop}}|beheerder]] om de blokkade te bespreken.\nU kunt geen gebruik maken van de functie \"Deze gebruiker e-mailen\", tenzij u een geldig e-mailadres hebt opgegeven in uw [[Special:Preferences|voorkeuren]] en het gebruik van deze functie niet geblokkeerd is.\nUw huidige IP-adres is $3 en het blokkadenummer is #$5.\nVermeld alle bovenstaande gegevens als u ergens op deze blokkade reageert.",
+       "autoblockedtext": "Uw IP-adres is automatisch geblokkeerd, omdat het gebruikt is door een andere gebruiker, die geblokkeerd is door $1.\nDe opgegeven reden is:\n\n:''$2''\n\n* Aanvang blokkade: $8\n* Einde blokkade: $6\n* Bedoeld te blokkeren: $7\n\nU kunt contact opnemen met $1 of een andere [[{{MediaWiki:Grouppage-sysop}}|beheerder]] om de blokkade te bespreken.\n\nU kunt geen gebruik maken van de functie \"Deze gebruiker e-mailen\", tenzij u een geldig e-mailadres hebt opgegeven in uw [[Special:Preferences|voorkeuren]] en het gebruik van deze functie niet geblokkeerd is.\n\nUw huidige IP-adres is $3 en het blokkadenummer is #$5.\nVermeld alle bovenstaande gegevens als u ergens op deze blokkade reageert.",
+       "systemblockedtext": "Uw gebruikersaccount of IP-adres is automatisch geblokkeerd door MediaWiki.\nDe opgegeven reden is:\n\n:<em>$2</em>\n\n* Aanvang blokkade: $8\n* Einde blokkade: $6\n* Bedoeld te blokkeren: $7\n\nUw huidige IP-adres is $3.\nVermeld alle bovenstaande gegevens als u ergens op deze blokkade reageert.",
        "blockednoreason": "geen reden opgegeven",
        "whitelistedittext": "U moet $1 om pagina's te bewerken.",
        "confirmedittext": "U moet uw e-mailadres bevestigen voor u kunt bewerken.\nVoer uw e-mailadres in en bevestig het via uw [[Special:Preferences|voorkeuren]].",
        "accmailtext": "Een willekeurig gegenereerd wachtwoord voor [[User talk:$1|$1]] is verzonden naar $2. Het kan worden gewijzigd op de pagina \"[[Special:ChangePassword|wachtwoord wijzigen]]\" na het aanmelden.",
        "newarticle": "(Nieuw)",
        "newarticletext": "Deze pagina bestaat niet.\nTyp in het onderstaande veld om de pagina aan te maken (meer informatie staat op de [$1 hulppagina]).\nGebruik de knop <strong>Terug</strong> in uw browser als u hier per ongeluk terecht bent gekomen.",
-       "anontalkpagetext": "----\n<em>Deze overlegpagina hoort bij een anonieme gebruiker die geen gebruikersnaam heeft of deze niet gebruikt.</em>\nDaarom wordt het IP-adres ter identificatie gebruikt.\nHet is mogelijk dat meerdere personen hetzelfde IP-adres gebruiken.\nMogelijk ontvangt u hier berichten die niet voor u bedoeld zijn.\nAls u dat wilt voorkomen, [[Special:CreateAccount|registreer u]] of [[Special:UserLogin|meld u aan]] om verwarring met andere anonieme gebruikers te voorkomen.",
+       "anontalkpagetext": "----\n<em>Deze overlegpagina hoort bij een anonieme gebruiker die nog geen account heeft aangemaakt, of het niet gebruikt.</em>\nDaarom wordt het IP-adres ter identificatie gebruikt.\nHet is mogelijk dat meerdere personen hetzelfde IP-adres gebruiken.\nMogelijk ontvangt u hier berichten die niet voor u bedoeld zijn.\nAls u dat wilt voorkomen, [[Special:CreateAccount|registreer u]] of [[Special:UserLogin|meld u aan]] om verwarring met andere anonieme gebruikers te voorkomen.",
        "noarticletext": "Deze pagina bevat geen tekst.\nU kunt [[Special:Search/{{PAGENAME}}|naar deze term zoeken]] in andere pagina's, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} de logboeken doorzoeken] of [{{fullurl:{{FULLPAGENAME}}|action=edit}} deze pagina aanmaken]</span>.",
        "noarticletext-nopermission": "Deze pagina bevat geen tekst.\nU kunt [[Special:Search/{{PAGENAME}}|naar deze term zoeken]] in andere pagina's of\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} de logboeken doorzoeken]</span>, maar u mag de pagina niet aanmaken.",
        "missing-revision": "De versie #$1 van de pagina \"{{FULLPAGENAME}}\" bestaat niet.\n\nDit wordt meestal veroorzaakt door het volgen van een verouderde koppeling naar een pagina die is verwijderd.\nMeer gegevens zijn mogelijk te vinden in het [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} verwijderingslogboek].",
-       "userpage-userdoesnotexist": "U bewerkt een gebruikerspagina van een gebruiker die niet bestaat (gebruiker \"$1\").\nControleer of u deze pagina wel wilt aanmaken of bewerken.",
-       "userpage-userdoesnotexist-view": "De gebruiker \"$1\" is niet geregistreerd.",
-       "blocked-notice-logextract": "Deze gebruiker is op het moment geblokkeerd.\nDe laatste regel uit het blokkeerlogboek wordt hieronder ter referentie weergegeven:",
+       "userpage-userdoesnotexist": "Gebruikersaccount \"$1\" bestaat niet.\nControleer of u deze pagina wel wilt aanmaken/bewerken.",
+       "userpage-userdoesnotexist-view": "Gebruikersaccount \"$1\" is niet geregistreerd.",
+       "blocked-notice-logextract": "Deze gebruiker is momenteel geblokkeerd.\nDe laatste regel uit het blokkeerlogboek wordt hieronder ter referentie weergegeven:",
        "clearyourcache": "<strong>Opmerking:</strong> nadat u de wijzigingen hebt opgeslagen is het wellicht nodig uw browsercache te legen.\n* <strong>Firefox / Safari:</strong> houd <em>Shift</em> ingedrukt terwijl u op <em>Vernieuwen</em> klikt of druk op <em>Ctrl-F5</em> of <em>Ctrl-R</em> (<em>⌘-Shift-R</em> op een Mac)\n* <strong>Google Chrome:</strong> druk op <em>Ctrl-Shift-R</em> (<em>⌘-Shift-R</em> op een Mac)\n* <strong>Internet Explorer:</strong> houd <em>Ctrl</em> ingedrukt terwijl u op <em>Vernieuwen</em> klikt of druk op <em>Ctrl-F5</em>\n* '''Opera:''' ga naar <em>Menu → Instellingen</em> (<em>Opera → Voorkeuren</em> op een Mac) en daarna naar <em>Privacy & beveiliging → Browsegegevens wissen... →  Tijdelijk opgeslgen afbeeldingen en bestanden</em>.",
        "usercssyoucanpreview": "'''Tip:''' gebruik de knop \"{{int:showpreview}}\" om uw nieuwe CSS te testen alvorens op te slaan.",
        "userjsyoucanpreview": "'''Tip:''' gebruik de knop \"{{int:showpreview}}\" om uw nieuwe JavaScript te testen alvorens op te slaan.",
        "yourlanguage": "Taal:",
        "yourvariant": "Taalvariant voor inhoud:",
        "prefs-help-variant": "Uw voorkeursvariant of -spelling om de inhoudspagina's van deze wiki in weer te geven.",
-       "yournick": "Tekst voor ondertekening:",
-       "prefs-help-signature": "Reacties op de overlegpagina's worden meestal ondertekend met \"<nowiki>~~~~</nowiki>\".\nDe tildes worden omgezet in uw ondertekening en een datum en tijd van de bewerking.",
+       "yournick": "Tekst voor handtekening:",
+       "prefs-help-signature": "Reacties op de overlegpagina's worden meestal ondertekend met \"<nowiki>~~~~</nowiki>\".\nDe tildes worden omgezet in uw handtekening en de datum en tijd van de bewerking.",
        "badsig": "Ongeldige ondertekening; controleer de HTML-labels.",
        "badsiglength": "Uw ondertekening is te lang.\nDeze moet minder dan $1 {{PLURAL:$1|teken|tekens}} bevatten.",
        "yourgender": "Hoe wilt u beschreven worden?",
        "email": "E-mail",
        "prefs-help-realname": "Echte naam is optioneel.\nAls u deze opgeeft, kan deze naam gebruikt worden om u erkenning te geven voor uw werk.",
        "prefs-help-email": "E-mailadres is optioneel, maar maakt het mogelijk om u uw wachtwoord te e-mailen als u het bent vergeten.",
-       "prefs-help-email-others": "U kunt ook anderen in staat stellen per e-mail contact met u op te nemen via een koppeling op uw gebruikers- en overlegpagina zonder dat u uw identiteit prijsgeeft.",
+       "prefs-help-email-others": "U kunt ook anderen in staat stellen per e-mail contact met u op te nemen via een koppeling op uw gebruikers- en overlegpagina.\nUw e-mailadres wordt niet prijsgegeven als andere gebruikers contact met u opnemen.",
        "prefs-help-email-required": "Hiervoor is een e-mailadres nodig.",
        "prefs-info": "Basisgegevens",
        "prefs-i18n": "Taalinstellingen",
-       "prefs-signature": "Ondertekening",
+       "prefs-signature": "Handtekening",
        "prefs-dateformat": "Datumopmaak",
        "prefs-timeoffset": "Tijdverschil",
        "prefs-advancedediting": "Algemene instellingen",
        "saveusergroups": "{{GENDER:$1|Gebruikersgroepen}} opslaan",
        "userrights-groupsmember": "Lid van:",
        "userrights-groupsmember-auto": "Impliciet lid van:",
-       "userrights-groups-help": "U kunt de groepen wijzigen waar deze gebruiker lid van is.\n* Een aangekruist vakje betekent dat de gebruiker lid is van de groep.\n* Een niet aangekruist vakje betekent dat de gebruiker geen lid is van de groep.\n* Een \"*\" betekent dat u een gebruiker niet uit een groep kunt verwijderen nadat u die hebt toegevoegd of vice versa.\n* Een \"#\" betekent dat u dit groepslidmaatschap alleen kunt verlengen. U kunt het niet verkorten.",
+       "userrights-groups-help": "U kunt de groepen wijzigen waar deze gebruiker lid van is:\n* Een aangekruist vakje betekent dat de gebruiker lid is van de groep.\n* Een niet aangekruist vakje betekent dat de gebruiker geen lid is van de groep.\n* Een \"*\" betekent dat u een gebruiker niet uit een groep kunt verwijderen nadat u die hebt toegevoegd of vice versa.\n* Een \"#\" betekent dat u dit groepslidmaatschap alleen kunt verlengen; u kunt het niet verkorten.",
        "userrights-reason": "Reden:",
        "userrights-no-interwiki": "U hebt geen rechten om gebruikersrechten op andere wiki's te wijzigen.",
        "userrights-nodatabase": "De database $1 bestaat niet of is geen lokale database.",
        "right-edit": "Pagina's bewerken",
        "right-createpage": "Pagina's aanmaken",
        "right-createtalk": "Overlegpagina's aanmaken",
-       "right-createaccount": "Nieuwe gebruikers aanmaken",
+       "right-createaccount": "Nieuwe gebruikersaccounts aanmaken",
        "right-autocreateaccount": "Automatisch aanmelden met een extern gebruikersaccount",
        "right-minoredit": "Bewerkingen als klein markeren",
        "right-move": "Pagina's hernoemen",
        "action-edit": "deze pagina te bewerken",
        "action-createpage": "deze pagina aan te maken",
        "action-createtalk": "deze overlegpagina aan te maken",
-       "action-createaccount": "deze gebruiker aan te maken",
-       "action-autocreateaccount": "dit externe gebruikersaccount automatisch aanmaken",
+       "action-createaccount": "dit gebruikersaccount aan te maken",
+       "action-autocreateaccount": "dit externe gebruikersaccount automatisch aan te maken",
        "action-history": "de geschiedenis van deze pagina te bekijken",
        "action-minoredit": "deze bewerking als klein te markeren",
        "action-move": "deze pagina te hernoemen",
        "rcfilters-savedqueries-apply-and-setdefault-label": "Standaard filter aanmaken",
        "rcfilters-savedqueries-cancel-label": "Annuleren",
        "rcfilters-savedqueries-add-new-title": "Huidige filter instellingen opslaan",
-       "rcfilters-savedqueries-already-saved": "Deze filters zijn al opgeslagen",
+       "rcfilters-savedqueries-already-saved": "Deze filters zijn al opgeslagen. Wijzig uw instellingen om een nieuw Filter op te slaan.",
        "rcfilters-restore-default-filters": "Standaard filters terugzetten",
        "rcfilters-clear-all-filters": "Alle filters verwijderen",
        "rcfilters-show-new-changes": "Toon nieuwste wijzigingen",
        "zip-bad": "Het bestand is een beschadigd of onleesbaar ZIP-bestand.\nDe veiligheid kan niet worden gecontroleerd.",
        "zip-unsupported": "Het bestand is een ZIP-bestand dat gebruik maakt van ZIP-mogelijkheden die MediaWiki niet ondersteunt.\nDe veiligheid kan niet worden gecontroleerd.",
        "uploadstash": "Verborgen uploads",
-       "uploadstash-summary": "Deze pagina biedt toegang tot bestanden die geüpload zijn of nog geüpload worden maar nog niet beschikbaar gemaakt zijn in de wiki. Deze bestanden zijn alleen zichtbaar voor de gebruiker die ze uploadt.",
+       "uploadstash-summary": "Deze pagina biedt toegang tot bestanden die geüpload zijn of nu geüpload worden, maar nog niet beschikbaar gemaakt zijn in de wiki. Deze bestanden zijn alleen zichtbaar voor de gebruiker die ze heeft geüpload.",
        "uploadstash-clear": "Verborgen bestanden weggooien",
        "uploadstash-nofiles": "Er zijn geen verborgen bestanden.",
        "uploadstash-badtoken": "Het uitvoeren van de handeling is mislukt, mogelijk doordat uw bewerkingsreferenties zijn verlopen. Probeer het opnieuw.",
        "log": "Logboeken",
        "logeventslist-submit": "Weergeven",
        "all-logs-page": "Alle openbare logboeken",
-       "alllogstext": "Dit is het gecombineerde logboek van {{SITENAME}}.\nU kunt ook kiezen voor specifieke logboeken en filteren op gebruiker (hoofdlettergevoelig) en paginanaam (hoofdlettergevoelig).",
+       "alllogstext": "Dit is het gecombineerde logboek van {{SITENAME}}.\nU kunt de lijst filteren door een specifiek logboektype te selecteren, of de gebruikersnaam (hoofdlettergevoelig) of paginatitel (tevens hoofdlettergevoelig) in te geven.",
        "logempty": "Er zijn geen regels in het logboek die voldoen aan deze criteria.",
        "log-title-wildcard": "Pagina's zoeken die met deze tekens beginnen",
        "showhideselectedlogentries": "Geselecteerde logboekregels weergeven of verbergen",
        "listgrouprights-namespaceprotection-namespace": "Naamruimte",
        "listgrouprights-namespaceprotection-restrictedto": "Recht(en) waardoor gebruiker kan bewerken",
        "listgrants": "Toestemmingen",
-       "listgrants-summary": "Hieronder staat een lijst met toestemmingen en de bijbehorende gebruikersrechten. Gebruikers kunnen toepassingen machtigen voor toegang tot hun gebruikers, maar met beperkte rechten gebaseerd op de toestemmingen die de gebruiker aan de toepassing heeft gegeven. Een toepassing die namens een gebruiker handelt, kan nooit rechten gebruiken die een gebruiker niet heeft.\nEr zijn mogelijk [[{{MediaWiki:Listgrouprights-helppage}}|aanvullende  gegevens]] over individuele rechten.",
+       "listgrants-summary": "Hieronder staat een lijst met toestemmingen en de bijbehorende gebruikersrechten. Gebruikers kunnen toepassingen machtigen voor toegang tot hun account, maar met beperkte rechten gebaseerd op de toestemmingen die de gebruiker aan de toepassing heeft gegeven. Een toepassing die namens een gebruiker handelt, kan nooit rechten gebruiken die een gebruiker niet heeft.\nEr zijn mogelijk [[{{MediaWiki:Listgrouprights-helppage}}|aanvullende  gegevens]] over individuele rechten.",
        "listgrants-grant": "Toestemming",
        "listgrants-rights": "Rechten",
        "trackingcategories": "Volgcategorieën",
        "emailuser-title-notarget": "Gebruiker e-mailen",
        "emailpagetext": "Via dit formulier kunt u een e-mail aan {{GENDER:$1|deze gebruiker}} verzenden.\nHet e-mailadres dat u hebt opgegeven bij [[Special:Preferences|uw voorkeuren]] wordt als afzender gebruikt.\nDe ontvanger kan dus direct naar u reageren.",
        "defemailsubject": "E-mail van {{SITENAME}}-gebruiker \"$1\"",
-       "usermaildisabled": "Gebruikerse-mail uitgeschakeld",
+       "usermaildisabled": "Gebruikers-e-mail uitgeschakeld",
        "usermaildisabledtext": "U kunt geen e-mail verzenden naar andere gebruikers op deze wiki",
        "noemailtitle": "Van deze gebruiker is geen e-mailadres bekend",
        "noemailtext": "Deze gebruiker heeft geen geldig e-mailadres opgegeven.",
        "unwatchthispage": "Niet meer volgen",
        "notanarticle": "Is geen pagina",
        "notvisiblerev": "De laatste versie van een andere gebruiker is verwijderd",
-       "watchlist-details": "Er {{PLURAL:$1|staat één pagina|staan $1 pagina's}} op uw volglijst (inclusief overlegpagina's).",
+       "watchlist-details": "Er {{PLURAL:$1|staat één pagina|staan $1 pagina's}} op uw volglijst (plus overlegpagina's).",
        "wlheader-enotif": "U wordt per e-mail gewaarschuwd.",
        "wlheader-showupdated": "Pagina's die zijn bewerkt sinds uw laatste bezoek worden '''vet''' weergegeven.",
        "wlnote": "Hieronder {{PLURAL:$1|staat de laaste wijziging|staan de laatste $1 wijzigingen}} in {{PLURAL:$2|het laatste uur|de laatste $2 uur}} per $3 om $4.",
        "sp-contributions-logs": "logboeken",
        "sp-contributions-talk": "overleg",
        "sp-contributions-userrights": "{{GENDER:$1|gebruikersrechtenbeheer}}",
-       "sp-contributions-blocked-notice": "Deze gebruiker is op het moment geblokkeerd.\nDe laatste regel uit het blokkeerlogboek wordt hieronder ter referentie weergegeven:",
+       "sp-contributions-blocked-notice": "Deze gebruiker is momenteel geblokkeerd.\nDe laatste regel uit het blokkeerlogboek wordt hieronder ter referentie weergegeven:",
        "sp-contributions-blocked-notice-anon": "Dit IP-adres is geblokkeerd.\nDe laatste regel uit het blokkeerlogboek wordt hieronder ter referentie weergegeven:",
        "sp-contributions-search": "Zoeken naar bijdragen",
        "sp-contributions-username": "IP-adres of gebruikersnaam:",
        "ipbexpiry": "Vervalt (maak een keuze):",
        "ipbreason": "Reden:",
        "ipbreason-dropdown": "*Veelvoorkomende redenen voor blokkades\n** Foutieve informatie invoeren\n** Verwijderen van informatie uit pagina's\n** Spamkoppeling naar externe websites\n** Invoegen van nonsens in pagina's\n** Intimiderend gedrag\n** Misbruik door meerdere gebruikers\n** Onaanvaardbare gebruikersnaam",
-       "ipb-hardblock": "Voorkomen dat aangemelde gebruikers vanaf dit IP-adres kunnen bewerken",
+       "ipb-hardblock": "Aangemelde gebruikers de mogelijkheid ontnemen om vanaf dit IP-adres te bewerken",
        "ipbcreateaccount": "Registreren accounts blokkeren",
-       "ipbemailban": "Gebruiker weerhouden van het sturen van e-mail",
-       "ipbenableautoblock": "Automatisch de IP-adressen van deze gebruiker blokkeren",
+       "ipbemailban": "Gebruiker de mogelijkheid ontnemen om e-mail te versturen",
+       "ipbenableautoblock": "Automatisch het laatste IP-adres van deze gebruiker blokkeren, en alle volgende IP-adressen waarvandaan degene probeert te bewerken",
        "ipbsubmit": "Deze gebruiker blokkeren",
        "ipbother": "Andere duur:",
        "ipboptions": "2 uur:2 hours,1 dag:1 day,3 dagen:3 days,1 week:1 week,2 weken:2 weeks,1 maand:1 month,3 maanden:3 months,6 maanden:6 months,1 jaar:1 year,onbepaald:infinite",
-       "ipbhidename": "Gebruiker in bewerkingen en lijsten verbergen",
-       "ipbwatchuser": "Gebruikerspagina en overlegpagina op volglijst plaatsen",
-       "ipb-disableusertalk": "Voorkomen dat deze gebruiker tijdens de blokkade de eigen overlegpagina kan bewerken",
+       "ipbhidename": "Gebruikersnaam in bewerkingen en lijsten verbergen",
+       "ipbwatchuser": "Gebruikerspagina en overlegpagina van deze gebruiker op de volglijst plaatsen",
+       "ipb-disableusertalk": "Deze gebruiker de mogelijkheid ontnemen om tijdens de blokkade de eigen overlegpagina te bewerken",
        "ipb-change-block": "De gebruiker opnieuw blokkeren met deze instellingen",
        "ipb-confirm": "Blokkade bevestigen",
        "badipaddress": "Geen geldig IP-adres",
        "blockipsuccesssub": "De blokkering is ingesteld",
        "blockipsuccesstext": "[[Special:Contributions/$1|$1]] is geblokkeerd.<br />\nZie de [[Special:BlockList|blokkadelijst]] voor recente blokkades.",
        "ipb-blockingself": "U staat op het punt uzelf te blokkeren. Weet u zeker dat u dat wilt doen?",
-       "ipb-confirmhideuser": "U staat op het punt een verborgen gebruiker te blokkeren. Hiervoor worden gebruikersnamen in alle lijsten en logboekregels verborgen. Weet u het zeker?",
+       "ipb-confirmhideuser": "U staat op het punt een verborgen gebruiker te blokkeren. Hiermee wordt de gebruikersnaam in alle lijsten en logboekregels verborgen. Weet u het zeker?",
        "ipb-confirmaction": "Weet u zeker dat u dit wilt doen? Selecteer dan het selectievakje \"{{int:ipb-confirm}}\" hieronder.",
        "ipb-edit-dropdown": "Lijst van redenen bewerken",
        "ipb-unblock-addr": "$1 deblokkeren",
        "ipb_expiry_invalid": "Ongeldige duur.",
        "ipb_expiry_old": "Vervaldatum is in het verleden.",
        "ipb_expiry_temp": "Blokkades voor verborgen gebruikers moeten permanent zijn.",
-       "ipb_hide_invalid": "Het is niet mogelijk deze gebruiker te verbergen; deze heeft meer dan {{PLURAL:$1|een bewerking|$1 bewerkingen}} uitgevoerd.",
+       "ipb_hide_invalid": "Het is niet mogelijk dit account te verbergen; het heeft meer dan {{PLURAL:$1|een bewerking|$1 bewerkingen}}.",
        "ipb_already_blocked": "\"$1\" is al geblokkeerd",
        "ipb-needreblock": "$1 is al geblokkeerd.\nWilt u de instellingen wijzigen?",
        "ipb-otherblocks-header": "Andere {{PLURAL:$1|blokkade|blokkades}}",
        "proxyblocker": "Proxyblocker",
        "proxyblockreason": "Uw IP-adres is geblokkeerd, omdat u gebruik maakt van een open proxyserver.\nNeem contact op met uw internetprovider of uw helpdesk en stel die op de hoogte van dit ernstige beveiligingsprobleem.",
        "sorbsreason": "Uw IP-adres staat bekend als open proxyserver in de DNS-blacklist die {{SITENAME}} gebruikt.",
-       "sorbs_create_account_reason": "Uw IP-adres staat bekend als open proxyserver in de DNS-blacklist die {{SITENAME}} gebruikt.\nU kunt geen gebruiker registreren.",
+       "sorbs_create_account_reason": "Uw IP-adres staat bekend als open proxyserver in de DNS-blacklist die {{SITENAME}} gebruikt.\nU kunt geen account aanmaken.",
        "softblockrangesreason": "Anonieme bijdragen zijn niet toegestaan op basis van uw IP-adres ($1). Gelieve in te loggen.",
        "xffblockreason": "Een IP-adres dat u gebruikt is geblokkeerd. Dit staat de X-Forwarded-For van de header. De oorspronkelijke blokkadereden is: $1",
        "cant-see-hidden-user": "De gebruiker die u probeert te blokken is al geblokkeerd en verborgen.\nOmdat u het recht \"hideuser\" niet hebt, kunt u de blokkade van de gebruiker niet bekijken of bewerken.",
        "confirmemail": "E-mailadres bevestigen",
        "confirmemail_noemail": "U hebt geen geldig e-mailadres opgegeven in uw [[Special:Preferences|gebruikersvoorkeuren]].",
        "confirmemail_text": "{{SITENAME}} eist bevestiging van uw e-mailadres voordat u de e-mailmogelijkheden kunt gebruiken.\nKlik op de onderstaande knop om een bevestigingsbericht te ontvangen.\nDit bericht bevat een koppeling met een code.\nOpen die koppeling om uw e-mailadres te bevestigen.",
-       "confirmemail_pending": "Er is al een bevestigingsbericht aan u verzonden.\nAls u recentelijk uw gebruiker hebt aangemaakt, wacht dan een paar minuten totdat die aankomt voordat u opnieuw een e-mail laat sturen.",
+       "confirmemail_pending": "Er is al een bevestigingsbericht aan u verzonden.\nAls u recentelijk uw account hebt aangemaakt, wacht dan een paar minuten totdat het bericht aankomt voordat u opnieuw een e-mail laat sturen.",
        "confirmemail_send": "Een bevestigingscode verzenden",
        "confirmemail_sent": "Bevestigingscode verzonden.",
        "confirmemail_oncreate": "Er is een bevestigingscode naar uw e-mailadres verzonden.\nDeze code is niet nodig om u aan te melden, maar u dient deze wel te bevestigen voordat u de e-mailmogelijkheden van deze wiki kunt gebruiken.",
        "confirmemail_success": "Uw e-mailadres is bevestigd.\nU kunt zich nu [[Special:UserLogin|aanmelden]] en de wiki gebruiken.",
        "confirmemail_loggedin": "Uw e-mailadres is nu bevestigd.",
        "confirmemail_subject": "Bevestiging e-mailadres voor {{SITENAME}}",
-       "confirmemail_body": "Iemand, waarschijnlijk u, met het IP-adres $1,\nheeft zich met dit e-mailadres geregistreerd als gebruiker \"$2\" op {{SITENAME}}.\n\nOpen de volgende koppeling in uw webbrowser om te bevestigen dat u deze gebruiker bent en om de e-mailmogelijkheden op {{SITENAME}} te activeren:\n\n$3\n\nAls u uzelf *niet* hebt aangemeld, volg dan de volgende koppeling om de bevestiging van uw e-mailadres te annuleren:\n\n$5\n\nDe bevestigingscode vervalt op $4.",
-       "confirmemail_body_changed": "Iemand, waarschijnlijk u, met het IP-adres $1,\nheeft het e-mailadres geregistreerd voor gebruiker \"$2\" op {{SITENAME}} gewijzigd naar dit e-mailadres.\n\nOpen de volgende koppeling in uw webbrowser om te bevestigen dat u deze gebruiker bent en om de e-mailmogelijkheden op {{SITENAME}} opnieuw te activeren:\n\n$3\n\nAls u uzelf *niet* hebt aangemeld, volg dan de volgende koppeling om de bevestiging van uw e-mailadres te annuleren:\n\n$5\n\nDe bevestigingscode vervalt op $4.",
-       "confirmemail_body_set": "Iemand, waarschijnlijk u, met het IP-adres $1,\nheeft het e-mailadres voor gebruiker \"$2\" op {{SITENAME}} ingesteld op dit e-mailadres.\n\nOpen de volgende koppeling in uw webbrowser om te bevestigen dat u deze gebruiker bent en om de e-mailmogelijkheden op {{SITENAME}} opnieuw te activeren:\n\n$3\n\nAls deze gebruiker *niet* aan u toebehoort, klik dan op de volgende koppeling om de bevestiging van uw e-mailadres te annuleren:\n\n$5\n\nDe bevestigingscode vervalt op $4.",
+       "confirmemail_body": "Iemand, waarschijnlijk u, vanaf IP-adres $1,\nheeft met dit e-mailadres een account \"$2\" aangemaakt op {{SITENAME}}.\n\nOpen de volgende koppeling in uw webbrowser om te bevestigen dat dit account echt aan u toebehoort en de e-mailmogelijkheden op {{SITENAME}} te activeren:\n\n$3\n\nAls u *niet* dit account hebt aangemaakt, volg dan de volgende koppeling om de bevestiging van uw e-mailadres te annuleren:\n\n$5\n\nDe bevestigingscode vervalt op $4.",
+       "confirmemail_body_changed": "Iemand, waarschijnlijk u, vanaf IP-adres $1,\nheeft het e-mailadres van het account \"$2\" op {{SITENAME}} gewijzigd naar dit e-mailadres.\n\nOpen de volgende koppeling in uw webbrowser om te bevestigen dat dit account echt aan u toebehoort en de e-mailmogelijkheden op {{SITENAME}} opnieuw te activeren:\n\n$3\n\nAls dit account *niet* aan u toebehoort, volg dan de volgende koppeling om de bevestiging van uw e-mailadres te annuleren:\n\n$5\n\nDe bevestigingscode vervalt op $4.",
+       "confirmemail_body_set": "Iemand, waarschijnlijk u, vanaf IP-adres $1,\nheeft het e-mailadres van het account \"$2\" op {{SITENAME}} ingesteld op dit e-mailadres.\n\nOpen de volgende koppeling in uw webbrowser om te bevestigen dat dit account echt aan u toebehoort en de e-mailmogelijkheden op {{SITENAME}} te activeren:\n\n$3\n\nAls dit account *niet* aan u toebehoort, volg dan de volgende koppeling om de bevestiging van uw e-mailadres te annuleren:\n\n$5\n\nDe bevestigingscode vervalt op $4.",
        "confirmemail_invalidated": "De e-mailbevestiging is geannuleerd",
        "invalidateemail": "E-mailbevestiging annuleren",
        "notificationemail_subject_changed": "{{SITENAME}} geregistreerd e-mailadres is gewijzigd",
        "notificationemail_subject_removed": "{{SITENAME}} geregistreerd e-mailadres is verwijderd",
-       "notificationemail_body_changed": "Iemand, waarschijnlijk u, met het IP-adres $1, heeft het e-mailadres van de gebruiker \"$2\" op {{SITENAME}} gewijzigd naar \"$3\". \n\nAls u dit niet was, neem dan onmiddellijk contact op met een sitebeheerder.",
-       "notificationemail_body_removed": "Iemand, waarschijnlijk u, met het IP-adres $1, heeft het e-mailadres geregistreerd voor gebruiker \"$2\" verwijderd op {{SITENAME}}. \n\nAls u dit niet was, neem dan onmiddellijk contact op met een sitebeheerder.",
+       "notificationemail_body_changed": "Iemand, waarschijnlijk u, vanaf IP-adres $1, heeft het e-mailadres van het account \"$2\" op {{SITENAME}} gewijzigd naar \"$3\". \n\nAls u dit niet was, neem dan onmiddellijk contact op met een sitebeheerder.",
+       "notificationemail_body_removed": "Iemand, waarschijnlijk u, vanaf IP-adres $1, heeft het e-mailadres van het account \"$2\" verwijderd op {{SITENAME}}. \n\nAls u dit niet was, neem dan onmiddellijk contact op met een sitebeheerder.",
        "scarytranscludedisabled": "[Interwiki-invoeging van sjablonen is uitgeschakeld]",
        "scarytranscludefailed": "[De sjabloon $1 kon niet opgehaald worden]",
        "scarytranscludefailed-httpstatus": "[De sjabloon $1 kon niet opgehaald worden: HTTP $2]",
        "logentry-patrol-patrol-auto": "$1 {{GENDER:$2|heeft}} versie $4 van pagina $3 automatisch gemarkeerd als gecontroleerd",
        "logentry-newusers-newusers": "Gebruikersaccount $1 {{GENDER:$2|is}} aangemaakt",
        "logentry-newusers-create": "Gebruikersaccount $1 {{GENDER:$2|is}} aangemaakt",
-       "logentry-newusers-create2": "Gebruiker $3 {{GENDER:$2|is}} aangemaakt door $1",
-       "logentry-newusers-byemail": "Gebruiker $3 {{GENDER:$2|is}} aangemaakt door $1 en het wachtwoord is per e-mail verzonden",
+       "logentry-newusers-create2": "Gebruikersaccount $3 is {{GENDER:$2|aangemaakt}} door $1",
+       "logentry-newusers-byemail": "Gebruikersaccount $3 is {{GENDER:$2|aangemaakt}} door $1 en het wachtwoord is per e-mail verzonden",
        "logentry-newusers-autocreate": "Gebruikersaccount $1 {{GENDER:$2|is}} automatisch aangemaakt",
        "logentry-protect-move_prot": "$1 heeft de beveiligingsinstellingen {{GENDER:$2|verplaatst}} van $4 naar $3",
        "logentry-protect-unprotect": "$1 heeft de beveiliging {{GENDER:$2|opgeheven}} van $3",
        "log-action-filter-upload-overwrite": "Herupload",
        "authmanager-authn-not-in-progress": "Verificatie is niet in uitvoering of de sessiegegevens zijn verloren gegaan. Gelieve opnieuw starten vanaf het begin.",
        "authmanager-authn-no-primary": "De meegeleverde inloggegevens kunnen niet worden geverifieerd.",
-       "authmanager-authn-no-local-user": "De ingevoerde inloggegevens zijn niet geassocieerd met een gebruiker op deze wiki.",
-       "authmanager-authn-no-local-user-link": "De meegeleverde inloggegevens zijn geldig, maar zijn niet gekoppeld aan enige gebruiker op deze wiki. Log in op een andere manier, of creëer een nieuwe gebruiker, en u heeft een optie om uw eerdere inloggegevens van te koppelen.",
+       "authmanager-authn-no-local-user": "De ingevoerde inloggegevens zijn niet gekoppeld aan een gebruiker op deze wiki.",
+       "authmanager-authn-no-local-user-link": "De ingevoerde inloggegevens zijn geldig, maar zijn niet gekoppeld aan een gebruiker op deze wiki. Meld u op een andere manier aan, of maak een nieuw account aan, en u krijgt een optie om uw eerdere inloggegevens aan dat account te koppelen.",
        "authmanager-authn-autocreate-failed": "Het automatisch aanmaken van een lokaal account is mislukt: $1",
        "authmanager-change-not-supported": "De meegeleverde inloggegevens kunnen niet worden gewijzigd, omdat niets deze zou gebruiken.",
        "authmanager-create-disabled": "Het aanmaken van accounts is uitgeschakeld.",
index 0caf39a..754e21e 100644 (file)
        "nosuchusershort": "Brak użytkownika o nazwie „$1”.\nSprawdź poprawność pisowni.",
        "nouserspecified": "Musisz podać nazwę użytkownika.",
        "login-userblocked": "Ten użytkownik jest zablokowany. Zalogowanie się jest niemożliwe.",
-       "wrongpassword": "Podane hasło jest nieprawidłowe. Spróbuj jeszcze raz.",
+       "wrongpassword": "Podany login lub hasło są nieprawidłowe. Spróbuj jeszcze raz.",
        "wrongpasswordempty": "Wprowadzone hasło jest puste. Spróbuj ponownie.",
        "passwordtooshort": "Hasło musi mieć co najmniej $1 {{PLURAL:$1|znak|znaki|znaków}}.",
        "passwordtoolong": "Hasło nie może być dłuższe niż  {{PLURAL:$1|1 znak|$1 znaków}}.",
        "delete_and_move_text": "Strona docelowa „[[:$1]]” już istnieje.\nCzy chcesz ją usunąć, by zrobić miejsce dla przenoszonej strony?",
        "delete_and_move_confirm": "Tak, usuń stronę",
        "delete_and_move_reason": "Usunięto, by zrobić miejsce dla przenoszonej strony „[[$1]]”",
-       "selfmove": "Nazwy stron źródłowej i docelowej są takie same.\nStrony nie można przenieść na nią samą.",
+       "selfmove": "Ta sama nazwa strony;\nstrony nie można przenieść na nią samą.",
        "immobile-source-namespace": "Nie można przenieść stron w przestrzeni nazw „$1”",
        "immobile-target-namespace": "Nie można przenieść stron do przestrzeni nazw „$1”",
        "immobile-target-namespace-iw": "Link interwiki jest nieprawidłowym tytułem, pod który miałaby być przeniesiona strona.",
index 2cc58b0..1313f7a 100644 (file)
        "changepassword-success": "ستاسې پټنوم بدل شو!",
        "changepassword-throttled": "تاسې څو واره هڅه کړې چې غونډال ته ورننوځۍ.\nلطفاً د بيا هڅې نه مخکې $1 شېبې تم شۍ.",
        "botpasswords": "روباټ پټنومونه",
+       "botpasswords-summary": "<em>د بوټ پټنومونه</em> د اصلي پټنوم کارولو څخه پرته د ای پی ادرس سره ګڼون ته لاسرسي نه ورکول کيږی. د کاروونکي موجوده لاسرسی ممکن محدود وي کله چې د روباټ پاسورډ داخل شي.\nکه تاسو نه پوهیږئ چې څه شی کولی شئ له دې سره وکړئ، تاسو ممکن هیڅکله هم ونه کړئ. هیڅوک باید له تاسو څخه د دې د ورکولو لپاره نه وپوښتل شي.",
+       "botpasswords-disabled": "د بوټ پټنوم ناممکنه دي.",
+       "botpasswords-no-central-id": "د بوټ د پټنوم کارولو لپاره، تاسو باید په مرکزي حساب کې ننوتلي ياست.",
        "botpasswords-existing": "د بوټ موجود پټ نومونه",
        "botpasswords-createnew": "نوی پټنوم (پاسورډ) جوړ کړي",
        "botpasswords-editexisting": "د بوټ موجود پاسورډ جوړ کړئ",
        "botpasswords-label-delete": "ړنگول",
        "botpasswords-label-resetpassword": "پټوم بدل کړي",
        "botpasswords-label-grants": "تطبیق وړ ګرانټ:",
+       "botpasswords-help-grants": "هر اجازه روبوټ ته اجازه ورکوي چې هغه واک ته لاسرسۍ ومومي کوم چې ستاسو حساب ساتي. د اجازې فعالول دلته، نوې لاسرسی نشته چې ستاسو حساب اوس مهال نلري. د نورو معلوماتو لپاره [[Special:ListGrants|table of grants]] وګورئ.",
        "botpasswords-label-grants-column": "ورکړل شو",
        "botpasswords-bad-appid": "د بوټ نوم \"$1\" وجود نلري.",
        "botpasswords-insert-failed": "د بوټ \"$1\" نوم په ورګډولو کي پاتې راغلې دا نوم د پخوا څخه ورګډ سوي وو?",
+       "botpasswords-update-failed": "د بوټ نوم په نوي کولو کې ناکام شوې \"$1\".ایا دا ړنګ شوی دی؟",
        "botpasswords-created-title": "د بوټ پټنوم جوړ شو",
        "botpasswords-created-body": "د بوټ پټنوم د بوټ \"$1\" د کارن \"$2\" لپاره جوړ شو.",
        "botpasswords-updated-title": "د بوټ پټنوم آپډيټ سو",
        "botpasswords-updated-body": "د بوټ پټنوم د بوټ \"$1\" د کارن \"$2\" لپاره آپډيټ شو.",
        "botpasswords-deleted-title": "د بوټ پټنوم ړنګ شو",
        "botpasswords-deleted-body": "د بوټ پټنوم د بوټ \"$1\" د کارن \"$2\" لپاره ړنګ شو.",
+       "botpasswords-newpassword": "<strong>$2</strong> د ګڼون سره د ننوتلو لپاره نوی پټنوم <strong>$1</strong> دی. <em>مهرباني وکړئ دا د راتلونکی لپاره وساتئ.</em> <br> (د زړو روبوټونو لپاره چې یو کارنومینر ته اړتیا لري چې خپل ګڼون سره سمون لري، تاسو کولی شئ له <strong>$3</strong> لاندې يو کارننوم او <strong>$4</strong> د پټنوم په توګه کاروئ.)",
+       "botpasswords-no-provider": "د بوټ شفر د غونډو وړاندې کول شتون نلري.",
+       "botpasswords-restriction-failed": "د بوټ پاسورډ محدودیتونه د دې ننوتنې مخه نیسي",
+       "botpasswords-invalid-name": "په کارنۍ نومول شوی کې د بوټو د پاسورډ جلا کول شامل نه دی (\"$1\").",
+       "botpasswords-not-exist": "کارن \"$1\"  يو روباټ پټنوم نه لري نوم \"$2\".",
        "resetpass_forbidden": "پټنومونه مو نه شي بدلېدلای",
+       "resetpass_forbidden-reason": "پټنومونه مو نه شي بدلېدلای: $1",
        "resetpass-no-info": "دې مخ ته د لاسرسي لپاره بايد غونډال کې ورننوځۍ.",
        "resetpass-submit-loggedin": "پټنوم بدلول",
        "resetpass-submit-cancel": "ناگارل",
-       "resetpass-wrong-oldpass": "Ù\84Ù\86Ú\89Ù\85Ù\87اÙ\84 Ø§Ù\88 Ù\8aا Ù\87Ù\85 Ø§Ù\88سÙ\86Û\8c Ù¾Ù¼Ù\86Ù\88Ù\85 Ù\85Ù\88 Ù\86اسÙ\85 Ø¯Û\8c",
+       "resetpass-wrong-oldpass": "Ù\86اسÙ\85 Ù\84Ù\86Ú\89Ù\85Ù\87اÙ\84Ù\87 Û\8cا Ø§Ù\88سÙ\86Û\8c Ù¾Ù¼Ù\86Ù\88Ù\85.\nتاسÙ\88 Ù\85Ù\85Ú©Ù\86Ù\86 Ù\85خکÛ\90 Ø®Ù¾Ù\84 Ù¾Ù¼Ù\86Ù\88Ù\85 Ø¨Ø¯Ù\84 Ú©Ú\93Û\8c Ù\88Ù\8a Û\8cا Ø¯ Ù\86Ù\88Ù\8a Ù\84Ù\86Ú\89Ù\85Ù\87اÙ\84Ù\87 Ù¾Ù¼Ù\86Ù\88Ù\85 ØºÙ\88Ú\9aتÙ\86Ù\87 Ú©Ú\93Û\90 Ù\88Û\8c.",
        "resetpass-recycled": "لطفاً پټنوم مو داسې وټاکئ چې له اوسني پټنوم سره يې توپير وي.",
        "resetpass-temp-emailed": "تاسې د يو لنډمهاله کوډ په مرسته چې دربرېښليک شوی و، ننوتلي ياست. \nد ننوتلو د بشپړولو لپاره بايد ځانته يو نوی پټنوم دلته وټاکئ:",
        "resetpass-temp-password": "لنډمهالی پټنوم:",
        "passwordreset-domain": "شپول:",
        "passwordreset-email": "برېښليک پته:",
        "passwordreset-emailtitle": "د {{SITENAME}} د گڼون څرگندنې",
+       "passwordreset-emailtext-ip": "یک نفر (شاید تاسو، دآی‌پی پتی سره$1) د خپل پټنوم بیا سمولو لپاره غوښتنه{{SITENAME}} ($4) کړی ده. {{PLURAL:$3|ګڼون|ګڼونونه}} لاندې کارن د دې برېښنالیک سره تړلی دی:\n\n$2\n\n{{PLURAL:$3|دا یو لنډمهاله پاسورډ دی|دا شفرونه لنډمهاله دي}} وروسته تر {{PLURAL:$5|یوه ورځ|$5 ورځی}} به باطل شي.\nتاسو باید اوس ننوځئ او نوي شفرونه غوره کړئ. که تاسو فکر کوئ چې بل چا دا غوښتنه کړې ده یا که تاسو خپل اصلي پټنوم په یاد ولرئ او تاسو نور یې نه غواړئ بدلون ومومئ، تاسو کولی شئ دا پیغام غفلت کړئ او خپل پخوانی پاسورډ کارولو ته دوام ورکړئ.",
+       "passwordreset-emailtext-user": "یک نفر (شاید تاسو، دآی‌پی پتی سره$1) د خپل پټنوم بیا سمولو لپاره غوښتنه{{SITENAME}} ($4) کړی ده. {{PLURAL:$3|ګڼون|ګڼونونه}} لاندې کارن د دې برېښنالیک سره تړلی دی:\n\n$2\n\n{{PLURAL:$3|دا یو لنډمهاله پاسورډ دی|دا شفرونه لنډمهاله دي}} وروسته تر {{PLURAL:$5|یوه ورځ|$5 ورځی}} به باطل شي.\nتاسو باید اوس ننوځئ او نوي شفرونه غوره کړئ. که تاسو فکر کوئ چې بل چا دا غوښتنه کړې ده یا که تاسو خپل اصلي پټنوم په یاد ولرئ او تاسو نور یې نه غواړئ بدلون ومومئ، تاسو کولی شئ دا پیغام غفلت کړئ او خپل پخوانی پاسورډ کارولو ته دوام ورکړئ.",
        "passwordreset-emailelement": "کارن-نوم: \n$1\n\nلنډمهاله پټنوم: \n$2",
        "passwordreset-emailsentemail": "د پټنوم بيا پرځای کېدنې لپاره برېښليک درولېږل شو.",
+       "passwordreset-emailsentusername": "که د دې کارن-نوم سره یو بریښنالیک پته شتون ولري نو بیا به د پاسورډ ری سیټ ای میل ته واستول شي.",
+       "passwordreset-nocaller": "يو اواز باید ورکړل شي",
+       "passwordreset-nosuchcaller": "کالر شتون نلري: $1",
+       "passwordreset-ignored": "د شفر بیاچالان سم نه و. کیدای شي کوم برابرونکي ترتیب نه وي؟",
        "passwordreset-invalidemail": "ناسمه برېښليک پته",
+       "passwordreset-nodata": "نه هم یو کاروونکي نوم او نه بریښنالیک پته ورکړل شوی",
        "changeemail": "برېښليک پته بدلول يا ليرې کول",
-       "changeemail-header": "د Ú¯Ú¼Ù\88Ù\86 Ø¨Ø±Û\90Ú\9aÙ\84Ù\8aÚ© Ù¾ØªÙ\87 Ø¨Ø¯Ù\84Ù\88Ù\84",
+       "changeemail-header": "د Ú¯Ú¼Ù\88Ù\86 Ø¯ Ø¨Ø±Û\90Ú\9aÙ\86اÙ\84Ù\8aÚ© Ø¨Ø¯Ù\84Ù\88Ù\84Ù\88 Ù\84پارÙ\87 Ø¯Ø§ Ù¾Ù\88رÙ\85 Ù¾Ù\88رÙ\87 Ú©Ú\93Ù\8a. Ú©Ù\87 ØªØ§Ø³Ù\88 ØºÙ\88اÚ\93ئ Ø¯ Ø®Ù¾Ù\84 Ù\87ر Ú\89Ù\88Ù\84 Ø­Ø³Ø§Ø¨ Ú\85Ø®Ù\87 Ø¯ Ø¨Ø±Û\8cÚ\9aÙ\86اÙ\84Ù\8aÚ© Ù¾ØªÙ\87 Ù\84رÛ\90 Ú©Ú\93Ù\8aØ\8c Ù\86Ù\88 Ø¯ Ø¨Ø±Ù\8aÚ\9aÙ\86اÙ\84Ù\8aÚ© Ú\81اÙ\8a Ù\85Ù\88 Ø¯ Ù¾Ù\88رÙ\85 Ø³Ù¾Ø§Ø±Ù\86Û\90 Ù¾Ù\87 Ù\88خت Ú©Û\90 Ø®Ø§Ù\84Ù\8a Ù¾Ø±Ù\8aÚ\96دÙ\8a.",
        "changeemail-no-info": "دې مخ ته د لاسرسي لپاره بايد غونډال کې ورننوځۍ.",
        "changeemail-oldemail": "اوسنۍ برېښليک پته:",
        "changeemail-newemail": "نوې برېښليک پته:",
+       "changeemail-newemail-help": "که تاسو غواړئ خپل بریښنالیک لرې کړئ نو دا ساحه باید پریښودل شي. تاسو به د هیر شوي شفر بیاپټولو توان نه لرئ او د دې ويکي نه بریښنالیکونه به مو ترلاسه نکړئ که چیرې مو برېښناليک لرې کړای شو.",
        "changeemail-none": "(هېڅ)",
        "changeemail-password": "ستاسې د{{SITENAME}} پټنوم:",
        "changeemail-submit": "برېښليک بدلول",
        "changeemail-throttled": "تاسې څو واره هڅه کړې چې غونډال ته ورننوځۍ.\nلطفاً د بيا هڅې نه مخکې $1 شېبې تم شۍ.",
+       "changeemail-nochange": "مهرباني وکړئ یو بل نوی برېښناليک پته ولیکئ.",
+       "resettokens": "د ټوکنونو بیاکتنه",
+       "resettokens-text": "تاسو کولی شئ  ټوکنونه بیا ځای پرځای کړئ کوم چې دلته ستاسو د حساب سره تړلی ځینې مشخصو معلوماتو ته دلا سرسۍ اجازه ورکوي.\n\nتاسو باید دا کار وکړئ که چیرې تاسو په ناڅاپي توګه له چا سره شریک کړي یا ستاسو حساب ورسره موافق وي.",
        "resettokens-tokens": "ټوکنونه:",
        "resettokens-token-label": "$1 (اوسنی ارزښت: $2)",
        "resettokens-done": "د رایو بیا راګرځول.",
        "newarticletext": "تاسې د يوې داسې تړنې څارنه کړې چې لا تر اوسه پورې نه شته.\nکه همدا مخ ليکل غواړۍ، نو په لانديني چوکاټ کې خپل متن وټاپئ (د لا نورو مالوماتو لپاره د [$1 لارښود مخ] وگورئ).\nکه چېرته تاسې دلته په تېروتنه راغلي ياست، نو يواځې د خپل د کتنمل '''مخ پر شا''' تڼۍ مو وټوکئ.",
        "anontalkpagetext": "----''دا د يوه ورکنومي کارن چې کارن-نوم نه لري او يا خپل کارن-نوم نه کاروي، د سکالو يوه پاڼه ده. نو د يوه کس د پېژندلو پخاطر موږ د هماغه کارن د انټرنېټ شمېره يا IP پته دلته ثبتوؤ. داسې يوه IP پته د ډېرو کارنانو لخوا هم کارېدلی شي. که تاسې يو ورکنومی کارن ياست او تاسې ته دا څرگندېږي چې تاسې ته نااړونده پېغامونه او تبصرې اشاره شوي، نو د نورو بې نومو کارنانو او ستاسې ترمېنځ د ټکنتوب د مخ نيونې لپاره لطفاً [[Special:CreateAccount|يو گڼون جوړ کړۍ]] او يا هم [[Special:UserLogin|غونډال ته ورننوځۍ]].''",
        "noarticletext": "دم مهال په دې مخ کې څه نشته.\nتاسې کولای شی چې په نورو مخونو کې [[Special:Search/{{PAGENAME}}|د دې مخ د سرليک پلټنه]]،\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} د اړوندو يادښتونو پلټنه] ،\nاو يا [{{fullurl:{{FULLPAGENAME}}|action=edit}} همدا مخ جوړ کړئ]</span>.",
-       "noarticletext-nopermission": "دم مهال په دې مخ کې متن نشته.\nتاسې کولای شی چې [[Special:Search/{{PAGENAME}}|همدا سرليک په نورو مخونو کې وپلټۍ]], يا هم <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} اړونده يادښتونه وپلټۍ]</span>، خو تاسې د دې مخ د جوړولو اجازه نه لرۍ.",
+       "noarticletext-nopermission": "دم مهال په دې مخ کې متن نشته.\nتاسې کولای شی چې [[Special:Search/{{PAGENAME}}|همدا سرليک په نورو مخونو کې وپلټۍ]]، يا هم <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} اړونده يادښتونه وپلټۍ]</span>، خو تاسې د دې مخ د جوړولو اجازه نه لرۍ.",
        "userpage-userdoesnotexist": "د \"<nowiki>$1</nowiki>\" گڼون نه دی ثبت شوی.\nلطفاً ځان ډاډه کړئ چې آيا تاسې په رښتيا همدا مخ جوړول/سمول غواړئ.",
        "userpage-userdoesnotexist-view": "د \"$1\" گڼون نه دی ثبت شوی.",
        "blocked-notice-logextract": "دم مهال په دې کارن بنديز لگېدلی.\nد بنديز يادښت تازه مالومات په لاندې توگه دي:",
        "longpageerror": "'''تېروتنه: کوم متن چې مو ليکلی {{PLURAL:$1|يو کيلوبايټه|$1 کيلوبايټه}} اوږد دی، چې دا پخپله د حد اکثر نه {{PLURAL:$2|يو کيلوبايټه|$2 کيلوبايټه}} اوږد دی.'''\nستاسې متن نه شي خوندي کېدلای.",
        "protectedpagewarning": "'''گواښنه: همدا مخ تړل شوی او يوازې هغه کارنان په دې مخ کې بدلونونه راوستلای شي چې د پازوالۍ د آسانتياوو نه برخمن دي.'''\nستاسې د مالوماتو لپاره د وروستني يادښت متن دلته په دې توگه راوړل شوی:",
        "semiprotectedpagewarning": "'''پاملرنه:''' دا مخ تړل شوی او يواځې ثبت شوي کارنان کولای شي چې په دې مخ کې بدلونونه راولي.\nستاسې د مالوماتو لپاره د وروستني يادښت متن دلته په دې توگه راوړل شوی:",
-       "cascadeprotectedwarning": "'''گواښنه:''' همدا مخ تړل شوی دی او يوازې هغه کارنان په دې مخ کې بدلونونه راوستلای شي چې د پازوالۍ د آسانتياوو نه برخمن دي، دا په دې خاطر چې همدا مخ د {{PLURAL:$1|لانديني مخ|لاندينيو مخونو}} په ځوړاوبيزې ژغورنې کې ورگډ دی:",
+       "cascadeprotectedwarning": "'''گواښنه:''' همدا مخ تړل شوی دی او يوازې هغه کارنان په دې مخ کې بدلونونه راوستلای شي چې د [[Special:ListGroupRights|د پازوالۍ د آسانتياوو]] نه برخمن دي، دا په دې خاطر چې همدا مخ د {{PLURAL:$1|لانديني مخ|لاندينيو مخونو}} په ځوړاوبيزې ژغورنې کې ورگډ دی:",
        "titleprotectedwarning": "'''گواښنه: همدا مخ تړل شوی دی او د دې د جوړولو لپاره تاسې ته د [[Special:ListGroupRights|ځانگړو رښتو]] د ترلاسه کولو اړتيا ده.'''\nستاسې د مالوماتو لپاره د وروستني يادښت متن دلته په دې توگه راوړل شوی:",
        "templatesused": "په دې مخ کارېدلې {{PLURAL:$1|کينډۍ|کينډۍ}}:",
        "templatesusedpreview": "يه دې مخليدنه کارېدلې {{PLURAL:$1|کينډۍ|کينډۍ}}:",
        "history-feed-description": "ددي مخ د بياکتنې تاريخ په ويکي کي",
        "history-feed-item-nocomment": "$1 په $2",
        "history-feed-empty": "ستاسې غوښتلی مخ نه شته.\nکېدای شي چې دا له ويکي نه ړنگ شوی وي، او يا هم په بل نوم بدل شوی وي.\nتاسې په دې ويکي د اړوندو نوؤ مخونو لپاره [[Special:Search|د پلټنې هڅه وکړۍ]].",
+       "history-edit-tags": "د غوره بڼې سمول نښانونه",
        "rev-deleted-comment": "(د سمون لنډيز لرې شو)",
        "rev-deleted-user": "(کارن-نوم ليري شوی)",
+       "rev-deleted-event": "(د ننوتنې مالومات ړنګ شوي)",
+       "rev-deleted-user-contribs": "[د کارن-نوم یا ای پی پته لرې کړه - له مرستو څخه پټ شوی ترمیم]",
        "rev-deleted-text-permission": "د دې مخ بڼه <strong>ړنگه شوه</strong>.\nد دې مخ اړونده تفصيل په [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} ړنگون يادښت] کې موندلی شی.",
+       "rev-suppressed-text-permission": "د دې مخ بڼه <strong>ړنگه شوه</strong>.\nد دې مخ اړونده تفصيل په [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} ړنگون يادښت] کې موندلی شی.",
+       "rev-suppressed-text-unhide": "د دې مخ بیاکتنه روانه <strong>ماتول ده</strong>.\nتفصيلات په دې ځای کې موندل کيداى شي[{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} د ماتولو يادښت].\nتاسو لا هم کولی شئ [$1 دا بیاکتنه وګورئ] که تاسو غواړئ چې پرمخ بوځي.",
+       "rev-deleted-text-view": "ددې مخ سمون ''' ړنګ شوی '''.\nتاسو کولی شئ دا وګورئ؛ دا کېدای شي اړوند معلومات [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}}ړنګ شوی] په کې شامل وي.",
+       "rev-suppressed-text-view": "د دې مخ بڼه <strong>ړنگه شوه</strong>.\nد دې مخ اړونده تفصيل په [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} ړنگون يادښت] کې موندلی شی.",
+       "rev-deleted-no-diff": "د دې مخ بڼه <strong>ړنگه شوه</strong>.\nد دې مخ اړونده تفصيل په [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} ړنگون يادښت] کې موندلی شی.",
+       "rev-suppressed-no-diff": "تاسو دا توپیر نه شو لیدلی ځکه چې د بیاکتنې څخه یو یې <strong>ړنګ شویدی</strong>.",
+       "rev-deleted-unhide-diff": "د دې مخ بیاکتنه روانه <strong>ماتول ده</strong>.\nتفصيلات په دې ځای کې موندل کيداى شي[{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} د ماتولو يادښت].\nتاسو لا هم کولی شئ [$1 دا بیاکتنه وګورئ] که تاسو غواړئ چې پرمخ بوځي.",
+       "rev-suppressed-unhide-diff": "د دې مخ بیاکتنه روانه <strong>ماتول ده</strong>.\nتفصيلات په دې ځای کې موندل کيداى شي[{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} د ماتولو يادښت].\nتاسو لا هم کولی شئ [$1 دا بیاکتنه وګورئ] که تاسو غواړئ چې پرمخ بوځي.",
+       "rev-deleted-diff-view": "ددې مخ سمون ''' ړنګ شوی '''.\nتاسو کولی شئ دا وګورئ؛ دا کېدای شي اړوند معلومات [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}}ړنګ شوی] په کې شامل وي.",
+       "rev-suppressed-diff-view": "د دې مخ بڼه <strong>ړنگه شوه</strong>.\nد دې مخ اړونده تفصيل په [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} ړنگون يادښت] کې موندلی شی.",
        "rev-delundel": "ښکارېدنه بدلول",
        "rev-showdeleted": "ښکاره کول",
        "revisiondelete": "د ړنگولو/ناړنگولو مخکتنې",
        "revdelete-nooldid-title": "ناباوره پيښنليک ته اشاره",
+       "revdelete-nooldid-text": "تاسو یا د کوم هدف بیا کتنه نده مشخص کړې چې دا فعالیت ترسره کړي، یا مشخص بیاکتنه شتون نلري، یا تاسو د اوسني بیاکتنې پټولو هڅه کوۍ.",
        "revdelete-no-file": "ځانگړې شوې دوتنه نشته.",
+       "revdelete-show-file-confirm": "ایا ته باوري یې چې دا دوتنه د خراب شوي څخه بیاکتنې ته واړوی\"<nowiki>$1</nowiki>\" له$2 په $3؟",
        "revdelete-show-file-submit": "هو",
        "revdelete-selected-text": "د [[:$2]] {{PLURAL:$1|ټاکلې بڼه|ټاکلې بڼې}}:",
        "revdelete-selected-file": "د [[:$2]] {{PLURAL:$1|ټاکلې دوتنې بڼه|ټاکلې دوتنې بڼې}}",
        "logdelete-selected": "{{PLURAL:$1|ټاکلي يادښت پېښه|ټاکلي يادښت پېښې}}:",
        "revdelete-text-text": "ړنگې شوې بڼې به لا تر اوسه پورې د مخ پېښليک کې ښکاري، خو د هغو ځينو برخو ته به عام خلک لاسرسی و نه لري.",
+       "revdelete-text-file": "ړنگې شوې بڼې به لا تر اوسه پورې د مخ پېښليک کې ښکاري، خو د هغو ځينو برخو ته به عام خلک لاسرسی و نه لري.",
+       "logdelete-text": "ړنگې شوې بڼې به لا تر اوسه پورې د مخ پېښليک کې ښکاري، خو د هغو ځينو برخو ته به عام خلک لاسرسی و نه لري.",
+       "revdelete-text-others": "نور پازوالان به لا هم د پټ راز محتوياتو ته لاسرسی ومومي او دا یې له منځه یوسي، مګر که نه بل ډول مشخص شوی.",
+       "revdelete-confirm": "لطفا دا تایید کړئ چې تاسو دا کار کول غواړئ، دا چې تاسو پایلې په پام کې لرئ او تاسو یې سره مطابقت کوئ[[{{MediaWiki:Policy-url}}|پالیسۍ]].",
        "revdelete-legend": "د ښکارېدنې محدوديتونه ټاکل",
        "revdelete-hide-text": "د مخکتنې متن",
        "revdelete-hide-image": "د دوتنې مېنځپانگه پټول",
        "userrights-user-editname": "يو کارن نوم ورکړئ:",
        "editusergroup": "{{GENDER:$1|کارن}} ډلې سمول",
        "editinguser": "د <strong>[[User:$1|$1]]</strong> {{GENDER:$1|کارن}} رښتې بدلول $2",
-       "userrights-editusergroup": "کارن ډلې سمول",
+       "userrights-editusergroup": "{{GENDER:$1|کارن}} ډلې سمول",
        "userrights-viewusergroup": "د{{GENDER:$1|کارن}} ګروپونه ښکاره کړي",
        "saveusergroups": "{{GENDER:$1|کارن}} ډلې خوندي کول",
        "userrights-groupsmember": "غړی د:",
        "action-userrights-interwiki": "په نورو ويکي گانو د کارنانو رښتې سمول",
        "action-siteadmin": "توکبنسټ کولپول يا نه کولپول",
        "action-sendemail": "برېښليکونه لېږل",
+       "action-editmyoptions": "خپل غوره توبونه سمول",
        "action-editmywatchlist": "خپل کتنلړ سمول",
        "action-viewmywatchlist": "خپل کتنلړ کتل",
        "action-viewmyprivateinfo": "خپل شخصي مالومات کتل",
        "rcfilters-savedqueries-add-new-title": "د امستنې اوسنۍ فيلټر خوندي کړي",
        "rcfilters-filterlist-title": "چاڼگران",
        "rcfilters-highlightmenu-title": "يو رنګ وټاکۍ",
+       "rcfilters-filter-user-experience-level-unregistered-label": "ناثبت",
        "rcfilters-filter-user-experience-level-newcomer-label": "نوي راغلي",
        "rcfilters-filter-user-experience-level-learner-label": "زده کوونکي",
-       "rcnotefrom": "دلته لاندې د <strong>$2</strong> څخه راپدېخوا پېښ شوي بدلونونه راغلي (تر <strong>$1</strong> پورې ښکاري).",
+       "rcfilters-filter-bots-label": "روباټ",
+       "rcfilters-filter-patrolled-label": "ګزمې شوی",
+       "rcfilters-filtergroup-changetype": "د بدلون ډول",
+       "rcnotefrom": "دلته لاندې د <strong>$3, $4</strong> (څخه <strong>$1</strong> {{PLURAL:$5|راپدېخوا پېښ شوي بدلونونه|ښکاري}}).",
        "rclistfrom": "نوي بدلونونه چې له $3، $2 څخه پيلېږي ښکاره کول",
        "rcshowhideminor": "وړې سمونې $1",
        "rcshowhideminor-show": "ښکاره کول",
        "recentchangeslinked-page": "د مخ نوم:",
        "recentchangeslinked-to": "د ورکړل شوي مخ پر ځای د اړونده تړلي مخونو بدلونونه ښکاره کول",
        "recentchanges-page-added-to-category": "[[:$1]] وېشنيزې کې ورگډ شو",
-       "recentchanges-page-added-to-category-bundled": "[[:$1]] او {{PLURAL:$2|يو مخ|$2 مخونه}} وېشنيزې کې ورگډ شول",
+       "recentchanges-page-added-to-category-bundled": "[[:$1]] له وېشنيزې وغورځول شول، [[Special:WhatLinksHere/$1|په نورو مخونو کې دا مخ موجود دی.]]",
        "recentchanges-page-removed-from-category": "[[:$1]] له وېشنيزې وغورځول شو",
-       "recentchanges-page-removed-from-category-bundled": "[[:$1]] او {{PLURAL:$2|يو مخ|$2 مخونه}} له وېشنيزې وغورځول شول",
+       "recentchanges-page-removed-from-category-bundled": "[[:$1]] له وېشنيزې وغورځول شول، [[Special:WhatLinksHere/$1|په نورو مخونو کې دا مخ موجود دی.]]",
        "autochange-username": "د مېډياويکي خپلکاره بدلون",
        "upload": "دوتنه پورته کول",
        "uploadbtn": "دوتنه پورته کول",
        "reuploaddesc": "پورته کېدنه ناگارل او بېرته د پورته کېدنې فورمې ته ورگرځېدل",
        "upload-tryagain": "د بدلون موندلې دوتنې څرگندونې سپارل",
+       "upload-tryagain-nostash": "د بیا رالېږل شوې دوتنې وړاندې کول او تعدیل شوی بیان وړاندې کړئ",
        "uploadnologin": "غونډال کې نه ياست ننوتي",
        "uploadnologintext": "د دوتنې پورته کولو لپاره بايد $1",
        "uploaderror": "د پورته کولو ستونزه",
        "upload-form-label-infoform-categories": "وېشنيزې",
        "upload-form-label-infoform-date": "نېټه",
        "backend-fail-notexists": "د $1 په نوم دوتنه نشته.",
+       "backend-fail-invalidpath": "\"$1\" د اعتبار وړ لاره نه ده.",
        "backend-fail-delete": "د \"$1\" دوتنه ړنګه نه شوه.",
+       "backend-fail-describe": "د $1 دوتنې لپاره مېټاډاټا نشو بدلولی",
        "backend-fail-alreadyexists": "د $1 دوتنه له پخوا نه شته.",
        "backend-fail-store": "په \"$2\" باندې د \"$1\" دوتنه نه زېرمل کېږي.",
        "backend-fail-copy": "د \"$1\" دوتنه و \"$2\" ته نه لمېسل کېږي.",
        "sharedupload": "دا دوتنه د $1 لخوا څخه ده او کېدای شي چې نورې پروژې به يې هم کاروي.",
        "sharedupload-desc-there": "دا دوتنه د $1 څخه ده او کېدای شي چې په نورو پروژو به هم کارېږي.\nد نورو مالوماتو لپاره لطفاً [د $2 دوتنې د څرگندونو مخ] وگورئ.",
        "sharedupload-desc-here": "دا دوتنه د $1 لخوا خپرېږې او کېدای شي چې دا په نورو پروژو هم کارېدلې وي.\nد دوتنې د کارېدنې لا نور مالومات د [$2 دوتنې د څرگندنو په مخ] کې لاندې ښودل شوی.",
+       "sharedupload-desc-edit": "دا دوتنه د $1 لخوا خپرېږې او کېدای شي چې دا په نورو پروژو هم کارېدلې وي.\nکېدای شي تاسو به غواړئ چې څرگندونې يې د دې دوتنې [د $2 دوتنې د څرگندنو په مخ] کې سمې کړئ.",
        "sharedupload-desc-create": "دا دوتنه د $1 لخوا خپرېږې او کېدای شي چې دا په نورو پروژو هم کارېدلې وي.\nکېدای شي تاسو به غواړئ چې څرگندونې يې د دې دوتنې [د $2 دوتنې د څرگندنو په مخ] کې سمې کړئ.",
        "filepage-nofile": "په دې نوم کومه دوتنه نشته.",
        "filepage-nofile-link": "په دې نوم کومه دوتنه نشته، خو تاسې يې [$1 پورته کولی شی].",
        "fewestrevisions": "لږ مخليدل شوي مخونه",
        "nbytes": "$1 {{PLURAL:$1|بايټ|بايټونه}}",
        "ncategories": "$1 {{PLURAL:$1|وېشنيزه|وېشنيزې}}",
-       "ninterwikis": "$1 {{PLURAL:$1|ويکي خپلمنځي|ويکي خپلمنځي}}",
+       "ninterwikis": "$1 {{PLURAL:$1|انټرویکی|انټرویکی}}",
        "nlinks": "$1 {{PLURAL:$1|تړنه|تړنې}}",
        "nmembers": "$1 {{PLURAL:$1|غړی|غړي}}",
        "nmemberschanged": "$1 → $2 {{PLURAL:$2|غړی|غړي}}",
        "booksources-invalid-isbn": "دا ISBN چې تاسې ورکړی سم نه ښکاري؛ د تېروتنو لپاره د لمېسلو اصلي سرچينه وگورئ.",
        "magiclink-tracking-isbn": "مخونه د اي اس بي ان جادوګر لينکونو سره",
        "specialloguserlabel": "ترسره کوونکی:",
-       "speciallogtitlelabel": "موخه (سرليک يا کارن):",
+       "speciallogtitlelabel": "موخه (سرليک يا {{ns:user}}:کارن نوم د کارن لپاره):",
        "log": "يادښتونه",
        "logeventslist-submit": "ښکاره کول",
        "all-logs-page": "ټول عام يادښتونه",
        "addedwatchtext-short": "د \"$1\" مخ ستاسې کتنلړ کې ورگډ شو.",
        "removewatch": "له کتنلړ نه غورځول",
        "removedwatchtext": "د \"[[:$1]]\" مخ [[Special:Watchlist|ستاسې کتنلړ]] نه لرې شو.",
+       "removedwatchtext-talk": "د \"[[:$1]]\" په نوم يو مخ ستاسې [[Special:Watchlist|کتنلړ]] کې ورگډ شو.\nپه راتلونکې کې چې په دغه مخ او د دې د خبرواترو مخ کې کوم بدلونونه راځي نو هغه به ستاسې کتنلړ کې ښکاري.",
        "removedwatchtext-short": "د \"$1\" مخ ستاسې له کتنلړ څخه لرې شو.",
        "watch": "کتل",
        "watchthispage": "همدا مخ کتل",
        "wlshowhideanons": "ورکنومي کارنان",
        "wlshowhidepatr": "څارل شوي سمونونه",
        "wlshowhidemine": "زما سمونونه",
+       "wlshowhidecategorization": "د مخ وېشنيزې",
        "watchlist-options": "د کتنلړ خوښنې",
        "watching": "د کتلو په حال کې...",
        "unwatching": "د نه کتلو په حال کې...",
        "version-libraries-license": "منښتليک",
        "version-libraries-description": "څرگندونه",
        "version-libraries-authors": "ليکوالان",
-       "redirect": "ورگرځېدنې د دوتنې، کارن، مخ يا بڼې پېژند له مخې",
+       "redirect": "د دوتنې ورگرځېدنې، کارن، مخ يا بڼې پېژند له مخې",
        "redirect-summary": "دا ځانګړی مخ د یوې دوتنې (د فیلمین لخوا ورکړ شوی)، یو مخ (د بیاکتنې پیژند یا د پاڼې پېژندل شوی اي ډي)، د کاروونکي پاڼه (د شمېره کاروونکي اي ډي ورکړه)، او یا د ننوتلو ننوتل (د ننوتنې اي ډي ورکړل شوی). کارول:[[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]], [[{{#Special:Redirect}}/user/101]], or [[{{#Special:Redirect}}/logid/186]].",
        "redirect-submit": "ورځه",
        "redirect-lookup": "وګوري:",
index 31a07bd..48bff79 100644 (file)
        "nosuchusershort": "Não existe um usuário com o nome \"$1\". Verifique o nome que introduziu.",
        "nouserspecified": "Você precisa especificar um nome de usuário.",
        "login-userblocked": "Este usuário está bloqueado. Entrada proibida.",
-       "wrongpassword": "A senha que introduziu é inválida. Por favor, tente novamente.",
+       "wrongpassword": "Nome de usuário ou senha incorretos inseridos.\nPor favor, tente novamente.",
        "wrongpasswordempty": "Foi fornecida uma senha em branco.\nTente novamente.",
        "passwordtooshort": "As senhas devem ter no mínimo {{PLURAL:$1|1 caractere|$1 caracteres}}.",
        "passwordtoolong": "Senhas não podem ser maiores do que {{PLURAL:$1|1 caractere|$1 caracteres}}.",
        "rcfilters-savedqueries-apply-and-setdefault-label": "Criar filtro padrão",
        "rcfilters-savedqueries-cancel-label": "Cancelar",
        "rcfilters-savedqueries-add-new-title": "Gravar configurações atuais de filtros",
+       "rcfilters-savedqueries-already-saved": "Esses filtros já foram salvos. Altere suas configurações para criar um novo Filtro Salvo.",
        "rcfilters-restore-default-filters": "Restaurar filtros padrão",
        "rcfilters-clear-all-filters": "Limpar todos os filtros",
        "rcfilters-show-new-changes": "Veja as novas mudanças",
        "rcfilters-filterlist-title": "Filtros",
        "rcfilters-filterlist-whatsthis": "Como funcionam estes?",
        "rcfilters-filterlist-feedbacklink": "Diga-nos o que você pensa sobre estas (novas) ferramentas de filtragem",
-       "rcfilters-highlightbutton-title": "Realçar os resultados",
+       "rcfilters-highlightbutton-title": "Destacar resultados",
        "rcfilters-highlightmenu-title": "Selecione uma cor",
        "rcfilters-highlightmenu-help": "Selecione uma cor para realçar esta propriedade",
        "rcfilters-filterlist-noresults": "Nenhum filtro encontrado",
        "rcfilters-filter-editsbyother-label": "Mudanças de outros",
        "rcfilters-filter-editsbyother-description": "Todas as mudanças, exceto a sua.",
        "rcfilters-filtergroup-userExpLevel": "Registro e experiência do usuário",
-       "rcfilters-filter-user-experience-level-registered-label": "Registrado",
+       "rcfilters-filter-user-experience-level-registered-label": "Registrados",
        "rcfilters-filter-user-experience-level-registered-description": "Editores registrados.",
        "rcfilters-filter-user-experience-level-unregistered-label": "Não registados",
        "rcfilters-filter-user-experience-level-unregistered-description": "Editores que não estão autenticados.",
index 862f64c..e5d4984 100644 (file)
        "diff-multi-sameuser": "This message appears in the revision history of a page when comparing two versions which aren't consecutive, and the intermediate revisions were all created by the same user as the new revision.\n\nParameters:\n* $1 - the number of revisions\n{{Related|Diff-multi}}",
        "diff-multi-otherusers": "This message appears in the revision history of a page when comparing two versions which aren't consecutive, and at least one of the intermediate revisions was created by a user other than the user who created the new revision.\n\nParameters:\n* $1 - the number of revisions\n* $2 - the number of distinct other users who made those revisions\n{{Related|Diff-multi}}",
        "diff-multi-manyusers": "This message appears in the revision history of a page when comparing two versions which aren't consecutive, and the intermediate revisions have been edited by more than 100 users.\n\nParameters:\n* $1 - the number of revisions, will always be 101 or more\n* $2 - the number of users that were found, which was limited at 100\n{{Related|Diff-multi}}",
+       "diff-paragraph-moved-tonew": "Explaining title tag for the indicating symbols when a paragraph was moved hinting to the new location in the Diff view.",
+       "diff-paragraph-moved-toold": "Explaining title tag for the indicating symbols when a paragraph was moved hinting to the old location in the Diff view.",
        "difference-missing-revision": "Text displayed when the requested revision does not exist using a diff link.\n\nExample: [{{canonicalurl:Project:News|diff=426850&oldid=99999999}} Diff with invalid revision#]\n\nParameters:\n* $1 - the list of missing revisions IDs\n* $2 - the number of items in $1 (one or two)",
        "search-summary": "{{doc-specialpagesummary|search}}",
        "searchresults": "This is the title of the page that contains the results of a search.\n\n{{Identical|Search results}}",
        "enotif_lastvisited": "Parameters:\n* $1 - a URL which points to diff\nSee also:\n* {{msg-mw|Enotif lastdiff}}",
        "enotif_lastdiff": "Email notification text to the latest page differences. Parameters:\n* $1 - a link which points to a diff, shown as a plain link\nSee also:\n* {{msg-mw|Enotif lastvisited}}",
        "enotif_anon_editor": "User name in an email notification when referring to an anonymous user. Parameters:\n* $1 - the anonymous user name (i.e. an IP address).",
-       "enotif_body": "Text of a notification email sent when a watched page has been edited or deleted.\n[[File:Screenshot_MediaWiki_e-mail_notifier.PNG|150px|right]]\n\nRefers to {{msg-mw|Helppage}}.\n\nParameters:\n*$WATCHINGUSERNAME is the username of the user receiving the notification.\n*$PAGEINTRO is the first line of the message, saying what happened. It currently can be either of:\n**{{msg-mw|Enotif body intro deleted}}\n**{{msg-mw|Enotif body intro created}}\n**{{msg-mw|Enotif body intro moved}}\n**{{msg-mw|Enotif body intro restored}}\n**{{msg-mw|Enotif body intro changed}} (for all the other cases).\n*$NEWPAGE consists of either\n**if the page is new (in older releases), {{msg-mw|Enotif newpagetext}}\n**if the page has a previous revision,\n***{{msg-mw|Enotif lastdiff}}\n***a newline\n***{{msg-mw|Enotif lastvisited}}\n*$PAGEEDITOR_EMAIL and $PAGEEDITOR_WIKI are links respectively to the email user special page and user page for the user who performed the action.\n*$PAGEEDITOR is the username of the user who performed the action.\n*$HELPPAGE is the full URL to the help page, defined by {{msg-mw|helppage}}.\n\nThe subject of the email is one of the following messages:\n*{{msg-mw|Enotif subject deleted}}\n*{{msg-mw|Enotif subject created}}\n*{{msg-mw|Enotif subject moved}}\n*{{msg-mw|Enotif subject restored}}\n*{{msg-mw|Enotif subject changed}}",
+       "enotif_body": "{{doc-important|Neither PLURAL nor GENDER are actually supported at the moment, at least until [[phab:T24769]] got resolved.}}\nText of a notification email sent when a watched page has been edited or deleted.\n[[File:Screenshot_MediaWiki_e-mail_notifier.PNG|150px|right]]\n\nRefers to {{msg-mw|Helppage}}.\n\nParameters:\n*$WATCHINGUSERNAME is the username of the user receiving the notification.\n*$PAGEINTRO is the first line of the message, saying what happened. It currently can be either of:\n**{{msg-mw|Enotif body intro deleted}}\n**{{msg-mw|Enotif body intro created}}\n**{{msg-mw|Enotif body intro moved}}\n**{{msg-mw|Enotif body intro restored}}\n**{{msg-mw|Enotif body intro changed}} (for all the other cases).\n*$NEWPAGE consists of either\n**if the page is new (in older releases), {{msg-mw|Enotif newpagetext}}\n**if the page has a previous revision,\n***{{msg-mw|Enotif lastdiff}}\n***a newline\n***{{msg-mw|Enotif lastvisited}}\n*$PAGEEDITOR_EMAIL and $PAGEEDITOR_WIKI are links respectively to the email user special page and user page for the user who performed the action.\n*$PAGEEDITOR is the username of the user who performed the action.\n*$HELPPAGE is the full URL to the help page, defined by {{msg-mw|helppage}}.\n\nThe subject of the email is one of the following messages:\n*{{msg-mw|Enotif subject deleted}}\n*{{msg-mw|Enotif subject created}}\n*{{msg-mw|Enotif subject moved}}\n*{{msg-mw|Enotif subject restored}}\n*{{msg-mw|Enotif subject changed}}",
        "enotif_minoredit": "Possible value (it's empty string for non-minor edits) in the {{msg-mw|Enotif body|notext=1}} message.  This can use magic words like <nowiki>{{GRAMMAR}}</nowiki>.  However, it will not be parsed, so you can not use wikitext (e.g. links) that generates HTML.\n{{Identical|minoredit}}",
        "created": "{{Optional}}\nPossible value for $CHANGEDORCREATED in the following messages:\n* {{msg-mw|enotif_subject}}\n* {{msg-mw|enotif_body}}\n{{Identical|Created}}",
        "changed": "{{Optional}}\nPossible value for $CHANGEDORCREATED in the following messages:\n* {{msg-mw|Enotif subject}}\n* {{msg-mw|Enotif body}}",
        "variantname-sr-ec": "{{optional}}\nVariant Option for wikis with variants conversion enabled.\n\nNote that <code>sr-ec</code> is not a conforming BCP47 language tag. Wikis should be migrated by:\n* allowing it only as a legacy alias of the preferred tag <code>sr-cyrl</code> (possibly insert a tracking category in templates as long as they must support the legacy tag),\n* making the new tag the default to look first, before looking for the old tag,\n* moving the translations to the new code by renaming them,\n* checking links in source pages still using the legacy tag to change it to the new tag,\n* possibly cleanup the redirect pages.",
        "variantname-sr-el": "{{optional}}\nVariant Option for wikis with variants conversion enabled.\n\nNote that <code>sr-el</code> is not a conforming BCP47 language tag. Wikis should be migrated by:\n* allowing it only as a legacy alias of the preferred tag <code>sr-latn</code> (possibly insert a tracking category in templates as long as they must support the legacy tag),\n* making the new tag the default to look first, before looking for the old tag,\n* moving the translations to the new code by renaming them,\n* checking links in source pages still using the legacy tag to change it to the new tag,\n* possibly cleanup the redirect pages.",
        "variantname-sr": "{{optional}}\nVariant Option for wikis with variants conversion enabled.",
-       "variantname-kk-kz": "{{optional}}\nVarient Option for wikis with variants conversion enabled.",
-       "variantname-kk-tr": "{{optional}}\nVarient Option for wikis with variants conversion enabled.",
-       "variantname-kk-cn": "{{optional}}\nVarient Option for wikis with variants conversion enabled.",
-       "variantname-kk-cyrl": "{{optional}}\nVarient Option for wikis with variants conversion enabled.",
-       "variantname-kk-latn": "{{optional}}\nVarient Option for wikis with variants conversion enabled.",
-       "variantname-kk-arab": "{{optional}}\nVarient Option for wikis with variants conversion enabled.",
-       "variantname-kk": "{{optional}}\nVarient Option for wikis with variants conversion enabled.",
-       "variantname-ku-arab": "{{optional}}\nVarient Option for wikis with variants conversion enabled.",
-       "variantname-ku-latn": "{{optional}}\nVarient Option for wikis with variants conversion enabled.",
-       "variantname-ku": "{{optional}}\nVarient Option for wikis with variants conversion enabled.",
+       "variantname-kk-kz": "{{optional}}\nVariant Option for wikis with variants conversion enabled.",
+       "variantname-kk-tr": "{{optional}}\nVariant Option for wikis with variants conversion enabled.",
+       "variantname-kk-cn": "{{optional}}\nVariant Option for wikis with variants conversion enabled.",
+       "variantname-kk-cyrl": "{{optional}}\nVariant Option for wikis with variants conversion enabled.",
+       "variantname-kk-latn": "{{optional}}\nVariant Option for wikis with variants conversion enabled.",
+       "variantname-kk-arab": "{{optional}}\nVariant Option for wikis with variants conversion enabled.",
+       "variantname-kk": "{{optional}}\nVariant Option for wikis with variants conversion enabled.",
+       "variantname-crh-cyrl": "{{optional}}\nVariant Option for wikis with variants conversion enabled.",
+       "variantname-crh-latn": "{{optional}}\nVariant Option for wikis with variants conversion enabled.",
+       "variantname-crh": "{{optional}}\nVariant Option for wikis with variants conversion enabled.",
+       "variantname-ku-arab": "{{optional}}\nVariant Option for wikis with variants conversion enabled.",
+       "variantname-ku-latn": "{{optional}}\nVariant Option for wikis with variants conversion enabled.",
+       "variantname-ku": "{{optional}}\nVariant Option for wikis with variants conversion enabled.",
        "variantname-tg-cyrl": "{{optional}}",
        "variantname-tg-latn": "{{optional}}",
        "variantname-tg": "{{optional}}",
index 81cc44d..8212a88 100644 (file)
        "nosuchusershort": "Non ge ste nisciune utende cu 'u nome \"$1\".\nCondrolle accume l'è scritte.",
        "nouserspecified": "A scrivere pe forze 'u nome de l'utende.",
        "login-userblocked": "Stu utende jè bloccate. Non ge puè trasè.",
-       "wrongpassword": "E mise 'na passuor sbagliete.\nPrueve n'otra vote.",
+       "wrongpassword": "Nome utende o passuord sbagliate.\nPrueve n'otra vote.",
        "wrongpasswordempty": "'A passuord 'nzerite jè vianghe.\nMitta n'otra vota.",
        "passwordtooshort": "Le password onne a essere almene {{PLURAL:$1|1 carattere|$1 carattere}}.",
        "passwordtoolong": "Le password non ge ponne essere cchiù longhe de {{PLURAL:$1|1 carattere}}.",
        "mergehistory-fail-bad-timestamp": "L'orarie de stambe non g'è valide.",
        "mergehistory-fail-invalid-source": "'A pàgene de origgene non g'è valide.",
        "mergehistory-fail-invalid-dest": "'A pàgene de destinazione non g'è valide.",
+       "mergehistory-fail-permission": "Permesse insufficiende pe aunì le cunde.",
+       "mergehistory-fail-self-merge": "Le pàggene de origgene e destinazione sò le stesse.",
        "mergehistory-fail-toobig": "Non ge pozze combletà 'u scuagghiamende s'u cunde purcé supranesce 'u limite de $1 {{PLURAL:$1|revisione|revisiune}} ca onna essere spustate.",
        "mergehistory-no-source": "'A pàgena sorgende $1 non g'esiste.",
        "mergehistory-no-destination": "'A pàgene de destinazione $1 non g'esiste.",
        "rcfilters-filter-minor-label": "Cangiaminde stuèdeche",
        "rcfilters-filter-watchlist-watched-label": "Jndr'à le pàggene condrollate",
        "rcfilters-filter-watchlist-notwatched-label": "Fore da le pàggene condrollate",
+       "rcfilters-filter-watchlistactivity-unseen-label": "Cangiaminde ca non g'è 'ndrucate",
+       "rcfilters-filter-watchlistactivity-seen-label": "Cangiaminde 'ndrucate",
        "rcfilters-filtergroup-changetype": "Tipe de cangiamende",
        "rcfilters-filter-pageedits-label": "Cangiaminde d'a pàgene",
        "rcfilters-filtergroup-lastRevision": "Urteme revisiune",
index 3293e4e..b8c6fda 100644 (file)
        "nosuchusershort": "Не существует участника с именем «$1». Проверьте написание имени.",
        "nouserspecified": "Вы должны указать имя участника.",
        "login-userblocked": "Участник заблокирован. Вход в систему запрещен.",
-       "wrongpassword": "Введённый вами пароль неверен. Попробуйте ещё раз.",
+       "wrongpassword": "Введены неверные имя участника или пароль.\nПопробуйте ещё раз.",
        "wrongpasswordempty": "Пожалуйста, введите непустой пароль.",
        "passwordtooshort": "Пароль должен состоять не менее, чем из $1 {{PLURAL:$1|1=символа|символов}}.",
        "passwordtoolong": "Пароль не может содержать более {{PLURAL:$1|1=$1 символа|$1 символов}}.",
        "uploadstash-refresh": "Обновить список файлов",
        "uploadstash-thumbnail": "показать миниатюру",
        "uploadstash-exception": "Не удалось сохранить загрузку во временное хранилище ($1): «$2».",
+       "uploadstash-bad-path": "Путь не существует.",
+       "uploadstash-bad-path-invalid": "Путь некорректен.",
+       "uploadstash-bad-path-unknown-type": "Неизвестный тип «$1».",
+       "uploadstash-file-not-found-no-thumb": "Не удалось получить миниатюру.",
+       "uploadstash-file-not-found-missing-content-type": "Отсутствует заголовок content-type.",
+       "uploadstash-wrong-owner": "Этот файл ($1) не принадлежит текущему участнику.",
+       "uploadstash-zero-length": "Файл нулевой длины.",
        "invalid-chunk-offset": "Недопустимое смещение фрагмента",
        "img-auth-accessdenied": "Доступ запрещён",
        "img-auth-nopathinfo": "Отсутствует <code>PATH_INFO</code>.\nВаш сервер не настроен для передачи этих сведений.\nВозможно, он работает на основе CGI и не поддерживает <code>img_auth</code>.\nСм. https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
index 41fe87e..779b1d2 100644 (file)
        "category-file-count-limited": "Latar reaḱ {{PLURAL:$1 rẽt rẽtko}} noa rokom sokomre menaḱa.",
        "listingcontinuesabbrev": "Calaḱa",
        "index-category": "Unuduḱ sakam ko do bạnuḱa",
-       "noindex-category": "Unuduḱ sakamkodo bạnuḱa",
+       "noindex-category": "ᱩᱱᱩᱫᱩᱜ ᱵᱟᱹᱱᱩᱜ-ᱟᱱ ᱥᱟᱦᱴᱟᱠᱚ",
        "broken-file-category": "Baṅ kạmi daṛeaḱ chubi joṛao soho sakamko",
        "about": "Lạgitte, Lạgti",
        "article": "Menaḱakat́ sakam",
        "userpage-userdoesnotexist": "\"<nowiki>$1</nowiki>\" ńutuman jahãe beoharićaḱ ekaunṭ do baṅ resṭri hoeakana. Daya kate biḍạo katet́ ńelmẽ noa sakam do benoa/sompadonem menet́ kana se baṅ.",
        "userpage-userdoesnotexist-view": "Beoharićaḱ \"$1\" ekaunṭ do baṅ resṭire akana.",
        "blocked-notice-logextract": "Nui beoharić do nitoḱe esetgea.\nRefarens lạgit́te nahaḱ boloḱ do latare em hoena:",
+       "clearyourcache": "<strong>Note:</strong> After saving, you may have to bypass your browser's cache to see the changes.\n* <strong>Firefox / Safari:</strong> Hold <em>Shift</em> while clicking <em>Reload</em>, or press either <em>Ctrl-F5</em> or <em>Ctrl-R</em> (<em>⌘-R</em> on a Mac)\n* <strong>Google Chrome:</strong> Press <em>Ctrl-Shift-R</em> (<em>⌘-Shift-R</em> on a Mac)\n* <strong>Internet Explorer:</strong> Hold <em>Ctrl</em> while clicking <em>Refresh</em>, or press <em>Ctrl-F5</em>\n* <strong>Opera:</strong> Go to <em>Menu → Settings</em> (<em>Opera → Preferences</em> on a Mac) and then to <em>Privacy & security → Clear browsing data → Cached images and files</em>.",
        "updated": "(Halot ruaṛ)",
        "note": "'''Noṭ:'''",
        "previewnote": "'''kheyalmẽ, noa do eken ńeloḱ lạgit.'''\nAmaḱ bodolaḱ kodo nit habićte bań rukhíạakana!",
        "nocreate-loggedin": "Nãwã sakam tear lạgit́te am do ạidạri em baṅ hoeakana.",
        "sectioneditnotsupported-title": "Pahaṭa sompadona do bae hataoeda",
        "sectioneditnotsupported-text": "Noa sompadona sakamre pahaṭa sompadona do bae hataoeda",
-       "permissionserrors": "Ạidạri vulko",
+       "permissionserrors": "á±\9fᱹᱭᱫá±\9fᱹᱨᱤ á±¦á±©á±²á±\9fá±¹á±\9c",
        "permissionserrorstext": "Noa kạmi amaḱ ạidạri do banuḱa, {{PLURAL:$1 gan karon reaḱ gan karon reaḱ}} lạgit:",
        "permissionserrorstext-withaction": "Amaḱ $2 kạmire ạydạri do bạnuḱa, Ona reaḱ {{PLURAL:$1 Karon/ Karonko}}:",
        "recreate-moveddeleted-warn": "'''Sontorokme: am do arhõ doṛhate sakamem teyareda oka do sedayre get giḍiyen.\nAm do gunạnme cet́ noa joṛao kạmi am lạgit́te ganoḱ ase bań.\nNoa get ar tala ocok sakam nonḍe em hoyena dhok lagit́te.",
        "right-move-subpages": "Sakam saõte kạtic sakamko ocogmẽ",
        "right-movefile": "Rẽtko ocogmẽ",
        "right-upload": "Rẽtko rakabmẽ",
-       "right-writeapi": "write API ᱵᱮᱵᱦᱟᱨ",
+       "right-writeapi": "ᱚᱞ API ᱨᱮᱱᱟᱜ ᱵᱮᱵᱷᱟᱨ",
        "right-delete": "Sakamko get giḍiymẽ",
        "newuserlogpage": "Laṛcaṛićaḱ tear cạbi",
        "rightslog": "ᱵᱮᱵᱷᱟᱨᱤᱭᱟᱹ ᱟᱹᱭᱫᱟᱹᱨ ᱞᱚᱜᱽ",
        "emailmessage": "Mesag",
        "emailsend": "Kulmẽ",
        "usermessage-editor": "ᱥᱤᱥᱴᱚᱢ ᱨᱟᱭᱵᱟᱨ",
-       "watchlist": "Inak' n'el ko",
+       "watchlist": "ᱧᱮᱞᱟᱜ ᱞᱤᱥᱴᱤ",
        "mywatchlist": "Ńeloḱgoḱ tạlika",
        "watchlistfor2": "$1 ($2) lạ̣gitte",
        "watch": "Ńelme",
        "simpleantispam-label": "Enṭi espam ńel\nDo <strong>not</strong> noa purạome!",
        "pageinfo-title": "\"$1\" ᱞᱟᱹᱜᱤᱫ ᱥᱩᱪᱱᱟ",
        "pageinfo-header-basic": "ᱢᱩᱬ ᱥᱩᱪᱱᱟ",
-       "pageinfo-header-edits": "Toṅgeko",
+       "pageinfo-header-edits": "ᱥᱟᱯᱲᱟᱣ ᱱᱟᱜᱟᱢ",
        "pageinfo-header-restrictions": "ᱥᱟᱦᱴᱟ ᱵᱟᱧᱪᱟᱣ",
        "pageinfo-header-properties": "ᱥᱟᱦᱴᱟ ᱜᱩᱱᱠᱚ",
        "pageinfo-display-title": "ᱩᱫᱩᱜ ᱧᱩᱛᱩᱢ",
        "pageinfo-robot-policy": "ᱨᱚᱵᱚᱴ ᱫᱟᱨᱟᱭᱛᱮ ᱩᱱᱩᱫᱩᱜ",
        "pageinfo-robot-index": "ᱚᱪᱚᱣᱟᱜ",
        "pageinfo-robot-noindex": "ᱵᱟᱝᱚᱪᱚ",
-       "pageinfo-watchers": "Ńeńelkoaḱ nombor",
+       "pageinfo-watchers": "ᱥᱟᱦᱴᱟ ᱧᱮᱧᱮᱞᱤᱭᱟᱹ ᱠᱚᱣᱟᱜ ᱮᱞ",
        "pageinfo-few-watchers": "$1 ᱠᱷᱚᱱ ᱠᱚᱢ {{PLURAL:$1|ᱧᱮᱧᱮᱞᱤᱭᱟᱹ|ᱧᱮᱧᱮᱞᱤᱭᱟᱹᱠᱚ}}",
        "pageinfo-redirects-name": "ᱱᱚᱶᱟ ᱥᱟᱦᱴᱟᱛᱮ ᱢᱚᱸᱦᱰᱟᱜᱠᱟᱱ ᱮᱞ",
        "pageinfo-subpages-name": "ᱱᱚᱶᱟ ᱥᱟᱦᱴᱟ ᱨᱮᱱᱟᱜ ᱪᱟᱸᱜᱟ ᱥᱟᱦᱴᱟ ᱠᱚᱣᱟᱜ ᱮᱞ",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|galmarao]])",
        "duplicate-defaultsort": "'''Sontoroḱmẽ:''' ḍifolṭ sajao reaḱ cạbi: $2 lahare ḍifolṭ sajao reaḱ sakam: ''$1'' e bae luturaḱ kana.",
        "redirect": "ᱨᱮᱫ, ᱵᱮᱵᱷᱟᱨᱩᱭᱟᱹ, ᱥᱟᱦᱴᱟ, ᱧᱮᱞ-ᱟᱹᱨᱩ, ᱵᱟᱝᱠᱷᱟᱱ ᱞᱚᱜᱽ ID ᱫᱟᱨᱟᱭᱛᱮ ᱢᱚᱦᱰᱟ",
+       "redirect-summary": "ᱱᱚᱶᱟ ᱥᱟᱦᱴᱟ ᱫᱚ ᱢᱚᱦᱰᱟ ᱟ ᱢᱤᱫ ᱨᱮᱫ (ᱮᱢᱟᱠᱟᱱ ᱨᱮᱫᱧᱩᱛᱩᱢ) ᱴᱷᱮᱱ, ᱢᱤᱫ ᱥᱟᱦᱴᱟ (ᱮᱢᱮᱱ ᱟᱹᱨᱩᱣᱟᱜ ID ᱟᱨᱵᱟᱝ ᱥᱟᱦᱴᱟ ID),  ᱢᱤᱫ ᱵᱮᱵᱷᱟᱨᱩᱭᱟᱹ ᱥᱟᱦᱴᱟ (ᱮᱢᱮᱱ ᱮᱞᱩᱠ ᱵᱮᱵᱷᱟᱨᱤᱭᱟᱹ ID ), ᱟᱨᱵᱟᱝ ᱢᱤᱫ ᱞᱚᱜᱽ ᱵᱚᱞᱚ (ᱮᱢᱮᱱ ᱞᱚᱜᱽ ID) ᱾ ᱵᱮᱵᱷᱟᱨᱟᱠᱟᱱ: [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]], [[{{#Special:Redirect}}/user/101]], ᱟᱨᱵᱟᱝ [[{{#Special:Redirect}}/logid/186]]",
        "redirect-submit": "ᱥᱮᱱᱚᱜ",
        "redirect-lookup": "ᱧᱮᱞᱢᱮ",
        "redirect-value": "ᱫᱟᱢ:",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|ᱥᱟᱛᱚᱢ|ᱥᱟᱛᱚᱢᱠᱩ}}]]: $2)",
        "tags-active-yes": "ᱦᱮᱸ",
        "tags-active-no": "ᱵᱟᱝ",
+       "tags-hitcount": "$1 {{PLURAL:$1|ᱟᱹᱨᱩ|ᱟᱹᱨᱩᱠᱚ}}",
        "logentry-delete-delete": "$3 ᱥᱟᱦᱴᱟ $1 {{GENDER:$2|ᱜᱮᱫ ᱠᱮᱜ-ᱟᱭ}}",
        "logentry-delete-restore": "$1 {{GENDER:$2|ᱨᱟᱠᱷᱟ ᱫᱚᱲᱦᱟ}} ᱠᱮᱜ-ᱟ ᱥᱟᱦᱴᱟ $3 ($4)",
        "logentry-delete-revision": "$1 {{GENDER:$2|ᱵᱚᱫᱚᱞᱠᱮᱜ-ᱟᱭ}} ᱧᱮᱞᱚᱜᱟᱜ {{PLURAL:$5|ᱫᱚᱦᱲᱟᱭᱮᱱᱟᱜ|$5 ᱫᱚᱦᱲᱟᱭᱮᱱᱟᱜ ᱠᱚ}} $3: $4 ᱥᱟᱦᱴᱟ ᱪᱮᱛᱟᱱᱨᱮ",
        "logentry-move-move": "$1 beoharić $3 sakam do $4 ńutumre {{GENDER:$2|ạcạr}} akada",
        "logentry-move-move-noredirect": "$1 {{GENDER:$2|ᱩᱪᱟᱹᱲᱠᱮᱜ-ᱟᱭ}} ᱥᱟᱦᱴᱟ $3 to $4 ᱢᱚᱦᱰᱟ ᱵᱤᱱ ᱵᱟᱹᱜᱤ ᱠᱟᱛᱮ",
        "logentry-move-move_redir": "$1 {{GENDER:$2|ᱩᱪᱟᱹᱲᱮᱱᱟ}} ᱥᱟᱦᱴᱟ $3 ᱠᱷᱚᱱ $4 ᱪᱮᱛᱟᱱ ᱢᱚᱸᱦᱰᱟ ᱦᱟᱠᱟᱱᱟ",
+       "logentry-patrol-patrol-auto": "$1 ᱟᱡᱛᱮᱜᱮ {{GENDER:$2|ᱪᱤᱱᱦᱟᱹᱭᱮᱱᱟ}} $4 ᱧᱮᱞᱟᱹᱨᱩ $3 ᱥᱟᱦᱴᱟ ᱨᱮᱱᱟᱜ ᱾",
        "logentry-newusers-create": "Beoharićaḱ hisạb khata $1 do jhićena",
        "logentry-newusers-autocreate": "ᱵᱮᱵᱷᱟᱨᱤᱭᱟᱹ ᱠᱷᱟᱛᱟ $1 ᱫᱚ {{GENDER:$2|ᱛᱮᱭᱟᱨᱮᱱᱟ}} ᱟᱡᱛᱮᱜᱮ",
        "logentry-upload-upload": "$1 {{GENDER:$2|rakaṕ akadae}} $3",
index 2583c81..2d18707 100644 (file)
        "rcfilters-filter-newpages-label": "လွင်ႈၵေႃႇသၢင်ႈ ၼႃႈလိၵ်ႈ",
        "rcfilters-filter-newpages-description": "မႄးထတ်း ဢၼ်ႁဵတ်းပဵၼ် ၼႃႈလိၵ်ႈဢၼ်မႂ်ႇ",
        "rcfilters-filter-categorization-label": "လႅၵ်ႈလၢႆႈ တွၼ်ႈၵၼ်",
+       "rcfilters-filtergroup-lastRevision": "ၵၢၼ်ၶူၼ်ႉၶႆႈ ၵမ်းလိုၼ်းသုတ်း",
+       "rcfilters-filter-lastrevision-label": "ၵၢၼ်ၶူၼ်ႉၶႆႈ ၵမ်းလိုၼ်းသုတ်း",
+       "rcfilters-filter-lastrevision-description": "ၸိူဝ်းဢၼ်မီးလွင်ႈလႅၵ်ႈလၢႆႈၸူး ၼႃႈလိၵ်ႈ ဢၼ်ပႆႇႁိုင်ၼၼ်ႉၵူၺ်း",
+       "rcfilters-filter-previousrevision-label": "ဢမ်ႇၸႂ်ႈၵၢၼ်ၶူၼ်ႉၶႆႈ ၵမ်းလိုၼ်းသုတ်း",
+       "rcfilters-filter-previousrevision-description": "ၸိူဝ်းလႅၵ်ႈလၢႆႈတင်းသဵင်ႈ ဢၼ်ဢမ်ႇၸႂ်ႈ \"လွင်ႈၶူၼ်ႉၶႆႈၵမ်းလိုၼ်းသုတ်း\"။",
        "rcnotefrom": "ၽၢႆႇတႂ်ႈ {{PLURAL:$5|ၼႆႉ ပဵၼ်လွင်ႈလႅၵ်ႈလၢႆႈ|ၸိူဝ်းၼႆႉ ပဵၼ်လွင်ႈလႅၵ်ႈလၢႆႈ}} ဝႆႉ ၸဵမ်မိူဝ်ႈ <strong>$3, $4</strong> (တေႃႇထိုင် <strong>$1</strong> ဢၼ်ၼႄဝႆႉ).",
        "rclistfrom": "ၼႄ လွင်ႈ​လႅၵ်ႈလၢႆႈဢၼ်မႂ်ႇ တႄႇတီႈ $2, $3",
        "rcshowhideminor": "$1 လွင်ႈမူၼ်ႉမႄး ဢိတ်းဢီႈ",
index 7f9f286..242f631 100644 (file)
        "internalerror": "اندر دی غلطی",
        "internalerror_info": "اندر دی غلطی:$1",
        "filedeleteerror": "مِسَل \"$1\" کوں مٹایا نی ونڄ سڳیا۔",
+       "filenotfound": "مِسَل \"$1\" کوں لبھ نی سڳیا۔",
        "formerror": "رپھڑ: فارم نی بھیج سڳے",
        "cannotdelete-title": "ورقہ\"$1\" نی مٹا سڳدے",
        "badtitle": "بھیڑا عنوان",
        "resetpass_submit": "پاس ورڈ بݨاؤ تے لاگ ان تھیوو",
        "changepassword-success": "تہاݙا پاس ورڈ تبدیل تھی ڳیا!",
        "botpasswords": "بوٹ پاس ورڈ",
+       "botpasswords-existing": "بوٹ دے موجودہ پاسورڈ",
+       "botpasswords-createnew": "بوٹ دا نواں پاسورڈ بݨاؤ",
        "botpasswords-label-appid": "بوٹ ناں:",
        "botpasswords-label-create": "بݨاؤ",
        "botpasswords-label-update": "اپ ݙیٹ",
        "botpasswords-label-resetpassword": "پاس ورڈ تبدیل کرو",
        "botpasswords-label-grants-column": "ݙے ݙتا ڳئے",
        "botpasswords-bad-appid": "\"$1\" بوٹ ناں ٹھیک کائنی۔",
+       "botpasswords-deleted-title": "بوٹ پاسورڈ مٹ ڳیا",
        "resetpass-submit-loggedin": "پاس ورڈ تبدیل کرو",
        "resetpass-submit-cancel": "منسوخ",
        "resetpass-temp-password": "عارضی لنگھݨ لفظ، پاس ورڈ",
index 6dbd987..37981db 100644 (file)
        "nosuchusershort": "Uporabnik z imenom »$1« ne obstaja.\nPreverite črkovanje.",
        "nouserspecified": "Prosimo, vpišite uporabniško ime.",
        "login-userblocked": "Ta uporabnik je blokiran. Prijava ni dovoljena.",
-       "wrongpassword": "Vnesli ste napačno geslo. Prosimo, poskusite znova.",
+       "wrongpassword": "Vnesli ste napačno uporabniško ime ali geslo.\nProsimo, poskusite znova.",
        "wrongpasswordempty": "Vpisali ste prazno geslo. Prosimo, poskusite znova.",
        "passwordtooshort": "Geslo mora imeti najmanj $1 {{PLURAL:$1|znak|znaka|znake|znakov|znakov}}.",
        "passwordtoolong": "Gesla ne morejo biti daljša od {{PLURAL:$1|1 znaka|$1 znakov}}.",
        "rcfilters-savedqueries-apply-and-setdefault-label": "Ustvari privzeti filter",
        "rcfilters-savedqueries-cancel-label": "Prekliči",
        "rcfilters-savedqueries-add-new-title": "Shrani nastavitve trenutnega filtra",
+       "rcfilters-savedqueries-already-saved": "Te filtre ste že shranili. Uporabite svoje nastavitve, da ustvarite nov Shranjen filter.",
        "rcfilters-restore-default-filters": "Obnovi privzete filtre",
        "rcfilters-clear-all-filters": "Počisti vse filtre",
        "rcfilters-show-new-changes": "Ogled najnovejših sprememb",
        "uploadstash-refresh": "Osveži seznam datotek",
        "uploadstash-thumbnail": "ogled sličice",
        "uploadstash-exception": "Nalaganja nismo uspeli shraniti v zalogo ($1): »$2«.",
+       "uploadstash-bad-path": "Pot ne obstaja.",
+       "uploadstash-bad-path-invalid": "Pot ni veljavna.",
+       "uploadstash-bad-path-unknown-type": "Neznana vrsta »$1«.",
+       "uploadstash-bad-path-unrecognized-thumb-name": "Neprepoznano ime sličice.",
+       "uploadstash-bad-path-no-handler": "Za mime $1 datoteke $2 nismo našli nobenega klica.",
+       "uploadstash-bad-path-bad-format": "Ključ »$1« ni pravilne oblike.",
+       "uploadstash-file-not-found": "Ključa »$1« nismo našli v shrambi.",
+       "uploadstash-file-not-found-no-thumb": "Ne moremo pridobiti sličice.",
+       "uploadstash-file-not-found-no-local-path": "Za pomanjšano zadevo ni lokalne poti.",
+       "uploadstash-file-not-found-no-object": "Ne moremo ustvariti lokalnega objekta datoteke za sličico.",
+       "uploadstash-file-not-found-no-remote-thumb": "Pridobivanje sličice je spodletelo: $1\nURL = $2",
+       "uploadstash-file-not-found-missing-content-type": "Manjkajoča glava content-type.",
+       "uploadstash-file-not-found-not-exists": "Ne moremo najti poti ali surove datoteke.",
+       "uploadstash-file-too-large": "Ne moremo streči datoteke, večje od $1 bajtov.",
+       "uploadstash-not-logged-in": "Noben uporabnik ni prijavljen; datoteke morajo pripadati uporabnikom.",
+       "uploadstash-wrong-owner": "Datoteka ($1) ne pripada trenutnemu uporabniku.",
+       "uploadstash-no-such-key": "Ni takšnega ključa ($1), zato ne moremo odstraniti.",
+       "uploadstash-no-extension": "Ni končnice.",
+       "uploadstash-zero-length": "Datoteka ima nično dolžino.",
        "invalid-chunk-offset": "Neveljaven odmik delčka",
        "img-auth-accessdenied": "Dostop zavrnjen",
        "img-auth-nopathinfo": "Manjka PATH_INFO.\nVaš strežnik ne poda te informacije.\nMorda temelji na CGI in ne more podpirati img_auth.\nOglejte si  https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
index 975bb87..85f314e 100644 (file)
@@ -71,6 +71,7 @@
        "tog-watchlisthideminor": "Сакриј мање измене са списка надгледања",
        "tog-watchlisthideliu": "Сакриј измене пријављених корисника са списка надгледања",
        "tog-watchlistreloadautomatically": "Аутоматски освежи списак надгледања кад год се филтер измени (потребан JavaScript)",
+       "tog-watchlistunwatchlinks": "Додај дугме за укључење/искључење надгледања свакој страни на списку надгледања (потребан Јаваскрипт за ефекат укључи/искључи)",
        "tog-watchlisthideanons": "Сакриј измене анонимних корисника са списка надгледања",
        "tog-watchlisthidepatrolled": "Сакриј патролиране измене са списка надгледања",
        "tog-watchlisthidecategorization": "Сакриј категоризацију страница",
        "anonpreviewwarning": "<em>Нисте пријављени. Ако објавите страницу, Ваша IP адреса ће бити јавно видљива у њеној историји измена и другде.</em>",
        "missingsummary": "'''Подсетник:''' Нисте унели опис измене.\nАко поново кликнете на „$1”, Ваша измена ће бити сачувана без описа.",
        "selfredirect": "<strong>Упозорење:</strong> Преусмеравате ову страницу на њу саму.\nМожда вам је одредишна страница за преусмерење погрешна или уређујете погрешну страницу.\nАко још једном притиснете „$1”, преусмерење ће свеједно бити направљено.",
-       "missingcommenttext": "УнеÑ\81иÑ\82е ÐºÐ¾Ð¼ÐµÐ½Ñ\82аÑ\80 Ð¸Ñ\81под.",
+       "missingcommenttext": "Ð\9cолимо Ñ\83неÑ\81иÑ\82е ÐºÐ¾Ð¼ÐµÐ½Ñ\82аÑ\80.",
        "missingcommentheader": "<strong>Напомена:</strong> Нисте унели наслов теме овог коментара.\nАко поново кликнете на „$1”, измена ће бити сачувана без наслова.",
        "summary-preview": "Преглед описа измене:",
        "subject-preview": "Преглед теме:",
        "page_last": "последња",
        "histlegend": "Избор разлика: изаберите кутијице измена за упоређивање и притисните ентер или дугме на дну.<br />\nОбјашњење: <strong>({{int:cur}})</strong> = разлика с тренутном изменом, <strong>({{int:last}})</strong> = разлика с претходном изменом, <strong>{{int:minoreditletter}}</strong> = мала измена",
        "history-fieldset-title": "Преглед измена",
-       "history-show-deleted": "Само обрисане измјене",
+       "history-show-deleted": "Само обрисане измене",
        "histfirst": "најстарије",
        "histlast": "најновије",
        "historysize": "({{PLURAL:$1|1 бајт|$1 бајта|$1 бајтова}})",
        "timezoneregion-europe": "Европа",
        "timezoneregion-indian": "Индијски океан",
        "timezoneregion-pacific": "Тихи океан",
-       "allowemail": "Омогући примање имејла од других корисника",
+       "allowemail": "Ð\9eмогÑ\83Ñ\9bи Ð¿Ñ\80имаÑ\9aе Ð¸Ð¼ÐµÑ\98лова Ð¾Ð´ Ð´Ñ\80Ñ\83гиÑ\85 ÐºÐ¾Ñ\80иÑ\81ника",
        "prefs-searchoptions": "Претрага",
        "prefs-namespaces": "Именски простори",
        "default": "подразумевано",
        "rcfilters-grouping-title": "Груписање",
        "rcfilters-activefilters": "Активни филтери",
        "rcfilters-advancedfilters": "Напредни филтери",
-       "rcfilters-limit-title": "Приказати измјена",
-       "rcfilters-limit-shownum": "Прикажи посљедњих $1 измјена",
+       "rcfilters-limit-title": "Приказати измена",
+       "rcfilters-limit-shownum": "Прикажи последњих $1 измена",
        "rcfilters-days-title": "Претходних неколико дана",
        "rcfilters-hours-title": "Претходних неколико сати",
        "rcfilters-days-show-days": "$1 {{PLURAL:$1|дан|дана}}",
        "rcfilters-days-show-hours": "$1 {{PLURAL:$1|сат|сата}}",
        "rcfilters-highlighted-filters-list": "Истакнуто: $1",
        "rcfilters-quickfilters": "Сачувани филтери",
-       "rcfilters-quickfilters-placeholder-title": "Ð\92езе Ñ\98оÑ\88 Ñ\83век Ð½Ð¸Ñ\81Ñ\83 Ñ\83памÑ\9bене",
-       "rcfilters-quickfilters-placeholder-description": "Ð\94а Ð±Ð¸Ñ\81Ñ\82е Ñ\81аÑ\87Ñ\83вали Ñ\81воÑ\98а Ð¿Ð¾Ð´ÐµÑ\88аваÑ\9aа Ñ\84илÑ\82еÑ\80а Ð¸ Ñ\83поÑ\82Ñ\80ебÑ\99авали Ð¸Ñ\85 ÐºÐ°Ñ\81ниÑ\98е, ÐºÐ»Ð¸ÐºÐ½Ð¸Ñ\82е Ð½Ð° Ð±Ñ\83кмаÑ\80к Ð¸ÐºÐ¾Ð½Ñ\83 Ñ\83 Ð¿Ð¾Ð´Ñ\80Ñ\83Ñ\87Ñ\98Ñ\83 Ð\90кÑ\82ивни Ñ\84илÑ\82еÑ\80и, испод.",
+       "rcfilters-quickfilters-placeholder-title": "Ð\88оÑ\88 Ñ\83век Ð½ÐµÐ¼Ð° Ñ\83памÑ\9bениÑ\85 Ñ\84илÑ\82еÑ\80а",
+       "rcfilters-quickfilters-placeholder-description": "Ð\94а Ð±Ð¸Ñ\81Ñ\82е Ñ\81аÑ\87Ñ\83вали Ñ\81воÑ\98а Ð¿Ð¾Ð´ÐµÑ\88аваÑ\9aа Ñ\84илÑ\82еÑ\80а Ð¸ Ñ\83поÑ\82Ñ\80ебÑ\99авали Ð¸Ñ\85 ÐºÐ°Ñ\81ниÑ\98е, ÐºÐ»Ð¸ÐºÐ½Ð¸Ñ\82е Ð½Ð° Ð¸ÐºÐ¾Ð½Ñ\83 Ð·Ð° Ð¾Ð·Ð½Ð°ÐºÑ\83 Ñ\83 Ð¿Ð¾Ð´Ñ\80Ñ\83Ñ\87Ñ\98Ñ\83 Ð°ÐºÑ\82ивниÑ\85 Ñ\84илÑ\82еÑ\80а, испод.",
        "rcfilters-savedqueries-defaultlabel": "Сачувани филтери",
        "rcfilters-savedqueries-rename": "Преименуј",
        "rcfilters-savedqueries-setdefault": "Постави као подразумевано",
        "rcfilters-savedqueries-apply-and-setdefault-label": "Направи подразумевани филтер",
        "rcfilters-savedqueries-cancel-label": "Откажи",
        "rcfilters-savedqueries-add-new-title": "Сачувај тренутне поставке филтера",
+       "rcfilters-savedqueries-already-saved": "Ови филтери су већ упамћени",
        "rcfilters-restore-default-filters": "Враћање подразумеваних филтера",
        "rcfilters-clear-all-filters": "Уклони све филтере",
        "rcfilters-show-new-changes": "Погледајте најновије измене",
-       "rcfilters-search-placeholder": "Филтер скорашњих измјена (претражите или почните куцати)",
+       "rcfilters-search-placeholder": "Филтрирај скорашње измене (употребите мени или потражите име филтра)",
        "rcfilters-invalid-filter": "Невалидан филтер",
        "rcfilters-empty-filter": "Нема активних филтера. Сви доприноси су приказани.",
        "rcfilters-filterlist-title": "Филтери",
        "rcfilters-filterlist-whatsthis": "Како ово функционише?",
-       "rcfilters-filterlist-feedbacklink": "Дајте повратне информације о новим (бета) филтерима",
+       "rcfilters-filterlist-feedbacklink": "Дајте повратне информације о новим (бета) алатима за филтрирање",
        "rcfilters-highlightbutton-title": "Истакни резултате",
        "rcfilters-highlightmenu-title": "Одабери боју",
        "rcfilters-highlightmenu-help": "Изаберите боју да бисте истакнули ово својство",
        "rcfilters-filter-user-experience-level-unregistered-label": "Нерегистровани",
        "rcfilters-filter-user-experience-level-unregistered-description": "Уредници који нису пријављени.",
        "rcfilters-filter-user-experience-level-newcomer-label": "Новајлије",
-       "rcfilters-filter-user-experience-level-newcomer-description": "Регистровани уредници са мање од 10 измјена и 4 дана активности.",
+       "rcfilters-filter-user-experience-level-newcomer-description": "Регистровани уредници са мање од 10 измена и 4 дана активности.",
        "rcfilters-filter-user-experience-level-learner-label": "Ученици",
        "rcfilters-filter-user-experience-level-learner-description": "Регистровани уредници са више искуства од „новајлија”, али мање од „искусних корисника”.",
        "rcfilters-filter-user-experience-level-experienced-label": "Искусни корисници",
-       "rcfilters-filter-user-experience-level-experienced-description": "Регистровани уредници са више од 500 измјена и 30 дана активности.",
+       "rcfilters-filter-user-experience-level-experienced-description": "Регистровани уредници са више од 500 измена и 30 дана активности.",
        "rcfilters-filtergroup-automated": "Аутоматизовани доприноси",
        "rcfilters-filter-bots-label": "Бот",
        "rcfilters-filter-bots-description": "Измјене направљене аутоматизованим алатима.",
-       "rcfilters-filter-humans-label": "Човјек (није бот)",
+       "rcfilters-filter-humans-label": "Човек (није бот)",
        "rcfilters-filter-humans-description": "Измјене које су направили људи-уредници.",
        "rcfilters-filtergroup-reviewstatus": "Патролираност",
        "rcfilters-filter-patrolled-label": "Патролирано",
        "rcfilters-filter-unpatrolled-label": "Непатролирано",
        "rcfilters-filter-unpatrolled-description": "Измјене које нису означене као патролиране.",
        "rcfilters-filtergroup-significance": "Значај",
-       "rcfilters-filter-minor-label": "Мање измјене",
+       "rcfilters-filter-minor-label": "Мање измене",
        "rcfilters-filter-minor-description": "Измјене које је аутор означио као мање.",
        "rcfilters-filter-major-label": "Не-мање измјене",
        "rcfilters-filter-major-description": "Измјене које нису означене као мање.",
        "rcfilters-filtergroup-watchlist": "Странице на списку надгледања",
        "rcfilters-filter-watchlist-watched-label": "На списку надгледања",
-       "rcfilters-filter-watchlist-watched-description": "Измјене страница на Вашем списку надгледања",
+       "rcfilters-filter-watchlist-watched-description": "Измјене страница на Вашем списку надгледања.",
        "rcfilters-filter-watchlist-watchednew-label": "Нове измјене на списку надгледања",
        "rcfilters-filter-watchlist-watchednew-description": "Измјене страница на списку надгледања које нисте посјетили од када су направљене измјене.",
        "rcfilters-filter-watchlist-notwatched-label": "Није на списку надгледања",
        "rcfilters-filter-watchlist-notwatched-description": "Све осим измјена страница на Вашем списку надгледања.",
+       "rcfilters-filtergroup-watchlistactivity": "Стање на списку надгледања",
+       "rcfilters-filter-watchlistactivity-unseen-label": "Непогледане измене",
+       "rcfilters-filter-watchlistactivity-unseen-description": "Измене страница које нисте посетили од када су направљене измене.",
+       "rcfilters-filter-watchlistactivity-seen-label": "Погледане измене",
+       "rcfilters-filter-watchlistactivity-seen-description": "Измене страница које сте посетили од када су направљене измене.",
        "rcfilters-filtergroup-changetype": "Тип измјене",
-       "rcfilters-filter-pageedits-label": "Измјене страница",
+       "rcfilters-filter-pageedits-label": "Измене страница",
        "rcfilters-filter-pageedits-description": "Измјене вики садржаја, расправа, описа категорија…",
        "rcfilters-filter-newpages-label": "Стварање страница",
-       "rcfilters-filter-newpages-description": "Измјене којима се стварају нове странице.",
+       "rcfilters-filter-newpages-description": "Измене којима се стварају нове странице.",
        "rcfilters-filter-categorization-label": "Измјене категорија",
        "rcfilters-filter-categorization-description": "Записи о страницама додатим или уклоњеним из категорија.",
-       "rcfilters-filter-logactions-label": "РадÑ\9aе Ð·Ð°Ð±Ð¸Ñ\99ежене у дневницима",
+       "rcfilters-filter-logactions-label": "РадÑ\9aе Ð·Ð°Ð±ÐµÐ»ежене у дневницима",
        "rcfilters-filter-logactions-description": "Административне акције, стварање налога, брисање страница, отпремања…",
        "rcfilters-hideminor-conflicts-typeofchange-global": "Филтер за „мање” измене је у сукобу са једним или више филтера типа измена, зато што одређени типови измена не могу да се означе као „мање”. Сукобљени филтери су означени у подручју Активни филтери, изнад.",
        "rcfilters-hideminor-conflicts-typeofchange": "Одређени типови измена не могу да се означе као „мање”, тако да је овај филтер у сукобу са следећим филтерима типа измена: $1",
        "rcfilters-filter-excluded": "Изостављено",
        "rcfilters-tag-prefix-namespace-inverted": "<strong>:није</strong> $1",
        "rcfilters-exclude-button-off": "Изостави означено",
+       "rcfilters-exclude-button-on": "Изостави одабрано",
        "rcfilters-view-tags": "Означене измјене",
        "rcfilters-view-namespaces-tooltip": "Филтер резултата према именском простору",
-       "rcfilters-view-tags-tooltip": "Филтер резултата према ознаци измјене",
+       "rcfilters-view-tags-tooltip": "Филтрирање резултата према ознаци измјене",
+       "rcfilters-view-return-to-default-tooltip": "Повратак на главни мени",
        "rcfilters-view-tags-help-icon-tooltip": "Сазнајте више о означеним измјенама",
        "rcfilters-liveupdates-button": "Ажурирања уживо",
+       "rcfilters-liveupdates-button-title-on": "Искључи ажурирања уживо",
+       "rcfilters-liveupdates-button-title-off": "Прикажи нове измене уживо",
        "rcfilters-watchlist-markseen-button": "Означи све измене као виђене",
-       "rcfilters-watchlist-showupdated": "Промјене на страницама које нисте посјетили од када је измјена извршена су <strong>подебљане</strong>, са испуњеним ознакама.",
+       "rcfilters-watchlist-edit-watchlist-button": "Промените Ваш списак надгледаних страница",
+       "rcfilters-watchlist-showupdated": "Измене на страницама које нисте посетили од када је измена извршена су <strong>подебљане</strong>, са испуњеним ознакама.",
        "rcfilters-preference-label": "Сакриј побољшану верзију скорашњих измена",
        "rcfilters-preference-help": "Поништава редизајн интерфејса из 2017. и све алатке додате тада и после.",
        "rcnotefrom": "Испод {{PLURAL:$5|је измена|су измене}} од <strong>$3, $4</strong> (до <strong>$1</strong> приказано).",
        "undeleteviewlink": "погледај",
        "undeleteinvert": "Обрни избор",
        "undeletecomment": "Разлог:",
-       "cannotundelete": "Враћање једне или свих ставник није успјело:\n$1",
+       "cannotundelete": "Враћање једне или свих није успело:\n$1",
        "undeletedpage": "<strong>Страница $1 је враћена</strong>\n\nПогледајте [[Special:Log/delete|дневник брисања]] за записе о скорашњим брисањима и враћањима.",
        "undelete-header": "Погледајте [[Special:Log/delete|историјат брисања]] за недавно обрисане странице.",
        "undelete-search-title": "Претрага обрисаних страница",
        "pageinfo-robot-index": "Дозвољено",
        "pageinfo-robot-noindex": "Није дозвољено",
        "pageinfo-watchers": "Број надгледача странице",
-       "pageinfo-visiting-watchers": "Број надгледача странице који су посјетили скорашње измјене",
+       "pageinfo-visiting-watchers": "Број надгледача странице који су посетили скорашње измене",
        "pageinfo-few-watchers": "Мање од $1 {{PLURAL:$1|пратиоца|пратиоца|пратилаца}}",
        "pageinfo-redirects-name": "Број преусмерења на ову страницу",
        "pageinfo-subpages-name": "Подстранице ове странице",
        "tag-filter-submit": "Филтрирај",
        "tag-list-wrapper": "([[Special:Tags|$1 {{PLURAL:$1|ознака|ознаке|ознака}}]]: $2)",
        "tag-mw-contentmodelchange": "промена модела садржаја",
+       "tag-mw-contentmodelchange-description": "Измене које мењају модел садржаја странице",
        "tags-title": "Ознаке",
        "tags-intro": "На овој страници је наведен списак ознака с којима програм може да означи измене и његово значење.",
        "tags-tag": "Назив ознаке",
index c4e1368..01f2e83 100644 (file)
@@ -26,7 +26,8 @@
                        "Mega Aleksandar",
                        "Asmen",
                        "Obsuser",
-                       "Zoranzoki21"
+                       "Zoranzoki21",
+                       "Prevodim"
                ]
        },
        "tog-underline": "Podvlačenje veza:",
        "page_last": "poslednja",
        "histlegend": "Izbor razlika: izaberite kutijice izmena za upoređivanje i pritisnite enter ili dugme na dnu.<br />\nObjašnjenje: <strong>({{int:cur}})</strong> = razlika s trenutnom izmenom, <strong>({{int:last}})</strong> = razlika s prethodnom izmenom, <strong>{{int:minoreditletter}}</strong> = mala izmena",
        "history-fieldset-title": "Pregled izmena",
-       "history-show-deleted": "Samo obrisane",
+       "history-show-deleted": "Samo obrisane izmene",
        "histfirst": "najstarije",
        "histlast": "najnovije",
        "historysize": "({{PLURAL:$1|1 bajt|$1 bajta|$1 bajtova}})",
        "timezoneregion-europe": "Evropa",
        "timezoneregion-indian": "Indijski okean",
        "timezoneregion-pacific": "Tihi okean",
-       "allowemail": "Omogući primanje imejla od drugih korisnika",
+       "allowemail": "Omogući primanje imejlova od drugih korisnika",
        "prefs-searchoptions": "Pretraga",
        "prefs-namespaces": "Imenski prostori",
        "default": "podrazumevano",
        "rcfilters-other-review-tools": "Ostali alati za pregled:",
        "rcfilters-activefilters": "Aktivni filteri",
        "rcfilters-advancedfilters": "Napredni filteri",
-       "rcfilters-limit-title": "Prikazati izmjena",
+       "rcfilters-limit-title": "Prikazati izmena",
        "rcfilters-limit-shownum": "Prikaži posljednjih $1 izmjena",
        "rcfilters-days-show-days": "$1 {{PLURAL:$1|dana|dana}}",
        "rcfilters-days-show-hours": "$1 {{PLURAL:$1|sat|sata}}",
+       "rcfilters-quickfilters-placeholder-description": "Da biste sačuvali svoja podešavanja filtera i upotrebljavali ih kasnije, kliknite na ikonu za oznaku u području aktivnih filtera, ispod.",
        "rcfilters-search-placeholder": "Filter skorašnjih izmjena (pretražite ili počnite kucati)",
        "rcfilters-filtergroup-authorship": "Autorstvo doprinosa",
+       "rcfilters-filter-editsbyself-label": "Vaše izmene",
+       "rcfilters-filter-editsbyother-label": "Izmene drugih",
+       "rcfilters-filter-editsbyother-description": "Sve izmene osim Vaših.",
        "rcfilters-filter-user-experience-level-registered-label": "Registrovani",
        "rcfilters-filter-user-experience-level-registered-description": "Prijavljeni urednici.",
        "rcfilters-filter-user-experience-level-unregistered-label": "Neregistrovani",
        "rcfilters-filter-user-experience-level-learner-label": "Učenici",
        "rcfilters-filter-user-experience-level-learner-description": "Više dana aktivnosti i izmjena od „novajlija”, ali manje od „iskusnih korisnika”.",
        "rcfilters-filter-user-experience-level-experienced-label": "Iskusni korisnici",
-       "rcfilters-filter-user-experience-level-experienced-description": "Preko 30 dana aktivnosti i 500 izmjena.",
-       "rcfilters-filter-humans-label": "Čovjek (nije bot)",
+       "rcfilters-filter-user-experience-level-experienced-description": "Registrovani urednici sa više od 500 izmena i 30 dana aktivnosti.",
+       "rcfilters-filter-bots-description": "Izmene napravljene automatizovanim alatima.",
+       "rcfilters-filter-humans-label": "Čovek (nije bot)",
+       "rcfilters-filter-humans-description": "Izmene koje su napravili ljudi-urednici.",
        "rcfilters-filter-patrolled-label": "Patrolirano",
+       "rcfilters-filter-patrolled-description": "Izmene označene kao patrolirane.",
        "rcfilters-filter-unpatrolled-label": "Nepatrolirano",
-       "rcfilters-filter-minor-label": "Manje izmjene",
-       "rcfilters-filter-pageedits-label": "Izmjene stranica",
-       "rcfilters-filter-pageedits-description": "Izmjene viki sadržaja, rasprava, opisa kategorija...",
+       "rcfilters-filter-unpatrolled-description": "Izmene koje nisu označene kao patrolirane.",
+       "rcfilters-filter-minor-label": "Manje izmene",
+       "rcfilters-filter-minor-description": "Izmene koje je autor označio kao manje.",
+       "rcfilters-filter-major-label": "Ne-manje izmene",
+       "rcfilters-filter-major-description": "Izmene koje nisu označene kao manje.",
+       "rcfilters-filter-watchlist-watched-description": "Izmene stranica koje su na Vašem spisku nadgledanja.",
+       "rcfilters-filter-watchlist-watchednew-label": "Nove izmene na spisku nadgledanja",
+       "rcfilters-filter-watchlist-watchednew-description": "Izmene stranica na spisku nadgledanja koje niste posetili od kada su napravljene izmene.",
+       "rcfilters-filter-watchlist-notwatched-description": "Sve osim izmena stranica na Vašem spisku nadgledanja.",
+       "rcfilters-filtergroup-changetype": "Vrsta izmene",
+       "rcfilters-filter-pageedits-label": "Izmene stranica",
+       "rcfilters-filter-pageedits-description": "Izmene viki sadržaja, rasprava, opisa kategorija...",
        "rcfilters-filter-newpages-label": "Stvaranje stranica",
-       "rcfilters-filter-newpages-description": "Izmjene kojima se stvaraju nove stranice.",
-       "rcfilters-filter-logactions-label": "Radnje zabilježene u dnevnicima",
-       "rcfilters-view-advanced-filters-label": "Napredni filteri",
+       "rcfilters-filter-newpages-description": "Izmene kojima se stvaraju nove stranice.",
+       "rcfilters-filter-categorization-label": "Izmene kategorija",
+       "rcfilters-filter-logactions-label": "Radnje zabeležene u dnevnicima",
+       "rcfilters-filtergroup-lastRevision": "Poslednje izmene",
+       "rcfilters-filter-lastrevision-label": "Poslednja izmena",
+       "rcfilters-filter-previousrevision-label": "Nije poslednja izmena",
+       "rcfilters-filter-previousrevision-description": "Sve izmene koje nisu „poslednje izmene”.",
+       "rcfilters-view-tags": "Označene izmene",
        "rcfilters-view-namespaces-tooltip": "Filter rezultata prema imenskom prostoru",
-       "rcfilters-view-tags-tooltip": "Filter rezultata prema oznaci izmjene",
+       "rcfilters-view-tags-tooltip": "Filtriranje rezultata prema oznaci izmene",
+       "rcfilters-view-tags-help-icon-tooltip": "Saznajte više o označenim izmenama",
        "rcfilters-liveupdates-button": "Ažuriranja uživo",
        "rcfilters-watchlist-markseen-button": "Označi sve izmene kao viđene",
+       "rcfilters-watchlist-showupdated": "Izmene na stranicama koje niste posetili od kada je izmena izvršena su <strong>podebljane</strong>, sa ispunjenim oznakama.",
        "rcfilters-preference-label": "Sakrij poboljšanu verziju skorašnjih izmena",
        "rcfilters-preference-help": "Poništava redizajn interfejsa iz 2017. i sve alatke dodate tada i posle.",
        "rcnotefrom": "Ispod {{PLURAL:$5|je izmena|su izmene}} od <strong>$3, $4</strong> (do <strong>$1</strong> prikazano).",
        "undeleteviewlink": "pogledaj",
        "undeleteinvert": "Obrni izbor",
        "undeletecomment": "Razlog:",
-       "cannotundelete": "Vraćanje nije uspelo:\n$1",
+       "cannotundelete": "Vraćanje jedne ili svih nije uspelo:\n$1",
        "undeletedpage": "<strong>Stranica $1 je vraćena</strong>\n\nPogledajte [[Special:Log/delete|dnevnik brisanja]] za zapise o skorašnjim brisanjima i vraćanjima.",
        "undelete-header": "Pogledajte [[Special:Log/delete|istorijat brisanja]] za nedavno obrisane stranice.",
        "undelete-search-title": "Pretraga obrisanih stranica",
        "pageinfo-robot-index": "Dozvoljeno",
        "pageinfo-robot-noindex": "Nije dozvoljeno",
        "pageinfo-watchers": "Broj nadgledača stranicе",
-       "pageinfo-visiting-watchers": "Broj nadgledača stranice koji su posjetili skorašnje izmjene",
+       "pageinfo-visiting-watchers": "Broj nadgledača stranice koji su posetili skorašnje izmene",
        "pageinfo-few-watchers": "Manje od $1 {{PLURAL:$1|pratioca|pratilaca}}",
        "pageinfo-redirects-name": "Broj preusmerenja na ovu stranicu",
        "pageinfo-subpages-name": "Podstranice ove stranice",
        "tag-filter": "Filter za [[Special:Tags|oznake]]:",
        "tag-filter-submit": "Filtriraj",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Oznaka|Oznake}}]]: $2)",
+       "tag-mw-contentmodelchange-description": "Izmene koje menjaju model sadržaja stranice",
        "tags-title": "Oznake",
        "tags-intro": "Na ovoj stranici je naveden spisak oznaka s kojima program može da označi izmene i njegovo značenje.",
        "tags-tag": "Naziv oznake",
index 770ba73..762538a 100644 (file)
        "editundo": "bolaykeun",
        "diff-empty": "(taya bédana)",
        "diff-multi-sameuser": "({{PLURAL:$1|Hiji révisi antara|$1 révisi antara}} karya pamaké nu sarua henteu ditémbongkeun)",
+       "diff-multi-otherusers": "({{PLURAL:$1|Hiji révisi antara|$1 révisi antara}} karya leuwih ti {{PLURAL:$2|hiji pamaké|$2 pamaké}} teu ditémbongkeun)",
        "diff-multi-manyusers": "({{PLURAL:$1|Hiji révisi antara|$1 révisi antara}} karya leuwih ti {{PLURAL:$2|pamaké|pamaké}} teu ditémbongkeun)",
        "searchresults": "Hasil maluruh",
        "searchresults-title": "Hasil nyusud \"$1\"",
        "rcfilters-tag-prefix-namespace-inverted": "<strong>:lain</strong> $1",
        "rcfilters-exclude-button-off": "Iwalkeun nu dipilih",
        "rcfilters-exclude-button-on": "Teu kaasup nu dipilih",
-       "rcfilters-view-advanced-filters-label": "Panyaringan leuwih jero",
        "rcfilters-view-tags": "Éditan ditandaan",
        "rcfilters-view-namespaces-tooltip": "Saring hasil dumasar ngarangspasi",
        "rcfilters-view-tags-tooltip": "Saring hasil maké tag éditan",
        "rcfilters-view-return-to-default-tooltip": "Balik ka menu panyaringan utama",
+       "rcfilters-view-tags-help-icon-tooltip": "Teuleuman ngeunaan éditan maké tag",
        "rcfilters-liveupdates-button": "Parobahan langsung",
        "rcfilters-liveupdates-button-title-on": "Pareuman parobahan langsung",
        "rcfilters-liveupdates-button-title-off": "Témbongkeun parobahan anyar nalika éta parobahan prung",
        "uploadstash-refresh": "Nyegerken deui daptar berkas",
        "uploadstash-thumbnail": "tempo miniatur",
        "uploadstash-exception": "Teu bisa nyimpen unjalan di panyimpenan ($1): \"$2\".",
+       "uploadstash-bad-path": "Euweuh galur",
+       "uploadstash-bad-path-invalid": "Galur teu sah.",
+       "uploadstash-bad-path-unknown-type": "Jinis teu dipikanyaho \"$1\".",
+       "uploadstash-bad-path-unrecognized-thumb-name": "Ngaran liwatan teu dipakawawuh.",
+       "uploadstash-file-not-found-no-thumb": "Teu bisa nyomot tampilan saliwat.",
+       "uploadstash-no-extension": "Éksténsi nyamos.",
+       "uploadstash-zero-length": "Berkas mangrupa nol panjang.",
        "invalid-chunk-offset": "Opsét potongan teu valid",
        "img-auth-accessdenied": "Aksés ditolak",
        "img-auth-badtitle": "Teu bisa nyieun judul nu valid tina \"$1\".",
        "filehist-comment": "Kamandang",
        "imagelinks": "Pamakéan berkas",
        "linkstoimage": "Kaca ieu  {{PLURAL:$1|numbu|$1 numbu}} ka gambar ieu :",
+       "linkstoimage-more": "Leuwih ti $1 {{PLURAL:$1|kaca nutumbu|kaca nutumbu}} ka ieu berkas.\nBéréndélan di handap némbongkeun {{PLURAL:$1|tutumbu kaca kahiji|$1 tutumbu kaca}} ka ieu berkas hungkul.\n[[Special:WhatLinksHere/$2|Béréndélan lengkepna]] aya.",
        "nolinkstoimage": "Teu aya kaca anu nutumbu ka ieu berkas.",
        "morelinkstoimage": "Témbong [[Special:WhatLinksHere/$1|tutumbu lianna]] ka ieu berkas.",
        "linkstoimage-redirect": "$1 (pangalihan berkas) $2",
        "listusers-blocked": "(diblokir)",
        "activeusers": "Béréndélan pamaké nu getol",
        "activeusers-intro": "Ieu béréndélan kontributor anu geus ngoprék $1 {{PLURAL:$1|poé|poé}} panungtung.",
-       "activeusers-count": "$1 {{PLURAL:$1|aktivitas}} dina {{PLURAL:$3|1 hari|$3 hari}} panungtung",
+       "activeusers-count": "$1 {{PLURAL:$1|aktivitas}} dina {{PLURAL:$3|sapoé|$3 poé}} panungtung",
        "activeusers-from": "Témbongkeun kontributor dimimitian ku:",
        "activeusers-groups": "Témbongkeun pamaké nu kaasup gorombolan:",
        "activeusers-excludegroups": "Samunikeun pamaké nu kaasup gorombolan:",
        "ipb_cant_unblock": "Éror: ID peungpeuk $1 teu kapanggih. Sigana mah geus dibuka.",
        "ip_range_invalid": "Angka IP teu bener.",
        "ip_range_toolarge": "Panteng blok leuwih badag tibatan /$1 teu diheugbaékeun.",
+       "ip_range_toolow": "Panteng UP sacara éféktif teu diidinan.",
        "proxyblocker": "Pameungpeuk proxy",
        "proxyblockreason": "Alamat IP anjeun dipeungpeuk sabab mangrupa proxy muka. Mangga tepungan ''Internet service provider'' atanapi ''tech support'' anjeun, béjakeun masalah serius ieu.",
        "sorbsreason": "Alamat IP anjeun kadaptar salaku ''open proxy'' dina DNSBL anu dipaké ku {{SITENAME}}.",
        "logentry-protect-unprotect": "$1 {{GENDER:$2|mupus}} panangtayungan ti $3",
        "logentry-protect-protect": "$1 {{GENDER:$2|ditangtayungan}} $3 $4",
        "logentry-upload-upload": "$1 {{GENDER:$2|ngamuat}} $3",
+       "logentry-upload-overwrite": "$1 {{GENDER:$2|ngunggah}} $3 vérsi anyar",
        "logentry-upload-revert": "$1 {{GENDER:$2|diunjal}} $3",
        "log-name-managetags": "Log pangokolaan tag",
        "logentry-managetags-create": "$1 {{GENDER:$2|nyieun}} tag \"$4\"",
index 0a647ad..c26c5e0 100644 (file)
        "nosuchusershort": "Det finns ingen användare som heter \"$1\". Kontrollera att du stavat rätt.",
        "nouserspecified": "Du måste ange ett användarnamn.",
        "login-userblocked": "Denna användare är blockerad. Inloggning är inte tillåtet.",
-       "wrongpassword": "Lösenordet du angav är felaktigt. Försök igen.",
+       "wrongpassword": "Användarnamnet eller lösenordet du angav är felaktigt.\nFörsök igen.",
        "wrongpasswordempty": "Lösenordet som angavs var tomt. Var god försök igen.",
        "passwordtooshort": "Lösenord måste innehålla minst {{PLURAL:$1|$1 tecken}}.",
        "passwordtoolong": "Lösenord kan inte vara längre än {{PLURAL:$1|1 tecken|$1 tecken}}.",
        "rcfilters-savedqueries-apply-and-setdefault-label": "Skapa standardfilter",
        "rcfilters-savedqueries-cancel-label": "Avbryt",
        "rcfilters-savedqueries-add-new-title": "Spara filterinställningar",
-       "rcfilters-savedqueries-already-saved": "Dessa filter har redan sparats",
+       "rcfilters-savedqueries-already-saved": "Dessa filter har redan sparats. Ändra dina inställningar för att skapa ett nytt sparat filter.",
        "rcfilters-restore-default-filters": "Återställ standardfilter",
        "rcfilters-clear-all-filters": "Rensa alla filter",
        "rcfilters-show-new-changes": "Visa de nyaste ändringarna",
        "uploadstash-file-not-found-no-object": "Kunde inte skapa lokalt filobjekt för miniatyr.",
        "uploadstash-file-not-found-no-remote-thumb": "Misslyckades att hämta miniatyrbild: $1\nURL = $2",
        "uploadstash-file-not-found-missing-content-type": "Header för content-type saknas.",
-       "uploadstash-file-not-found-not-exists": "Kan inte hitta sökvägen eller filen består inte av ren text.",
+       "uploadstash-file-not-found-not-exists": "Kan inte hitta sökvägen eller filen.",
        "uploadstash-file-too-large": "Kan inte behandla en fil som är större än $1 byte.",
        "uploadstash-not-logged-in": "Ingen användare är inloggad, filer måste tillhöra användare.",
        "uploadstash-wrong-owner": "Denna fil ($1) tillhör inte aktuell användare.",
        "exif-compression-6": "JPEG (gammal)",
        "exif-copyrighted-true": "Upphovsrättsskyddat",
        "exif-copyrighted-false": "Upphovsrättsstatus inte angivet",
+       "exif-photometricinterpretation-0": "Svartvitt (vit är 0)",
        "exif-photometricinterpretation-1": "Svart och vit (svart är 0)",
        "exif-unknowndate": "Okänt datum",
        "exif-orientation-1": "Normal",
        "version-entrypoints": "Startpunkts-URL:er",
        "version-entrypoints-header-entrypoint": "Startpunkt",
        "version-entrypoints-header-url": "URL",
+       "version-entrypoints-articlepath": "[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgArticlePath Artikelsökväg]",
+       "version-entrypoints-scriptpath": "[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgScriptPath Skriptsökväg]",
        "version-libraries": "Installerade bibliotek",
        "version-libraries-library": "Bibliotek",
        "version-libraries-version": "Version",
index d759fbe..22d93f5 100644 (file)
        "underline-never": "எப்போதுமில்லை",
        "underline-default": "தோல் அல்லது உலாவி இயல்பிருப்பு",
        "editfont-style": "தொகுத்தல் பெட்டி எழுத்துரு:",
-       "editfont-default": "உலாவி இயல்பிருப்பு",
        "editfont-monospace": "சீரான இடைவெளிகொண்ட எழுத்துரு",
        "editfont-sansserif": "சேன்சு-செரீஃப் எழுத்துரு",
        "editfont-serif": "செரிஃப் எழுத்துரு",
        "anontalk": "பேச்சு",
        "navigation": "வழிசெலுத்தல்",
        "and": " மற்றும்",
-       "qbfind": "கண்டுபிடி",
-       "qbbrowse": "உலவு",
-       "qbedit": "தொகு",
-       "qbpageoptions": "பக்க விருப்பத் தேர்வுகள்",
-       "qbmyoptions": "என் விருப்பத்தேர்வுகள்",
        "faq": "அடிக்கடி கேட்கப்படும் கேள்விகள்",
-       "faqpage": "Project:அடிக்கடி கேட்கப்படும் கேள்விகள்",
        "actions": "செயல்கள்",
        "namespaces": "பெயர்வெளிகள்",
        "variants": "மாற்றுக்கள்\n\nமாற்றுருவங்கள்",
        "edit-local": "தன்விவரத்தை திருத்து",
        "create": "உருவாக்கு",
        "create-local": "தன்னிட விளக்கத்தை சேர்",
-       "editthispage": "இப்பக்கத்தைத் தொகு",
-       "create-this-page": "இப்பக்கத்தை உருவாக்கு",
        "delete": "நீக்கவும்",
-       "deletethispage": "இப்பக்கத்தை நீக்கு",
-       "undeletethispage": "அழித்த பக்கத்தை மறுபடியும் கொண்டு வா",
        "undelete_short": "{{PLURAL:$1|ஒரு தொகுப்பை|$1 தொகுப்புக்களை}} மீட்டெடு",
        "viewdeleted_short": "{{PLURAL:$1|ஒரு நீக்கப்பட்ட தொகுப்பை|$1 நீக்கப்பட்ட தொகுப்புகளை}}  பார்.",
        "protect": "காக்கவும்",
        "protect_change": "மாற்று",
-       "protectthispage": "இப்பக்கத்தைக் காக்கவும்",
        "unprotect": "காப்பை மாற்று",
-       "unprotectthispage": "இப்பக்கத்தின் காப்பை மாற்று",
        "newpage": "புதிய பக்கம்",
-       "talkpage": "இப்பக்கம்பற்றிக் கலந்துரையாடு",
        "talkpagelinktext": "பேச்சு",
        "specialpage": "சிறப்புப் பக்கம்",
        "personaltools": "தனிப்பட்ட பயன்பாட்டுக் கருவிகள்",
-       "articlepage": "உள்ளடக்கப் பக்கத்தைப் பார்",
        "talk": "உரையாடல்",
        "views": "பார்வைகள்",
        "toolbox": "கருவிகள்",
-       "userpage": "பயனர் பக்கத்தைப் பார்",
-       "projectpage": "திட்டப் பக்கத்தைப் பார்",
        "imagepage": "கோப்புப் பக்கத்தை நோக்க",
        "mediawikipage": "தகவல் பக்கத்தைப் பார்",
        "templatepage": "வார்ப்புரு பக்கத்தைப் பார்",
        "explainconflict": "நீங்கள் தொகுக்கத் தொடங்கியதின் பின்னர் வேறு பயனரொருவர் இப் பக்கத்தில் மாற்றங்கள்ச் செய்துள்ளார்.\nஉரைப் பகுதியின்  மேற்பக்கம் தற்போதுள்ள உரைகளைக் கொண்டிருகிறது.\nநீங்கள் செய்த மாற்றங்கள் உரையின் கீழே தரப்பட்டுள்ளன.\nநீங்கள் உங்களுடைய மாற்றங்களை ஏற்கெனவேயிருக்கும் உரையுடன் ஒருங்கிணைக்க வேண்டியிருக்கும்.\n\"$1\" என்ற பொத்தானை அழுத்தும்போது உரைப்பகுதியின் மேற்பக்கத்திலுள்ள உரை '''மட்டுமே''' சேமிக்கப்படும்.",
        "yourtext": "உங்கள் உரை",
        "storedversion": "சேமிக்கப்பட்ட பதிப்பு",
-       "nonunicodebrowser": "'''எச்சரிக்கை: உங்களது உலாவி ஒருங்குக்குறியை ஆதரிக்கவில்லை. உங்கள் தொகுப்புக்களைத் பாதுகாப்பகத் தொடர்ந்துச் செய்ய வழிமுறையொன்றுள்ளது: அஸ்கியில்லாத எழுத்துகள் அடி பதினாறைக் கொண்ட குறிகளாக தொகுப்புக் கட்டத்தில் தென்படும்.'''",
        "editingold": "'''எச்சரிக்கை: நீங்கள் தொகுத்துக்கொண்டிருப்பது இப்பக்கத்தின் பழைய திருத்தமொன்றையாகும். இதை நீங்கள் சேமித்தால், மேற்படி திருத்தத்தின் பின்னர் செய்யப்பட்ட மாற்றங்கள் அனைத்தும் இழக்கப்படும்.'''",
        "yourdiff": "வேறுபாடுகள்",
        "copyrightwarning": "{{SITENAME}} தளத்தில் உங்கள் பங்களிப்புகள் $2 உரிமத்தின் கீழ் அளிக்கப்பட்டதாக கருதப்படும்.(மேலதிக தகவல்களுக்கு $1 பக்கத்தைப் பார்க்க) உங்களுடைய எழுத்துக்கள் கடுமையாகத் தொகுக்கப்படுவதையோ, விரும்பியபடி பகிரப்படுவதையோ நீங்கள் விரும்பாவிடில் இங்கே சமர்ப்பிக்காதீர்.<br />\nஅத்துடன் நீங்களே இதை எழுதியதாகவோ, அல்லது வேறு பொதுக் களம் அல்லது அது போன்ற விடுதலையளிக்கும் மூலங்களிலிருந்து பிரதி பண்ணியிருப்பதாகவோ உறுதி கூறுகிறீர்கள்.\n'''பதிப்புரிமையுள்ள ஆக்கங்களை அனுமதியின்றி சமர்ப்பிக்க வேண்டாம்!'''",
        "last": "முந்திய",
        "page_first": "முதல்",
        "page_last": "கடைசி",
-       "histlegend": "வà¯\87à®±à¯\81பாà®\9fà¯\8dà®\9fà¯\88à®\95à¯\8d à®\95ாண à®µà¯\87ணà¯\8dà®\9fிய à®\87ரணà¯\8dà®\9fà¯\81 à®ªà®¤à¯\8dதிபà¯\8dபà¯\81à®\95à¯\8dà®\95ளà¯\88 à®¤à¯\86ரிவà¯\81à®\9aà¯\8d செய்து கீழுள்ள பொத்தானை அழுத்தவும்.<br />\nகுறியீட்டு விளக்கம்: (நடப்பு) = நடைமுறையிலுள்ள பதிப்புடனான வேறுபாடு,\n(கடைசி) = முந்திய பதிப்புடனான வேறுபாடு, சி = சிறு தொகுப்பு",
+       "histlegend": "வà¯\87à®±à¯\81பாà®\9fà¯\8dà®\9fà¯\88à®\95à¯\8d à®\95ாண à®µà¯\87ணà¯\8dà®\9fிய à®\87ரணà¯\8dà®\9fà¯\81 à®ªà®¤à®¿à®ªà¯\8dபà¯\81à®\95ளà¯\88தà¯\8d à®¤à¯\86ரிவà¯\81 செய்து கீழுள்ள பொத்தானை அழுத்தவும்.<br />\nகுறியீட்டு விளக்கம்: (நடப்பு) = நடைமுறையிலுள்ள பதிப்புடனான வேறுபாடு,\n(கடைசி) = முந்திய பதிப்புடனான வேறுபாடு, சி = சிறு தொகுப்பு",
        "history-fieldset-title": "வரலாற்றில் தேடவும்",
        "history-show-deleted": "நீக்கப்பட்டவை மட்டும்",
        "histfirst": "மிகப் பழைய",
        "mergehistory-from": "மூலப் பக்கம்:",
        "mergehistory-into": "இலக்குப் பக்கம்:",
        "mergehistory-list": "இணைக்கபடக்கூடிய தொகுப்பு வரலாறு",
-       "mergehistory-merge": "[[:$1]] பக்கத்துக்கான பின்வரும் திருத்தங்கள் [[:$2]] பக்கத்துக்கு இணைக்கப்பட முடியும். ரேடியோ பொத்தான் நிரலைப் பயன்படுத்தி ஒரு குறித்த நேரம் உட்பட அதற்கு முன்னர் செய்யப்பட்ட திருத்தங்களைத் தெரிவுச் செய்யலாம். இணைப்புகளைப் பயன்படுத்தல் இந்நிரலை இயல்பிருப்பு நிலைக்கு மீட்டமைக்கும்.",
+       "mergehistory-merge": "[[:$1]] பக்கத்துக்கான பின்வரும் திருத்தங்கள் [[:$2]] பக்கத்துக்கு இணைக்கப்பட முடியும். ரேடியோ பொத்தான் நிரலைப் பயன்படுத்தி ஒரு குறித்த நேரம் உட்பட அதற்கு முன்னர் செய்யப்பட்ட திருத்தங்களைத் தெரிவு செய்யலாம். இணைப்புகளைப் பயன்படுத்தல் இந்நிரலை இயல்பிருப்பு நிலைக்கு மீட்டமைக்கும்.",
        "mergehistory-go": "இணைக்கப்படக் கூடிய தொகுப்புக்களைக் காட்டு",
        "mergehistory-submit": "திருத்தங்களை இணை",
        "mergehistory-empty": "இணைக்கப்படக்கூடிய திருத்தங்கள் எதுவுமில்லை.",
        "rcfilters-filterlist-title": "வடிப்பான்கள்",
        "rcfilters-highlightmenu-title": "ஒரு நிறத்தை தேர்ந்தெடுக்கவும்",
        "rcfilters-filterlist-noresults": "எந்த வடிப்பானும் காணப்படவில்லை",
-       "rcfilters-filtergroup-registration": "பயனர் பதிகை",
-       "rcfilters-filter-registered-label": "பதிவுசெய்யப்பட்டது",
-       "rcfilters-filter-unregistered-label": "பதிவு நீக்கம் செய்யப்பட்டது",
        "rcfilters-filter-editsbyself-label": "தங்களின் சொந்த தொகுப்புகள்",
        "rcfilters-filter-editsbyself-description": "தங்களது தொகுப்புகள்.",
        "rcfilters-filter-editsbyother-label": "மற்றவர் தொகுப்புகள்",
+       "rcfilters-filter-user-experience-level-registered-label": "பதிவுசெய்யப்பட்டது",
+       "rcfilters-filter-user-experience-level-unregistered-label": "பதிவு நீக்கம் செய்யப்பட்டது",
        "rcfilters-filter-user-experience-level-newcomer-label": "புது வரவுகள்",
        "rcfilters-filter-user-experience-level-learner-label": "கற்போர்",
        "rcfilters-filter-user-experience-level-experienced-label": "அனுபவமுள்ள பயனர்கள்",
        "upload-curl-error28-text": "இத்தளம் பதில் கொடுக்க நீண்ட நேரம் எடுத்தது. அருள் கூர்ந்து இணையத் தளம் செயல்படுகிறதா என்பதை பார்த்து மீண்டும் முயல்க. அல்லது இணைய நெரிசல் குறைவான நேரத்தில் முயலவும்",
        "license": "அனுமதி:",
        "license-header": "அனுமதி",
-       "nolicense": "தெரிவுச் செய்யப்படவில்லை",
+       "nolicense": "தெரிவு செய்யப்படவில்லை",
        "licenses-edit": "உரிம விருப்பங்களைத் திருத்து",
        "license-nopreview": "(முன்தோற்றம் கையிலிருப்பில் இல்லை)",
        "upload_source_url": " (நீங்கள் தெரிவு செய்த, பொதுவில் அணுக்கூடிய செல்லத்தக்க இணைய முகவரியில் உள்ள கோப்பு)",
        "block": "பயனரைத் தடைசெய்",
        "unblock": "பயனர் தடையை நீக்கு",
        "blockip": "தடுக்கவும் {{GENDER:$1|பயனர்}}",
-       "blockip-legend": "பயனரைத் தடு",
        "blockiptext": "ஒரு குறிப்பிட்ட ஐபி முகவரி அல்லது பயனரிடமிருந்து எழுத்து அணுக்கத்தைத் தடுப்பதற்குக் கீழேயுள்ள படிவத்தை உபயோகிக்கவும். இது விசமத்தனத்தைத் தடுப்பதற்கும் [[{{MediaWiki:Policy-url}}|{{SITENAME}} கொள்கை]]க்கு எற்புடைய வகையிலும் மட்டுமே பயன்படுத்தப்பட வேண்டும்.\nகுறிப்பிட்ட காரணமொன்றைக் கீழே நிரப்புக (எடுத்துக்காட்டாக, விசமத்தனம் செய்யப்பட்ட பக்கங்களை எடுத்துக் காட்டவும்).",
        "ipaddressorusername": "ஐ.பி. அல்லது பயனர் பெயர்:",
        "ipbexpiry": "காலாவதி:",
        "thumbnail_image-missing": "$1 கோப்பு காணாமல் போயிருக்கலாம் என தெரிகிறது.",
        "import": "பக்கங்களை இறக்கு",
        "importinterwiki": "மற்றொரு விக்கியில் இருந்து இறக்கு",
-       "import-interwiki-text": "à®\87à®±à®\95à¯\8dà®\95à¯\81மதிà®\95à¯\8dà®\95ான à®µà®¿à®\95à¯\8dà®\95ியà¯\88யà¯\81à®®à¯\8d à®ªà®\95à¯\8dà®\95தà¯\8dதà¯\88யà¯\81à®®à¯\8d à®¤à¯\86ரிவà¯\81à®\9aà¯\8d à®\9aà¯\86யà¯\8dà®\95.\nதிரà¯\81தà¯\8dத à®¨à®¾à®\9fà¯\8dà®\95ளà¯\8d, à®¤à¯\8aà®\95à¯\81தà¯\8dதவரà¯\8dà®\95ளினà¯\8d à®ªà¯\86யரà¯\8dà®\95ளà¯\8d à®\8eனà¯\8dபன à®ªà¯\87னபà¯\8dபà®\9fà¯\81à®®à¯\8d.\nà®\8eலà¯\8dலா à®µà®¿à®\95à¯\8dà®\95ியிà®\9fà¯\88 à®\87à®±à®\95à¯\8dà®\95à¯\81மதிà®\95ளà¯\81à®®à¯\8d [[Special:Log/import|à®\87à®±à®\95à¯\8dà®\95à¯\81மதிப் பதிகையில்]] பதியப்படும்.",
+       "import-interwiki-text": "தரவிறà®\95à¯\8dà®\95à¯\81வதறà¯\8dà®\95ான à®µà®¿à®\95à¯\8dà®\95ியà¯\88யà¯\81à®®à¯\8d à®ªà®\95à¯\8dà®\95தà¯\8dதà¯\88யà¯\81à®®à¯\8d à®¤à¯\86ரிவà¯\81 à®\9aà¯\86யà¯\8dà®\95.\nதிரà¯\81தà¯\8dத à®¨à®¾à®\9fà¯\8dà®\95ளà¯\8d, à®¤à¯\8aà®\95à¯\81தà¯\8dதவரà¯\8dà®\95ளினà¯\8d à®ªà¯\86யரà¯\8dà®\95ளà¯\8d à®\8eனà¯\8dபன à®ªà¯\87ணபà¯\8dபà®\9fà¯\81à®®à¯\8d.\nà®\8eலà¯\8dலா à®µà®¿à®\95à¯\8dà®\95ியிà®\9fà¯\88 à®¤à®°à®µà®¿à®±à®\95à¯\8dà®\95à®\99à¯\8dà®\95ளà¯\81à®®à¯\8d [[Special:Log/import|தரவிறà®\95à¯\8dà®\95à¯\81ப் பதிகையில்]] பதியப்படும்.",
        "import-interwiki-sourcewiki": "மூல விக்கி:",
        "import-interwiki-sourcepage": "மூலப் பக்கம்:",
        "import-interwiki-history": "இப்பக்கத்தின் அனைத்து வரலாற்றுப் பதிப்புகளையும் நகலெடு",
        "fileduplicatesearch-noresults": "\"$1\" என்ற பெயர் கொண்ட கோப்பு எதுவும் காணப்படவில்லை.",
        "specialpages": "சிறப்புப் பக்கங்கள்",
        "specialpages-note-top": "உள்பட்டியல்",
-       "specialpages-note": " * சராசரி சிறப்புப் பக்கங்கள்.\n * <span class=\"mw-specialpagerestricted\">வரையறுக்கப்பட்ட சிறப்புப் பக்கங்கள்.</span>",
        "specialpages-group-maintenance": "பராமரிப்பு அறிக்கைகள்",
        "specialpages-group-other": "ஏனைய சிறப்புப் பக்கங்கள்",
        "specialpages-group-login": "புகுபதிகை/பயனர் கணக்கு தொடக்கம்",
        "special-characters-title-endash": "சிறு கோடு",
        "special-characters-title-emdash": "பெரு கோடு",
        "special-characters-title-minus": "கழித்தல் குறி",
-       "mw-widgets-dateinput-no-date": "திகதி தெரிவுச் செய்யப்படவில்லை",
+       "mw-widgets-dateinput-no-date": "திகதி தெரிவு செய்யப்படவில்லை",
        "mw-widgets-mediasearch-noresults": "முடிவுகள் எதுவும் காணப்படவில்லை.",
        "mw-widgets-titleinput-description-new-page": "இப்பக்கம் இன்னும் உருவாக்கப்படவில்லை",
        "mw-widgets-titleinput-description-redirect": "$1-க்கு வழிமாற்று",
index 07c79e6..16cca85 100644 (file)
        "mytalk": "Usapan",
        "anontalk": "Usapan",
        "navigation": "Paglilibot",
-       "and": ",&#32;at",
+       "and": "&#32;at",
        "faq": "Mga malilimit itanong",
        "actions": "Mga kilos",
        "namespaces": "Mga ngalan-espasyo",
        "edithelp": "Tulong sa pagbabago",
        "helppage-top-gethelp": "Tulong",
        "mainpage": "Unang Pahina",
-       "mainpage-description": "Unang Pahina",
+       "mainpage-description": "Unang pahina",
        "policy-url": "Project:Patakaran",
        "portal": "Puntahan ng pamayanan",
        "portal-url": "Project:Puntahan ng pamayanan",
index fd51e98..b60268e 100644 (file)
@@ -34,7 +34,6 @@
        "underline-always": "Кезээде",
        "underline-never": "Кажан-даа",
        "underline-default": "Кештиң азы веб-браузерниң ниити үнези",
-       "editfont-default": "Веб-браузерниң ниити үнези",
        "sunday": "Улуг-хүн",
        "monday": "Бир дугаар хүн",
        "tuesday": "Ийи дугаар хүн",
@@ -94,7 +93,7 @@
        "hidden-category-category": "Чажыт бөлүктер",
        "category-subcat-count": "{{PLURAL:$2|1=Ук аңгылал чүгле дараазында иштики аңгылалдыг.|Ук аңгылалда бар-ла $2 иштики аңгылалдарның $1 иштики аңгылалы көстүп турар.}}",
        "category-subcat-count-limited": "Ук аңгылалда {{PLURAL:$1|1=бир|$1}} иштики аңгылал бар.",
-       "category-article-count": "{{PLURAL:$2|1=Ук аңгылалда чүгле чаңгыс арын бар.|Ук аңгылалда бар $2 арыннарының аразындан}} |{{PLURAL:$1 арынны көргүскен| $1 арыннарны көргүскен.}}",
+       "category-article-count": "{{PLURAL:$2|Ук аңгылалда чүгле чаңгыс арын бар.|Аңгылалда ниитизи-биле $2 арын бар. Мында чүгле {{PLURAL:$1|арын|$1 арын}} көргүскен}}",
        "category-file-count": "{{PLURAL:$2|1=Ук аңгылал чүгле чаңгыс файлдыг.|Ук аңгылалдың шупту $2 файлдарының аразындан $1 файлын көргүскен.}}",
        "listingcontinuesabbrev": "(уланчы)",
        "noindex-category": "Индекстелбес арынар",
        "anontalk": "Бо ИП-адрестиң чугаазы",
        "navigation": "Навигация",
        "and": "&#32;болгаш",
-       "qbfind": "Дилээри",
-       "qbbrowse": "Каралаары",
-       "qbedit": "Өскертири",
-       "qbpageoptions": "Бо арын",
-       "qbmyoptions": "Мээң арыннарым",
        "faq": "Бо-ла салыр айтырыглар (БлСА)",
-       "faqpage": "Project:БлСА",
        "actions": "Кылыглар",
        "namespaces": "Аттар делгемнери",
        "variants": "Янзы-хевирлери",
        "edit": "Эдер",
        "create": "Чогаадыры",
        "create-local": "Кызыы тайылбыр немээр",
-       "editthispage": "Бо арынны өскертири",
-       "create-this-page": "Бо арынны чогаадыры",
        "delete": "Ыраары",
-       "deletethispage": "Бо арынны ырадыры",
        "undelete_short": "$1 {{PLURAL:$1|1=эдигни|эдиглерни}} катап үндүрери",
        "viewdeleted_short": "{{PLURAL:$1|1=Бир ыраткан өскерлиишкинни|$1 ыраткан өскерлиишкиннерни}} көөрү",
        "protect": "Камгалаары",
        "protect_change": "өскертири",
-       "protectthispage": "Бо арынны камгалаар",
        "unprotect": "Камгалалды өскертири",
-       "unprotectthispage": "Бо арынның камгалалын өскертири",
        "newpage": "Чаа арын",
-       "talkpage": "Бо арын дугайында чугаалажыры",
        "talkpagelinktext": "Чугаалажып сайгарар",
        "specialpage": "Тускай арын",
        "personaltools": "Хууда херекселдер",
-       "articlepage": "Допчу арынны көөрү",
        "talk": "Сайгарылга",
        "views": "Көрүлделер",
        "toolbox": "Херекселдер",
-       "userpage": "Ажыглакчының арынын көөрү",
-       "projectpage": "Төлевилелдиң арынын көөрү",
        "imagepage": "Файлдың арынын көөрү",
        "mediawikipage": "Чагаа арынын көөрү",
        "templatepage": "Майык арынын көөрү",
        "whatlinkshere-filters": "Шүүрлер",
        "block": "Ажыглакчыны кызыгаарлаары",
        "blockip": "Ажыглакчыны кызыгаарлаары",
-       "blockip-legend": "Ажыглакчыны кызыгаарлаары",
        "ipaddressorusername": "ИП-адрес азы aжыглaкчының aды",
        "ipbreason": "Чылдагаан:",
        "ipbsubmit": "Бо ажыглакчыны кызыгаарлаары",
        "namespacesall": "шупту",
        "monthsall": "шупту",
        "recreate": "Катап чогаадыры",
+       "confirm-purge-title": "Ук арында кешти аштаар",
        "confirm_purge_button": "Чөп",
+       "confirm-purge-top": "Ук арында кешти аштаар бе?",
+       "confirm-purge-bottom": "Арында кешти аштаза, аңаа арынның сөөлгү хевири көстүр.",
        "confirm-watch-button": "Чөп",
        "confirm-unwatch-button": "Чөп",
        "imgmultipageprev": "← эрткен арын",
index 9ec4cce..b627abc 100644 (file)
@@ -33,7 +33,8 @@
                        "Junaid Ahmad",
                        "Abuaneeqa",
                        "Saraiki",
-                       "BukhariSaeed"
+                       "BukhariSaeed",
+                       "Zainab Meher"
                ]
        },
        "tog-underline": "ربط کی خط کشیدگی:",
        "period-am": "صبح",
        "period-pm": "شام",
        "pagecategories": "{{PLURAL:$1|زمرہ|زمرہ جات}}",
-       "category_header": "زمرہ \"$1\" میں صفحات",
+       "category_header": "زمرہ «$1» میں صفحات",
        "subcategories": "ذیلی زمرہ جات",
        "category-media-header": "زمرہ \"$1\" میں میڈیا",
        "category-empty": "<em>اس زمرہ میں ابھی کوئی صفحہ یا میڈیا موجود نہیں ہے۔</em>",
        "site-rss-feed": "$1 آر.ایس.ایس فیڈ",
        "site-atom-feed": "$1 اٹوم فیڈ",
        "page-rss-feed": "\"$1\" آر.ایس.ایس فیڈ",
-       "page-atom-feed": "\"$1\" اٹوم خورد",
+       "page-atom-feed": "اٹوم فیڈ برائے «$1»",
        "feed-atom": "اٹوم",
        "feed-rss": "آر ایس ایس",
        "red-link-title": "$1 (صفحہ موجود نہیں)",
        "nosuchusershort": "\"$1\" کے نام سے کوئی صارف موجود نہیں.\nاپنا ہجہ جانچئے.",
        "nouserspecified": "آپ کو ایک اسمِ صارف مخصوص کرنا ہے.",
        "login-userblocked": "اِس صارف پر پابندی ہے. داخلِ نوشتہ ہونے کی اجازت نہیں.",
-       "wrongpassword": "آپ نے غلط کلمۂ شناخت درج کیا ہے۔ دوبارہ کو شش کریں۔",
+       "wrongpassword": "آپ نے غلط پاس ورڈ یا صارف نام درج کیا ہے۔ براہ مہربانی دوبارہ کوشش کریں۔",
        "wrongpasswordempty": "کلمۂ شناخت ندارد۔ دوبارہ کوشش کریں۔",
        "passwordtooshort": "آپکا منتخب کردہ پارلفظ مختصر ہے. پارلفظ کم از کم {{PLURAL:$1|1 محرف|$1 محارف}} ہونا چاہئے.",
        "passwordtoolong": "خفیہ رمز (پاس ورڈ) {{PLURAL:$1|1 حرف|$1 حروف}} سے زیادہ طویل نہیں ہو سکتا۔",
        "botpasswords-label-delete": "حذف کریں",
        "botpasswords-label-resetpassword": "پاس ورڈ تبدیل کریں",
        "botpasswords-label-grants": "قابل تطبیق عطیے:",
+       "botpasswords-help-grants": "عطیہ آپ کو حقوق تک پہنچنے دیتا ہے، جو آپ کے کھاتے کے پاس پہلے سے ہے۔ عطیہ کو فعال کرنے سے کوئی حق فائز نہیں ہوگا، جو آپ کے کھاتے ميں نہ ہو۔ مزید معلومات کے لیے [[Special:ListGrants|فہرست عطیات]] دیکھیں۔",
        "botpasswords-label-grants-column": "دے دیا گیا",
        "botpasswords-bad-appid": "روبہ نام \"$1\" درست نہیں۔",
        "botpasswords-insert-failed": "روبہ نام \"$1\" کو شامل کرنے میں ناکامی۔ کیا اسے پہلے شامل کیا جا چکا ہے؟",
        "passwordreset-domain": "ساحہ:",
        "passwordreset-email": "برقی ڈاک پتہ:",
        "passwordreset-emailtitle": "{{SITENAME}} کھاتہ کی تفصیلات",
+       "passwordreset-emailtext-ip": "کسی نے (شاید آپ نے اپنی آئی پی پتے $1 سے) {{SITENAME}} ($4) کے لیے آپ کے پاسورڈ کو ری سیٹ کرنے کی درخواست دی ہے۔ ذیل میں موجود صارف {{PLURAL:$3|کھاتہ|کھاتے}} اس برقی پتے سے منسلک ہے:\n\n$2\n\n{{PLURAL:$3|یہ پاسورڈ|یہ پاسورڈز}} {{PLURAL:$5|ایک دن|$5 دنوں}} میں ایکسپائر {{PLURAL:$3|ہوجائے گا|ہوجائیں گے}}۔\n\nآپ کو لاگ ان ہوکر ایک نیا پاسورڈ منتخب کرنا چاہیے۔ اگر کسی اور نے یہ درخواست دی ہے، یا اگر آپ اپنا اصلی پاسورڈ جانتے ہیں، اور آپ اس کو تبدیل کرنے کی خواہش نہیں رکھتے تو آپ اس پیغام کو نظر انداز کر کے اپنے پرانے پاسورڈ کا ہی استعمال جاری رکھ سکتے ہیں۔",
+       "passwordreset-emailtext-user": "کسی نے (شاید آپ نے اپنی آئی پی پتے $1 سے) {{SITENAME}} ($4) کے لیے آپ کے پاسورڈ کو ری سیٹ کرنے کی درخواست دی ہے۔ ذیل میں موجود صارف {{PLURAL:$3|کھاتہ|کھاتے}} اس برقی پتے سے منسلک ہے:\n\n$2\n\n{{PLURAL:$3|یہ پاسورڈ|یہ پاسورڈز}} {{PLURAL:$5|ایک دن|$5 دنوں}} میں ایکسپائر {{PLURAL:$3|ہوجائے گا|ہوجائیں گے}}۔\n\nآپ کو لاگ ان ہوکر ایک نیا پاسورڈ منتخب کرنا چاہیے۔ اگر کسی اور نے یہ درخواست دی ہے، یا اگر آپ اپنا اصلی پاسورڈ جانتے ہیں، اور آپ اس کو تبدیل کرنے کی خواہش نہیں رکھتے تو آپ اس پیغام کو نظر انداز کر کے اپنے پرانے پاسورڈ کا ہی استعمال جاری رکھ سکتے ہیں۔",
        "passwordreset-emailelement": "صارف نام:\n$1\n\nعارضی پاس ورڈ: \n$2",
        "passwordreset-emailsentemail": "اگر یہ برقی ڈاک پتا آپ کے کھاتے سے منسلک ہے تو پاس ورڈ کی ترتیب نو کا برقی خط بھیج دیا جائے گا۔",
        "passwordreset-emailsentusername": "اگر کوئی برقی ڈاک پتا آپ کے کھاتے سے منسلک ہے تو پاس ورڈ کی ترتیب نو کا برقی خط بھیج دیا جائے گا۔",
        "changeemail-throttled": "آپ نے متعدد مرتبہ داخل ہونے کی کوشش کی ہے۔\nدوبارہ کوشش کرنے سے پہلے $1 انتظار فرمائیں۔",
        "changeemail-nochange": "براہ کرم کوئی دوسرا برقی ڈاک پتہ درج کریں۔",
        "resettokens": "ٹوکنوں کی ترتیب نو",
+       "resettokens-text": "آپ یہاں اپنے کھاتے سے منسلک مخصوص ذاتی ڈیٹا تک رسائی ٹوکنوں کو ری سیٹ کر کے پا سکتے ہیں۔\n\nآپ کو یہ تب کرنا چاہیے جب آپ نے اسے کسی کے ساتھ غلطی یا حادثاتی طور پر شیئر کردیا ہو یا اگر آپ کے کھاتے نے سمجھوتہ اختیار کرلیا ہو۔",
        "resettokens-no-tokens": "ترتیب نو کے لیے کوئی ٹوکن موجود نہیں۔",
        "resettokens-tokens": "ٹوکن:",
        "resettokens-token-label": "$1 (موجودہ قدر: $2)",
        "previewerrortext": "آپ کی تبدیلیوں کی نمائش دکھانے کے دوران میں کوئی نقص واقع ہو گیا ہے۔",
        "blockedtitle": "صارف مسدود ہے",
        "blockedtext": "'''آپکا اسمِ صارف یا آئی پی پتہ پر پابندی ہے.'''\n\n$1 نے پابندی لگائی تھی.\nوجہ یہ بتائی گئی کہ ''$2''.\n\n* پابندی کی ابتداء : $8\n* پابندی کا اختتام : $6\n* Intended blockee: $7\n\nآپ $1 یا کسی دوسرے [[{{MediaWiki:Grouppage-sysop}}|منتظم]] سے رابطہ کرکے پابندی پر بات چیت کرسکتے ہیں.\nآپ ‘صارف کو برقی خط ارسال کریں’ کی خاصیت اُس وقت تک استعمال نہیں کرسکتے جب تک آپ اپنے [[Special:Preferences|کھاتہ کے ترجیحات]] میں صحیح برقی پتہ معیّن نہ کریں، اور آپ کو اِسے استعمال کرنے سے پابند نہیں کیا گیا ہے.\nآپکا موجودہ آئی پی پتہ $3 ہے، اور پابندی کی شناخت #$5 ہے.\nبراہِ مہربانی کسی بھی قسم کے استفسار میں درج بالا تمام تفاصیل شامل کریں.",
+       "autoblockedtext": "آپ کے آئی پی پتے پر خودکارانہ طریقہ سے پابندی لگا دی گئی ہے کیونکہ یہ دوسرے صارف نے استعمال کی تھی، جس پر $1 نے پابندی لگائی ہوئی ہے۔\nوجہ یہ بتائی گئی کہ:\n<em>$2</em>\n\n*پابندی کی ابتدا: $8\n*پابندی کا اختتام: $6\n*Intended blockee: $7\n\nآپ $1 سے یا دوسرے [[{{MediaWiki:Grouppage-sysop}}|منتظمین]] سے رابطہ کر کے پابندی پر بات چیت کرسکتے ہیں۔\n\nیاد رکھیں کہ ”صارف کو برقی خط ارسال کریں“ کی خاصیت اُس وقت تک استعمال نہیں کرسکتے جب تک آپ اپنے [[Special:Preferences|کھاتے کی ترجیحات]] میں صحیح برقی پتہ معیّن نہ کریں، اور آپ کو اِسے استعمال کرنے سے پابند نہیں کیا گیا ہے۔\nآپ کا موجودہ آئی پی پتہ $3 ہے، اور پابندی کی شناخت #$5 ہے۔\nبراہِ مہربانی کسی بھی قسم کے استفسار میں درج بالا تمام تفصیل شامل کریں۔",
+       "systemblockedtext": "آپ کے اسم صارف یا آئی پی پتے پر میڈیاویکی کی جانب سے خودکارانہ طریقے سے پابندی لگا دی گئی ہے۔\nاور وجہ یہ بتائی گئی ہے کہ:\n<em>$2</em>\n\n*پابندی کی ابتدا: $8\n*پابندی کا اختتام: $6\n*Intended blockee: $7\n\nآپ کا موجودہ آئی پی پتہ $3 ہے۔\nبراہِ مہربانی کسی بھی قسم کے استفسار میں درج بالا تمام تفصیل شامل کریں۔",
        "blockednoreason": "کوئی وجہ نہیں دی گئی",
        "whitelistedittext": "ترمیم کیلئے $1 ضروری ہے.",
        "confirmedittext": "صفحات میں ترمیم کرنے سے پہلے آپ اپنے برقی پتہ کی تصدیق کریں.\nبرائے مہربانی! اپنی [[Special:Preferences|ترجیحات]] کے ذریعے اپنا برقی پتہ کا تعیّن اور تصدیق کیجئے.",
        "anontalkpagetext": "----\n<em>یہ تبادلۂ خیال صفحہ ایک ایسے صارف کا ہے جس نے اب تک اپنا کھاتہ نہیں بنایا یا یہ صفحہ اس کے زیر استعمال نہیں۔</em> \nلہٰذا ہمیں اس کی شناخت کے لئے ایک آئی پی پتہ استعمال کرنا پڑ رہا ہے۔ \nاس قسم کا آئی پی پتہ ایک سے زائد صارفین کے درمیان میں مشترک بھی ہوسکتا ہے۔ \nاگر آپ کی موجودہ حیثیت ایک گمنام صارف کی ہے اور آپ محسوس کریں کہ اس صفحہ پر آپ کے متعلق یہ تبصرے غیر متعلق ہیں تو براہ کرم [[Special:CreateAccount|ایک کھاتہ بنا لیں]] یا [[Special:UserLogin|داخل ہو جائیں]] تاکہ مستقبل میں آپ کو گمنام صارفین میں شمار کرنے سے گریز کیا جائے۔",
        "noarticletext": "اِس صفحہ میں فی الحال کوئی متن موجود نہیں ہے۔\nآپ دیگر صفحات میں [[Special:Search/{{PAGENAME}}|اِس صفحہ کے عنوان کو تلاش کر سکتے ہیں]]، <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} متعلقہ نوشتہ جات میں تلاش کر سکتے ہیں]،\nیا [{{fullurl:{{FULLPAGENAME}}|action=edit}} اِس صفحہ کو تخلیق کر سکتے ہیں]</span>۔",
        "noarticletext-nopermission": "اس صفحہ میں فی الحال کوئی متن موجود نہیں ہے۔\nآپ دیگر صفحات میں [[Special:Search/{{PAGENAME}}|اِس صفحہ کے عنوان کے لیے]] یا <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} متعلقہ نوشتہ جات تلاش کرسکتے ہیں]</span>",
+       "missing-revision": "صفحہ \"{{FULLPAGENAME}}\" کے فرق #$1 کا نسخہ نہیں ملا۔\n\nعموماً ایسا اس وقت ہوتا ہے جب کسی حذف شدہ صفحہ کے نسخوں کے درمیان میں فرق تلاش کرنے کی کوشش کی جائے۔\nمزید تفصیلات [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} نوشتہ حذف شدگی] میں دیکھی جا سکتی ہیں۔",
        "userpage-userdoesnotexist": "«$1» کے نام سے صارف کھاتہ موجود نہیں ہے۔\nاگر آپ اس صفحہ کو تخلیق یا اس میں ترمیم کرنا چاہتے ہیں تو براہ کرم پہلے جانچ لیں۔",
        "userpage-userdoesnotexist-view": "صارف کھاتہ \"$1\" مندرج نہیں ہے۔",
        "blocked-notice-logextract": "یہ صارف معطل ہے۔\nحوالہ کے لیے نوشتہ پابندی کا تازہ ترین اندراج ذیل میں دستیاب ہے:",
        "previewconflict": "اس نمائش میں خانہ ترمیم کے اوپر موجود متن جس انداز میں ظاہر ہو رہا ہے، محفوظ کرنے کے بعد اسی طرح نظر آئے گا۔",
        "session_fail_preview": "معذرت! نشست کے مواد میں خامی کی وجہ سے آپ کی  ترمیم مکمل نہیں ہو سکی۔\n\nشاید آپ اپنے کھاتے سے خارج ہو گئے ہیں۔ <strong>براہ کرم اس بات کی تصدیق کر لیں کہ آپ داخل ہیں اور دوبارہ کوشش کریں۔</strong> اگر آپ کو پھر بھی مشکل پیش آرہی ہو تو ایک بار [[Special:UserLogout|خارج ہو کر]] واپس داخل ہو جائیں اور اپنے براؤزر کو جانچ لیں کہ آیا وہ اس سائٹ کی کوکیز اخذ کر رہا ہے یا نہیں۔",
        "session_fail_preview_html": "معاف کیجیے گا!سیشن ڈاٹا کے نقصان کی وجہ سے ہم آپ کی تبدیلی نافذنہیں کرسکتے.\n<em>چونکہ{{SITENAME}}نے نامکملHTMLکوفعال کررکھاہے,اس لئےنمائش کوجاوااسکرپٹ کےحملوں سےحفاظت کےلئےپوشیدہ کردیاگیاہے.</em>\n<strong>اگرتبدیلی کی یہ جدوجہدقانونی ہےتوبرائےمہربانی دوبارہ کوشش کریں.<strong>\nاگراس کےبعدبھی کام نہ کرے تو[[Special:UserLogout|logging out]]لاگ آؤٹ ہوکردوبارہ لاگ ان کیجیے.اورجانچ کیجیے کہ آپ کابراؤزراس سائٹ کےکوکیزکی اجأزت دیتاہےیانہیں.",
+       "token_suffix_mismatch": "<strong>آپ کی ترمیم رد کردی گئی ہے کیونکہ آپ کے اسامی نے ٹوکن ترمیم میں رموزو اوقاف علامتوں کی دھجیاں اڑا دی تھیں۔</strong>\n\nترمیم صفحے کے متن میں خرابی سے روکنے کے لیے رد کی گئی ہے۔\nیہ بعض دفعہ تب ہوتا ہے جب آپ buggy ویب پر مبنی نامعلوم پروکسی سروس کا استعمال کررہے ہوں۔",
        "edit_form_incomplete": "<strong>خانہ ترمیم سے کچھ حصے سرور تک نہیں پہنچ سکے ہیں؛ براہ کرم اپنی ترامیم کو دوبارہ جانچ لیں کہ آیا وہ برقرار ہیں یا نہیں اور دوبارہ کوشش کریں۔</strong>",
        "editing": "آپ \"$1\" میں ترمیم کر رہے ہیں۔",
        "creating": "زیر تخلیق $1",
        "yourtext": "آپ کی تحریر",
        "storedversion": "ذخیرہ شدہ نظرثانی",
        "editingold": "'''انتباہ: آپ اس صفحے کا ایک پرانا مسودہ مرتب کررہے ہیں۔ اگر آپ اسے محفوظ کرتے ہیں تو اس صفحے کے اس پرانے مسودے سے اب تک کی جانے والی تمام تدوین ضائع ہو جاۓ گی۔'''",
+       "unicode-support-fail": "ایسا معلوم ہوتا ہے کہ آپ کا براؤزر یونی کوڈ سپورٹ نہیں کرتا ہے صفحات کو ترمیم کرنا مطلوبہ ہے، اس لیے آپ کی ترمیم محفوظ نہیں کی گئی۔",
        "yourdiff": "تضادات",
        "copyrightwarning": "یہ یادآوری کرلیجیۓ کہ {{SITENAME}} میں تمام تحریری شراکت جی این یو آزاد مسوداتی اجازہ ($2)کے تحت تصور کی جاتی ہے (مزید تفصیل کیلیۓ $1 دیکھیۓ)۔ اگر آپ اس بات سے متفق نہیں کہ آپکی تحریر میں ترمیمات کری جائیں اور اسے آزادانہ (جیسے ضرورت ہو) استعمال کیا جاۓ تو براۓ کرم اپنی تصانیف یہاں داخل نہ کیجیۓ۔ اگر آپ یہاں اپنی تحریر جمع کراتے ہیں تو آپ اس بات کا بھی اقرار کر رہے ہیں کہ، اسے آپ نے خود تصنیف کیا ہے یا دائرہ ءعام (پبلک ڈومین) سے حاصل کیا ہے یا اس جیسے کسی اور آذاد وسیلہ سے۔'''بلااجازت ایسا کام داخل نہ کیجیۓ جسکا حق ِطبع و نشر محفوظ ہو!'''",
        "copyrightwarning2": "براہ کرم اس بات کا خیال رکھیں کہ {{SITENAME}} میں آپ کی جانب سے کی جانے والی تمام ترمیموں میں دیگر صارفین بھی حذف و اضافہ کر سکتے ہیں۔\nاگر آپ اپنی تحریر کے ساتھ اس قسم کے سلوک کے روادار نہیں تو براہ کرم اسے یہاں شائع نہ کریں۔<br />\nنیز اس تحریر کو شائع کرتے وقت آپ ہم سے یہ وعدہ بھی کر رہے ہیں کہ اسے آپ نے خود لکھا ہے یا اسے دائرہ عام یا کسی آزاد ماخذ سے یہاں نقل کر رہے ہیں (تفصیلات کے لیے $1 ملاحظہ فرمائیں)۔\n<strong>براہ کرم اجازت کے بغیر کسی کاپی رائٹ شدہ مواد کو یہاں شائع نہ کریں۔</strong>",
        "editpage-cannot-use-custom-model": "اس صفحہ کے مواد کے ماڈل کو تبدیل نہیں کیا جا سکتا۔",
+       "longpageerror": "<strong>خطا: آپ کا درج کردہ متن {{PLURAL:$1|ایک کلو بائٹ|$1 کلو بائٹز}} لمبا ہے، جو کم سے کم {{PLURAL:$2|ایک کلو بائٹ|$2 کلو بائٹز}} سے زیادہ ہے۔</strong>\nاسے محفوظ نہیں کیا جاسکتا۔",
        "readonlywarning": "<strong>انتباہ: انتظامی نگہداشت کی خاطر ڈیٹابیس کو مقفل کر دیا گیا ہے، لہذا اس وقت آپ اپنی ترامیم کو محفوظ نہیں کر سکتے۔</strong>\nآپ اپنی تحریر کو کسی ٹیکسٹ فائل میں محفوظ کر سکتے ہیں تاکہ وہ ضائع نہ ہو اور آئندہ اسے استعمال کیا جا سکے۔\n\nانتظامیہ کی جانب سے مقفل کرنے کی حسب ذیل وجہ بیان کی گئی ہے:\n\n$1",
        "protectedpagewarning": "<strong>انتباہ: اس صفحہ میں ترمیم کاری کو مقفل کر دیا گیا ہے اور محض انتظامی اختیارات کے حامل صارفین ہی اس میں ترمیم کر سکتے ہیں۔</strong>\nحوالہ کے لیے ذیل میں نوشتہ جاتی اندراج فراہم کیا گیا ہے:",
        "semiprotectedpagewarning": "<strong>اطلاع:</strong> اس صفحہ کو محفوظ کر دیا گیا ہے، لہذا اب اس میں محض اندراج شدہ صارفین ہی ترمیم کر سکتے ہیں۔\nحوالہ کے لیے ذیل میں نوشتہ کا تازہ ترین اندراج درج ہے:",
-       "cascadeprotectedwarning": "<strong>انتباہ:</strong> اس صفحہ میں ترمیم کاری کو مقفل کر دیا گیا ہے اور محض انتظامی اختیارات کے حامل صارفین ہی اس میں ترمیم کر سکتے ہیں۔ اسے مقفل کرنے کی وجہ یہ ہے کہ پیش نظر صفحہ درج ذیل محفوظ {{PLURAL:$1|صفحہ|صفحات}} کی آبشاری حفاظت میں شامل ہے:",
+       "cascadeprotectedwarning": "<strong>انتباہ:</strong> اس صفحہ میں ترمیم کاری کو محفوظ کر دیا گیا ہے اور محض [[Special:ListGroupRights|انتظامی اختیارات]] کے حامل صارفین ہی اس میں ترمیم کر سکتے ہیں۔ اسے محفوظ کرنے کی وجہ یہ ہے کہ پیش نظر صفحہ درج ذیل محفوظ {{PLURAL:$1|صفحہ|صفحات}} کی آبشاری حفاظت میں شامل ہے:",
        "titleprotectedwarning": "<strong>انتباہ: اس صفحہ کو محفوظ کر دیا گیا ہے، چنانچہ اسے تخلیق کرنے کے لیے [[Special:ListGroupRights|خصوصی اختیارات]] درکار ہونگے۔</strong>\nحوالہ کے لیے ذیل میں نوشتہ کا تازہ ترین اندراج موجود ہے:",
        "templatesused": "اِس صفحہ پر مستعمل {{PLURAL:$1|سانچہ|سانچے}}:",
        "templatesusedpreview": "اِس پیش منظر میں مستعمل {{PLURAL:$1|سانچہ|سانچے}}:",
        "nocreate-loggedin": "آپ کو نئے صفحات تخلیق کرنے کی اجازت نہیں ہے.",
        "sectioneditnotsupported-title": "قطعہ کی تدوین حمایت شدہ نہیں ہے",
        "sectioneditnotsupported-text": "اِس صفحہ میں قطعہ کی تدوین حمایت شدہ نہیں ہے.",
-       "permissionserrors": "خطائے اجازت",
+       "permissionserrors": "نقص اجازت",
        "permissionserrorstext": "درج ذیل {{PLURAL:$1|وجہ|وجوہات}} کی بناء پر آپ کو ایسا کرنے کی اجازت نہیں ہے:",
        "permissionserrorstext-withaction": "درج ذیل {{PLURAL:$1|وجہ|وجوہات}} کی بناء پر آپ کو $2  کی اجازت نہیں ہے:",
        "contentmodelediterror": "آپ اس نسخے میں ترمیم نہیں کر سکتے کیونکہ اس کے مواد کا ماڈل ‌‌<code>$1</code> ہے جو اس صفحہ کے مواد کے موجودہ ماڈل <code>$2</code> سے مختلف ہے۔",
        "deprecated-self-close-category-desc": "اس صفحہ میں ایچ ٹی ایم ایل کے نادرست ٹیگ مثلاً <code>&lt;b/></code> or <code>&lt;span/></code> استعمال کیے گئے ہیں۔ چونکہ ایچ ٹی ایم ایل 5 میں ان ٹیگوں کا رویہ تبدیل ہو جائے گا، لہذا ویکی متن میں ان کا استعمال متروک ہو چکا ہے۔",
        "duplicate-args-category": "سانچے میں دوہرے آرگومنٹ کے حامل صفحات",
        "duplicate-args-category-desc": "وہ صفحات جن میں مکرر یا دوہرے آرگومنٹ مستعمل ہیں، مثلاً <code><nowiki>{{foo|bar=1|bar=2}}</nowiki></code> یا <code><nowiki>{{foo|bar|1=baz}}</nowiki></code>۔",
+       "expensive-parserfunction-warning": "<strong>انتباہ:</strong>  یہ صفحہ کئی گراں بہا پارسر فنکشن کالز رکھتا ہے۔\n\nاسے $2 {{PLURAL:$2|کال|کالز}} سے کم ہونا چاہیے، یہاں اب {{PLURAL:$1|$1 کال|$1 کالز}} ہیں۔",
        "expensive-parserfunction-category": "سنگین پارسر فنکشنوں کے بے پناہ استعمال والے صفحات",
        "post-expand-template-inclusion-warning": "<strong>انتباہ:</strong> سانچہ کا حجم بہت زیادہ ہے۔ کچھ سانچے شامل نہیں ہو سکیں گے۔",
        "post-expand-template-inclusion-category": "حجم سے متجاوز سانچوں والے صفحات",
        "post-expand-template-argument-warning": "<strong>انتباہ:</strong> اس صفحہ میں موجود سانچہ کے کم از کم کسی ایک پیرامیٹر کا حجم بہت زیادہ ہے۔\nان پیرامیٹروں کو ترک کر دیا گیا ہے۔",
        "post-expand-template-argument-category": "سانچہ کے ترک کردہ پیرامیٹروں کے حامل صفحات",
        "parser-template-loop-warning": "سانچہ میں تکرار پایا گیا: [[$1]]",
+       "template-loop-category": "صفحات مع لوپ سانچہ",
        "parser-template-recursion-depth-warning": "سانچہ میں تکرار کی گہرائی اپنی حد سے تجاوز کر گئی ($1)",
        "language-converter-depth-warning": "لسانی مبدل کی گہرائی اپنی حد سے تجاوز کر گئی ($1)",
        "node-count-exceeded-category": "گرہوں کی تعداد سے تجاوز کرنے والے صفحات",
        "undo-failure": "درمیان میں متنازع ترامیم کی موجودگی کی بنا پر اس ترمیم کو واپس نہیں پھیرا جا سکا۔",
        "undo-norev": "اس ترمیم کو واپس نہیں پھیرا جا سکا کیونکہ یہ موجود ہی نہیں یا حذف کر دی گئی ہے۔",
        "undo-nochange": "معلوم ہوتا ہے کہ اس ترمیم کو پہلے ہی واپس پھیر دیا گیا ہے۔",
-       "undo-summary": "''[[خاص:شراکتیں/$2|$2]]'' نے ''([[تبادلۂ خیال صارف:$2|تبادلۂ خیال]])'' کی جانب سے کی گئی '''$1''' ویں ترمیم رد کر دی گئی ہے۔",
+       "undo-summary": "[[Special:Contributions/$2|$2]] ([[User talk:$2|تبادلۂ خیال]]) کی جانب سے کی گئی $1ویں ترمیم رد کر دی گئی ہے۔",
        "undo-summary-username-hidden": "پوشیدہ صارف کے نسخہ $1 کو واپس پھیریں",
        "cantcreateaccount-text": "[[User:$3|$3]] نے اس آئی پی پتہ (<strong>$1</strong>) کی کھاتہ سازی پر پابندی لگا رکھی ہے۔\n\n$3 نے «<em>$2</em>» وجہ بیان کی ہے",
        "cantcreateaccount-range-text": "[[User:$3|$3]] نے <strong>$1</strong> رینج کے آئی پی پتوں پر جس میں آپ کا آئی پی پتہ (<strong>$4</strong>) بھی موجود ہے پر پابندی لگا دی ہے۔\n\n$3 نے «<em>$2</em>» وجہ بیان کی ہے",
        "previousrevision": "→ پرانا نسخہ",
        "nextrevision": "→اگلا اعادہ",
        "currentrevisionlink": "تازہ ترین نسخہ",
-       "cur": " رائج",
+       "cur": "موجودہ",
        "next": "آگے",
        "last": "سابقہ",
        "page_first": "پہلا",
        "histlast": "تازہ ترین",
        "historysize": "({{PLURAL:$1|1 بائٹ|$1 بائٹ}})",
        "historyempty": "(خالی)",
-       "history-feed-title": "تارÛ\8cØ®Ú\86Û\82 Ù\86ظرثاÙ\86Û\8c",
+       "history-feed-title": "تارÛ\8cØ®Ú\86Û\81 ØªØ±Ù\85Û\8cÙ\85",
        "history-feed-description": "ویکی پر اِس صفحہ کا تاریخچۂ نظرثانی",
        "history-feed-item-nocomment": "بہ $2 $1",
        "history-feed-empty": "درخواست شدہ صفحہ موجود نہیں.\nیا تو یہ ویکی سے حذف کیا گیا ہے اور یا اِس کا نام تبدیل کردیا گیا ہے.\nآپ متعلقہ نئے صفحات کیلئے [[Special:Search|ویکی پر تلاش]] کرسکتے ہیں.",
        "mergelog": "نوشتہ کا انضمام",
        "revertmerge": "غیر ضم",
        "mergelogpagetext": "ذیل میں ان صفحات کی فہرست ہے جن کے تاریخچے حال ہی میں دوسرے صفحوں میں ضم کیے گئے ہیں۔",
-       "history-title": "\"$1\" کا نظرثانی تاریخچہ",
+       "history-title": "\"$1\" کا ترمیمی تاریخچہ",
        "difference-title": "\"$1\" کے نسخوں کے درمیان فرق",
        "difference-title-multipage": "«$1» اور «$2» صفحوں کے درمیان فرق",
        "difference-multipage": "(فرق مابین صفحات)",
        "search-section": "(قطعہ $1)",
        "search-category": "(زمرہ $1)",
        "search-file-match": "فائل مواد سے ملتا ہے",
-       "search-suggest": "کیا آپ کا مطلب تھا: $1",
+       "search-suggest": "کیا آپ کی مراد «$1» ہے؟",
        "search-rewritten": "$1 کے نتائج کی نمائش، اس کی بجائے آپ $2 کو تلاش کر سکتے ہیں۔",
        "search-interwiki-caption": "ساتھی منصوبوں سے اخذ کردہ نتائج",
        "search-interwiki-default": "$1 نتائج:",
        "search-external": "بیرونی تلاش",
        "searchdisabled": "{{SITENAME}} تلاش غیرفعال.\nآپ فی الحال گوگل کے ذریعے تلاش کرسکتے ہیں.\nیاد رکھئے کہ اُن کے {{SITENAME}} اشاریے ممکناً پرانے ہوسکتے ہیں.",
        "search-error": "تلاش کے دوران میں کوئی نقص واقع ہوا: $1",
+       "search-warning": "تلاش کے دوران میں کوئی انتباہ واقع ہوا: $1",
        "preferences": "ترجیحات",
        "mypreferences": "ترجیحات",
        "prefs-edits": "تعداد ترامیم:",
        "userrights-user-editname": "کوئی اسم‌صارف داخل کیجئے:",
        "editusergroup": "حلقہ ہائے صارف دکھائیں",
        "editinguser": "{{GENDER:$1|صارف}} <strong>[[User:$1|$1]]</strong> $2 کے اختیارات میں تبدیلی",
+       "viewinguserrights": "{{GENDER:$1|صارف}} <strong>[[User:$1|$1]]</strong> $2 کے اختیارات میں تبدیلی",
        "userrights-editusergroup": "حلقہ ہائے {{GENDER:$1|صارف}} میں ترمیم کریں",
        "userrights-viewusergroup": "جائزہ {{GENDER:$1|صارف}} گروہان",
        "saveusergroups": "حلقہ ہائے {{GENDER:$1|صارف}} کو محفوظ کریں",
        "action-deleterevision": "یہ نسخہ حذف کرنے",
        "action-deletelogentry": "نوشتے کے اندراجات کو حذف کریں",
        "action-deletedhistory": "اس صفحہ کا حذف شدہ تاریخچہ دیکھیں",
+       "action-deletedtext": "حذف کیے گئے متن کا فرق دیکھیے۔",
        "action-browsearchive": "حذف شدہ صفحات میں تلاش کرنے",
        "action-undelete": "یہ صفحہ بحال کرنے",
        "action-suppressrevision": "اس پوشیدہ ترمیم کی نظرثانی اور بحال کرنے",
        "recentchanges-legend": "اِختیاراتِ حالیہ تبدیلیاں",
        "recentchanges-summary": "اس صفحے پر ویکی میں ہونے والی تازہ تریں تبدیلیوں کا مشاہدہ کریں۔",
        "recentchanges-noresult": "مقررہ مدت کے دوران میں اس معیار سے مشابہت رکھنے والی کوئی تبدیلی نہیں ہوئی۔",
+       "recentchanges-timeout": "اس تلاش کا وقت ختم ہوگیا ہے۔ آپ پیرامیٹروں کی مختلف تلاش کرسکتے ہیں۔",
+       "recentchanges-network": "تکنیکی خطاؤں کی وجوہات کی بنا پر کسی قسم کے نتیجے لوڈ نہیں ہوئے۔ براہ مہربانی صفحہ کو تازہ کر کے کوشش کریں۔",
        "recentchanges-feed-description": "اس فیڈ میں ویکی پر ہونے والی تازہ تریں تبدیلیوں کا مشاہدہ کریں۔",
        "recentchanges-label-newpage": "یہ ترمیم ایک نئے صفحے کی تخلیق ہے",
        "recentchanges-label-minor": "یہ ایک معمولی ترمیم ہے",
        "rcfilters-savedqueries-apply-and-setdefault-label": "طے شدہ فلٹر بنائیں",
        "rcfilters-savedqueries-cancel-label": "منسوخ کریں",
        "rcfilters-savedqueries-add-new-title": "فلٹر کی موجودہ ترتیبات محفوظ کریں",
+       "rcfilters-savedqueries-already-saved": "یہ فلٹرز پھلے سے محفوظ کر دیۓ گۓ ہیں۔نۓ فحفوظ شدہ فلٹرز بنانے کے لیۓاپنی ترکیب تبدیل کریں۔",
        "rcfilters-restore-default-filters": "طے شدہ فلٹر بحال کریں",
        "rcfilters-clear-all-filters": "تمام فلٹروں کو ہٹائیں",
        "rcfilters-show-new-changes": "تازہ ترین تبدیلیاں دیکھیں",
-       "rcfilters-search-placeholder": "حاÙ\84Û\8cÛ\81 ØªØ¨Ø¯Û\8cÙ\84Û\8cÙ\88Úº Ú©Ø§ Ù\81Ù\84ٹر (تÙ\84اش Û\8cا ØªØ­Ø±Û\8cر کریں)",
+       "rcfilters-search-placeholder": "تبدÛ\8cÙ\84Û\8cÙ\88Úº Ú©Ù\88 Ù\81Ù\84ٹر Ú©Ø±Û\8cÚº (Ù\81Ù\84ٹر Ú©Ø§ Ù\86اÙ\85 Ø¬Ø§Ù\86Ù\86Û\92 Ú©Û\92 Ù\84Û\8cÛ\92 Ù\85Û\8cÙ\86Ù\88 Ø§Ø³ØªØ¹Ù\85اÙ\84 Ú©Ø±Û\8cÚº Û\8cا ØªÙ\84اش کریں)",
        "rcfilters-invalid-filter": "نادرست فلٹر",
        "rcfilters-empty-filter": "کوئی فلٹر فعال نہیں ہے چنانچہ تمام شراکتیں دکھائی جا رہی ہیں۔",
        "rcfilters-filterlist-title": "فلٹروں کی فہرست",
        "rcshowhideliu": "مندرج صارفین $1",
        "rcshowhideliu-show": "دکھائیں",
        "rcshowhideliu-hide": "چھپائیں",
-       "rcshowhideanons": "گمنام صارف $1",
+       "rcshowhideanons": "گمنام صارفین $1",
        "rcshowhideanons-show": "دکھائیں",
        "rcshowhideanons-hide": "چھپائیں",
        "rcshowhidepatr": "$1 مراجعت شدہ ترامیم",
        "uploadbtn": "فائل اپلوڈ کریں",
        "reuploaddesc": "اپلوڈ منسوخ کرکے اپلوڈ فارم کی جانب واپس جائیں",
        "upload-tryagain": "فائل کی تبدیل شدہ وضاحت روانہ کریں",
+       "upload-tryagain-nostash": "دوبارہ اپلوڈ فائل درج اور وضاحت کی تبدیلی کریں۔",
        "uploadnologin": "آپ داخل شدہ حالت میں نہیں",
        "uploadnologintext": "فائلیں اپلوڈ کرنے کے لیے براہ کرم $1 ہوں",
        "upload_directory_missing": "اپلوڈ فولڈر ($1) موجود نہیں اور ویب سرور کے ذریعہ اسے تخلیق نہیں کیا جا سکا۔",
        "file-deleted-duplicate-notitle": "اس فائل سے ملتی جلتی دوسری فائل کو پہلے حذف کیا اور اس عنوان کو ممنوع قرار دیا جا چکا ہے۔\nاسے دوبارہ اپلوڈ کرنے سے قبل کسی ایسے شخص سے اس صورت حال کا جائزہ لینے کی درخواست کریں جسے ممنوع فائلوں کی معلومات تک رسائی حاصل ہو۔",
        "uploadwarning": "اپلوڈ انتباہ",
        "uploadwarning-text": "ذیل میں موجود فائل کی وضاحت میں تبدیلی کریں اور دوبارہ کوشش کریں۔",
+       "uploadwarning-text-nostash": "براہ مہربانی فائل دوبارہ اپلوڈ کریں، درجہ ذیل وضاحت کی تبدیلی کریں اور دوبارہ کوشش کریں۔",
        "savefile": "فائل محفوظ کریں",
        "uploaddisabled": "اپلوڈ غیر فعال ہے۔",
        "copyuploaddisabled": "بذریعہ یوآرایل اپلوڈ غیر فعال ہے۔",
        "php-uploaddisabledtext": "پی ایچ پی کی فائلیں اپلوڈ نہیں کی جا سکتیں۔\nبراہ کرم  file_uploads کی ترتیبات جانچ لیں۔",
        "uploadscripted": "اس فائل میں ایچ ٹی ایم ایل یا اسکرپٹ کوڈ کا استعمال کیا گیا ہے لہذا عین ممکن ہے کہ کوئی ویب براؤزر اس کی غلط تشریح کرے۔",
        "upload-scripted-pi-callback": "ایسی کسی فائل کو اپلوڈ نہیں کیا جا سکتا جس میں ایکس ایم ایل اسٹائل شیٹ پر عمل کرنے کی ہدایت ہو۔",
+       "upload-scripted-dtd": "SVG فائل کو اپلوڈ نہیں کیا جاسکتا جس ميں ایک غیر معیاری DTD کا اظہار ہو۔",
+       "uploaded-script-svg": "اپلوڈ کردہ SVG فائل میں scriptable عنصر \"$1\" ملا۔",
        "uploaded-hostile-svg": "اپلوڈ کردہ ایس وی جی فائل کے اسٹائل عنصر میں غیر محفوظ سی ایس ایس دریافت ہوئی ہے۔",
        "uploadscriptednamespace": "اس ایس وی جی فائل میں غیر قانونی نام فضا \"<nowiki>$1</nowiki>\" موجود ہے۔",
        "uploadinvalidxml": "اپلوڈ کردہ فائل میں موجود ایکس ایم ایل کا تجزیہ نہیں کیا جا سکا۔",
        "uploadstash-refresh": "فائلوں کی فہرست کو تازہ کریں",
        "uploadstash-thumbnail": "تھمب نیل دیکھیں",
        "uploadstash-exception": "اپلوڈ کردہ کو نہاں خانہ ($1) میں رکھا نہ جا سکا: ''$2''۔",
+       "uploadstash-bad-path": "راہ موجود نہیں ہے۔",
+       "uploadstash-bad-path-invalid": "راہ درست نہیں ہے۔",
+       "uploadstash-bad-path-unknown-type": "نامعلوم قسم \"$1\"",
+       "uploadstash-bad-path-unrecognized-thumb-name": "غیر معروف نام تصغیر",
+       "uploadstash-no-extension": "توسیع نہیں ہے۔",
+       "uploadstash-zero-length": "فائل کا طول صفر ہے۔",
        "invalid-chunk-offset": "آفسیٹ کا قطعہ نادرست ہے",
        "img-auth-accessdenied": "رسائی معطل",
        "img-auth-nopathinfo": "PATH_INFO مفقود ہے۔\nآپ کے سرور کو اس معلومات کی ترسیل کے لیے مرتب نہیں کیا گیا ہے۔\nممکن ہے یہ سی جی آئی پر مبنی ہو اور img_auth کو قبول نہ کرتا ہو۔\nبراہ کرم https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization کو ملاحظہ کریں۔",
        "mostimages": "سب سے زیادہ استعمال کردہ تصاویر",
        "mostinterwikis": "کثیر اندرونی ربط والے صفحات",
        "mostrevisions": "زیادہ تجدید نظر کیے جانے والے صفحات",
-       "prefixindex": "تمام صفحات بمع سابقہ",
+       "prefixindex": "تمام صفحات مع سابقہ",
        "prefixindex-namespace": "($1 نام فضا) کے سابقہ کے ساتھ تمام صفحات",
        "prefixindex-submit": "دکھائیں",
        "prefixindex-strip": "نام فضا کے سابقہ کے بغیر نتائج",
        "apisandbox-loading-results": "اے پی آئی کے نتائج موصول ہو رہے ہیں۔۔۔",
        "apisandbox-results-error": "اے پی آئی کوئری کا جواب لوڈ ہونے کے دوران میں نقص واقع ہوا: $1",
        "apisandbox-request-url-label": "درخواست کا ربط:",
+       "apisandbox-request-json-label": "JSON درخواست:",
        "apisandbox-request-time": "درخواست کا وقت: {{PLURAL:$1|$1 ملی سیکنڈ}}",
        "apisandbox-results-fixtoken": "ٹوکن کو درست کرکے دوبارہ بھیجیں",
        "apisandbox-results-fixtoken-fail": "\"$1\" ٹوکن اخذ کرنے میں ناکامی۔",
        "apisandbox-alert-page": "اس صفحہ میں موجود خانے نادرست ہیں۔",
        "apisandbox-alert-field": "اس خانے کی قدر نادرست ہے۔",
+       "apisandbox-continue": "جاری رکھیں",
+       "apisandbox-continue-clear": "صاف کریں",
+       "apisandbox-multivalue-all-namespaces": "$1 (تمام نام فضا)",
+       "apisandbox-multivalue-all-values": "$1 (تمام قدریں)",
        "booksources": "کتابی وسائل",
-       "booksources-search-legend": "تلاش برائے مآخذاتِ کتاب",
+       "booksources-search-legend": "تلاش برائے مآخذ کتاب",
        "booksources-search": "تلاش",
        "booksources-invalid-isbn": "درج کردہ آئی ایس بی این درست نہیں معلوم ہوتا؛ اصل ماخذ سے نقل کے دوران میں ہوئی غلطیوں کو جانچ لیں۔",
+       "magiclink-tracking-rfc": "صفحات مع RFC جادوئی روابط",
+       "magiclink-tracking-pmid": "صفحات مع PMID جادوئی روابط",
+       "magiclink-tracking-isbn": "صفحات مع ISBN جادوئی روابط",
        "specialloguserlabel": "صارف:",
        "speciallogtitlelabel": "ہدف (عنوان یا {{ns:user}}:صارف نام برائے صارف):",
        "log": "نوشتہ جات",
        "logeventslist-submit": "دکھائیں",
        "all-logs-page": "تمام عوامی نوشتہ جات",
+       "alllogstext": "{{SITENAME}} کے تمام دستیاب لاگز کا پیوستہ ڈسپلے۔\nآپ اور باریکی کے لیے لاگ کی قسم، صارف نام (حساس معاملہ)، یا متاثرہ صفحہ (یہ بھی حساس معاملہ) چن سکتے ہیں۔",
        "logempty": "نوشتہ میں اس سے مشابہ کوئی اندراج موجود نہیں ہے۔",
        "log-title-wildcard": "اس عبارت سے شروع ہونے والے عناوین میں تلاش کریں",
        "showhideselectedlogentries": "نوشتہ کے منتخب اندراج کی مرئیت تبدیل کریں",
        "prevpage": "پچھلا صفحہ ($1)",
        "allpagesfrom": "اس حرف سے شروع ہونے والے صفحات دکھائیں:",
        "allpagesto": "اس حرف پر ختم ہونے والے صفحات دکھائیں:",
-       "allarticles": "تمام مقالات",
+       "allarticles": "تمام صفحات",
        "allinnamespace": "تمام صفحات ($1 نام فضا)",
        "allpagessubmit": "چلیں",
        "allpagesprefix": "مطلوبہ سابقہ سے شروع ہونے والے صفحات کی نمائش:",
        "cachedspecial-viewing-cached-ttl": "آپ اس وقت اس صفحہ کا کیشے شدہ نسخہ دیکھ رہے ہیں جو ممکن ہے $1 پرانا ہو۔",
        "cachedspecial-viewing-cached-ts": "آپ اس وقت اس صفحہ کا کیشے شدہ نسخہ دیکھ رہے ہیں جو شاید مکمل طور پر اصلی نہ ہو۔",
        "cachedspecial-refresh-now": "تازہ ترین دیکھیں۔",
-       "categories": "زمرہ",
+       "categories": "زمرہ جات",
        "categories-submit": "دکھائیں",
        "categoriespagetext": "ذیل میں موجود {{PLURAL:$1|زمرہ|زمرہ جات}} میں صفحات یا میڈیا موجود ہے۔\n[[Special:UnusedCategories|غیر مستعمل زمرہ جات]] یہاں نہیں دکھائے گئے ہیں۔\nنیز [[Special:WantedCategories|مطلوبہ زمرہ جات کی فہرست]] بھی ملاحظہ فرمائیں۔",
        "categoriesfrom": "اس حرف سے شروع ہونے والے زمرے دکھائیں:",
        "activeusers-intro": "ذیل میں ان صارفین کی فہرست ہے جو گزشتہ $1 {{PLURAL:$1|دن|دنوں}} میں کسی بھی قسم کی سرگرمی میں شریک رہے ہوں۔",
        "activeusers-count": "گزشتہ {{PLURAL:$3|دن|$3 دنوں}} میں $1 {{PLURAL:$1|ترمیم|ترامیم}}",
        "activeusers-from": "اس حرف سے شروع ہونے والے صارفین کے نام دکھائیں:",
+       "activeusers-groups": "حسب ذیل حلقوں سے متعلق صارفین کو دکھائیں:",
        "activeusers-excludegroups": "ان حلقوں سے متعلق صارفین کو مستثنیٰ کریں:",
        "activeusers-noresult": "یہ صارف نہیں مل سکا",
        "activeusers-submit": "فعال صارفین دکھائیں",
        "editcomment": "خلاصہ ترمیم یہ تھا: <em>«$1»</em>.",
        "revertpage": "[[Special:Contributions/$2|$2]] ([[User talk:$2|تبادلۂ خیال]]) کی ترامیم [[User:$1|$1]] کی گذشتہ ترمیم کی جانب واپس پھیر دی گئیں۔",
        "revertpage-nouser": "(حذف شدہ صارف نام) کی ترامیم {{GENDER:$1|[[User:$1|$1]]}} کی گذشتہ ترمیم کی جانب واپس پھیر دی گئیں",
-       "rollback-success": "$1 کی ترامیم واپس پھیر دی گئیں؛\nصفحہ واپس $2 کی آخری ترمیم کی جانب منتقل کر دیا گیا۔",
+       "rollback-success": "{{GENDER:$3|$1}} کی ترامیم واپس پھیر دی گئیں؛\nصفحہ واپس {{GENDER:$4|$2}} کی آخری ترمیم کی جانب منتقل کر دیا گیا۔",
        "rollback-success-notify": "$1 کی ترامیم واپس پھیر دی گئیں؛\nصفحہ واپس $2 کی آخری ترمیم کی جانب منتقل کر دیا گیا۔ [$3 تبدیلیاں دکھائیں]",
        "sessionfailure-title": "نشست میں خامی",
        "changecontentmodel": "صفحہ کے مواد کے ماڈل میں تبدیلی کریں",
        "modifiedarticleprotection": "«[[$1]]» کا درجہ حفاظت تبدیل کیا",
        "unprotectedarticle": "«[[$1]]» کو غیر محفوظ کیا",
        "movedarticleprotection": "نے \"[[$2]]\" کا درجہ حفاظت \"[[$1]]\" کی جانب منتقل کیا",
+       "protectedarticle-comment": "«[[$1]]» کو {{GENDER:$2|محفوظ کیا}}",
+       "modifiedarticleprotection-comment": "«[[$1]]» کا {{GENDER:$2|درجہ حفاظت تبدیل کیا}}",
+       "unprotectedarticle-comment": "«[[$1]]» کو {{GENDER:$2|غیر محفوظ کیا}}",
        "protect-title": "«$1» کا درجہ حفاظت تبدیل کریں",
        "protect-title-notallowed": "«$1» کا درجہ حفاظت دیکھیں",
        "prot_1movedto2": "[[$1]] بجانب [[$2]] منتقل",
        "undelete-header": "حالیہ حذف شدہ صفحات کے لیے [[Special:Log/delete|نوشتۂ حذف شدگی]] دیکھیں۔",
        "undelete-search-title": "حذف شدہ صفحات میں تلاش کریں",
        "undelete-search-box": "حذف شدہ صفحات میں تلاش کریں",
-       "undelete-search-prefix": "اظہار صفحات بآغاز از:",
+       "undelete-search-prefix": "حسب ذیل حروف سے شروع ہونے والے صفحات دکھائیں:",
+       "undelete-search-full": "حسب ذیل عبارت پر مشتمل صفحے کے عناوین دکھائیں:",
        "undelete-search-submit": "تلاش",
        "undelete-no-results": "حذف شدہ صفحات میں ایسا کوئی صفحہ نہیں ملا",
        "undelete-filename-mismatch": " $1 کے نسخے کو بحال نہیں کیا جا سکتا: فائل کا نام مطابقت نہیں رکھتا۔",
        "sp-contributions-uploads": "اپلوڈ کردہ",
        "sp-contributions-logs": "نوشتہ جات",
        "sp-contributions-talk": "تبادلۂ خیال",
-       "sp-contributions-userrights": "انتظام اختیارات صارف",
+       "sp-contributions-userrights": "انتظام اختیارات {{GENDER:$1|صارف}}",
        "sp-contributions-blocked-notice": "اس صارف پر پابندی لگائی گئی ہے۔\nحوالہ کے لیے نوشتہ پابندی کا تازہ ترین اندراج ذیل میں دستیاب ہے:",
        "sp-contributions-blocked-notice-anon": "اس آئی پی پتے پر پابندی لگا دی گئی ہے۔\nحوالہ کے لیے نوشتہ پابندی کا تازہ ترین اندراج ذیل میں دستیاب ہے:",
        "sp-contributions-search": "شراکتوں میں تلاش کریں",
        "whatlinkshere-hidetrans": "استعمالات $1",
        "whatlinkshere-hidelinks": "روابط $1",
        "whatlinkshere-hideimages": "تصویر کے روابط $1",
-       "whatlinkshere-filters": "Ù\85Ù\82طارات",
+       "whatlinkshere-filters": "مقطرات",
        "whatlinkshere-submit": "ٹھیک",
        "autoblockid": "خودکار پابندی #$1",
        "block": "صارف مسدود کریں",
        "blockipsuccesssub": "پابندی لگا دی گئی",
        "blockipsuccesstext": "[[Special:Contributions/$1|$1]] پر پابندی لگادی گئی۔<br />\nپابندیوں پر نظر ثانی کے لیے [[Special:BlockList|فہرست پابندی]] دیکھیں۔",
        "ipb-blockingself": "آپ اپنے آپ پر پابندی لگانے جا رہے ہیں! کیا آپ واقعی ایسا کرنا چاہتے ہیں؟",
+       "ipb-confirmaction": "اگر آپ واقعی میں یہ کرنا چاہتے ہیں تو براہ مہربانی \"{{int:ipb-confirm}}\" کو آخر میں دیکھ لیں۔",
        "ipb-edit-dropdown": "وجوہات پابندی میں ترمیم کریں",
        "ipb-unblock-addr": "$1 سے پابندی ہٹائیں",
        "ipb-unblock": "صارف نام یا آئی پی پتے سے پابندی ہٹائیں",
        "blocklist": "ممنوع صارفین",
        "autoblocklist": "خودکار پابندیاں",
        "autoblocklist-submit": "تلاش",
+       "autoblocklist-legend": "خودکار پابندیوں کی فہرست",
+       "autoblocklist-localblocks": "{{PLURAL:$1|خودکار مقامی پابندی|خودکار مقامی پابندیاں}}",
+       "autoblocklist-total-autoblocks": "خودکار پابندیوں کی کُل تعداد: $1",
+       "autoblocklist-empty": "خودکار پابندی کی فہرست خالی ہے۔",
+       "autoblocklist-otherblocks": "دیگر {{PLURAL:$1|خودکار مقامی پابندی|خودکار مقامی پابندیاں}}",
        "ipblocklist": "ممنوع صارفین",
        "ipblocklist-legend": "ممنوع صارف کو تلاش کریں",
        "blocklist-userblocks": "کھاتے کی پابندیاں چھپائیں",
        "ipb_blocked_as_range": "نقص: آئی پتا $1 پر براہ راست پابندی نہیں ہے اور اس کی پابندی ختم نہیں کی جا سکتی۔\nتاہم اس آئی پی پر $2 رینج کے ایک حصے کے طور پر پابندی لگائی گئی ہے، چنانچہ اس رینج کی پابندی ختم کی جا سکتی ہے۔",
        "ip_range_invalid": "آئی پی پتے کی رینج نادرست ہے۔",
        "ip_range_toolarge": "/$1 سے زیادہ بڑی رینج پابندیوں کی اجازت نہیں ہے۔",
+       "ip_range_exceeded": "آئی پی رینج منتہا حد سے تجاوز کر گئی ہے۔ مقررہ رینج: /$1۔",
+       "ip_range_toolow": "آئی پی رینجوں کی مؤثر طور پر اجازت نہیں ہے۔",
        "proxyblocker": "پراکسی مسدود کنندہ",
        "proxyblockreason": "آپ کے آئی پی پتے پر پابندی لگا دی گئی ہے کیونکہ یہ اوپن پراکسی ہے۔\nبراہ کرم انٹرنیٹ خدمات فراہم کرنے والے یا اپنی تنظیم کے تکنیکی معاون سے رابطہ کریں اور انہیں اس سنجیدہ مسئلہ سے آگاہ کریں۔",
        "sorbsreason": "{{SITENAME}} کے زیر استعمال DNSBL میں آپ کا آئی پی پتا اوپن پراکسی کے طور پر درج فہرست ہے۔",
        "sorbs_create_account_reason": "{{SITENAME}} کے زیر استعمال DNSBL میں آپ کا آئی پی پتا اوپن پراکسی کے طور پر درج فہرست ہے۔\nآپ کھاتہ نہیں بنا سکتے۔",
+       "softblockrangesreason": "آپ کے آئی پی پتے ($1) سے نامعلوم شراکتوں کی اجازت نہیں۔ پراہ مہربانی لاگ ان ہوں۔",
        "ipbblocked": "آپ دیگر صارفین پر پابندی لگا یا ہٹا نہیں سکتے کیونکہ خود آپ پر پابندی عائد کی گئی ہے۔",
        "ipbnounblockself": "آپ کو اپنی ذات سے پابندی ہٹانے کی اجازت نہیں ہے۔",
        "lockdb": "ڈیٹابیس مقفل کریں",
        "cant-move-to-user-page": "کسی صفحہ کو کسی صارف صفحہ میں منتقل کرنے کی اجازت نہیں ہے (صارف کا ذیلی صفحہ اس سے مستثنی ہے)۔",
        "cant-move-category-page": "آپ کو زمرہ جات منتقل کرنے کی اجازت نہیں ہے۔",
        "cant-move-to-category-page": "کسی صفحہ کو زمرے میں منتقل کرنے کی اجازت نہیں ہے۔",
+       "cant-move-subpages": "آپ کو ذیلی صفحات منتقل کرنے کی اجازت نہیں ہے۔",
+       "namespace-nosubpages": "«$1» نام فضا ميں ذیلی صفحات کی اجازت نہیں ہے۔",
        "newtitle": "نـیــا عـنــوان:",
        "move-watch": "اصل اور ہدف صفحہ کو زیر نظر کریں",
        "movepagebtn": "مـنـتـقـل",
        "tooltip-pt-mycontris": "آپ کی شراکتوں کی فہرست",
        "tooltip-pt-anoncontribs": "اس آئی پی پتے سے انجام دی جانے والی تمام ترامیم کی فہرست",
        "tooltip-pt-login": "کھاتے میں داخل ہونے کی سفارش کی جاتی ہے؛ تاہم یہ ضروری نہیں",
+       "tooltip-pt-login-private": "اس ویکی کو استعمال کرنے کے لیے اپنے کھاتے میں داخل ہونا ضروری ہے",
        "tooltip-pt-logout": "خارج ہوجائیں",
        "tooltip-pt-createaccount": "کھاتہ بنانے یا اس میں داخل ہونے کی سفارش کی جاتی ہے؛ تاہم یہ ضروری نہیں",
        "tooltip-ca-talk": "مضمون کے متعلق گفتگو کریں",
        "log-show-hide-patrol": "$1 نوشتہ مراجعت",
        "log-show-hide-tag": "$1 نوشتہ ٹیگ",
        "confirm-markpatrolled-button": "ٹھیک ہے",
+       "confirm-markpatrolled-top": "$2 کے نسخہ $3 کو بطور مراجعت شدہ نشان زد کریں؟",
        "deletedrevision": "حذف شدہ پرانی ترمیم $1۔",
        "filedeleteerror-short": "فائل حذف کاری میں نقص: $1",
        "filedeleteerror-long": "فائل حذف کرنے کے دوران میں نقص واقع ہوا:\n\n$1",
        "filedelete-current-unregistered": "«$1» کے عنوان سے کوئی فائل ڈیٹابیس میں موجود نہیں ہے۔",
        "filedelete-archive-read-only": "$1 کی وثق ڈائرکٹری میں ویب سرور نہیں لکھ پا رہا ہے۔",
        "previousdiff": "→ پرانی ترمیم",
-       "nextdiff": "صفحہ کا نام:",
+       "nextdiff": "نئی ترمیم ←",
        "mediawarning": "<strong>انتباہ:</strong> شاید اس نوع کی فائل میں نقصان دہ کوڈ موجود ہے۔\nممکن ہے اسے چلانے پر آپ کا سسٹم مشکوک ہاتھوں میں چلا جائے۔",
        "imagemaxsize": "تصویر کی جسامت کی حد:<br /><em>(فائل کے توضیحی صفحات کے لیے)</em>",
        "thumbsize": "تھمب نیل کی جسامت:",
        "newimages-legend": "مقطار",
        "newimages-label": "فائل کا نام (یا اس کا جزو):",
        "newimages-user": "آئی پی پتہ یا صارف نام",
+       "newimages-newbies": "محض نئے کھاتوں کی شراکتیں دکھائیں",
        "newimages-showbots": "روبہ جات کے ذریعہ اپلوڈ کردہ فائلیں دکھائیں",
        "newimages-hidepatrolled": "مراجعت شدہ اپلوڈ چھپائیں",
        "newimages-mediatype": "میڈیا قسم:",
        "tags-deactivate": "غیر فعال  کریں",
        "tags-hitcount": "$1 {{PLURAL:$1|تبدیلی|تبدیلیاں}}",
        "tags-manage-no-permission": "آپ کو تبدیلی کے ٹیگوں کے انتظام کی اجازت نہیں ہے۔",
-       "tags-manage-blocked": "آپ بحالت پابندی تبدیلی کے ٹیگوں کا انتظام نہیں کر سکتے۔",
+       "tags-manage-blocked": "{{GENDER:$1|آپ}} بحالت پابندی تبدیلی کے ٹیگوں کا انتظام نہیں کر سکتے۔",
        "tags-create-heading": "نیا ٹیگ بنائیں",
        "tags-create-explanation": "ابتدائی طور پر نو تخلیق شدہ ٹیگ صارفین اور روبہ جات کے استعمال کے لیے دستیاب ہونگے۔",
        "tags-create-tag-name": "ٹیگ کا نام:",
        "tags-deactivate-not-allowed": "«$1» ٹیگ کو غیر فعال کرنا ممکن نہیں۔",
        "tags-deactivate-submit": "غیر فعال",
        "tags-apply-no-permission": "آپ کو اپنی تبدیلیوں پر تبدیلی کے ٹیگوں کو نافذ کرنے کی اجازت نہیں ہے۔",
-       "tags-apply-blocked": "بحالت پابندی آپ اپنی تبدیلیوں پر تبدیلی کے ٹیگ نافذ نہیں کر سکتے۔",
+       "tags-apply-blocked": "بحالت پابندی {{GENDER:$1|آپ}} اپنی تبدیلیوں پر تبدیلی کے ٹیگ نافذ نہیں کر سکتے۔",
        "tags-apply-not-allowed-one": "«$1» ٹیگ کو دستی طور پر نافذ کرنے کی اجازت نہیں ہے۔",
        "tags-apply-not-allowed-multi": "درج ذیل {{PLURAL:$2|ٹیگ|ٹیگوں}} کو دستی طور پر نافذ کرنے کی اجازت نہیں ہے: $1",
        "tags-update-no-permission": "آپ کو انفرادی نسخوں یا نوشتہ کے اندراجات سے تبدیلی کے ٹیگوں کو ہٹانے یا ان میں لگانے کی اجازت نہیں ہے۔",
-       "tags-update-blocked": "بحالت پابندی آپ تبدیلی کے ٹیگوں کو لگا یا ہٹا نہیں سکتے۔",
+       "tags-update-blocked": "بحالت پابندی {{GENDER:$1|آپ}} تبدیلی کے ٹیگوں کو لگا یا ہٹا نہیں سکتے۔",
        "tags-update-add-not-allowed-one": "«$1» ٹیگ کو دستی طور پر لگانے کی اجازت نہیں ہے۔",
        "tags-update-add-not-allowed-multi": "درج ذیل {{PLURAL:$2|ٹیگ|ٹیگوں}} کو دستی طور پر لگانے کی اجازت نہیں ہے: $1",
        "tags-update-remove-not-allowed-one": "«$1» کو ہٹانے کی اجازت نہیں ہے۔",
        "compare-title-not-exists": "آپ کا اختصاصی عنوان موجود نہیں۔",
        "compare-revision-not-exists": "آپ کی اختصاصی نظرثانی موجود نہیں۔",
        "diff-form": "فرق",
+       "diff-form-oldid": "پرانا نسخہ شناخت (اختیاری)",
+       "diff-form-revid": "نسخہ شناخت کا فرق",
+       "diff-form-submit": "فرق دکھائیں",
        "permanentlink": "مستقل ربط",
+       "permanentlink-revid": "نسخہ کا شناختی نمبر",
+       "permanentlink-submit": "نسخہ کو دیکھیں",
        "dberr-problems": "افسوس! اس ویب سائٹ کو تکنیکی مشکلات کا سامنا ہے۔",
        "dberr-again": "چند منٹ انتظار کے بعد دوبارہ کوشش کریں۔",
        "dberr-info": "(ڈیٹا بیس تک رسائی نہیں مل سکی: $1)",
        "logentry-delete-delete": "$1 {{GENDER:$2|حذف کیا گیا}} صفحہ $3",
        "logentry-delete-delete_redir": "$1 نے بر تحریر کرتے ہوئے $3 رجوع مکرر کو {{GENDER:$2|حذف کیا}}",
        "logentry-delete-restore": "$1 نے صفحہ $3 کو {{GENDER:$2|بحال کیا}}",
+       "logentry-delete-restore-nocount": "$1 نے صفحہ $3 کو {{GENDER:$2|بحال کیا}}",
+       "restore-count-revisions": "{{PLURAL:$1|1 نسخہ|$1 نسخے}}",
+       "restore-count-files": "{{PLURAL:$1|1 فائل|$1 فائلیں}}",
        "logentry-delete-event": "$1 نے $3 میں موجود {{PLURAL:$5|ایک واقعۂ نوشتہ|$5 واقعات نوشتہ}} کی مرئیت کو {{GENDER:$2|تبدیل کیا}}: $4",
        "logentry-delete-revision": "$1 نے $3 میں موجود {{PLURAL:$5|ایک نسخے|$5 نسخوں}} کی مرئیت کو {{GENDER:$2|تبدیل کیا}}: $4",
        "logentry-delete-event-legacy": "$1 نے $3 میں موجود واقعات نوشتہ کی مرئیت کو {{GENDER:$2|تبدیل کیا}}",
        "logentry-patrol-patrol": "$1 نے صفحہ $3 کے نسخہ $4 کو مراجعت شدہ {{GENDER:$2|نشان زد کیا}}",
        "logentry-patrol-patrol-auto": "$1 نے صفحہ $3 کے نسخہ $4 کو خودکار طور پر مراجعت شدہ {{GENDER:$2|نشان زد کیا}}",
        "logentry-newusers-newusers": "صارف کھاتہ $1 {{GENDER:$2|تخلیق ہو چکا ہے}}",
-       "logentry-newusers-create": "صارف کھاتہ $1 {{GENDER:$2|بنایا گیا}}",
+       "logentry-newusers-create": "$1 کے نام سے صارف کھاتہ {{GENDER:$2|بنایا گیا}}",
        "logentry-newusers-create2": "$1 نے صارف کھاتہ $3 {{GENDER:$2|تخلیق کیا}}",
        "logentry-newusers-byemail": "$1 نے صارف کھاتہ $3 {{GENDER:$2|تخلیق کیا}} اور پاس ورڈ بذریعہ برقی خط روانہ کیا گیا ہے",
        "logentry-newusers-autocreate": "صارف کھاتہ $1 خودکار طور پر {{GENDER:$2|تخلیق ہوا}}",
        "logentry-tag-update-revision": "$1 نے صفحہ $3 کے نسخہ $4 پر موجود ٹیگوں کو {{GENDER:$2|تازہ کیا}} ({{PLURAL:$7|شامل کیا گیا|شامل کیے گئے}} $6؛ {{PLURAL:$9|حذف کیا گیا|حذف کیے گئے}} $8)",
        "logentry-tag-update-logentry": "$1 نے صفحہ $3 کے اندراج نوشتہ $5 پر موجود ٹیگوں کو {{GENDER:$2|تازہ کیا}} ({{PLURAL:$7|شامل کیا گیا|شامل کیے گئے}} $6؛ {{PLURAL:$9|حذف کیا گیا|حذف کیے گئے}} $8)",
        "rightsnone": "(کچھ نہیں)",
+       "rightslogentry-temporary-group": "$1 (عارضی، تا $2)",
        "feedback-adding": "صفحہ میں تبصرہ درج کیا جا رہا ہے۔۔۔",
        "feedback-back": "واپس",
        "feedback-bugcheck": "زبردست! جانچ لیں کہ کہیں پہلے ہی [$1 اس کی اطلاع نہ دے دی گئی ہو]۔",
        "pagelang-select-lang": "زبان کا انتخاب کریں",
        "pagelang-reason": "وجہ",
        "pagelang-submit": "ٹھیک ہے",
+       "pagelang-nonexistent-page": "صفحہ $1 موجود نہیں ہے۔",
+       "pagelang-db-failed": "ڈیٹابیس زبان کو تبدیل کرنے میں ناکام رہا۔",
        "right-pagelang": "صفحے کی زبان تبدیل کریں",
        "action-pagelang": "صفحے کی زبان تبدیل کریں",
        "log-name-pagelang": "نوشتہ تبدیلی زبان",
        "special-characters-group-thai": "سیامی",
        "special-characters-group-lao": "لاوسی",
        "special-characters-group-khmer": "کھمیری",
+       "special-characters-group-canadianaboriginal": "کینیڈی قدیمی",
        "special-characters-title-endash": "علامت خط",
        "special-characters-title-emdash": "خط فاصل کشیدہ",
        "special-characters-title-minus": "علامت وضع",
        "mw-widgets-dateinput-no-date": "کسی تاریخ کو منتخب نہیں کیا گیا",
+       "mw-widgets-mediasearch-input-placeholder": "میڈیا کی تلاش",
        "mw-widgets-mediasearch-noresults": "مطلوبہ نتائج نہیں ملے۔",
        "mw-widgets-titleinput-description-new-page": "صفحہ ابھی تک موجود نہیں",
        "mw-widgets-titleinput-description-redirect": "$1 کا رجوع مکرر",
        "log-action-filter-block-reblock": "تبدیلیٔ پابندی",
        "log-action-filter-block-unblock": "پابندی ختم",
        "log-action-filter-contentmodel-change": "مواد کے ماڈل کی تبدیلی",
-       "log-action-filter-contentmodel-new": "غیر معیاری contentmodel پر مشتمل صفحہ سازی",
+       "log-action-filter-contentmodel-new": "غیر ابتدائی ماڈل کے مواد پر مشتمل صفحہ سازی",
        "log-action-filter-delete-delete": "صفحہ کی حذف شدگی",
+       "log-action-filter-delete-delete_redir": "رجوع مکرر برتحریر",
        "log-action-filter-delete-restore": "صفحہ کی بحالی",
        "log-action-filter-delete-event": "نوشتہ کی حذف شدگی",
        "log-action-filter-delete-revision": "ترمیم کی حذف شدگی",
        "restrictionsfield-badip": "آئی پی پتا یا رینج نادرست ہے: $1",
        "restrictionsfield-label": "آئی پی کی اجازت یافتہ رینج:",
        "restrictionsfield-help": "فی سطر ایک آئی پی پتا یا سی آئی ڈی آر رینج۔ تمام کو فعال کرنے کے لیے <br><code>0.0.0.0/0</code><br><code>::/0</code> استعمال کریں",
-       "pagedata-title": "صفحے کا ڈیٹا"
+       "revid": "نسخہ $1",
+       "pageid": "صفحہ کا شناختی نمبر $1",
+       "rawhtml-notallowed": "\n&lt;html&gt; ٹیگ عام صفحات کے باہر استعمال نہیں کیا جا سکتا۔",
+       "gotointerwiki": "{{SITENAME}} سے باہر جا رہے ہیں",
+       "gotointerwiki-invalid": "درج کردہ عنوان درست نہیں ہے۔",
+       "gotointerwiki-external": "[[$2]] ایک علاحدہ ویب سائٹ ہے، جس ميں جانے کے لیے آپ {{SITENAME}} کو چھوڑ رہے ہیں۔\n\n''[$1 پر جاری $1]'''",
+       "undelete-cantedit": "آپ اس صفحہ کو بحال نہیں کر سکتے، کیونکہ آپ کو اس صفحہ میں ترمیم کرنے کی اجازت نہیں ہے۔",
+       "undelete-cantcreate": "آپ اس صفحہ کو بحال نہیں کر سکتے، کیونکہ اس نام سے کوئی صفحہ موجود نہیں ہے اور آپ کو اس صفحہ کو تخلیق کرنے کی اجازت بھی نہیں ہے۔",
+       "pagedata-title": "صفحے کا ڈیٹا",
+       "pagedata-not-acceptable": "کوئی مساوی فارمیٹ نہی ملا۔ سپورٹ کرنے والی MIME اقسام: $1",
+       "pagedata-bad-title": "نادرست عنوان: $1"
 }
index 5199b07..08e1f8c 100644 (file)
        "nosuchusershort": "没有名为“$1”的用户。请检查您的拼写。",
        "nouserspecified": "您必须指定一个用户名。",
        "login-userblocked": "该用户已被封禁,禁止登录。",
-       "wrongpassword": "您输入的密码错误。请重试。",
+       "wrongpassword": "您输入的用户名或密码错误。请重试。",
        "wrongpasswordempty": "密码输入为空。请重试。",
        "passwordtooshort": "您的密码至少需要$1个字符。",
        "passwordtoolong": "密码不能超过{{PLURAL:$1|$1个字符}}。",
        "rcfilters-savedqueries-apply-and-setdefault-label": "创建默认过滤器",
        "rcfilters-savedqueries-cancel-label": "取消",
        "rcfilters-savedqueries-add-new-title": "保存当前过滤器设置",
-       "rcfilters-savedqueries-already-saved": "这些过滤器已保存",
+       "rcfilters-savedqueries-already-saved": "这些过滤器已保存。更改您的设置以创建新的保存过滤器。",
        "rcfilters-restore-default-filters": "恢复默认过滤器",
        "rcfilters-clear-all-filters": "清空所有过滤器",
        "rcfilters-show-new-changes": "显示最新更改",
index e21a647..33f569a 100644 (file)
@@ -91,7 +91,8 @@
                        "WhitePhosphorus",
                        "Translatealcd",
                        "蘭斯特",
-                       "机智的小鱼君"
+                       "机智的小鱼君",
+                       "Wbxshiori"
                ]
        },
        "tog-underline": "底線標示連結:",
        "apisandbox": "API 沙盒",
        "apisandbox-jsonly": "需要 JavaScript 才能使用 API 沙箱。",
        "apisandbox-api-disabled": "此網站已關閉 API。",
-       "apisandbox-intro": "使用此頁面可測試 <strong>MediaWiki web service API</strong>。\n請參考 [[mw:API:Main page|API 說明文件]] 以取得詳細資訊。例:[https://www.mediawiki.org/wiki/API#A_simple_example 取得主頁的內容]。 請選擇動作以取得更多範例。\n\n請注意,雖然此為沙盒,您在此頁所執行的動作仍有可能會修改到 Wiki。",
+       "apisandbox-intro": "使用此頁面可測試 <strong>MediaWiki web service API</strong>。\n請參考 [[mw:API:Main page|API 說明文件]] 以取得詳細資訊。例:[https://www.mediawiki.org/wiki/API#A_simple_example 取得主頁的內容]。 請選擇動作以取得更多範例。\n\n請注意,雖然此為沙盒,您在此頁所執行的動作仍有可能會修改到 Wiki。",
        "apisandbox-fullscreen": "展開面板",
        "apisandbox-fullscreen-tooltip": "展開沙盒面板來填滿瀏覽器視窗。",
        "apisandbox-unfullscreen": "顯示頁面",
        "unblocked-id": "已經移除 $1 的封鎖。",
        "unblocked-ip": "[[Special:Contributions/$1|$1]] 已解除封鎖。",
        "blocklist": "已封鎖的使用者",
-       "autoblocklist": "自動封",
+       "autoblocklist": "自動封",
        "autoblocklist-submit": "搜尋",
        "autoblocklist-legend": "列出自動封鎖",
        "autoblocklist-localblocks": "本地{{PLURAL:$1|自動封鎖|自動封鎖}}",
        "articleexists": "該頁面名稱已存在,或您選擇的名稱無效。\n請改選擇其他名稱。",
        "cantmove-titleprotected": "您選擇的新標題已被禁止使用,您不可移動頁面到該位置。",
        "movetalk": "移動相關的對話頁面",
-       "move-subpages": "移動子頁面 (共 $1 頁)",
+       "move-subpages": "移動子頁面(至多 $1 頁)",
        "move-talk-subpages": "移動對話頁面的子頁面 (共 $1 頁)",
        "movepage-page-exists": "頁面 $1 已存在,無法自動覆蓋。",
        "movepage-page-moved": "已移動頁面 $1 到 $2。",
        "tooltip-ca-nstab-category": "檢視分類頁面",
        "tooltip-minoredit": "標記此為次要編輯",
        "tooltip-save": "儲存您的變更",
-       "tooltip-publish": "發佈您的更改",
+       "tooltip-publish": "發佈您的變更",
        "tooltip-preview": "請在儲存前預覽您的變更!",
        "tooltip-diff": "顯示您對內容所做的變更",
        "tooltip-compareselectedversions": "查閱此頁面兩個已選擇的修訂間的差異",
        "htmlform-user-not-valid": "<strong>$1</strong> 不是有效的使用者名稱。",
        "logentry-delete-delete": "$1 刪除頁面 $3",
        "logentry-delete-delete_redir": "$1 透過覆寫{{GENDER:$2|刪除了}}重新導向 $3",
-       "logentry-delete-restore": "$1{{GENDER:$2|還原}}頁面 $3($4)",
+       "logentry-delete-restore": "$1 {{GENDER:$2|還原}}頁面 $3($4)",
        "logentry-delete-restore-nocount": "$1 {{GENDER:$2|已還原}}頁面 $3",
        "restore-count-revisions": "{{PLURAL:$1|1 修訂|$1 修訂}}",
        "restore-count-files": "{{PLURAL:$1|1 檔案|$1 檔案}}",
index f85461f..05d2aba 100644 (file)
@@ -1,5 +1,5 @@
 <?php
-/** Crimean Turkish (Qırımtatarca)
+/** Crimean Tatar (Qırımtatarca)
  *
  * To improve a translation please visit https://translatewiki.net
  *
index ff68ad8..1ad2b56 100644 (file)
@@ -1,5 +1,5 @@
 <?php
-/** Crimean Turkish (Cyrillic script) (къырымтатарджа (Кирилл)‎)
+/** Crimean Tatar (Cyrillic script) (къырымтатарджа (Кирилл)‎)
  *
  * To improve a translation please visit https://translatewiki.net
  *
index 9a993ea..7a2c97f 100644 (file)
@@ -1,5 +1,5 @@
 <?php
-/** Crimean Turkish (Latin script) (qırımtatarca (Latin)‎)
+/** Crimean Tatar (Latin script) (qırımtatarca (Latin)‎)
  *
  * To improve a translation please visit https://translatewiki.net
  *
index 0dcfe2d..89c6e9a 100644 (file)
@@ -7,6 +7,7 @@
  * @file
  *
  * @author Alchimista
+ * @author Athena
  * @author Cecílio
  * @author MCruz
  * @author Malafaya
@@ -20,8 +21,8 @@ $namespaceNames = [
        NS_MEDIA            => 'Media',
        NS_SPECIAL          => 'Special',
        NS_TALK             => 'Cumbersa',
-       NS_USER             => 'Outelizador',
-       NS_USER_TALK        => 'Cumbersa_outelizador',
+       NS_USER             => 'Outelizador(a)',
+       NS_USER_TALK        => 'Cumbersa_outelizador(a)',
        NS_PROJECT_TALK     => '$1_cumbersa',
        NS_FILE             => 'Fexeiro',
        NS_FILE_TALK        => 'Cumbersa_fexeiro',
@@ -53,11 +54,16 @@ $namespaceAliases = [
        'Categoria_Discussão' => NS_CATEGORY_TALK,
 ];
 
+$namespaceGenderAliases = [
+       NS_USER => [ 'male' => 'Outelizador', 'female' => 'Outelizadora' ],
+       NS_USER_TALK => [ 'male' => 'Cumbersa_outelizador', 'female' => 'Cumbersa_outelizadora' ],
+]; // T180052
+
 $specialPageAliases = [
-       'CreateAccount'             => [ 'Criar Cuonta' ],
-       'Lonelypages'               => [ 'Páiginas Uorfanas' ],
-       'Uncategorizedcategories'   => [ 'Catadories sien catadories' ],
-       'Uncategorizedimages'       => [ 'Eimaiges sien catadories' ],
+       'CreateAccount'             => [ 'Criar_Cuonta' ],
+       'Lonelypages'               => [ 'Páiginas_Uorfanas' ],
+       'Uncategorizedcategories'   => [ 'Catadories_sien_catadories' ],
+       'Uncategorizedimages'       => [ 'Eimaiges_sien_catadories' ],
        'Userlogin'                 => [ 'Antrar' ],
        'Userlogout'                => [ 'Salir' ],
 ];
index b504bde..e5b4c13 100644 (file)
@@ -25,7 +25,7 @@ class CheckComposerLockUpToDate extends Maintenance {
                        $lockLocation = "$IP/vendor/composer.lock";
                        if ( !file_exists( $lockLocation ) ) {
                                $this->error(
-                                       'Could not find composer.lock file. Have you run "composer install"?',
+                                       'Could not find composer.lock file. Have you run "composer install --no-dev"?',
                                        1
                                );
                        }
@@ -53,7 +53,7 @@ class CheckComposerLockUpToDate extends Maintenance {
                if ( $found ) {
                        $this->error(
                                'Error: your composer.lock file is not up to date. ' .
-                                       'Run "composer update" to install newer dependencies',
+                                       'Run "composer update --no-dev" to install newer dependencies',
                                1
                        );
                } else {
index ba66c76..f8f5dcd 100755 (executable)
@@ -170,6 +170,26 @@ class UpdateMediaWiki extends Maintenance {
 
                $time1 = microtime( true );
 
+               $badPhpUnit = dirname( __DIR__ ) . '/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php';
+               if ( file_exists( $badPhpUnit ) ) {
+                       // @codingStandardsIgnoreStart Generic.Files.LineLength.TooLong
+                       // Bad versions of the file are:
+                       // https://raw.githubusercontent.com/sebastianbergmann/phpunit/c820f915bfae34e5a836f94967a2a5ea5ef34f21/src/Util/PHP/eval-stdin.php
+                       // https://raw.githubusercontent.com/sebastianbergmann/phpunit/3aaddb1c5bd9b9b8d070b4cf120e71c36fd08412/src/Util/PHP/eval-stdin.php
+                       // @codingStandardsIgnoreEnd
+                       $md5 = md5_file( $badPhpUnit );
+                       if ( $md5 === '120ac49800671dc383b6f3709c25c099'
+                               || $md5 === '28af792cb38fc9a1b236b91c1aad2876'
+                       ) {
+                               $success = unlink( $badPhpUnit );
+                               if ( $success ) {
+                                       $this->output( "Removed PHPUnit eval-stdin.php to protect against CVE-2017-9841\n" );
+                               } else {
+                                       $this->error( "Unable to remove $badPhpUnit, you should manually. See CVE-2017-9841" );
+                               }
+                       }
+               }
+
                $shared = $this->hasOption( 'doshared' );
 
                $updates = [ 'core', 'extensions' ];
diff --git a/maintenance/userOptions.inc b/maintenance/userOptions.inc
deleted file mode 100644 (file)
index 8ac7f91..0000000
+++ /dev/null
@@ -1,292 +0,0 @@
-<?php
-/**
- * Helper class for userOptions.php script.
- *
- * 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 Maintenance
- */
-
-// Options we will use
-$options = [ 'list', 'nowarn', 'quiet', 'usage', 'dry' ];
-$optionsWithArgs = [ 'old', 'new' ];
-
-require_once __DIR__ . '/commandLine.inc';
-
-/**
- * @ingroup Maintenance
- */
-class UserOptions {
-       public $mQuick;
-       public $mQuiet;
-       public $mDry;
-       public $mAnOption;
-       public $mOldValue;
-       public $mNewValue;
-
-       private $mMode, $mReady;
-
-       /**
-        * Constructor. Will show usage and exit if script options are not correct
-        * @param array $opts
-        * @param array $args
-        */
-       function __construct( $opts, $args ) {
-               if ( !$this->checkOpts( $opts, $args ) ) {
-                       self::showUsageAndExit();
-               } else {
-                       $this->mReady = $this->initializeOpts( $opts, $args );
-               }
-       }
-
-       /**
-        * This is used to check options. Only needed on construction
-        *
-        * @param array $opts
-        * @param array $args
-        *
-        * @return bool
-        */
-       private function checkOpts( $opts, $args ) {
-               // The three possible ways to run the script:
-               $list = isset( $opts['list'] );
-               $usage = isset( $opts['usage'] ) && ( count( $args ) <= 1 );
-               $change = isset( $opts['old'] ) && isset( $opts['new'] ) && ( count( $args ) <= 1 );
-
-               // We want only one of them
-               $isValid = ( ( $list + $usage + $change ) == 1 );
-
-               return $isValid;
-       }
-
-       /**
-        * load script options in the object
-        *
-        * @param array $opts
-        * @param array $args
-        *
-        * @return bool
-        */
-       private function initializeOpts( $opts, $args ) {
-               $this->mQuick = isset( $opts['nowarn'] );
-               $this->mQuiet = isset( $opts['quiet'] );
-               $this->mDry = isset( $opts['dry'] );
-
-               // Set object properties, specially 'mMode' used by run()
-               if ( isset( $opts['list'] ) ) {
-                       $this->mMode = 'LISTER';
-               } elseif ( isset( $opts['usage'] ) ) {
-                       $this->mMode = 'USAGER';
-                       $this->mAnOption = isset( $args[0] ) ? $args[0] : false;
-               } elseif ( isset( $opts['old'] ) && isset( $opts['new'] ) ) {
-                       $this->mMode = 'CHANGER';
-                       $this->mOldValue = $opts['old'];
-                       $this->mNewValue = $opts['new'];
-                       $this->mAnOption = $args[0];
-               } else {
-                       die( "There is a bug in the software, this should never happen\n" );
-               }
-
-               return true;
-       }
-
-       /**
-        * Dumb stuff to run a mode.
-        * @return bool
-        */
-       public function run() {
-               if ( !$this->mReady ) {
-                       return false;
-               }
-
-               $this->{$this->mMode}();
-
-               return true;
-       }
-
-       /**
-        * List default options and their value
-        */
-       private function LISTER() {
-               $def = User::getDefaultOptions();
-               ksort( $def );
-               $maxOpt = 0;
-               foreach ( $def as $opt => $value ) {
-                       $maxOpt = max( $maxOpt, strlen( $opt ) );
-               }
-               foreach ( $def as $opt => $value ) {
-                       printf( "%-{$maxOpt}s: %s\n", $opt, $value );
-               }
-       }
-
-       /**
-        * List options usage
-        */
-       private function USAGER() {
-               $ret = [];
-               $defaultOptions = User::getDefaultOptions();
-
-               // We list user by user_id from one of the replica DBs
-               $dbr = wfGetDB( DB_REPLICA );
-               $result = $dbr->select( 'user',
-                       [ 'user_id' ],
-                       [],
-                       __METHOD__
-               );
-
-               foreach ( $result as $id ) {
-                       $user = User::newFromId( $id->user_id );
-
-                       // Get the options and update stats
-                       if ( $this->mAnOption ) {
-                               if ( !array_key_exists( $this->mAnOption, $defaultOptions ) ) {
-                                       print "Invalid user option. Use --list to see valid choices\n";
-                                       exit;
-                               }
-
-                               $userValue = $user->getOption( $this->mAnOption );
-                               if ( $userValue <> $defaultOptions[$this->mAnOption] ) {
-                                       // @codingStandardsIgnoreStart Ignore silencing errors is discouraged warning
-                                       @$ret[$this->mAnOption][$userValue]++;
-                                       // @codingStandardsIgnoreEnd
-                               }
-                       } else {
-
-                               foreach ( $defaultOptions as $name => $defaultValue ) {
-                                       $userValue = $user->getOption( $name );
-                                       if ( $userValue <> $defaultValue ) {
-                                               // @codingStandardsIgnoreStart Ignore silencing errors is discouraged warning
-                                               @$ret[$name][$userValue]++;
-                                               // @codingStandardsIgnoreEnd
-                                       }
-                               }
-                       }
-               }
-
-               foreach ( $ret as $optionName => $usageStats ) {
-                       print "Usage for <$optionName> (default: '{$defaultOptions[$optionName]}'):\n";
-                       foreach ( $usageStats as $value => $count ) {
-                               print " $count user(s): '$value'\n";
-                       }
-                       print "\n";
-               }
-       }
-
-       /**
-        * Change our users options
-        */
-       private function CHANGER() {
-               $this->warn();
-
-               // We list user by user_id from one of the replica DBs
-               $dbr = wfGetDB( DB_REPLICA );
-               $result = $dbr->select( 'user',
-                       [ 'user_id' ],
-                       [],
-                       __METHOD__
-               );
-
-               foreach ( $result as $id ) {
-                       $user = User::newFromId( $id->user_id );
-
-                       $curValue = $user->getOption( $this->mAnOption );
-                       $username = $user->getName();
-
-                       if ( $curValue == $this->mOldValue ) {
-                               if ( !$this->mQuiet ) {
-                                       print "Setting {$this->mAnOption} for $username from '{$this->mOldValue}' " .
-                                               "to '{$this->mNewValue}'): ";
-                               }
-
-                               // Change value
-                               $user->setOption( $this->mAnOption, $this->mNewValue );
-
-                               // Will not save the settings if run with --dry
-                               if ( !$this->mDry ) {
-                                       $user->saveSettings();
-                               }
-                               if ( !$this->mQuiet ) {
-                                       print " OK\n";
-                               }
-                       } elseif ( !$this->mQuiet ) {
-                               print "Not changing '$username' using <{$this->mAnOption}> = '$curValue'\n";
-                       }
-               }
-       }
-
-       /**
-        * Return an array of option names
-        * @return array
-        */
-       public static function getDefaultOptionsNames() {
-               $def = User::getDefaultOptions();
-               $ret = [];
-               foreach ( $def as $optname => $defaultValue ) {
-                       array_push( $ret, $optname );
-               }
-
-               return $ret;
-       }
-
-       public static function showUsageAndExit() {
-               print <<<USAGE
-
-This script pass through all users and change one of their options.
-The new option is NOT validated.
-
-Usage:
-       php userOptions.php --list
-       php userOptions.php [user option] --usage
-       php userOptions.php [options] <user option> --old <old value> --new <new value>
-
-Switchs:
-       --list : list available user options and their default value
-
-       --usage : report all options statistics or just one if you specify it.
-
-       --old <old value> : the value to look for
-       --new <new value> : new value to update users with
-
-Options:
-       --nowarn: hides the 5 seconds warning
-       --quiet : do not print what is happening
-       --dry   : do not save user settings back to database
-
-USAGE;
-               exit( 0 );
-       }
-
-       /**
-        * The warning message and countdown
-        * @return bool
-        */
-       public function warn() {
-               if ( $this->mQuick ) {
-                       return true;
-               }
-
-               print <<<WARN
-The script is about to change the skin for ALL USERS in the database.
-Users with option <$this->mAnOption> = '$this->mOldValue' will be made to use '$this->mNewValue'.
-
-Abort with control-c in the next five seconds....
-WARN;
-               wfCountDown( 5 );
-
-               return true;
-       }
-}
index 53db48c..7cf16b6 100644 (file)
  * @author Antoine Musso <hashar at free dot fr>
  */
 
-// This is a command line script, load tools and parse args
-require_once 'userOptions.inc';
+require_once __DIR__ . '/Maintenance.php';
 
-// Load up our tool system, exit with usage() if options are not fine
-$uo = new UserOptions( $options, $args );
+/**
+ * @ingroup Maintenance
+ */
+class UserOptionsMaintenance extends Maintenance {
+
+       function __construct() {
+               parent::__construct();
+
+               $this->addDescription( 'Pass through all users and change one of their options.
+The new option is NOT validated.' );
+
+               $this->addOption( 'list', 'List available user options and their default value' );
+               $this->addOption( 'usage', 'Report all options statistics or just one if you specify it' );
+               $this->addOption( 'old', 'The value to look for', false, true );
+               $this->addOption( 'new', 'Rew value to update users with', false, true );
+               $this->addOption( 'nowarn', 'Hides the 5 seconds warning' );
+               $this->addOption( 'dry', 'Do not save user settings back to database' );
+               $this->addArg( 'option name', 'Name of the option to change or provide statistics about', false );
+       }
+
+       /**
+        * Do the actual work
+        */
+       public function execute() {
+               if ( $this->hasOption( 'list' ) ) {
+                       $this->listAvailableOptions();
+               } elseif ( $this->hasOption( 'usage' ) ) {
+                       $this->showUsageStats();
+               } elseif ( $this->hasOption( 'old' )
+                       && $this->hasOption( 'new' )
+                       && $this->hasArg( 0 )
+               ) {
+                       $this->updateOptions();
+               } else {
+                       $this->maybeHelp( /* force = */ true );
+               }
+       }
+
+       /**
+        * List default options and their value
+        */
+       private function listAvailableOptions() {
+               $def = User::getDefaultOptions();
+               ksort( $def );
+               $maxOpt = 0;
+               foreach ( $def as $opt => $value ) {
+                       $maxOpt = max( $maxOpt, strlen( $opt ) );
+               }
+               foreach ( $def as $opt => $value ) {
+                       $this->output( sprintf( "%-{$maxOpt}s: %s\n", $opt, $value ) );
+               }
+       }
+
+       /**
+        * List options usage
+        */
+       private function showUsageStats() {
+               $option = $this->getArg( 0 );
+
+               $ret = [];
+               $defaultOptions = User::getDefaultOptions();
+
+               // We list user by user_id from one of the replica DBs
+               $dbr = wfGetDB( DB_REPLICA );
+               $result = $dbr->select( 'user',
+                       [ 'user_id' ],
+                       [],
+                       __METHOD__
+               );
+
+               foreach ( $result as $id ) {
+                       $user = User::newFromId( $id->user_id );
+
+                       // Get the options and update stats
+                       if ( $option ) {
+                               if ( !array_key_exists( $option, $defaultOptions ) ) {
+                                       $this->error( "Invalid user option. Use --list to see valid choices\n", 1 );
+                               }
+
+                               $userValue = $user->getOption( $option );
+                               if ( $userValue <> $defaultOptions[$option] ) {
+                                       // @codingStandardsIgnoreStart Ignore silencing errors is discouraged warning
+                                       @$ret[$option][$userValue]++;
+                                       // @codingStandardsIgnoreEnd
+                               }
+                       } else {
+
+                               foreach ( $defaultOptions as $name => $defaultValue ) {
+                                       $userValue = $user->getOption( $name );
+                                       if ( $userValue != $defaultValue ) {
+                                               // @codingStandardsIgnoreStart Ignore silencing errors is discouraged warning
+                                               @$ret[$name][$userValue]++;
+                                               // @codingStandardsIgnoreEnd
+                                       }
+                               }
+                       }
+               }
+
+               foreach ( $ret as $optionName => $usageStats ) {
+                       $this->output( "Usage for <$optionName> (default: '{$defaultOptions[$optionName]}'):\n" );
+                       foreach ( $usageStats as $value => $count ) {
+                               $this->output( " $count user(s): '$value'\n" );
+                       }
+                       print "\n";
+               }
+       }
+
+       /**
+        * Change our users options
+        */
+       private function updateOptions() {
+               $dryRun = $this->hasOption( 'dry' );
+               $option = $this->getArg( 0 );
+               $from = $this->getOption( 'old' );
+               $to = $this->getOption( 'new' );
+
+               if ( !$dryRun ) {
+                       $this->warn( $option, $from, $to );
+               }
+
+               // We list user by user_id from one of the replica DBs
+               // @todo: getting all users in one query does not scale
+               $dbr = wfGetDB( DB_REPLICA );
+               $result = $dbr->select( 'user',
+                       [ 'user_id' ],
+                       [],
+                       __METHOD__
+               );
+
+               foreach ( $result as $id ) {
+                       $user = User::newFromId( $id->user_id );
+
+                       $curValue = $user->getOption( $option );
+                       $username = $user->getName();
+
+                       if ( $curValue == $from ) {
+                               $this->output( "Setting {$option} for $username from '{$from}' to '{$to}'): " );
+
+                               // Change value
+                               $user->setOption( $option, $to );
+
+                               // Will not save the settings if run with --dry
+                               if ( !$dryRun ) {
+                                       $user->saveSettings();
+                               }
+                               $this->output( " OK\n" );
+                       } else {
+                               $this->output( "Not changing '$username' using <{$option}> = '$curValue'\n" );
+                       }
+               }
+       }
+
+       /**
+        * The warning message and countdown
+        *
+        * @param string $option
+        * @param string $from
+        * @param string $to
+        */
+       private function warn( $option, $from, $to ) {
+               if ( $this->hasOption( 'nowarn' ) ) {
+                       return;
+               }
+
+               $this->output( <<<WARN
+The script is about to change the options for ALL USERS in the database.
+Users with option <$option> = '$from' will be made to use '$to'.
 
-$uo->run();
+Abort with control-c in the next five seconds....
+WARN
+               );
+               $this->countDown( 5 );
+       }
+}
 
-print "Done.\n";
+$maintClass = 'UserOptionsMaintenance';
+require RUN_MAINTENANCE_IF_MAIN;
index 981143e..6a601ad 100644 (file)
@@ -43,12 +43,6 @@ class WrapOldPasswords extends Maintenance {
        }
 
        public function execute() {
-               global $wgAuth;
-
-               if ( !$wgAuth->allowSetLocalPassword() ) {
-                       $this->error( '$wgAuth does not allow local passwords. Aborting.', true );
-               }
-
                $passwordFactory = new PasswordFactory();
                $passwordFactory->init( RequestContext::getMain()->getConfig() );
 
index 34b0836..d9fa8e0 100644 (file)
@@ -2083,10 +2083,11 @@ return [
        'mediawiki.special.pagesWithProp' => [
                'styles' => 'resources/src/mediawiki.special/mediawiki.special.pagesWithProp.css',
        ],
-       'mediawiki.special.preferences' => [
+       'mediawiki.special.preferences.ooui' => [
                'scripts' => [
                        'resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js',
                        'resources/src/mediawiki.special/mediawiki.special.preferences.convertmessagebox.js',
+                       'resources/src/mediawiki.special/mediawiki.special.preferences.editfont.js',
                        'resources/src/mediawiki.special/mediawiki.special.preferences.tabs.js',
                        'resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js',
                ],
@@ -2100,9 +2101,12 @@ return [
                        'mediawiki.language',
                        'mediawiki.confirmCloseWindow',
                        'mediawiki.notification.convertmessagebox',
+                       'oojs-ui-widgets',
+                       'mediawiki.widgets.SelectWithInputWidget',
+                       'mediawiki.editfont.styles',
                ],
        ],
-       'mediawiki.special.preferences.styles' => [
+       'mediawiki.special.preferences.styles.ooui' => [
                'styles' => 'resources/src/mediawiki.special/mediawiki.special.preferences.styles.css',
        ],
        'mediawiki.special.recentchanges' => [
@@ -2226,12 +2230,14 @@ return [
                ],
        ],
        'mediawiki.special.userrights' => [
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.userrights.css',
                'scripts' => 'resources/src/mediawiki.special/mediawiki.special.userrights.js',
                'dependencies' => [
                        'mediawiki.notification.convertmessagebox',
                ],
        ],
+       'mediawiki.special.userrights.styles' => [
+               'styles' => 'resources/src/mediawiki.special/mediawiki.special.userrights.css',
+       ],
        'mediawiki.special.watchlist' => [
                'scripts' => 'resources/src/mediawiki.special/mediawiki.special.watchlist.js',
                'messages' => [
index cad530b..73ec1a7 100644 (file)
@@ -11,6 +11,7 @@
 
 #pagehistory li.selected {
        background-color: #f8f9fa;
+       color: #252525;
        border: 1px dashed #a2a9b1;
 }
 
index 6931c7d..58e00f9 100644 (file)
@@ -69,16 +69,16 @@ a.stub {
 }
 
 /* Expand URLs for printing */
-.mw-body-content a.external.text:after,
-.mw-body-content a.external.autonumber:after {
+.mw-parser-output a.external.text:after,
+.mw-parser-output a.external.autonumber:after {
        content: ' (' attr( href ) ')';
        word-break: break-all;
        word-wrap: break-word;
 }
 
 /* Expand protocol-relative URLs for printing */
-.mw-body-content a.external.text[ href^='//' ]:after,
-.mw-body-content a.external.autonumber[ href^='//' ]:after {
+.mw-parser-output a.external.text[ href^='//' ]:after,
+.mw-parser-output a.external.autonumber[ href^='//' ]:after {
        content: ' (https:' attr( href ) ')';
 }
 
index 7b2d711..596b0d6 100644 (file)
@@ -220,28 +220,6 @@ table.toc td {
        font-size: larger;
 }
 
-/* preference page with js-genrated toc */
-#preftoc {
-       float: left;
-       margin: 1em 1em 1em 1em;
-       width: 13em;
-}
-
-#preftoc li {
-       border: 1px solid #fff;
-}
-
-#preftoc li.selected {
-       background-color: #f9f9f9;
-       border: 1px dashed #aaa;
-}
-
-#preftoc a,
-#preftoc a:active {
-       display: block;
-       color: #005189;
-}
-
 .mw-prefs-buttons {
        clear: left;
        float: left;
index d519648..5386291 100644 (file)
                        this.changesListModel.update(
                                pieces.changes,
                                pieces.fieldset,
-                               pieces.noResultsDetails === 'NO_RESULTS_TIMEOUT',
+                               pieces.noResultsDetails,
                                true // We're using existing DOM elements
                        );
                }
index c25aa73..621c200 100644 (file)
@@ -85,9 +85,6 @@
                        )
                );
 
-               // Remove excluded params from the url
-               uri.query = this.filtersModel.removeExcludedParams( uri.query );
-
                // Reapply unrecognized params and url version
                uri.query = $.extend( true, {}, uri.query, unrecognizedParams, { urlversion: '2' } );
 
index db43a53..51fc9bc 100644 (file)
                this.excludeLabel = new OO.ui.LabelWidget( {
                        label: mw.msg( 'rcfilters-filter-excluded' )
                } );
-               this.excludeLabel.toggle( this.itemModel.isSelected() && this.invertModel.isSelected() );
+               this.excludeLabel.toggle(
+                       this.itemModel.getGroupModel().getView() === 'namespaces' &&
+                       this.itemModel.isSelected() &&
+                       this.invertModel.isSelected()
+               );
 
                layout = new OO.ui.FieldLayout( this.checkboxWidget, {
                        label: $label,
                this.checkboxWidget.setSelected( this.itemModel.isSelected() );
 
                this.highlightButton.toggle( this.filtersViewModel.isHighlightEnabled() );
-               this.excludeLabel.toggle( this.itemModel.isSelected() && this.invertModel.isSelected() );
+               this.excludeLabel.toggle(
+                       this.itemModel.getGroupModel().getView() === 'namespaces' &&
+                       this.itemModel.isSelected() &&
+                       this.invertModel.isSelected()
+               );
        };
 
        /**
index 2a64aa3..d82ffe0 100644 (file)
@@ -9,7 +9,7 @@
  * compatibility ( browsers able to understand gradient syntax support also SVG ).
  * http://pauginer.tumblr.com/post/36614680636/invisible-gradient-technique */
 
-.mw-body-content a.external,
+.mw-parser-output a.external,
 .link-https {
        background: url( images/external-ltr.png ) center right no-repeat;
        /* @embed */
@@ -19,7 +19,7 @@
        padding-right: 15px;
 }
 
-.mw-body-content a.external[ href^='mailto:' ],
+.mw-parser-output a.external[ href^='mailto:' ],
 .link-mailto {
        background: url( images/mail.png ) center right no-repeat;
        /* @embed */
@@ -27,7 +27,7 @@
        padding-right: 15px;
 }
 
-.mw-body-content a.external[ href^='ftp://' ],
+.mw-parser-output a.external[ href^='ftp://' ],
 .link-ftp {
        background: url( images/ftp-ltr.png ) center right no-repeat;
        /* @embed */
@@ -35,8 +35,8 @@
        padding-right: 15px;
 }
 
-.mw-body-content a.external[ href^='irc://' ],
-.mw-body-content a.external[ href^='ircs://' ],
+.mw-parser-output a.external[ href^='irc://' ],
+.mw-parser-output a.external[ href^='ircs://' ],
 .link-irc {
        background: url( images/chat-ltr.png ) center right no-repeat;
        /* @embed */
        padding-right: 15px;
 }
 
-.mw-body-content a.external[ href$='.ogg' ],
-.mw-body-content a.external[ href$='.OGG' ],
-.mw-body-content a.external[ href$='.mid' ],
-.mw-body-content a.external[ href$='.MID' ],
-.mw-body-content a.external[ href$='.midi' ],
-.mw-body-content a.external[ href$='.MIDI' ],
-.mw-body-content a.external[ href$='.mp3' ],
-.mw-body-content a.external[ href$='.MP3' ],
-.mw-body-content a.external[ href$='.wav' ],
-.mw-body-content a.external[ href$='.WAV' ],
-.mw-body-content a.external[ href$='.wma' ],
-.mw-body-content a.external[ href$='.WMA' ],
+.mw-parser-output a.external[ href$='.ogg' ],
+.mw-parser-output a.external[ href$='.OGG' ],
+.mw-parser-output a.external[ href$='.mid' ],
+.mw-parser-output a.external[ href$='.MID' ],
+.mw-parser-output a.external[ href$='.midi' ],
+.mw-parser-output a.external[ href$='.MIDI' ],
+.mw-parser-output a.external[ href$='.mp3' ],
+.mw-parser-output a.external[ href$='.MP3' ],
+.mw-parser-output a.external[ href$='.wav' ],
+.mw-parser-output a.external[ href$='.WAV' ],
+.mw-parser-output a.external[ href$='.wma' ],
+.mw-parser-output a.external[ href$='.WMA' ],
 .link-audio {
        background: url( images/audio-ltr.png ) center right no-repeat;
        /* @embed */
        padding-right: 15px;
 }
 
-.mw-body-content a.external[ href$='.ogm' ],
-.mw-body-content a.external[ href$='.OGM' ],
-.mw-body-content a.external[ href$='.avi' ],
-.mw-body-content a.external[ href$='.AVI' ],
-.mw-body-content a.external[ href$='.mpeg' ],
-.mw-body-content a.external[ href$='.MPEG' ],
-.mw-body-content a.external[ href$='.mpg' ],
-.mw-body-content a.external[ href$='.MPG' ],
+.mw-parser-output a.external[ href$='.ogm' ],
+.mw-parser-output a.external[ href$='.OGM' ],
+.mw-parser-output a.external[ href$='.avi' ],
+.mw-parser-output a.external[ href$='.AVI' ],
+.mw-parser-output a.external[ href$='.mpeg' ],
+.mw-parser-output a.external[ href$='.MPEG' ],
+.mw-parser-output a.external[ href$='.mpg' ],
+.mw-parser-output a.external[ href$='.MPG' ],
 .link-video {
        background: url( images/video.png ) center right no-repeat;
        /* @embed */
        padding-right: 15px;
 }
 
-.mw-body-content a.external[ href$='.pdf' ],
-.mw-body-content a.external[ href$='.PDF' ],
-.mw-body-content a.external[ href*='.pdf#' ],
-.mw-body-content a.external[ href*='.PDF#' ],
-.mw-body-content a.external[ href*='.pdf?' ],
-.mw-body-content a.external[ href*='.PDF?' ],
+.mw-parser-output a.external[ href$='.pdf' ],
+.mw-parser-output a.external[ href$='.PDF' ],
+.mw-parser-output a.external[ href*='.pdf#' ],
+.mw-parser-output a.external[ href*='.PDF#' ],
+.mw-parser-output a.external[ href*='.pdf?' ],
+.mw-parser-output a.external[ href*='.PDF?' ],
 .link-document {
        background: url( images/document-ltr.png ) center right no-repeat;
        /* @embed */
 }
 
 /* Interwiki styling */
-.mw-body-content a.extiw,
-.mw-body-content a.extiw:active {
+.mw-parser-output a.extiw,
+.mw-parser-output a.extiw:active {
        color: #36b;
 }
 
 /* External link color */
-.mw-body-content a.external {
+.mw-parser-output a.external {
        color: #36b;
 }
index 366c5a9..58fd500 100644 (file)
@@ -13,7 +13,9 @@ a {
        background: none;
 }
 
-a:not( [ href ] ) {
+/* Support: Firefox 57 - it can't parse `rule[ attr ]` and the LESS compiler doesn't */
+/* strip whitespace inside the :not() (T180138) */
+a:not( [href] ) { /* stylelint-disable-line selector-attribute-brackets-space-inside */
        cursor: pointer; /* Always cursor:pointer even without href */
 }
 
@@ -53,33 +55,33 @@ a.new:visited,
 }
 
 /* Interwiki Styling */
-.mw-body-content a.extiw,
-.mw-body-content a.extiw:active {
+.mw-parser-output a.extiw,
+.mw-parser-output a.extiw:active {
        color: #36b;
 }
 
-.mw-body-content a.extiw:visited {
+.mw-parser-output a.extiw:visited {
        color: #636;
 }
 
-.mw-body-content a.extiw:active {
+.mw-parser-output a.extiw:active {
        color: #b63;
 }
 
 /* External links */
-.mw-body-content a.external {
+.mw-parser-output a.external {
        color: #36b;
 }
 
-.mw-body-content a.external:visited {
+.mw-parser-output a.external:visited {
        color: #636; /* T5112 */
 }
 
-.mw-body-content a.external:active {
+.mw-parser-output a.external:active {
        color: #b63;
 }
 
-.mw-body-content a.external.free {
+.mw-parser-output a.external.free {
        word-wrap: break-word;
 }
 
index 3e0d2b9..8cd25d8 100644 (file)
@@ -22,6 +22,7 @@ textarea {
 
 .editOptions {
        background-color: #eaecf0;
+       color: #252525;
        border: 1px solid #c8ccd1;
        border-top: 0;
        padding: 1em 1em 1.5em 1em;
index 7f3b09a..5d0ec49 100644 (file)
        color: #72777d;
        font-size: 90%;
 }
-
-/* Special:UserRights */
-.mw-userrights-disabled {
-       color: #72777d;
-}
-.mw-userrights-groups * td,
-.mw-userrights-groups * th {
-       padding-right: 1.5em;
-}
-
-.mw-userrights-groups * th {
-       text-align: left;
-}
index 45df37f..fe127eb 100644 (file)
@@ -4,9 +4,11 @@
  */
 ( function ( mw, $ ) {
        $( function () {
-               var allowCloseWindow;
+               var allowCloseWindow, saveButton, restoreButton;
 
-               // Check if all of the form values are unchanged
+               // Check if all of the form values are unchanged.
+               // (This function could be changed to infuse and check OOUI widgets, but that would only make it
+               // slower and more complicated. It works fine to treat them as HTML elements.)
                function isPrefsChanged() {
                        var inputs = $( '#mw-prefs-form :input[name]' ),
                                input, $input, inputType,
                        return false;
                }
 
+               saveButton = OO.ui.infuse( $( '#prefcontrol' ) );
+               restoreButton = OO.ui.infuse( $( '#mw-prefs-restoreprefs' ) );
+
                // Disable the button to save preferences unless preferences have changed
                // Check if preferences have been changed before JS has finished loading
                if ( !isPrefsChanged() ) {
-                       $( '#prefcontrol' ).prop( 'disabled', true );
-                       $( '#preferences > fieldset' ).one( 'change keydown mousedown', function () {
-                               $( '#prefcontrol' ).prop( 'disabled', false );
+                       saveButton.setDisabled( true );
+                       $( '#preferences .oo-ui-fieldsetLayout' ).one( 'change keydown mousedown', function () {
+                               saveButton.setDisabled( false );
                        } );
                }
 
                        namespace: 'prefswarning'
                } );
                $( '#mw-prefs-form' ).submit( $.proxy( allowCloseWindow, 'release' ) );
-               $( '#mw-prefs-restoreprefs' ).click( $.proxy( allowCloseWindow, 'release' ) );
+               restoreButton.on( 'click', function () {
+                       allowCloseWindow.release();
+                       // The default behavior of events in OOUI is always prevented. Follow the link manually.
+                       // Note that middle-click etc. still works, as it doesn't emit a OOUI 'click' event.
+                       location.href = restoreButton.getHref();
+               } );
        } );
 }( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.editfont.js b/resources/src/mediawiki.special/mediawiki.special.preferences.editfont.js
new file mode 100644 (file)
index 0000000..fe48886
--- /dev/null
@@ -0,0 +1,32 @@
+/*!
+ * JavaScript for Special:Preferences: editfont field enhancements.
+ */
+( function ( mw, $ ) {
+       $( function () {
+               var widget, lastValue;
+
+               try {
+                       widget = OO.ui.infuse( $( '#mw-input-wpeditfont' ) );
+               } catch ( err ) {
+                       // This preference could theoretically be disabled ($wgHiddenPrefs)
+                       return;
+               }
+
+               // Style options
+               widget.dropdownWidget.menu.items.forEach( function ( item ) {
+                       item.$label.addClass( 'mw-editfont-' + item.getData() );
+               } );
+
+               function updateLabel( value ) {
+                       // Style selected item label
+                       widget.dropdownWidget.$label
+                               .removeClass( 'mw-editfont-' + lastValue )
+                               .addClass( 'mw-editfont-' + value );
+                       lastValue = value;
+               }
+
+               widget.on( 'change', updateLabel );
+               updateLabel( widget.getValue() );
+
+       } );
+}( mediaWiki, jQuery ) );
index 33b630a..7be6ce1 100644 (file)
@@ -1,27 +1,28 @@
 /* Reuses colors from mediawiki.legacy/shared.css */
-.mw-email-not-authenticated .mw-input,
-.mw-email-none .mw-input {
+.mw-email-not-authenticated .oo-ui-labelWidget,
+.mw-email-none .oo-ui-labelWidget {
        border: 1px solid #fde29b;
        background-color: #fdf1d1;
        color: #000;
 }
 /* Authenticated email field has its own class too. Unstyled by default */
 /*
-.mw-email-authenticated .mw-input { }
+.mw-email-authenticated .oo-ui-labelWidget { }
 */
-/* This breaks due to nolabel styling */
-#preferences > fieldset td.mw-label {
-       width: 20%;
-}
 
-#preferences > fieldset table {
-       width: 100%;
+/* This is needed because add extra buttons in a weird way */
+.mw-prefs-buttons .mw-htmlform-submit-buttons {
+       margin: 0;
+       display: inline;
 }
-#preferences > fieldset table.mw-htmlform-matrix {
-       width: auto;
+
+.mw-prefs-buttons {
+       margin-top: 1em;
 }
 
-/* The CSS below is also for JS enabled version, because we want to prevent FOUC */
+#prefcontrol {
+       margin-right: 0.5em;
+}
 
 /*
  * Hide, but keep accessible for screen-readers.
        zoom: 1;
 }
 
-.client-nojs #preftoc {
-       display: none;
+/* Override OOUI styles so that dropdowns near the bottom of the form don't get clipped,
+ * e.g.'Appearance' / 'Threshold for stub link formatting'. This is hacky and bad, it would be
+ * better solved by setting overlays for the widgets, but we can't do it from PHP... */
+#preferences .oo-ui-panelLayout {
+       position: static;
+       overflow: visible;
+       -webkit-transform: none;
+       transform: none;
+}
+
+#preferences .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed {
+       border-color: #c8ccd1;
+       border-width: 1px 0 0;
+       border-radius: 0;
+       padding-left: 0;
+       padding-right: 0;
+       box-shadow: none;
+}
+
+/* Tweak the margins to reduce the shifting of form contents
+ * after JS code loads and rearranges the page */
+.client-js #preferences > .oo-ui-panelLayout {
+       margin: 1em 0;
+}
+
+.client-js #preferences .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed {
+       margin-left: 0.25em;
+}
+
+.client-js #preferences .oo-ui-tabPanelLayout {
+       padding-top: 0.5em;
+       padding-bottom: 0.5em;
 }
 
-.client-js #preferences > fieldset {
-       display: none;
+.client-js #preferences .oo-ui-tabPanelLayout .oo-ui-panelLayout-framed {
+       margin-left: 0;
+       margin-bottom: 0;
+       border: 0;
+       padding-top: 0;
 }
 
-/* Only the 1st tab is shown by default in JS mode */
-.client-js #preferences #mw-prefsection-personal {
-       display: block;
+.client-js #preferences > .oo-ui-panelLayout > .oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-header {
+       margin-bottom: 1em;
 }
index dcfad27..9f1691c 100644 (file)
@@ -3,29 +3,10 @@
  */
 ( function ( mw, $ ) {
        $( function () {
-               var $preftoc, $preferences, $fieldsets, labelFunc, previousTab;
+               var $preferences, tabs, wrapper, previousTab;
 
-               labelFunc = function () {
-                       return this.id.replace( /^mw-prefsection/g, 'preftab' );
-               };
-
-               $preftoc = $( '#preftoc' );
                $preferences = $( '#preferences' );
 
-               $fieldsets = $preferences.children( 'fieldset' )
-                       .attr( {
-                               role: 'tabpanel',
-                               'aria-labelledby': labelFunc
-                       } );
-               $fieldsets.not( '#mw-prefsection-personal' )
-                       .hide()
-                       .attr( 'aria-hidden', 'true' );
-
-               // T115692: The following is kept for backwards compatibility with older skins
-               $preferences.addClass( 'jsprefs' );
-               $fieldsets.addClass( 'prefsection' );
-               $fieldsets.children( 'legend' ).addClass( 'mainLegend' );
-
                // Make sure the accessibility tip is selectable so that screen reader users take notice,
                // but hide it per default to reduce interface clutter. Also make sure it becomes visible
                // when selected. Similar to jquery.mw-jump
                                } else {
                                        $( this ).css( 'height', 'auto' );
                                }
-                       } ).insertBefore( $preftoc );
+                       } ).prependTo( '#mw-content-text' );
 
-               /**
-                * It uses document.getElementById for security reasons (HTML injections in $()).
-                *
-                * @ignore
-                * @param {string} name the name of a tab without the prefix ("mw-prefsection-")
-                * @param {string} [mode] A hash will be set according to the current
-                *  open section. Set mode 'noHash' to surpress this.
-                */
-               function switchPrefTab( name, mode ) {
-                       var $tab, scrollTop;
+               tabs = new OO.ui.IndexLayout( {
+                       expanded: false,
+                       // Do not remove focus from the tabs menu after choosing a tab
+                       autoFocus: false
+               } );
+
+               mw.config.get( 'wgPreferencesTabs' ).forEach( function ( tabConfig ) {
+                       var panel, $panelContents;
+
+                       panel = new OO.ui.TabPanelLayout( tabConfig.name, {
+                               expanded: false,
+                               label: tabConfig.label
+                       } );
+                       $panelContents = $( '#mw-prefsection-' + tabConfig.name );
+
+                       // Hide the unnecessary PHP PanelLayouts
+                       // (Do not use .remove(), as that would remove event handlers for everything inside them)
+                       $panelContents.parent().detach();
+
+                       panel.$element.append( $panelContents );
+                       tabs.addTabPanels( [ panel ] );
+
+                       // Remove duplicate labels
+                       // (This must be after .addTabPanels(), otherwise the tab item doesn't exist yet)
+                       $panelContents.children( 'legend' ).remove();
+                       $panelContents.attr( 'aria-labelledby', panel.getTabItem().getElementId() );
+               } );
+
+               wrapper = new OO.ui.PanelLayout( {
+                       expanded: false,
+                       padded: false,
+                       framed: true
+               } );
+               wrapper.$element.append( tabs.$element );
+               $preferences.prepend( wrapper.$element );
+
+               function updateHash( panel ) {
+                       var scrollTop, active;
                        // Handle hash manually to prevent jumping,
                        // therefore save and restore scrollTop to prevent jumping.
                        scrollTop = $( window ).scrollTop();
-                       if ( mode !== 'noHash' ) {
-                               location.hash = '#mw-prefsection-' + name;
+                       // Changing the hash apparently causes keyboard focus to be lost?
+                       // Save and restore it. This makes no sense though.
+                       active = document.activeElement;
+                       location.hash = '#mw-prefsection-' + panel.getName();
+                       if ( active ) {
+                               active.focus();
                        }
                        $( window ).scrollTop( scrollTop );
-
-                       $preftoc.find( 'li' ).removeClass( 'selected' )
-                               .find( 'a' ).attr( {
-                                       tabIndex: -1,
-                                       'aria-selected': 'false'
-                               } );
-
-                       $tab = $( document.getElementById( 'preftab-' + name ) );
-                       if ( $tab.length ) {
-                               $tab.attr( {
-                                       tabIndex: 0,
-                                       'aria-selected': 'true'
-                               } ).focus()
-                                       .parent().addClass( 'selected' );
-
-                               $preferences.children( 'fieldset' ).hide().attr( 'aria-hidden', 'true' );
-                               $( document.getElementById( 'mw-prefsection-' + name ) ).show().attr( 'aria-hidden', 'false' );
-                       }
                }
 
-               // Enable keyboard users to use left and right keys to switch tabs
-               $preftoc.on( 'keydown', function ( event ) {
-                       var keyLeft = 37,
-                               keyRight = 39,
-                               $el;
-
-                       if ( event.keyCode === keyLeft ) {
-                               $el = $( '#preftoc li.selected' ).prev().find( 'a' );
-                       } else if ( event.keyCode === keyRight ) {
-                               $el = $( '#preftoc li.selected' ).next().find( 'a' );
-                       } else {
-                               return;
+               tabs.on( 'set', updateHash );
+
+               /**
+                * @ignore
+                * @param {string} name the name of a tab without the prefix ("mw-prefsection-")
+                * @param {string} [mode] A hash will be set according to the current
+                *  open section. Set mode 'noHash' to supress this.
+                */
+               function switchPrefTab( name, mode ) {
+                       if ( mode === 'noHash' ) {
+                               tabs.off( 'set', updateHash );
                        }
-                       if ( $el.length > 0 ) {
-                               switchPrefTab( $el.attr( 'href' ).replace( '#mw-prefsection-', '' ) );
+                       tabs.setTabPanel( name );
+                       if ( mode === 'noHash' ) {
+                               tabs.on( 'set', updateHash );
                        }
-               } );
+               }
 
                // Jump to correct section as indicated by the hash.
                // This function is called onload and onhashchange.
                        }
                }
 
-               // In browsers that support the onhashchange event we will not bind click
-               // handlers and instead let the browser do the default behavior (clicking the
-               // <a href="#.."> will naturally set the hash, handled by onhashchange.
-               // But other things that change the hash will also be caught (e.g. using
+               // Handle other things that change the hash (e.g. using
                // the Back and Forward browser navigation).
-               // Note the special check for IE "compatibility" mode.
-               if ( 'onhashchange' in window &&
-                       ( document.documentMode === undefined || document.documentMode >= 8 )
-               ) {
+               if ( 'onhashchange' in window ) {
                        $( window ).on( 'hashchange', function () {
                                var hash = location.hash;
                                if ( hash.match( /^#mw-[\w-]+/ ) ) {
                                } else if ( hash === '' ) {
                                        switchPrefTab( 'personal', 'noHash' );
                                }
-                       } )
-                               // Run the function immediately to select the proper tab on startup.
-                               .trigger( 'hashchange' );
-               // In older browsers we'll bind a click handler as fallback.
-               // We must not have onhashchange *and* the click handlers, otherwise
-               // the click handler calls switchPrefTab() which sets the hash value,
-               // which triggers onhashchange and calls switchPrefTab() again.
-               } else {
-                       $preftoc.on( 'click', 'li a', function ( e ) {
-                               switchPrefTab( $( this ).attr( 'href' ).replace( '#mw-prefsection-', '' ) );
-                               e.preventDefault();
                        } );
-                       // If we've reloaded the page or followed an open-in-new-window,
-                       // make the selected tab visible.
-                       detectHash();
                }
 
+               // If we've reloaded the page or followed an open-in-new-window,
+               // make the selected tab visible.
+               detectHash();
+
                // Restore the active tab after saving the preferences
                previousTab = mw.storage.session.get( 'mwpreferences-prevTab' );
                if ( previousTab ) {
                }
 
                $( '#mw-prefs-form' ).on( 'submit', function () {
-                       var value = $( $preftoc ).find( 'li.selected a' ).attr( 'id' ).replace( 'preftab-', '' );
+                       var value = tabs.getCurrentTabPanelName();
                        mw.storage.session.set( 'mwpreferences-prevTab', value );
                } );
 
index 03656ee..7fbcc77 100644 (file)
@@ -4,13 +4,19 @@
 ( function ( mw, $ ) {
        $( function () {
                var
-                       $tzSelect, $tzTextbox, $localtimeHolder, servertime;
+                       timezoneWidget, $localtimeHolder, servertime;
 
                // Timezone functions.
                // Guesses Timezone from browser and updates fields onchange.
 
-               $tzSelect = $( '#mw-input-wptimecorrection' );
-               $tzTextbox = $( '#mw-input-wptimecorrection-other' );
+               // This is identical to OO.ui.infuse( ... ), but it makes the class name of the result known.
+               try {
+                       timezoneWidget = mw.widgets.SelectWithInputWidget.static.infuse( $( '#wpTimeCorrection' ) );
+               } catch ( err ) {
+                       // This preference could theoretically be disabled ($wgHiddenPrefs)
+                       timezoneWidget = null;
+               }
+
                $localtimeHolder = $( '#wpLocalTime' );
                servertime = parseInt( $( 'input[name="wpServerTime"]' ).val(), 10 );
 
 
                function updateTimezoneSelection() {
                        var minuteDiff, localTime,
-                               type = $tzSelect.val();
+                               type = timezoneWidget.dropdowninput.getValue();
 
                        if ( type === 'other' ) {
                                // User specified time zone manually in <input>
                                // Grab data from the textbox, parse it.
-                               minuteDiff = hoursToMinutes( $tzTextbox.val() );
+                               minuteDiff = hoursToMinutes( timezoneWidget.textinput.getValue() );
                        } else {
                                // Time zone not manually specified by user
                                if ( type === 'guess' ) {
                                        // Get browser timezone & fill it in
                                        minuteDiff = -( new Date().getTimezoneOffset() );
-                                       $tzTextbox.val( minutesToHours( minuteDiff ) );
-                                       $tzSelect.val( 'other' );
+                                       timezoneWidget.textinput.setValue( minutesToHours( minuteDiff ) );
+                                       timezoneWidget.dropdowninput.setValue( 'other' );
                                } else {
-                                       // Grab data from the $tzSelect value
+                                       // Grab data from the dropdown value
                                        minuteDiff = parseInt( type.split( '|' )[ 1 ], 10 ) || 0;
                                }
                        }
@@ -76,9 +82,9 @@
                        $localtimeHolder.text( mw.language.convertNumber( minutesToHours( localTime ) ) );
                }
 
-               if ( $tzSelect.length && $tzTextbox.length ) {
-                       $tzSelect.change( updateTimezoneSelection );
-                       $tzTextbox.blur( updateTimezoneSelection );
+               if ( timezoneWidget ) {
+                       timezoneWidget.dropdowninput.on( 'change', updateTimezoneSelection );
+                       timezoneWidget.textinput.on( 'change', updateTimezoneSelection );
                        updateTimezoneSelection();
                }
 
index a4b4087..1ffdf70 100644 (file)
        display: inline-block;
        vertical-align: middle;
 }
+
+.mw-userrights-disabled {
+       color: #72777d;
+}
+.mw-userrights-groups * td,
+.mw-userrights-groups * th {
+       padding-right: 1.5em;
+}
+
+.mw-userrights-groups * th {
+       text-align: left;
+}
+
+/* Dynamically show/hide the expiry selection underneath each checkbox */
+input.mw-userrights-groupcheckbox:not( :checked ) ~ .mw-userrights-nested {
+       display: none;
+}
+
+/* Initial hide the expiry fields to prevent a FOUC on loading */
+/* The input fields gets unhidden by JavaScript when needed */
+.client-js .mw-userrights-expiryfield {
+       display: none;
+}
index 3f864dd..d3494f7 100644 (file)
@@ -6,13 +6,8 @@
        // Replace successbox with notifications
        convertmessagebox();
 
-       // Dynamically show/hide the expiry selection underneath each checkbox
-       $( '#mw-userrights-form2 input[type=checkbox]' ).on( 'change', function ( e ) {
-               $( '#mw-userrights-nested-' + e.target.id ).toggle( e.target.checked );
-       } ).trigger( 'change' );
-
-       // Also dynamically show/hide the "other time" input under each dropdown
+       // Dynamically show/hide the "other time" input under each dropdown
        $( '.mw-userrights-nested select' ).on( 'change', function ( e ) {
                $( e.target.parentNode ).find( 'input' ).toggle( $( e.target ).val() === 'other' );
-       } ).trigger( 'change' );
+       } );
 }( jQuery ) );
index 1607489..e78a4ca 100644 (file)
@@ -1,4 +1,4 @@
-/* OOUIHTMLForm styles */
+// OOUIHTMLForm styles
 .mw-htmlform-ooui-wrapper.oo-ui-panelLayout-padded {
        padding: 1em 1.25em 1.25em;
 }
        }
 }
 
-/* Flatlist styling for PHP widgets... */
+// HACK: Hide empty fieldsetLayoutHeaders, until T180535 isn't resolved
+.oo-ui-fieldsetLayout:not( .oo-ui-labelElement ):not( .oo-ui-iconElement ) > .oo-ui-fieldsetLayout-header {
+       display: none;
+}
+
+// Flatlist styling for PHP widgets...
 .mw-htmlform-flatlist .oo-ui-fieldLayout-align-inline,
-/* ...and for JS widgets */
+// ...and for JS widgets
 .mw-htmlform-flatlist .oo-ui-radioOptionWidget,
 .mw-htmlform-flatlist .oo-ui-checkboxMultioptionWidget {
        display: inline-block;
index 1f6e54c..7260a0a 100644 (file)
@@ -137,6 +137,7 @@ td.diff-marker {
 .mw-diff-movedpara-left:hover,
 .mw-diff-movedpara-right:hover {
        text-decoration: none;
+       color: transparent;
 }
 
 .mw-diff-movedpara-left:after,
index 633798d..481980f 100644 (file)
@@ -15,6 +15,7 @@
        margin-bottom: 0.5em;
        border: solid 1px #ddd;
        background-color: #fcfcfc;
+       color: #252525;
        /* Click handler in mediawiki.notification.js */
        cursor: pointer;
 
index ca31fbc..993f8d3 100644 (file)
@@ -64,6 +64,7 @@ $wgAutoloadClasses += [
        'LessFileCompilationTest' => "$testDir/phpunit/LessFileCompilationTest.php",
 
        # tests/phpunit/includes
+       'RevisionDbTestBase' => "$testDir/phpunit/includes/RevisionDbTestBase.php",
        'RevisionTestModifyableContent' => "$testDir/phpunit/includes/RevisionTestModifyableContent.php",
        'RevisionTestModifyableContentHandler' => "$testDir/phpunit/includes/RevisionTestModifyableContentHandler.php",
        'TestLogger' => "$testDir/phpunit/includes/TestLogger.php",
@@ -104,6 +105,9 @@ $wgAutoloadClasses += [
        # tests/phpunit/includes/diff
        'FakeDiffOp' => "$testDir/phpunit/includes/diff/FakeDiffOp.php",
 
+       # tests/phpunit/includes/externalstore
+       'ExternalStoreForTesting' => "$testDir/phpunit/includes/externalstore/ExternalStoreForTesting.php",
+
        # tests/phpunit/includes/logging
        'LogFormatterTestCase' => "$testDir/phpunit/includes/logging/LogFormatterTestCase.php",
 
index cef935c..a505cde 100644 (file)
@@ -5706,7 +5706,7 @@ Examples from RFC 2732, section 2:
 
 !! html/php
 <p><a rel="nofollow" class="external free" href="http://[2404:130:0:1000::187:2]/index.php">http://[2404:130:0:1000::187:2]/index.php</a>
-</p><p>Examples from <a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc2373">RFC 2373</a>, section 2.2:
+</p><p>Examples from <a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc2373">RFC 2373</a>, section 2.2:
 </p>
 <ul><li> <a rel="nofollow" class="external free" href="http://[1080::8:800:200C:417A]/unicast">http://[1080::8:800:200C:417A]/unicast</a></li>
 <li> <a rel="nofollow" class="external free" href="http://[FF01::101]/multicast">http://[FF01::101]/multicast</a></li>
@@ -5714,7 +5714,7 @@ Examples from RFC 2732, section 2:
 <li> <a rel="nofollow" class="external free" href="http://[::]/unspecified">http://[::]/unspecified</a></li>
 <li> <a rel="nofollow" class="external free" href="http://[::13.1.68.3]/ipv4compat">http://[::13.1.68.3]/ipv4compat</a></li>
 <li> <a rel="nofollow" class="external free" href="http://[::FFFF:129.144.52.38]/ipv4compat">http://[::FFFF:129.144.52.38]/ipv4compat</a></li></ul>
-<p>Examples from <a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc2732">RFC 2732</a>, section 2:
+<p>Examples from <a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc2732">RFC 2732</a>, section 2:
 </p>
 <ul><li> <a rel="nofollow" class="external free" href="http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html">http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html</a></li>
 <li> <a rel="nofollow" class="external free" href="http://[1080:0:0:0:8:800:200C:417A]/index.html">http://[1080:0:0:0:8:800:200C:417A]/index.html</a></li>
@@ -5727,7 +5727,7 @@ Examples from RFC 2732, section 2:
 !! html/parsoid
 <p><a rel="mw:ExtLink" href="http://[2404:130:0:1000::187:2]/index.php">http://[2404:130:0:1000::187:2]/index.php</a></p>
 
-<p>Examples from <a href="//tools.ietf.org/html/rfc2373" rel="mw:ExtLink">RFC 2373</a>, section 2.2:</p>
+<p>Examples from <a href="https://tools.ietf.org/html/rfc2373" rel="mw:ExtLink">RFC 2373</a>, section 2.2:</p>
 <ul><li> <a rel="mw:ExtLink" href="http://[1080::8:800:200C:417A]/unicast">http://[1080::8:800:200C:417A]/unicast</a></li>
 <li> <a rel="mw:ExtLink" href="http://[FF01::101]/multicast">http://[FF01::101]/multicast</a></li>
 <li> <a rel="mw:ExtLink" href="http://[::1]/loopback">http://[::1]/loopback</a></li>
@@ -5735,7 +5735,7 @@ Examples from RFC 2732, section 2:
 <li> <a rel="mw:ExtLink" href="http://[::13.1.68.3]/ipv4compat">http://[::13.1.68.3]/ipv4compat</a></li>
 <li> <a rel="mw:ExtLink" href="http://[::FFFF:129.144.52.38]/ipv4compat">http://[::FFFF:129.144.52.38]/ipv4compat</a></li></ul>
 
-<p>Examples from <a href="//tools.ietf.org/html/rfc2732" rel="mw:ExtLink">RFC 2732</a>, section 2:</p>
+<p>Examples from <a href="https://tools.ietf.org/html/rfc2732" rel="mw:ExtLink">RFC 2732</a>, section 2:</p>
 <ul><li> <a rel="mw:ExtLink" href="http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html">http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html</a></li>
 <li> <a rel="mw:ExtLink" href="http://[1080:0:0:0:8:800:200C:417A]/index.html">http://[1080:0:0:0:8:800:200C:417A]/index.html</a></li>
 <li> <a rel="mw:ExtLink" href="http://[3ffe:2a00:100:7031::1]">http://[3ffe:2a00:100:7031::1]</a></li>
@@ -5769,7 +5769,7 @@ Examples from RFC 2732, section 2:
 
 !! html/php
 <p><a rel="nofollow" class="external text" href="http://[2404:130:0:1000::187:2]/index.php">test</a>
-</p><p>Examples from <a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc2373">RFC 2373</a>, section 2.2:
+</p><p>Examples from <a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc2373">RFC 2373</a>, section 2.2:
 </p>
 <ul><li> <a rel="nofollow" class="external text" href="http://[1080::8:800:200C:417A]">unicast</a></li>
 <li> <a rel="nofollow" class="external text" href="http://[FF01::101]">multicast</a></li>
@@ -5777,7 +5777,7 @@ Examples from RFC 2732, section 2:
 <li> <a rel="nofollow" class="external text" href="http://[::]">unspecified</a></li>
 <li> <a rel="nofollow" class="external text" href="http://[::13.1.68.3]">ipv4compat</a></li>
 <li> <a rel="nofollow" class="external text" href="http://[::FFFF:129.144.52.38]">ipv4compat</a></li></ul>
-<p>Examples from <a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc2732">RFC 2732</a>, section 2:
+<p>Examples from <a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc2732">RFC 2732</a>, section 2:
 </p>
 <ul><li> <a rel="nofollow" class="external text" href="http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html">1</a></li>
 <li> <a rel="nofollow" class="external text" href="http://[1080:0:0:0:8:800:200C:417A]/index.html">2</a></li>
@@ -5790,7 +5790,7 @@ Examples from RFC 2732, section 2:
 !! html/parsoid
 <p><a rel="mw:ExtLink" href="http://[2404:130:0:1000::187:2]/index.php">test</a></p>
 
-<p>Examples from <a href="//tools.ietf.org/html/rfc2373" rel="mw:ExtLink">RFC 2373</a>, section 2.2:</p>
+<p>Examples from <a href="https://tools.ietf.org/html/rfc2373" rel="mw:ExtLink">RFC 2373</a>, section 2.2:</p>
 <ul><li> <a rel="mw:ExtLink" href="http://[1080::8:800:200C:417A]">unicast</a></li>
 <li> <a rel="mw:ExtLink" href="http://[FF01::101]">multicast</a></li>
 <li> <a rel="mw:ExtLink" href="http://[::1]/">loopback</a></li>
@@ -5798,7 +5798,7 @@ Examples from RFC 2732, section 2:
 <li> <a rel="mw:ExtLink" href="http://[::13.1.68.3]">ipv4compat</a></li>
 <li> <a rel="mw:ExtLink" href="http://[::FFFF:129.144.52.38]">ipv4compat</a></li></ul>
 
-<p>Examples from <a href="//tools.ietf.org/html/rfc2732" rel="mw:ExtLink">RFC 2732</a>, section 2:</p>
+<p>Examples from <a href="https://tools.ietf.org/html/rfc2732" rel="mw:ExtLink">RFC 2732</a>, section 2:</p>
 <ul><li> <a rel="mw:ExtLink" href="http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html">1</a></li>
 <li> <a rel="mw:ExtLink" href="http://[1080:0:0:0:8:800:200C:417A]/index.html">2</a></li>
 <li> <a rel="mw:ExtLink" href="http://[3ffe:2a00:100:7031::1]">3</a></li>
@@ -6350,7 +6350,7 @@ Accept "!!" in table data
 
 !! html/parsoid
 <table>
-<tbody><tr data-parsoid='{"autoInsertedEnd":true,"autoInsertedStart":true}'><td data-parsoid='{"autoInsertedEnd":true}'> Foo!! </td><td data-parsoid='{"stx_v":"row","autoInsertedEnd":true}'></td></tr>
+<tbody><tr data-parsoid='{"autoInsertedEnd":true,"autoInsertedStart":true}'><td data-parsoid='{"autoInsertedEnd":true}'> Foo!! </td><td data-parsoid='{"stx":"row","autoInsertedEnd":true}'></td></tr>
 </tbody></table>
 !! end
 
@@ -6626,7 +6626,7 @@ parsoid=wt2html,html2html
 !! html/parsoid
 <table><tbody>
 <tr>
-<td data-parsoid='{"startTagSrc":"| ","attrSepSrc":"|","autoInsertedEnd":true}'>[<a rel="mw:ExtLink" href="ftp://%7Cx" data-parsoid='{"stx":"url","a":{"href":"ftp://%7Cx"},"sa":{"href":"ftp://|x"}}'>ftp://%7Cx</a></td><td data-parsoid='{"stx_v":"row","autoInsertedEnd":true}'>]" onmouseover="alert(document.cookie)">test</td></tr></tbody></table>
+<td data-parsoid='{"startTagSrc":"| ","attrSepSrc":"|","autoInsertedEnd":true}'>[<a rel="mw:ExtLink" href="ftp://%7Cx" data-parsoid='{"stx":"url","a":{"href":"ftp://%7Cx"},"sa":{"href":"ftp://|x"}}'>ftp://%7Cx</a></td><td data-parsoid='{"stx":"row","autoInsertedEnd":true}'>]" onmouseover="alert(document.cookie)">test</td></tr></tbody></table>
 !! end
 
 !! test
@@ -6683,7 +6683,7 @@ parsoid=wt2html
 
 !! html/parsoid
 <table>
-<tbody><tr><td> style="color: red !important;" data-contrived="put this here </td><td data-parsoid='{"stx_v":"row","a":{"\"":null},"sa":{"\"":""},"autoInsertedEnd":true}'> foo</td></tr>
+<tbody><tr><td> style="color: red !important;" data-contrived="put this here </td><td data-parsoid='{"stx":"row","a":{"\"":null},"sa":{"\"":""},"autoInsertedEnd":true}'> foo</td></tr>
 </tbody></table>
 !! end
 
@@ -10998,10 +10998,10 @@ Magic links: RFC (T2479)
 !! wikitext
 RFC 822
 !! html/php
-<p><a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc822">RFC 822</a>
+<p><a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc822">RFC 822</a>
 </p>
 !! html/parsoid
-<p><a href="//tools.ietf.org/html/rfc822" rel="mw:ExtLink">RFC 822</a></p>
+<p><a href="https://tools.ietf.org/html/rfc822" rel="mw:ExtLink">RFC 822</a></p>
 !! end
 
 !! test
@@ -11009,10 +11009,10 @@ Magic links: RFC (T67278)
 !! wikitext
 This is RFC 822 but thisRFC 822 is not RFC 822linked.
 !! html/php
-<p>This is <a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc822">RFC 822</a> but thisRFC 822 is not RFC 822linked.
+<p>This is <a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc822">RFC 822</a> but thisRFC 822 is not RFC 822linked.
 </p>
 !! html/parsoid
-<p>This is <a href="//tools.ietf.org/html/rfc822" rel="mw:ExtLink">RFC 822</a> but thisRFC 822 is not RFC 822linked.</p>
+<p>This is <a href="https://tools.ietf.org/html/rfc822" rel="mw:ExtLink">RFC 822</a> but thisRFC 822 is not RFC 822linked.</p>
 !! end
 
 !! test
@@ -11022,12 +11022,12 @@ RFC &nbsp;&#160;&#0160;&#xA0;&#Xa0; 822
 RFC
 822
 !! html/php
-<p><a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc822">RFC 822</a>
+<p><a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc822">RFC 822</a>
 RFC
 822
 </p>
 !! html/parsoid
-<p><a href="//tools.ietf.org/html/rfc822" rel="mw:ExtLink">RFC <span typeof="mw:Entity" data-parsoid='{"src":"&amp;nbsp;","srcContent":" "}'> </span><span typeof="mw:Entity" data-parsoid='{"src":"&amp;#160;","srcContent":" "}'> </span><span typeof="mw:Entity" data-parsoid='{"src":"&amp;#0160;","srcContent":" "}'> </span><span typeof="mw:Entity" data-parsoid='{"src":"&amp;#xA0;","srcContent":" "}'> </span><span typeof="mw:Entity" data-parsoid='{"src":"&amp;#Xa0;","srcContent":" "}'> </span> 822</a>
+<p><a href="https://tools.ietf.org/html/rfc822" rel="mw:ExtLink">RFC <span typeof="mw:Entity" data-parsoid='{"src":"&amp;nbsp;","srcContent":" "}'> </span><span typeof="mw:Entity" data-parsoid='{"src":"&amp;#160;","srcContent":" "}'> </span><span typeof="mw:Entity" data-parsoid='{"src":"&amp;#0160;","srcContent":" "}'> </span><span typeof="mw:Entity" data-parsoid='{"src":"&amp;#xA0;","srcContent":" "}'> </span><span typeof="mw:Entity" data-parsoid='{"src":"&amp;#Xa0;","srcContent":" "}'> </span> 822</a>
 RFC
 822</p>
 !! end
@@ -11123,14 +11123,14 @@ Magic links: use appropriate serialization for "almost" magic links.
 !! wikitext
 X[[Special:BookSources/0978739256|foo]]
 
-X[//tools.ietf.org/html/rfc1234 foo]
+X[https://tools.ietf.org/html/rfc1234 foo]
 !! html/php
 <p>X<a href="/wiki/Special:BookSources/0978739256" title="Special:BookSources/0978739256">foo</a>
-</p><p>X<a rel="nofollow" class="external text" href="//tools.ietf.org/html/rfc1234">foo</a>
+</p><p>X<a rel="nofollow" class="external text" href="https://tools.ietf.org/html/rfc1234">foo</a>
 </p>
 !! html/parsoid
 <p>X<a rel="mw:WikiLink" href="./Special:BookSources/0978739256" title="Special:BookSources/0978739256">foo</a></p>
-<p>X<a rel="mw:ExtLink" href="//tools.ietf.org/html/rfc1234">foo</a></p>
+<p>X<a rel="mw:ExtLink" href="https://tools.ietf.org/html/rfc1234">foo</a></p>
 !! end
 
 !! test
@@ -11492,9 +11492,9 @@ Abort table cell attribute parsing on wikilink
 
 !! html/parsoid
 <table>
-<tbody><tr data-parsoid='{"autoInsertedEnd":true,"autoInsertedStart":true}'><td data-parsoid='{"autoInsertedEnd":true}'> testing <a rel="mw:WikiLink" href="./One" title="One" data-parsoid='{"stx":"piped","a":{"href":"./One"},"sa":{"href":"one"}}'>two</a> | three </td><td data-parsoid='{"stx_v":"row","autoInsertedEnd":true}'> four</td>
-<td data-parsoid='{"a":{"testing":null,"one":null,"two":null},"sa":{"testing":"","one":"","two":""},"autoInsertedEnd":true}'> three </td><td data-parsoid='{"stx_v":"row","autoInsertedEnd":true}'> four</td>
-<td> testing="<a rel="mw:WikiLink" href="./One" title="One" data-parsoid='{"stx":"piped","a":{"href":"./One"},"sa":{"href":"one"}}'>two</a>" | three </td><td data-parsoid='{"stx_v":"row","autoInsertedEnd":true}'> four</td></tr>
+<tbody><tr data-parsoid='{"autoInsertedEnd":true,"autoInsertedStart":true}'><td data-parsoid='{"autoInsertedEnd":true}'> testing <a rel="mw:WikiLink" href="./One" title="One" data-parsoid='{"stx":"piped","a":{"href":"./One"},"sa":{"href":"one"}}'>two</a> | three </td><td data-parsoid='{"stx":"row","autoInsertedEnd":true}'> four</td>
+<td data-parsoid='{"a":{"testing":null,"one":null,"two":null},"sa":{"testing":"","one":"","two":""},"autoInsertedEnd":true}'> three </td><td data-parsoid='{"stx":"row","autoInsertedEnd":true}'> four</td>
+<td> testing="<a rel="mw:WikiLink" href="./One" title="One" data-parsoid='{"stx":"piped","a":{"href":"./One"},"sa":{"href":"one"}}'>two</a>" | three </td><td data-parsoid='{"stx":"row","autoInsertedEnd":true}'> four</td></tr>
 </tbody></table>
 !! end
 
@@ -14831,7 +14831,43 @@ Alt image option should handle most kinds of wikitext without barfing
 <div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="This is a link and a bold template." src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a>  <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>This is the image caption</div></div></div>
 
 !! html/parsoid
-<figure class="mw-default-size" typeof="mw:Image/Thumb mw:ExpandedAttrs" about="#mwt2" data-parsoid='{"optList":[{"ck":"thumbnail","ak":"thumb"},{"ck":"caption","ak":"This is the image caption"},{"ck":"alt","ak":"alt=This is a [[link]] and a {{echo|&#39;&#39;bold template&#39;&#39;}}."}]}' data-mw='{"attribs":[["thumbnail",{"html":"thumb"}],["alt",{"html":"alt=This is a &lt;a rel=\"mw:WikiLink\" href=\"./Link\" title=\"Link\" data-parsoid=&#39;{\"stx\":\"simple\",\"a\":{\"href\":\"./Link\"},\"sa\":{\"href\":\"link\"},\"dsr\":[65,73,2,2]}&#39;>link&lt;/a> and a &lt;i about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=&#39;{\"dsr\":[80,106,null,null],\"pi\":[[{\"k\":\"1\"}]]}&#39; data-mw=&#39;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"&amp;#39;&amp;#39;bold template&amp;#39;&amp;#39;\"}},\"i\":0}}]}&#39;>bold template&lt;/i>."}]]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img alt="This is a link and a bold template." resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220" data-parsoid='{"a":{"alt":"This is a link and a bold template.","resource":"./File:Foobar.jpg","height":"25","width":"220"},"sa":{"alt":"alt=This is a [[link]] and a {{echo|&#39;&#39;bold template&#39;&#39;}}.","resource":"Image:Foobar.jpg"}}'/></a><figcaption>This is the image caption</figcaption></figure>
+<figure class="mw-default-size" typeof="mw:Image/Thumb mw:ExpandedAttrs" about="#mwt2" data-parsoid='{"optList":[{"ck":"thumbnail","ak":"thumb"},{"ck":"caption","ak":"This is the image caption"},{"ck":"alt","ak":"alt=This is a [[link]] and a {{echo|&apos;&apos;bold template&apos;&apos;}}."}]}' data-mw='{"attribs":[["thumbnail",{"html":"thumb"}],["alt",{"html":"alt=This is a &lt;a rel=\"mw:WikiLink\" href=\"./Link\" title=\"Link\" data-parsoid=&apos;{\"stx\":\"simple\",\"a\":{\"href\":\"./Link\"},\"sa\":{\"href\":\"link\"},\"dsr\":[65,73,2,2]}&apos;>link&lt;/a> and a &lt;i about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid=&apos;{\"dsr\":[80,106,null,null],\"pi\":[[{\"k\":\"1\"}]]}&apos; data-mw=&apos;{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"&amp;apos;&amp;apos;bold template&amp;apos;&amp;apos;\"}},\"i\":0}}]}&#39;>bold template&lt;/i>."}]]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{}}'><img alt="This is a link and a bold template." resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220" data-parsoid='{"a":{"alt":"This is a link and a bold template.","resource":"./File:Foobar.jpg","height":"25","width":"220"},"sa":{"alt":"alt=This is a [[link]] and a {{echo|&#39;&#39;bold template&#39;&#39;}}.","resource":"Image:Foobar.jpg"}}'/></a><figcaption>This is the image caption</figcaption></figure>
+!! end
+
+!! test
+Image with table with attributes in caption
+!! options
+parsoid=wt2html,html2html
+!! wikitext
+[[File:Foobar.jpg|thumb|
+{| class="123" |
+|- class="456" |
+| ha
+|}
+]]
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Thumb" data-parsoid='{"optList":[{"ck":"thumbnail","ak":"thumb"},{"ck":"caption","ak":"\n{| class=\"123\" |\n|- class=\"456\" |\n| ha\n|}\n"}]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{"href":"File:Foobar.jpg"}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"25","width":"220"},"sa":{"resource":"File:Foobar.jpg"}}'/></a><figcaption>
+<table class="123">
+<tbody><tr class="456" data-parsoid='{"startTagSrc":"|-"}'>
+<td> ha</td></tr>
+</tbody></table>
+</figcaption></figure>
+!! end
+
+!! test
+Image with table with rows from templates in caption
+!! wikitext
+[[File:Foobar.jpg|thumb|
+{|
+{{echo|{{!}} hi}}
+|}
+]]
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Thumb" data-parsoid='{"optList":[{"ck":"thumbnail","ak":"thumb"},{"ck":"caption","ak":"\n{|\n{{echo|{{!}} hi}}\n|}\n"}]}'><a href="./File:Foobar.jpg" data-parsoid='{"a":{"href":"./File:Foobar.jpg"},"sa":{"href":"File:Foobar.jpg"}}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"25","width":"220"},"sa":{"resource":"File:Foobar.jpg"}}'/></a><figcaption>
+<table>
+<tbody about="#mwt4" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"{{!}} hi"}},"i":0}},"\n"]}'><tr><td> hi</td></tr>
+</tbody></table>
+</figcaption></figure>
 !! end
 
 !! test
@@ -15140,9 +15176,9 @@ SVG thumbnails with invalid language code
 !! options
 parsoid=wt2html,wt2wt,html2html
 !! wikitext
-[[File:Foobar.svg|thumb|caption|lang=invalid.language.code]]
+[[File:Foobar.svg|thumb|caption|lang=invalid:language:code]]
 !! html/php
-<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.svg" class="image"><img alt="" src="http://example.com/images/thumb/f/ff/Foobar.svg/180px-Foobar.svg.png" width="180" height="135" class="thumbimage" srcset="http://example.com/images/thumb/f/ff/Foobar.svg/270px-Foobar.svg.png 1.5x, http://example.com/images/thumb/f/ff/Foobar.svg/360px-Foobar.svg.png 2x" /></a>  <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.svg" class="internal" title="Enlarge"></a></div>lang=invalid.language.code</div></div></div>
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.svg" class="image"><img alt="" src="http://example.com/images/thumb/f/ff/Foobar.svg/180px-Foobar.svg.png" width="180" height="135" class="thumbimage" srcset="http://example.com/images/thumb/f/ff/Foobar.svg/270px-Foobar.svg.png 1.5x, http://example.com/images/thumb/f/ff/Foobar.svg/360px-Foobar.svg.png 2x" /></a>  <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.svg" class="internal" title="Enlarge"></a></div>lang=invalid:language:code</div></div></div>
 
 !! html/parsoid
 <figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.svg"><img resource="./File:Foobar.svg" src="//example.com/images/f/ff/Foobar.svg" data-file-width="240" data-file-height="180" data-file-type="drawing" height="165" width="220"/></a><figcaption>lang=invalid.language.code</figcaption></figure>
@@ -15164,10 +15200,10 @@ T3887: A RFC with a thumbnail
 !! wikitext
 [[File:Foobar.jpg|thumb|This is RFC 12354]]
 !! html/php
-<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a>  <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>This is <a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc12354">RFC 12354</a></div></div></div>
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a>  <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>This is <a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc12354">RFC 12354</a></div></div></div>
 
 !! html/parsoid
-<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption>This is <a href="//tools.ietf.org/html/rfc12354" rel="mw:ExtLink">RFC 12354</a></figcaption></figure>
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption>This is <a href="https://tools.ietf.org/html/rfc12354" rel="mw:ExtLink">RFC 12354</a></figcaption></figure>
 !! end
 
 !! test
@@ -18505,6 +18541,20 @@ all additional text is vanished
 <p>all additional text is vanished</p>
 !! end
 
+!! test
+Language converter glossary rules inside attributes (T119158)
+!! options
+language=sr variant=sr-el
+!! wikitext
+-{H|foAjrjvi=>sr-el:" onload="alert(1)" data-foo="}-
+
+[[File:Foobar.jpg|alt=-{}-foAjrjvi-{}-]]
+!! html
+<p>
+</p><p><a href="/wiki/%D0%94%D0%B0%D1%82%D0%BE%D1%82%D0%B5%D0%BA%D0%B0:Foobar.jpg" class="image"><img alt="&quot; onload=&quot;alert(1)&quot; data-foo=&quot;" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! end
+
 !! test
 Self closed html pairs (T7487)
 !! wikitext
@@ -20978,7 +21028,7 @@ Double RFC
 !! wikitext
 RFC RFC 1234
 !! html
-<p>RFC <a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc1234">RFC 1234</a>
+<p>RFC <a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc1234">RFC 1234</a>
 </p>
 !! end
 
@@ -20996,10 +21046,10 @@ RFC code coverage
 !! wikitext
 RFC   983&#x20;987
 !! html
-<p><a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc983">RFC 983</a>&#x20;987
+<p><a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc983">RFC 983</a>&#x20;987
 </p>
 !! html+tidy
-<p><a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc983">RFC 983</a> 987</p>
+<p><a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc983">RFC 983</a> 987</p>
 !! end
 
 !! test
@@ -22271,7 +22321,7 @@ Nested: -{zh-hans:Hi -{zh-cn:China;zh-sg:Singapore;}-;zh-hant:Hello -{zh-tw:Taiw
 <p>Nested: Hello Hong Kong!
 </p>
 !! html/parsoid
-<p>Nested: <span typeof="mw:LanguageVariant" data-parsoid='{"tSp":[7]}' data-mw-variant='{"twoway":[{"l":"zh-hans","t":"Hi &lt;span typeof=\"mw:LanguageVariant\" data-mw-variant=&#39;{\"twoway\":[{\"l\":\"zh-cn\",\"t\":\"China\"},{\"l\":\"zh-sg\",\"t\":\"Singapore\"}]}&#39; data-parsoid=&#39;{\"fl\":[],\"tSp\":[7],\"dsr\":[21,53,null,2]}&#39;>&lt;/span>"},{"l":"zh-hant","t":"Hello &lt;span typeof=\"mw:LanguageVariant\" data-mw-variant=&#39;{\"twoway\":[{\"l\":\"zh-tw\",\"t\":\"Taiwan\"},{\"l\":\"zh-hk\",\"t\":\"H&amp;lt;span typeof=\\\"mw:LanguageVariant\\\" data-mw-variant=&amp;#39;{\\\"disabled\\\":{\\\"t\\\":\\\"ong\\\"}}&amp;#39; data-parsoid=&amp;#39;{\\\"fl\\\":[],\\\"dsr\\\":[90,97,null,2]}&amp;#39;>&amp;lt;/span> K&amp;lt;span typeof=\\\"mw:LanguageVariant\\\" data-mw-variant=&amp;#39;{\\\"disabled\\\":{\\\"t\\\":\\\"\\\"}}&amp;#39; data-parsoid=&amp;#39;{\\\"fl\\\":[],\\\"dsr\\\":[99,103,null,2]}&amp;#39;>&amp;lt;/span>ong\"}]}&#39; data-parsoid=&#39;{\"fl\":[],\"tSp\":[7],\"dsr\":[68,109,null,2]}&#39;>&lt;/span>"}]}'></span>!</p>
+<p>Nested: <span typeof="mw:LanguageVariant" data-parsoid='{"tSp":[7]}' data-mw-variant='{"twoway":[{"l":"zh-hans","t":"Hi &lt;span typeof=\"mw:LanguageVariant\" data-mw-variant=&apos;{\"twoway\":[{\"l\":\"zh-cn\",\"t\":\"China\"},{\"l\":\"zh-sg\",\"t\":\"Singapore\"}]}&apos; data-parsoid=&apos;{\"fl\":[],\"tSp\":[7],\"dsr\":[21,53,null,2]}&apos;>&lt;/span>"},{"l":"zh-hant","t":"Hello &lt;span typeof=\"mw:LanguageVariant\" data-mw-variant=&apos;{\"twoway\":[{\"l\":\"zh-tw\",\"t\":\"Taiwan\"},{\"l\":\"zh-hk\",\"t\":\"H&amp;lt;span typeof=\\\"mw:LanguageVariant\\\" data-mw-variant=&amp;apos;{\\\"disabled\\\":{\\\"t\\\":\\\"ong\\\"}}&amp;apos; data-parsoid=&amp;apos;{\\\"fl\\\":[],\\\"dsr\\\":[90,97,null,2]}&amp;apos;>&amp;lt;/span> K&amp;lt;span typeof=\\\"mw:LanguageVariant\\\" data-mw-variant=&amp;apos;{\\\"disabled\\\":{\\\"t\\\":\\\"\\\"}}&amp;apos; data-parsoid=&amp;apos;{\\\"fl\\\":[],\\\"dsr\\\":[99,103,null,2]}&amp;apos;>&amp;lt;/span>ong\"}]}&apos; data-parsoid=&apos;{\"fl\":[],\"tSp\":[7],\"dsr\":[68,109,null,2]}&apos;>&lt;/span>"}]}'></span>!</p>
 !! end
 
 !! test
@@ -22284,7 +22334,7 @@ language=zh variant=zh-cn
 <p><span title="X">A</span>
 </p>
 !! html/parsoid
-<p><span typeof="mw:LanguageVariant" data-mw-variant='{"filter":{"l":["zh","zh-hans","zh-hant"],"t":"&lt;span title=\"\" about=\"#mwt1\" typeof=\"mw:ExpandedAttrs\" data-parsoid=&#39;{\"stx\":\"html\",\"a\":{\"title\":\"\"},\"sa\":{\"title\":\"-{X}-\"},\"dsr\":[21,49,20,7]}&#39; data-mw=&#39;{\"attribs\":[[{\"txt\":\"title\"},{\"html\":\"&amp;lt;span typeof=\\\"mw:LanguageVariant\\\" data-mw-variant=&amp;#39;{\\\"disabled\\\":{\\\"t\\\":\\\"X\\\"}}&amp;#39; data-parsoid=&amp;#39;{\\\"fl\\\":[],\\\"dsr\\\":[34,39,null,2]}&amp;#39;>&amp;lt;/span>\"}]]}&#39;>A&lt;/span>"}}'></span></p>
+<p><span typeof="mw:LanguageVariant" data-mw-variant='{"filter":{"l":["zh","zh-hans","zh-hant"],"t":"&lt;span title=\"\" about=\"#mwt1\" typeof=\"mw:ExpandedAttrs\" data-parsoid=&#39;{\"stx\":\"html\",\"a\":{\"title\":\"\"},\"sa\":{\"title\":\"-{X}-\"},\"dsr\":[21,49,20,7]}&#39; data-mw=&#39;{\"attribs\":[[{\"txt\":\"title\"},{\"html\":\"&amp;lt;span typeof=\\\"mw:LanguageVariant\\\" data-mw-variant=&amp;apos;{\\\"disabled\\\":{\\\"t\\\":\\\"X\\\"}}&amp;apos; data-parsoid=&amp;apos;{\\\"fl\\\":[],\\\"dsr\\\":[34,39,null,2]}&amp;apos;>&amp;lt;/span>\"}]]}&#39;>A&lt;/span>"}}'></span></p>
 !! end
 
 !! test
@@ -22297,7 +22347,7 @@ language=zh variant=zh-cn
 <p><span title="X">A</span>
 </p>
 !! html/parsoid
-<p><span typeof="mw:LanguageVariant" data-mw-variant='{"disabled":{"t":"&lt;span title=\"\" about=\"#mwt1\" typeof=\"mw:ExpandedAttrs\" data-parsoid=&#39;{\"stx\":\"html\",\"a\":{\"title\":\"\"},\"sa\":{\"title\":\"-{X}-\"},\"dsr\":[2,30,20,7]}&#39; data-mw=&#39;{\"attribs\":[[{\"txt\":\"title\"},{\"html\":\"&amp;lt;span typeof=\\\"mw:LanguageVariant\\\" data-mw-variant=&amp;#39;{\\\"disabled\\\":{\\\"t\\\":\\\"X\\\"}}&amp;#39; data-parsoid=&amp;#39;{\\\"fl\\\":[],\\\"dsr\\\":[15,20,null,2]}&amp;#39;>&amp;lt;/span>\"}]]}&#39;>A&lt;/span>"}}'></span></p>
+<p><span typeof="mw:LanguageVariant" data-mw-variant='{"disabled":{"t":"&lt;span title=\"\" about=\"#mwt1\" typeof=\"mw:ExpandedAttrs\" data-parsoid=&#39;{\"stx\":\"html\",\"a\":{\"title\":\"\"},\"sa\":{\"title\":\"-{X}-\"},\"dsr\":[2,30,20,7]}&#39; data-mw=&#39;{\"attribs\":[[{\"txt\":\"title\"},{\"html\":\"&amp;lt;span typeof=\\\"mw:LanguageVariant\\\" data-mw-variant=&amp;apos;{\\\"disabled\\\":{\\\"t\\\":\\\"X\\\"}}&amp;apos; data-parsoid=&amp;apos;{\\\"fl\\\":[],\\\"dsr\\\":[15,20,null,2]}&amp;apos;>&amp;lt;/span>\"}]]}&#39;>A&lt;/span>"}}'></span></p>
 !! end
 
 # Parsoid and PHP disagree on how to parse this example: Parsoid
@@ -25754,9 +25804,9 @@ Links 8. Add <nowiki/>s between text-nodes and RFC-links when required (T66300)
 !! options
 parsoid=html2wt
 !! html/parsoid
-<p><a href="//tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>4
-<a href="//tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>y
-X<a href="//tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>y</p>
+<p><a href="https://tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>4
+<a href="https://tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>y
+X<a href="https://tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>y</p>
 !! wikitext
 RFC 123<nowiki/>4
 RFC 123<nowiki/>y
@@ -25768,18 +25818,18 @@ Links 9. Don't add spurious <nowiki/>s between text-nodes and RFC-links (T66300)
 !! options
 parsoid=html2wt
 !! html/parsoid
-<p><a href="//tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>?foo
-<a href="//tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>&amp;foo
--<a href="//tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>-
+<p><a href="https://tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>?foo
+<a href="https://tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>&amp;foo
+-<a href="https://tools.ietf.org/html/rfc123" rel="mw:ExtLink" data-parsoid='{"stx":"magiclink"}'>RFC 123</a>-
 </p>
 !! wikitext
 RFC 123?foo
 RFC 123&foo
 -RFC 123-
 !! html/php
-<p><a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc123">RFC 123</a>?foo
-<a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc123">RFC 123</a>&amp;foo
--<a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc123">RFC 123</a>-
+<p><a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc123">RFC 123</a>?foo
+<a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc123">RFC 123</a>&amp;foo
+-<a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc123">RFC 123</a>-
 </p>
 !! end
 
@@ -28040,9 +28090,9 @@ Edited RFC links not serializable as RFC links should serialize as extlinks
 !! options
 parsoid=html2wt
 !! html/parsoid
-<a href="//tools.ietf.org/html/rfc123" rel="mw:ExtLink">New RFC</a>
+<a href="https://tools.ietf.org/html/rfc123" rel="mw:ExtLink">New RFC</a>
 !! wikitext
-[//tools.ietf.org/html/rfc123 New RFC]
+[https://tools.ietf.org/html/rfc123 New RFC]
 !! end
 
 !! test
@@ -28186,7 +28236,7 @@ Magic links inside image captions (autolinked)
 <div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a>
 <div class="thumbcaption">
 <div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>
-<a class="external mw-magiclink-rfc" rel="nofollow" href="//tools.ietf.org/html/rfc1234">RFC 1234</a></div>
+<a class="external mw-magiclink-rfc" rel="nofollow" href="https://tools.ietf.org/html/rfc1234">RFC 1234</a></div>
 </div>
 </div>
 <div class="thumb tright">
@@ -28205,7 +28255,7 @@ Magic links inside image captions (autolinked)
 </div>
 !! html/parsoid
 <figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><a rel="mw:ExtLink" href="http://example.com">http://example.com</a></figcaption></figure>
-<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><a href="//tools.ietf.org/html/rfc1234" rel="mw:ExtLink">RFC 1234</a></figcaption></figure>
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><a href="https://tools.ietf.org/html/rfc1234" rel="mw:ExtLink">RFC 1234</a></figcaption></figure>
 <figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><a href="//www.ncbi.nlm.nih.gov/pubmed/1234?dopt=Abstract" rel="mw:ExtLink">PMID 1234</a></figcaption></figure>
 <figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><a href="./Special:BookSources/123456789X" rel="mw:WikiLink">ISBN 123456789x</a></figcaption></figure>
 !! end
@@ -28305,17 +28355,17 @@ parsoid=html2wt
 <tbody>
 <tr><td>a
 b
-</td><td data-parsoid='{"stx_v":"row"}'>c</td></tr>
+</td><td data-parsoid='{"stx":"row"}'>c</td></tr>
 <tr><td><p>x</p>
-</td><td data-parsoid='{"stx_v":"row", "startTagSrc": "{{!}}{{!}}"}'>y</td></tr>
+</td><td data-parsoid='{"stx":"row", "startTagSrc": "{{!}}{{!}}"}'>y</td></tr>
 </tbody></table>
 <table>
 <tbody>
 <tr><th>a
 b
-</th><th data-parsoid='{"stx_v":"row"}'>c</th></tr>
+</th><th data-parsoid='{"stx":"row"}'>c</th></tr>
 <tr><th><p>x</h>
-</th><th data-parsoid='{"stx_v":"row"}'>y</th></tr>
+</th><th data-parsoid='{"stx":"row"}'>y</th></tr>
 </tbody></table>
 !! wikitext
 {|
index 106ab68..6f09d4c 100644 (file)
@@ -38,16 +38,13 @@ $maintenance->setup();
 // to $maintenance->mSelf. Keep that here for b/c
 $self = $maintenance->getName();
 global $IP;
-# Start the autoloader, so that extensions can derive classes from core files
-require_once "$IP/includes/AutoLoader.php";
-# Grab profiling functions
-require_once "$IP/includes/profiler/ProfilerFunctions.php";
-
-# Start the profiler
+# Get profiler configuraton
 $wgProfiler = [];
 if ( file_exists( "$IP/StartProfiler.php" ) ) {
        require "$IP/StartProfiler.php";
 }
+# Start the autoloader, so that extensions can derive classes from core files
+require_once "$IP/includes/AutoLoader.php";
 
 $requireOnceGlobalsScope = function ( $file ) use ( $self ) {
        foreach ( array_keys( $GLOBALS ) as $varName ) {
diff --git a/tests/phpunit/includes/GlobalFunctions/wfStringToBoolTest.php b/tests/phpunit/includes/GlobalFunctions/wfStringToBoolTest.php
new file mode 100644 (file)
index 0000000..7f56b60
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * @group GlobalFunctions
+ * @covers ::wfStringToBool
+ */
+class WfStringToBoolTest extends MediaWikiTestCase {
+
+       public function getTestCases() {
+               return [
+                       [ 'true', true ],
+                       [ 'on', true ],
+                       [ 'yes', true ],
+                       [ 'TRUE', true ],
+                       [ 'YeS', true ],
+                       [ 'On', true ],
+                       [ '1', true ],
+                       [ '+1', true ],
+                       [ '01', true ],
+                       [ '-001', true ],
+                       [ '  1', true ],
+                       [ '-1  ', true ],
+                       [ '', false ],
+                       [ '0', false ],
+                       [ 'false', false ],
+                       [ 'NO', false ],
+                       [ 'NOT', false ],
+                       [ 'never', false ],
+                       [ '!&', false ],
+                       [ '-0', false ],
+                       [ '+0', false ],
+                       [ 'forget about it', false ],
+                       [ ' on', false ],
+                       [ 'true ', false ],
+               ];
+       }
+
+       /**
+        * @dataProvider getTestCases
+        * @param string $str
+        * @param bool $bool
+        */
+       public function testStr2Bool( $str, $bool ) {
+               if ( $bool ) {
+                       $this->assertTrue( wfStringToBool( $str ) );
+               } else {
+                       $this->assertFalse( wfStringToBool( $str ) );
+               }
+       }
+
+}
index d78c1e7..4a15225 100644 (file)
@@ -150,13 +150,9 @@ class PreferencesTest extends MediaWikiTestCase {
 
        /** Helper */
        protected function prefsFor( $user_key ) {
-               $preferences = [];
-               Preferences::profilePreferences(
+               return Preferences::getPreferences(
                        $this->prefUsers[$user_key],
-                       $this->context,
-                       $preferences
+                       $this->context
                );
-
-               return $preferences;
        }
 }
diff --git a/tests/phpunit/includes/RevisionContentHandlerDbTest.php b/tests/phpunit/includes/RevisionContentHandlerDbTest.php
new file mode 100644 (file)
index 0000000..fa0153d
--- /dev/null
@@ -0,0 +1,14 @@
+<?php
+
+/**
+ * @group Database
+ * @group medium
+ * @group ContentHandler
+ */
+class RevisionContentHandlerDbTest extends RevisionDbTestBase {
+
+       protected function getContentHandlerUseDB() {
+               return true;
+       }
+
+}
diff --git a/tests/phpunit/includes/RevisionDbTestBase.php b/tests/phpunit/includes/RevisionDbTestBase.php
new file mode 100644 (file)
index 0000000..20e69ae
--- /dev/null
@@ -0,0 +1,1385 @@
+<?php
+
+/**
+ * RevisionDbTestBase contains test cases for the Revision class that have Database interactions.
+ *
+ * @group Database
+ * @group medium
+ */
+abstract class RevisionDbTestBase extends MediaWikiTestCase {
+
+       /**
+        * @var WikiPage $testPage
+        */
+       private $testPage;
+
+       public function __construct( $name = null, array $data = [], $dataName = '' ) {
+               parent::__construct( $name, $data, $dataName );
+
+               $this->tablesUsed = array_merge( $this->tablesUsed,
+                       [
+                               'page',
+                               'revision',
+                               'ip_changes',
+                               'text',
+                               'archive',
+
+                               'recentchanges',
+                               'logging',
+
+                               'page_props',
+                               'pagelinks',
+                               'categorylinks',
+                               'langlinks',
+                               'externallinks',
+                               'imagelinks',
+                               'templatelinks',
+                               'iwlinks'
+                       ]
+               );
+       }
+
+       protected function setUp() {
+               global $wgContLang;
+
+               parent::setUp();
+
+               $this->mergeMwGlobalArrayValue(
+                       'wgExtraNamespaces',
+                       [
+                               12312 => 'Dummy',
+                               12313 => 'Dummy_talk',
+                       ]
+               );
+
+               $this->mergeMwGlobalArrayValue(
+                       'wgNamespaceContentModels',
+                       [
+                               12312 => DummyContentForTesting::MODEL_ID,
+                       ]
+               );
+
+               $this->mergeMwGlobalArrayValue(
+                       'wgContentHandlers',
+                       [
+                               DummyContentForTesting::MODEL_ID => 'DummyContentHandlerForTesting',
+                               RevisionTestModifyableContent::MODEL_ID => 'RevisionTestModifyableContentHandler',
+                       ]
+               );
+
+               $this->setMwGlobals( 'wgContentHandlerUseDB', $this->getContentHandlerUseDB() );
+
+               MWNamespace::clearCaches();
+               // Reset namespace cache
+               $wgContLang->resetNamespaces();
+               if ( !$this->testPage ) {
+                       /**
+                        * We have to create a new page for each subclass as the page creation may result
+                        * in different DB fields being filled based on configuration.
+                        */
+                       $this->testPage = $this->createPage( __CLASS__, __CLASS__ );
+               }
+       }
+
+       protected function tearDown() {
+               global $wgContLang;
+
+               parent::tearDown();
+
+               MWNamespace::clearCaches();
+               // Reset namespace cache
+               $wgContLang->resetNamespaces();
+       }
+
+       abstract protected function getContentHandlerUseDB();
+
+       private function makeRevisionWithProps( $props = null ) {
+               if ( $props === null ) {
+                       $props = [];
+               }
+
+               if ( !isset( $props['content'] ) && !isset( $props['text'] ) ) {
+                       $props['text'] = 'Lorem Ipsum';
+               }
+
+               if ( !isset( $props['comment'] ) ) {
+                       $props['comment'] = 'just a test';
+               }
+
+               if ( !isset( $props['page'] ) ) {
+                       $props['page'] = $this->testPage->getId();
+               }
+
+               $rev = new Revision( $props );
+
+               $dbw = wfGetDB( DB_MASTER );
+               $rev->insertOn( $dbw );
+
+               return $rev;
+       }
+
+       /**
+        * @param string $titleString
+        * @param string $text
+        * @param string|null $model
+        *
+        * @return WikiPage
+        */
+       private function createPage( $titleString, $text, $model = null ) {
+               if ( !preg_match( '/:/', $titleString ) &&
+                       ( $model === null || $model === CONTENT_MODEL_WIKITEXT )
+               ) {
+                       $ns = $this->getDefaultWikitextNS();
+                       $titleString = MWNamespace::getCanonicalName( $ns ) . ':' . $titleString;
+               }
+
+               $title = Title::newFromText( $titleString );
+               $wikipage = new WikiPage( $title );
+
+               // Delete the article if it already exists
+               if ( $wikipage->exists() ) {
+                       $wikipage->doDeleteArticle( "done" );
+               }
+
+               $content = ContentHandler::makeContent( $text, $title, $model );
+               $wikipage->doEditContent( $content, __METHOD__, EDIT_NEW );
+
+               return $wikipage;
+       }
+
+       private function assertRevEquals( Revision $orig, Revision $rev = null ) {
+               $this->assertNotNull( $rev, 'missing revision' );
+
+               $this->assertEquals( $orig->getId(), $rev->getId() );
+               $this->assertEquals( $orig->getPage(), $rev->getPage() );
+               $this->assertEquals( $orig->getTimestamp(), $rev->getTimestamp() );
+               $this->assertEquals( $orig->getUser(), $rev->getUser() );
+               $this->assertEquals( $orig->getContentModel(), $rev->getContentModel() );
+               $this->assertEquals( $orig->getContentFormat(), $rev->getContentFormat() );
+               $this->assertEquals( $orig->getSha1(), $rev->getSha1() );
+       }
+
+       /**
+        * @covers Revision::getRecentChange
+        */
+       public function testGetRecentChange() {
+               $rev = $this->testPage->getRevision();
+               $recentChange = $rev->getRecentChange();
+
+               // Make sure various attributes look right / the correct entry has been retrieved.
+               $this->assertEquals( $rev->getTimestamp(), $recentChange->getAttribute( 'rc_timestamp' ) );
+               $this->assertEquals(
+                       $rev->getTitle()->getNamespace(),
+                       $recentChange->getAttribute( 'rc_namespace' )
+               );
+               $this->assertEquals(
+                       $rev->getTitle()->getDBkey(),
+                       $recentChange->getAttribute( 'rc_title' )
+               );
+               $this->assertEquals( $rev->getUser(), $recentChange->getAttribute( 'rc_user' ) );
+               $this->assertEquals( $rev->getUserText(), $recentChange->getAttribute( 'rc_user_text' ) );
+               $this->assertEquals( $rev->getComment(), $recentChange->getAttribute( 'rc_comment' ) );
+               $this->assertEquals( $rev->getPage(), $recentChange->getAttribute( 'rc_cur_id' ) );
+               $this->assertEquals( $rev->getId(), $recentChange->getAttribute( 'rc_this_oldid' ) );
+       }
+
+       /**
+        * @covers Revision::insertOn
+        */
+       public function testInsertOn_success() {
+               $parentId = $this->testPage->getLatest();
+
+               // If an ExternalStore is set don't use it.
+               $this->setMwGlobals( 'wgDefaultExternalStore', false );
+
+               $rev = new Revision( [
+                       'page' => $this->testPage->getId(),
+                       'title' => $this->testPage->getTitle(),
+                       'text' => 'Revision Text',
+                       'comment' => 'Revision comment',
+               ] );
+
+               $revId = $rev->insertOn( wfGetDB( DB_MASTER ) );
+
+               $this->assertInternalType( 'integer', $revId );
+               $this->assertInternalType( 'integer', $rev->getTextId() );
+               $this->assertSame( $revId, $rev->getId() );
+
+               $this->assertSelect(
+                       'text',
+                       [ 'old_id', 'old_text' ],
+                       "old_id = {$rev->getTextId()}",
+                       [ [ strval( $rev->getTextId() ), 'Revision Text' ] ]
+               );
+               $this->assertSelect(
+                       'revision',
+                       [
+                               'rev_id',
+                               'rev_page',
+                               'rev_text_id',
+                               'rev_user',
+                               'rev_minor_edit',
+                               'rev_deleted',
+                               'rev_len',
+                               'rev_parent_id',
+                               'rev_sha1',
+                       ],
+                       "rev_id = {$rev->getId()}",
+                       [ [
+                               strval( $rev->getId() ),
+                               strval( $this->testPage->getId() ),
+                               strval( $rev->getTextId() ),
+                               '0',
+                               '0',
+                               '0',
+                               '13',
+                               strval( $parentId ),
+                               's0ngbdoxagreuf2vjtuxzwdz64n29xm',
+                       ] ]
+               );
+       }
+
+       /**
+        * @covers Revision::insertOn
+        */
+       public function testInsertOn_exceptionOnNoPage() {
+               // If an ExternalStore is set don't use it.
+               $this->setMwGlobals( 'wgDefaultExternalStore', false );
+               $this->setExpectedException(
+                       MWException::class,
+                       "Cannot insert revision: page ID must be nonzero"
+               );
+
+               $rev = new Revision( [] );
+
+               $rev->insertOn( wfGetDB( DB_MASTER ) );
+       }
+
+       /**
+        * @covers Revision::newFromTitle
+        */
+       public function testNewFromTitle_withoutId() {
+               $latestRevId = $this->testPage->getLatest();
+
+               $rev = Revision::newFromTitle( $this->testPage->getTitle() );
+
+               $this->assertTrue( $this->testPage->getTitle()->equals( $rev->getTitle() ) );
+               $this->assertEquals( $latestRevId, $rev->getId() );
+       }
+
+       /**
+        * @covers Revision::newFromTitle
+        */
+       public function testNewFromTitle_withId() {
+               $latestRevId = $this->testPage->getLatest();
+
+               $rev = Revision::newFromTitle( $this->testPage->getTitle(), $latestRevId );
+
+               $this->assertTrue( $this->testPage->getTitle()->equals( $rev->getTitle() ) );
+               $this->assertEquals( $latestRevId, $rev->getId() );
+       }
+
+       /**
+        * @covers Revision::newFromTitle
+        */
+       public function testNewFromTitle_withBadId() {
+               $latestRevId = $this->testPage->getLatest();
+
+               $rev = Revision::newFromTitle( $this->testPage->getTitle(), $latestRevId + 1 );
+
+               $this->assertNull( $rev );
+       }
+
+       /**
+        * @covers Revision::newFromRow
+        */
+       public function testNewFromRow() {
+               $orig = $this->makeRevisionWithProps();
+
+               $dbr = wfGetDB( DB_REPLICA );
+               $revQuery = Revision::getQueryInfo();
+               $res = $dbr->select( $revQuery['tables'], $revQuery['fields'], [ 'rev_id' => $orig->getId() ],
+                  __METHOD__, [], $revQuery['joins'] );
+               $this->assertTrue( is_object( $res ), 'query failed' );
+
+               $row = $res->fetchObject();
+               $res->free();
+
+               $rev = Revision::newFromRow( $row );
+
+               $this->assertRevEquals( $orig, $rev );
+       }
+
+       public function provideNewFromArchiveRow() {
+               yield [
+                       function ( $f ) {
+                               return $f;
+                       },
+               ];
+               yield [
+                       function ( $f ) {
+                               return $f + [ 'ar_namespace', 'ar_title' ];
+                       },
+               ];
+               yield [
+                       function ( $f ) {
+                               unset( $f['ar_text_id'] );
+                               return $f;
+                       },
+               ];
+       }
+
+       /**
+        * @dataProvider provideNewFromArchiveRow
+        * @covers Revision::newFromArchiveRow
+        */
+       public function testNewFromArchiveRow( $selectModifier ) {
+               $page = $this->createPage(
+                       'RevisionStorageTest_testNewFromArchiveRow',
+                       'Lorem Ipsum',
+                       CONTENT_MODEL_WIKITEXT
+               );
+               $orig = $page->getRevision();
+               $page->doDeleteArticle( 'test Revision::newFromArchiveRow' );
+
+               $dbr = wfGetDB( DB_REPLICA );
+               $arQuery = Revision::getArchiveQueryInfo();
+               $arQuery['fields'] = $selectModifier( $arQuery['fields'] );
+               $res = $dbr->select(
+                       $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ],
+                       __METHOD__, [], $arQuery['joins']
+               );
+               $this->assertTrue( is_object( $res ), 'query failed' );
+
+               $row = $res->fetchObject();
+               $res->free();
+
+               $rev = Revision::newFromArchiveRow( $row );
+
+               $this->assertRevEquals( $orig, $rev );
+       }
+
+       /**
+        * @covers Revision::newFromArchiveRow
+        */
+       public function testNewFromArchiveRowOverrides() {
+               $page = $this->createPage(
+                       'RevisionStorageTest_testNewFromArchiveRow',
+                       'Lorem Ipsum',
+                       CONTENT_MODEL_WIKITEXT
+               );
+               $orig = $page->getRevision();
+               $page->doDeleteArticle( 'test Revision::newFromArchiveRow' );
+
+               $dbr = wfGetDB( DB_REPLICA );
+               $arQuery = Revision::getArchiveQueryInfo();
+               $res = $dbr->select(
+                       $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ],
+                       __METHOD__, [], $arQuery['joins']
+               );
+               $this->assertTrue( is_object( $res ), 'query failed' );
+
+               $row = $res->fetchObject();
+               $res->free();
+
+               $rev = Revision::newFromArchiveRow( $row, [ 'comment' => 'SOMEOVERRIDE' ] );
+
+               $this->assertNotEquals( $orig->getComment(), $rev->getComment() );
+               $this->assertEquals( 'SOMEOVERRIDE', $rev->getComment() );
+       }
+
+       /**
+        * @covers Revision::newFromId
+        */
+       public function testNewFromId() {
+               $orig = $this->testPage->getRevision();
+               $rev = Revision::newFromId( $orig->getId() );
+               $this->assertRevEquals( $orig, $rev );
+       }
+
+       /**
+        * @covers Revision::newFromPageId
+        */
+       public function testNewFromPageId() {
+               $rev = Revision::newFromPageId( $this->testPage->getId() );
+               $this->assertRevEquals(
+                       $this->testPage->getRevision(),
+                       $rev
+               );
+       }
+
+       /**
+        * @covers Revision::newFromPageId
+        */
+       public function testNewFromPageIdWithLatestId() {
+               $rev = Revision::newFromPageId(
+                       $this->testPage->getId(),
+                       $this->testPage->getLatest()
+               );
+               $this->assertRevEquals(
+                       $this->testPage->getRevision(),
+                       $rev
+               );
+       }
+
+       /**
+        * @covers Revision::newFromPageId
+        */
+       public function testNewFromPageIdWithNotLatestId() {
+               $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
+               $rev = Revision::newFromPageId(
+                       $this->testPage->getId(),
+                       $this->testPage->getRevision()->getPrevious()->getId()
+               );
+               $this->assertRevEquals(
+                       $this->testPage->getRevision()->getPrevious(),
+                       $rev
+               );
+       }
+
+       /**
+        * @covers Revision::fetchRevision
+        */
+       public function testFetchRevision() {
+               // Hidden process cache assertion below
+               $this->testPage->getRevision()->getId();
+
+               $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
+               $id = $this->testPage->getRevision()->getId();
+
+               $res = Revision::fetchRevision( $this->testPage->getTitle() );
+
+               # note: order is unspecified
+               $rows = [];
+               while ( ( $row = $res->fetchObject() ) ) {
+                       $rows[$row->rev_id] = $row;
+               }
+
+               $this->assertEquals( 1, count( $rows ), 'expected exactly one revision' );
+               $this->assertArrayHasKey( $id, $rows, 'missing revision with id ' . $id );
+       }
+
+       /**
+        * @covers Revision::getPage
+        */
+       public function testGetPage() {
+               $page = $this->testPage;
+
+               $orig = $this->makeRevisionWithProps( [ 'page' => $page->getId() ] );
+               $rev = Revision::newFromId( $orig->getId() );
+
+               $this->assertEquals( $page->getId(), $rev->getPage() );
+       }
+
+       /**
+        * @covers Revision::isCurrent
+        */
+       public function testIsCurrent() {
+               $rev1 = $this->testPage->getRevision();
+
+               # @todo find out if this should be true
+               # $this->assertTrue( $rev1->isCurrent() );
+
+               $rev1x = Revision::newFromId( $rev1->getId() );
+               $this->assertTrue( $rev1x->isCurrent() );
+
+               $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
+               $rev2 = $this->testPage->getRevision();
+
+               # @todo find out if this should be true
+               # $this->assertTrue( $rev2->isCurrent() );
+
+               $rev1x = Revision::newFromId( $rev1->getId() );
+               $this->assertFalse( $rev1x->isCurrent() );
+
+               $rev2x = Revision::newFromId( $rev2->getId() );
+               $this->assertTrue( $rev2x->isCurrent() );
+       }
+
+       /**
+        * @covers Revision::getPrevious
+        */
+       public function testGetPrevious() {
+               $oldestRevision = $this->testPage->getOldestRevision();
+               $latestRevision = $this->testPage->getLatest();
+
+               $this->assertNull( $oldestRevision->getPrevious() );
+
+               $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
+               $newRevision = $this->testPage->getRevision();
+
+               $this->assertNotNull( $newRevision->getPrevious() );
+               $this->assertEquals( $latestRevision, $newRevision->getPrevious()->getId() );
+       }
+
+       /**
+        * @covers Revision::getNext
+        */
+       public function testGetNext() {
+               $rev1 = $this->testPage->getRevision();
+
+               $this->assertNull( $rev1->getNext() );
+
+               $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
+               $rev2 = $this->testPage->getRevision();
+
+               $this->assertNotNull( $rev1->getNext() );
+               $this->assertEquals( $rev2->getId(), $rev1->getNext()->getId() );
+       }
+
+       /**
+        * @covers Revision::newNullRevision
+        */
+       public function testNewNullRevision() {
+               $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
+               $orig = $this->testPage->getRevision();
+
+               $dbw = wfGetDB( DB_MASTER );
+               $rev = Revision::newNullRevision( $dbw, $this->testPage->getId(), 'a null revision', false );
+
+               $this->assertNotEquals( $orig->getId(), $rev->getId(),
+                       'new null revision should have a different id from the original revision' );
+               $this->assertEquals( $orig->getTextId(), $rev->getTextId(),
+                       'new null revision should have the same text id as the original revision' );
+               $this->assertEquals( __METHOD__, $rev->getContent()->getNativeData() );
+       }
+
+       /**
+        * @covers Revision::insertOn
+        */
+       public function testInsertOn() {
+               $ip = '2600:387:ed7:947e:8c16:a1ad:dd34:1dd7';
+
+               $orig = $this->makeRevisionWithProps( [
+                       'user_text' => $ip
+               ] );
+
+               // Make sure the revision was copied to ip_changes
+               $dbr = wfGetDB( DB_REPLICA );
+               $res = $dbr->select( 'ip_changes', '*', [ 'ipc_rev_id' => $orig->getId() ] );
+               $row = $res->fetchObject();
+
+               $this->assertEquals( IP::toHex( $ip ), $row->ipc_hex );
+               $this->assertEquals( $orig->getTimestamp(), $row->ipc_rev_timestamp );
+       }
+
+       public static function provideUserWasLastToEdit() {
+               yield 'actually the last edit' => [ 3, true ];
+               yield 'not the current edit, but still by this user' => [ 2, true ];
+               yield 'edit by another user' => [ 1, false ];
+               yield 'first edit, by this user, but another user edited in the mean time' => [ 0, false ];
+       }
+
+       /**
+        * @dataProvider provideUserWasLastToEdit
+        */
+       public function testUserWasLastToEdit( $sinceIdx, $expectedLast ) {
+               $userA = User::newFromName( "RevisionStorageTest_userA" );
+               $userB = User::newFromName( "RevisionStorageTest_userB" );
+
+               if ( $userA->getId() === 0 ) {
+                       $userA = User::createNew( $userA->getName() );
+               }
+
+               if ( $userB->getId() === 0 ) {
+                       $userB = User::createNew( $userB->getName() );
+               }
+
+               $ns = $this->getDefaultWikitextNS();
+
+               $dbw = wfGetDB( DB_MASTER );
+               $revisions = [];
+
+               // create revisions -----------------------------
+               $page = WikiPage::factory( Title::newFromText(
+                       'RevisionStorageTest_testUserWasLastToEdit', $ns ) );
+               $page->insertOn( $dbw );
+
+               $revisions[0] = new Revision( [
+                       'page' => $page->getId(),
+                       // we need the title to determine the page's default content model
+                       'title' => $page->getTitle(),
+                       'timestamp' => '20120101000000',
+                       'user' => $userA->getId(),
+                       'text' => 'zero',
+                       'content_model' => CONTENT_MODEL_WIKITEXT,
+                       'summary' => 'edit zero'
+               ] );
+               $revisions[0]->insertOn( $dbw );
+
+               $revisions[1] = new Revision( [
+                       'page' => $page->getId(),
+                       // still need the title, because $page->getId() is 0 (there's no entry in the page table)
+                       'title' => $page->getTitle(),
+                       'timestamp' => '20120101000100',
+                       'user' => $userA->getId(),
+                       'text' => 'one',
+                       'content_model' => CONTENT_MODEL_WIKITEXT,
+                       'summary' => 'edit one'
+               ] );
+               $revisions[1]->insertOn( $dbw );
+
+               $revisions[2] = new Revision( [
+                       'page' => $page->getId(),
+                       'title' => $page->getTitle(),
+                       'timestamp' => '20120101000200',
+                       'user' => $userB->getId(),
+                       'text' => 'two',
+                       'content_model' => CONTENT_MODEL_WIKITEXT,
+                       'summary' => 'edit two'
+               ] );
+               $revisions[2]->insertOn( $dbw );
+
+               $revisions[3] = new Revision( [
+                       'page' => $page->getId(),
+                       'title' => $page->getTitle(),
+                       'timestamp' => '20120101000300',
+                       'user' => $userA->getId(),
+                       'text' => 'three',
+                       'content_model' => CONTENT_MODEL_WIKITEXT,
+                       'summary' => 'edit three'
+               ] );
+               $revisions[3]->insertOn( $dbw );
+
+               $revisions[4] = new Revision( [
+                       'page' => $page->getId(),
+                       'title' => $page->getTitle(),
+                       'timestamp' => '20120101000200',
+                       'user' => $userA->getId(),
+                       'text' => 'zero',
+                       'content_model' => CONTENT_MODEL_WIKITEXT,
+                       'summary' => 'edit four'
+               ] );
+               $revisions[4]->insertOn( $dbw );
+
+               // test it ---------------------------------
+               $since = $revisions[$sinceIdx]->getTimestamp();
+
+               $wasLast = Revision::userWasLastToEdit( $dbw, $page->getId(), $userA->getId(), $since );
+
+               $this->assertEquals( $expectedLast, $wasLast );
+       }
+
+       /**
+        * @param string $text
+        * @param string $title
+        * @param string $model
+        * @param string $format
+        *
+        * @return Revision
+        */
+       private function newTestRevision( $text, $title = "Test",
+               $model = CONTENT_MODEL_WIKITEXT, $format = null
+       ) {
+               if ( is_string( $title ) ) {
+                       $title = Title::newFromText( $title );
+               }
+
+               $content = ContentHandler::makeContent( $text, $title, $model, $format );
+
+               $rev = new Revision(
+                       [
+                               'id' => 42,
+                               'page' => 23,
+                               'title' => $title,
+
+                               'content' => $content,
+                               'length' => $content->getSize(),
+                               'comment' => "testing",
+                               'minor_edit' => false,
+
+                               'content_format' => $format,
+                       ]
+               );
+
+               return $rev;
+       }
+
+       public function provideGetContentModel() {
+               // NOTE: we expect the help namespace to always contain wikitext
+               return [
+                       [ 'hello world', 'Help:Hello', null, null, CONTENT_MODEL_WIKITEXT ],
+                       [ 'hello world', 'User:hello/there.css', null, null, CONTENT_MODEL_CSS ],
+                       [ serialize( 'hello world' ), 'Dummy:Hello', null, null, DummyContentForTesting::MODEL_ID ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetContentModel
+        * @covers Revision::getContentModel
+        */
+       public function testGetContentModel( $text, $title, $model, $format, $expectedModel ) {
+               $rev = $this->newTestRevision( $text, $title, $model, $format );
+
+               $this->assertEquals( $expectedModel, $rev->getContentModel() );
+       }
+
+       public function provideGetContentFormat() {
+               // NOTE: we expect the help namespace to always contain wikitext
+               return [
+                       [ 'hello world', 'Help:Hello', null, null, CONTENT_FORMAT_WIKITEXT ],
+                       [ 'hello world', 'Help:Hello', CONTENT_MODEL_CSS, null, CONTENT_FORMAT_CSS ],
+                       [ 'hello world', 'User:hello/there.css', null, null, CONTENT_FORMAT_CSS ],
+                       [ serialize( 'hello world' ), 'Dummy:Hello', null, null, DummyContentForTesting::MODEL_ID ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetContentFormat
+        * @covers Revision::getContentFormat
+        */
+       public function testGetContentFormat( $text, $title, $model, $format, $expectedFormat ) {
+               $rev = $this->newTestRevision( $text, $title, $model, $format );
+
+               $this->assertEquals( $expectedFormat, $rev->getContentFormat() );
+       }
+
+       public function provideGetContentHandler() {
+               // NOTE: we expect the help namespace to always contain wikitext
+               return [
+                       [ 'hello world', 'Help:Hello', null, null, 'WikitextContentHandler' ],
+                       [ 'hello world', 'User:hello/there.css', null, null, 'CssContentHandler' ],
+                       [ serialize( 'hello world' ), 'Dummy:Hello', null, null, 'DummyContentHandlerForTesting' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetContentHandler
+        * @covers Revision::getContentHandler
+        */
+       public function testGetContentHandler( $text, $title, $model, $format, $expectedClass ) {
+               $rev = $this->newTestRevision( $text, $title, $model, $format );
+
+               $this->assertEquals( $expectedClass, get_class( $rev->getContentHandler() ) );
+       }
+
+       public function provideGetContent() {
+               // NOTE: we expect the help namespace to always contain wikitext
+               return [
+                       [ 'hello world', 'Help:Hello', null, null, Revision::FOR_PUBLIC, 'hello world' ],
+                       [
+                               serialize( 'hello world' ),
+                               'Hello',
+                               DummyContentForTesting::MODEL_ID,
+                               null,
+                               Revision::FOR_PUBLIC,
+                               serialize( 'hello world' )
+                       ],
+                       [
+                               serialize( 'hello world' ),
+                               'Dummy:Hello',
+                               null,
+                               null,
+                               Revision::FOR_PUBLIC,
+                               serialize( 'hello world' )
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetContent
+        * @covers Revision::getContent
+        */
+       public function testGetContent( $text, $title, $model, $format,
+               $audience, $expectedSerialization
+       ) {
+               $rev = $this->newTestRevision( $text, $title, $model, $format );
+               $content = $rev->getContent( $audience );
+
+               $this->assertEquals(
+                       $expectedSerialization,
+                       is_null( $content ) ? null : $content->serialize( $format )
+               );
+       }
+
+       /**
+        * @covers Revision::getContent
+        */
+       public function testGetContent_failure() {
+               $rev = new Revision( [
+                       'page' => $this->testPage->getId(),
+                       'content_model' => $this->testPage->getContentModel(),
+                       'text_id' => 123456789, // not in the test DB
+               ] );
+
+               $this->assertNull( $rev->getContent(),
+                       "getContent() should return null if the revision's text blob could not be loaded." );
+
+               // NOTE: check this twice, once for lazy initialization, and once with the cached value.
+               $this->assertNull( $rev->getContent(),
+                       "getContent() should return null if the revision's text blob could not be loaded." );
+       }
+
+       public function provideGetSize() {
+               return [
+                       [ "hello world.", CONTENT_MODEL_WIKITEXT, 12 ],
+                       [ serialize( "hello world." ), DummyContentForTesting::MODEL_ID, 12 ],
+               ];
+       }
+
+       /**
+        * @covers Revision::getSize
+        * @dataProvider provideGetSize
+        */
+       public function testGetSize( $text, $model, $expected_size ) {
+               $rev = $this->newTestRevision( $text, 'RevisionTest_testGetSize', $model );
+               $this->assertEquals( $expected_size, $rev->getSize() );
+       }
+
+       public function provideGetSha1() {
+               return [
+                       [ "hello world.", CONTENT_MODEL_WIKITEXT, Revision::base36Sha1( "hello world." ) ],
+                       [
+                               serialize( "hello world." ),
+                               DummyContentForTesting::MODEL_ID,
+                               Revision::base36Sha1( serialize( "hello world." ) )
+                       ],
+               ];
+       }
+
+       /**
+        * @covers Revision::getSha1
+        * @dataProvider provideGetSha1
+        */
+       public function testGetSha1( $text, $model, $expected_hash ) {
+               $rev = $this->newTestRevision( $text, 'RevisionTest_testGetSha1', $model );
+               $this->assertEquals( $expected_hash, $rev->getSha1() );
+       }
+
+       /**
+        * Tests whether $rev->getContent() returns a clone when needed.
+        *
+        * @covers Revision::getContent
+        */
+       public function testGetContentClone() {
+               $content = new RevisionTestModifyableContent( "foo" );
+
+               $rev = new Revision(
+                       [
+                               'id' => 42,
+                               'page' => 23,
+                               'title' => Title::newFromText( "testGetContentClone_dummy" ),
+
+                               'content' => $content,
+                               'length' => $content->getSize(),
+                               'comment' => "testing",
+                               'minor_edit' => false,
+                       ]
+               );
+
+               /** @var RevisionTestModifyableContent $content */
+               $content = $rev->getContent( Revision::RAW );
+               $content->setText( "bar" );
+
+               /** @var RevisionTestModifyableContent $content2 */
+               $content2 = $rev->getContent( Revision::RAW );
+               // content is mutable, expect clone
+               $this->assertNotSame( $content, $content2, "expected a clone" );
+               // clone should contain the original text
+               $this->assertEquals( "foo", $content2->getText() );
+
+               $content2->setText( "bla bla" );
+               // clones should be independent
+               $this->assertEquals( "bar", $content->getText() );
+       }
+
+       /**
+        * Tests whether $rev->getContent() returns the same object repeatedly if appropriate.
+        * @covers Revision::getContent
+        */
+       public function testGetContentUncloned() {
+               $rev = $this->newTestRevision( "hello", "testGetContentUncloned_dummy", CONTENT_MODEL_WIKITEXT );
+               $content = $rev->getContent( Revision::RAW );
+               $content2 = $rev->getContent( Revision::RAW );
+
+               // for immutable content like wikitext, this should be the same object
+               $this->assertSame( $content, $content2 );
+       }
+
+       /**
+        * @covers Revision::loadFromId
+        */
+       public function testLoadFromId() {
+               $rev = $this->testPage->getRevision();
+               $this->assertRevEquals(
+                       $rev,
+                       Revision::loadFromId( wfGetDB( DB_MASTER ), $rev->getId() )
+               );
+       }
+
+       /**
+        * @covers Revision::loadFromPageId
+        */
+       public function testLoadFromPageId() {
+               $this->assertRevEquals(
+                       $this->testPage->getRevision(),
+                       Revision::loadFromPageId( wfGetDB( DB_MASTER ), $this->testPage->getId() )
+               );
+       }
+
+       /**
+        * @covers Revision::loadFromPageId
+        */
+       public function testLoadFromPageIdWithLatestRevId() {
+               $this->assertRevEquals(
+                       $this->testPage->getRevision(),
+                       Revision::loadFromPageId(
+                               wfGetDB( DB_MASTER ),
+                               $this->testPage->getId(),
+                               $this->testPage->getLatest()
+                       )
+               );
+       }
+
+       /**
+        * @covers Revision::loadFromPageId
+        */
+       public function testLoadFromPageIdWithNotLatestRevId() {
+               $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
+               $this->assertRevEquals(
+                       $this->testPage->getRevision()->getPrevious(),
+                       Revision::loadFromPageId(
+                               wfGetDB( DB_MASTER ),
+                               $this->testPage->getId(),
+                               $this->testPage->getRevision()->getPrevious()->getId()
+                       )
+               );
+       }
+
+       /**
+        * @covers Revision::loadFromTitle
+        */
+       public function testLoadFromTitle() {
+               $this->assertRevEquals(
+                       $this->testPage->getRevision(),
+                       Revision::loadFromTitle( wfGetDB( DB_MASTER ), $this->testPage->getTitle() )
+               );
+       }
+
+       /**
+        * @covers Revision::loadFromTitle
+        */
+       public function testLoadFromTitleWithLatestRevId() {
+               $this->assertRevEquals(
+                       $this->testPage->getRevision(),
+                       Revision::loadFromTitle(
+                               wfGetDB( DB_MASTER ),
+                               $this->testPage->getTitle(),
+                               $this->testPage->getLatest()
+                       )
+               );
+       }
+
+       /**
+        * @covers Revision::loadFromTitle
+        */
+       public function testLoadFromTitleWithNotLatestRevId() {
+               $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
+               $this->assertRevEquals(
+                       $this->testPage->getRevision()->getPrevious(),
+                       Revision::loadFromTitle(
+                               wfGetDB( DB_MASTER ),
+                               $this->testPage->getTitle(),
+                               $this->testPage->getRevision()->getPrevious()->getId()
+                       )
+               );
+       }
+
+       /**
+        * @covers Revision::loadFromTimestamp()
+        */
+       public function testLoadFromTimestamp() {
+               $this->assertRevEquals(
+                       $this->testPage->getRevision(),
+                       Revision::loadFromTimestamp(
+                               wfGetDB( DB_MASTER ),
+                               $this->testPage->getTitle(),
+                               $this->testPage->getRevision()->getTimestamp()
+                       )
+               );
+       }
+
+       /**
+        * @covers Revision::getParentLengths
+        */
+       public function testGetParentLengths_noRevIds() {
+               $this->assertSame(
+                       [],
+                       Revision::getParentLengths(
+                               wfGetDB( DB_MASTER ),
+                               []
+                       )
+               );
+       }
+
+       /**
+        * @covers Revision::getParentLengths
+        */
+       public function testGetParentLengths_oneRevId() {
+               $text = '831jr091jr0921kr21kr0921kjr0921j09rj1';
+               $textLength = strlen( $text );
+
+               $this->testPage->doEditContent( new WikitextContent( $text ), __METHOD__ );
+               $rev[1] = $this->testPage->getLatest();
+
+               $this->assertSame(
+                       [ $rev[1] => strval( $textLength ) ],
+                       Revision::getParentLengths(
+                               wfGetDB( DB_MASTER ),
+                               [ $rev[1] ]
+                       )
+               );
+       }
+
+       /**
+        * @covers Revision::getParentLengths
+        */
+       public function testGetParentLengths_multipleRevIds() {
+               $textOne = '831jr091jr0921kr21kr0921kjr0921j09rj1';
+               $textOneLength = strlen( $textOne );
+               $textTwo = '831jr091jr092121j09rj1';
+               $textTwoLength = strlen( $textTwo );
+
+               $this->testPage->doEditContent( new WikitextContent( $textOne ), __METHOD__ );
+               $rev[1] = $this->testPage->getLatest();
+               $this->testPage->doEditContent( new WikitextContent( $textTwo ), __METHOD__ );
+               $rev[2] = $this->testPage->getLatest();
+
+               $this->assertSame(
+                       [ $rev[1] => strval( $textOneLength ), $rev[2] => strval( $textTwoLength ) ],
+                       Revision::getParentLengths(
+                               wfGetDB( DB_MASTER ),
+                               [ $rev[1], $rev[2] ]
+                       )
+               );
+       }
+
+       /**
+        * @covers Revision::getTitle
+        */
+       public function testGetTitle_fromExistingRevision() {
+               $this->assertTrue(
+                       $this->testPage->getTitle()->equals(
+                               $this->testPage->getRevision()->getTitle()
+                       )
+               );
+       }
+
+       /**
+        * @covers Revision::getTitle
+        */
+       public function testGetTitle_fromRevisionWhichWillLoadTheTitle() {
+               $rev = new Revision( [ 'id' => $this->testPage->getLatest() ] );
+               $this->assertTrue(
+                       $this->testPage->getTitle()->equals(
+                               $rev->getTitle()
+                       )
+               );
+       }
+
+       /**
+        * @covers Revision::getTitle
+        */
+       public function testGetTitle_forBadRevision() {
+               $rev = new Revision( [] );
+               $this->assertNull( $rev->getTitle() );
+       }
+
+       /**
+        * @covers Revision::isMinor
+        */
+       public function testIsMinor_true() {
+               // Use a sysop to ensure we can mark edits as minor
+               $sysop = $this->getTestSysop()->getUser();
+
+               $this->testPage->doEditContent(
+                       new WikitextContent( __METHOD__ ),
+                       __METHOD__,
+                       EDIT_MINOR,
+                       false,
+                       $sysop
+               );
+               $rev = $this->testPage->getRevision();
+
+               $this->assertSame( true, $rev->isMinor() );
+       }
+
+       /**
+        * @covers Revision::isMinor
+        */
+       public function testIsMinor_false() {
+               $this->testPage->doEditContent(
+                       new WikitextContent( __METHOD__ ),
+                       __METHOD__,
+                       0
+               );
+               $rev = $this->testPage->getRevision();
+
+               $this->assertSame( false, $rev->isMinor() );
+       }
+
+       /**
+        * @covers Revision::getTimestamp
+        */
+       public function testGetTimestamp() {
+               $testTimestamp = wfTimestampNow();
+
+               $this->testPage->doEditContent(
+                       new WikitextContent( __METHOD__ ),
+                       __METHOD__
+               );
+               $rev = $this->testPage->getRevision();
+
+               $this->assertInternalType( 'string', $rev->getTimestamp() );
+               $this->assertTrue( strlen( $rev->getTimestamp() ) == strlen( 'YYYYMMDDHHMMSS' ) );
+               $this->assertContains( substr( $testTimestamp, 0, 10 ), $rev->getTimestamp() );
+       }
+
+       /**
+        * @covers Revision::getUser
+        * @covers Revision::getUserText
+        */
+       public function testGetUserAndText() {
+               $sysop = $this->getTestSysop()->getUser();
+
+               $this->testPage->doEditContent(
+                       new WikitextContent( __METHOD__ ),
+                       __METHOD__,
+                       0,
+                       false,
+                       $sysop
+               );
+               $rev = $this->testPage->getRevision();
+
+               $this->assertSame( $sysop->getId(), $rev->getUser() );
+               $this->assertSame( $sysop->getName(), $rev->getUserText() );
+       }
+
+       /**
+        * @covers Revision::isDeleted
+        */
+       public function testIsDeleted_nothingDeleted() {
+               $rev = $this->testPage->getRevision();
+
+               $this->assertSame( false, $rev->isDeleted( Revision::DELETED_TEXT ) );
+               $this->assertSame( false, $rev->isDeleted( Revision::DELETED_COMMENT ) );
+               $this->assertSame( false, $rev->isDeleted( Revision::DELETED_RESTRICTED ) );
+               $this->assertSame( false, $rev->isDeleted( Revision::DELETED_USER ) );
+       }
+
+       /**
+        * @covers Revision::getVisibility
+        */
+       public function testGetVisibility_nothingDeleted() {
+               $rev = $this->testPage->getRevision();
+
+               $this->assertSame( 0, $rev->getVisibility() );
+       }
+
+       /**
+        * @covers Revision::getComment
+        */
+       public function testGetComment_notDeleted() {
+               $expectedSummary = 'goatlicious summary';
+
+               $this->testPage->doEditContent(
+                       new WikitextContent( __METHOD__ ),
+                       $expectedSummary
+               );
+               $rev = $this->testPage->getRevision();
+
+               $this->assertSame( $expectedSummary, $rev->getComment() );
+       }
+
+       /**
+        * @covers Revision::isUnpatrolled
+        */
+       public function testIsUnpatrolled_returnsRecentChangesId() {
+               $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
+               $rev = $this->testPage->getRevision();
+
+               $this->assertGreaterThan( 0, $rev->isUnpatrolled() );
+               $this->assertSame( $rev->getRecentChange()->getAttribute( 'rc_id' ), $rev->isUnpatrolled() );
+       }
+
+       /**
+        * @covers Revision::isUnpatrolled
+        */
+       public function testIsUnpatrolled_returnsZeroIfPatrolled() {
+               // This assumes that sysops are auto patrolled
+               $sysop = $this->getTestSysop()->getUser();
+               $this->testPage->doEditContent(
+                       new WikitextContent( __METHOD__ ),
+                       __METHOD__,
+                       0,
+                       false,
+                       $sysop
+               );
+               $rev = $this->testPage->getRevision();
+
+               $this->assertSame( 0, $rev->isUnpatrolled() );
+       }
+
+       /**
+        * This is a simple blanket test for all simple content getters and is methods to provide some
+        * coverage before the split of Revision into multiple classes for MCR work.
+        * @covers Revision::getContent
+        * @covers Revision::getSerializedData
+        * @covers Revision::getContentModel
+        * @covers Revision::getContentFormat
+        * @covers Revision::getContentHandler
+        */
+       public function testSimpleContentGetters() {
+               $expectedText = 'testSimpleContentGetters in Revision. Goats love MCR...';
+               $expectedSummary = 'goatlicious testSimpleContentGetters summary';
+
+               $this->testPage->doEditContent(
+                       new WikitextContent( $expectedText ),
+                       $expectedSummary
+               );
+               $rev = $this->testPage->getRevision();
+
+               $this->assertSame( $expectedText, $rev->getContent()->getNativeData() );
+               $this->assertSame( $expectedText, $rev->getSerializedData() );
+               $this->assertSame( $this->testPage->getContentModel(), $rev->getContentModel() );
+               $this->assertSame( $this->testPage->getContent()->getDefaultFormat(), $rev->getContentFormat() );
+               $this->assertSame( $this->testPage->getContentHandler(), $rev->getContentHandler() );
+       }
+
+       /**
+        * @covers Revision::newKnownCurrent
+        */
+       public function testNewKnownCurrent() {
+               // Setup the services
+               $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
+               $this->setService( 'MainWANObjectCache', $cache );
+               $db = wfGetDB( DB_MASTER );
+
+               // Get a fresh revision to use during testing
+               $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
+               $rev = $this->testPage->getRevision();
+
+               // Clear any previous cache for the revision during creation
+               $key = $cache->makeGlobalKey( 'revision', $db->getDomainID(), $rev->getPage(), $rev->getId() );
+               $cache->delete( $key, WANObjectCache::HOLDOFF_NONE );
+               $this->assertFalse( $cache->get( $key ) );
+
+               // Get the new revision and make sure it is in the cache and correct
+               $newRev = Revision::newKnownCurrent( $db, $rev->getPage(), $rev->getId() );
+               $this->assertRevEquals( $rev, $newRev );
+               $this->assertRevEquals( $rev, $cache->get( $key ) );
+       }
+
+       public function provideUserCanBitfield() {
+               yield [ 0, 0, [], null, true ];
+               // Bitfields match, user has no permissions
+               yield [ Revision::DELETED_TEXT, Revision::DELETED_TEXT, [], null, false ];
+               yield [ Revision::DELETED_COMMENT, Revision::DELETED_COMMENT, [], null, false ];
+               yield [ Revision::DELETED_USER, Revision::DELETED_USER, [], null, false ];
+               yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [], null, false ];
+               // Bitfields match, user (admin) does have permissions
+               yield [ Revision::DELETED_TEXT, Revision::DELETED_TEXT, [ 'sysop' ], null, true ];
+               yield [ Revision::DELETED_COMMENT, Revision::DELETED_COMMENT, [ 'sysop' ], null, true ];
+               yield [ Revision::DELETED_USER, Revision::DELETED_USER, [ 'sysop' ], null, true ];
+               // Bitfields match, user (admin) does not have permissions
+               yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [ 'sysop' ], null, false ];
+               // Bitfields match, user (oversight) does have permissions
+               yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [ 'oversight' ], null, true ];
+               // Check permissions using the title
+               yield [
+                       Revision::DELETED_TEXT,
+                       Revision::DELETED_TEXT,
+                       [ 'sysop' ],
+                       Title::newFromText( __METHOD__ ),
+                       true,
+               ];
+               yield [
+                       Revision::DELETED_TEXT,
+                       Revision::DELETED_TEXT,
+                       [],
+                       Title::newFromText( __METHOD__ ),
+                       false,
+               ];
+       }
+
+       /**
+        * @dataProvider provideUserCanBitfield
+        * @covers Revision::userCanBitfield
+        */
+       public function testUserCanBitfield( $bitField, $field, $userGroups, $title, $expected ) {
+               $this->setMwGlobals(
+                       'wgGroupPermissions',
+                       [
+                               'sysop' => [
+                                       'deletedtext' => true,
+                                       'deletedhistory' => true,
+                               ],
+                               'oversight' => [
+                                       'viewsuppressed' => true,
+                                       'suppressrevision' => true,
+                               ],
+                       ]
+               );
+               $user = $this->getTestUser( $userGroups )->getUser();
+
+               $this->assertSame(
+                       $expected,
+                       Revision::userCanBitfield( $bitField, $field, $user, $title )
+               );
+
+               // Fallback to $wgUser
+               $this->setMwGlobals(
+                       'wgUser',
+                       $user
+               );
+               $this->assertSame(
+                       $expected,
+                       Revision::userCanBitfield( $bitField, $field, null, $title )
+               );
+       }
+
+       public function provideUserCan() {
+               yield [ 0, 0, [], true ];
+               // Bitfields match, user has no permissions
+               yield [ Revision::DELETED_TEXT, Revision::DELETED_TEXT, [], false ];
+               yield [ Revision::DELETED_COMMENT, Revision::DELETED_COMMENT, [], false ];
+               yield [ Revision::DELETED_USER, Revision::DELETED_USER, [], false ];
+               yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [], false ];
+               // Bitfields match, user (admin) does have permissions
+               yield [ Revision::DELETED_TEXT, Revision::DELETED_TEXT, [ 'sysop' ], true ];
+               yield [ Revision::DELETED_COMMENT, Revision::DELETED_COMMENT, [ 'sysop' ], true ];
+               yield [ Revision::DELETED_USER, Revision::DELETED_USER, [ 'sysop' ], true ];
+               // Bitfields match, user (admin) does not have permissions
+               yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [ 'sysop' ], false ];
+               // Bitfields match, user (oversight) does have permissions
+               yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [ 'oversight' ], true ];
+       }
+
+       /**
+        * @dataProvider provideUserCan
+        * @covers Revision::userCan
+        */
+       public function testUserCan( $bitField, $field, $userGroups, $expected ) {
+               $this->setMwGlobals(
+                       'wgGroupPermissions',
+                       [
+                               'sysop' => [
+                                       'deletedtext' => true,
+                                       'deletedhistory' => true,
+                               ],
+                               'oversight' => [
+                                       'viewsuppressed' => true,
+                                       'suppressrevision' => true,
+                               ],
+                       ]
+               );
+               $user = $this->getTestUser( $userGroups )->getUser();
+               $revision = new Revision( [ 'deleted' => $bitField ] );
+
+               $this->assertSame(
+                       $expected,
+                       $revision->userCan( $field, $user )
+               );
+       }
+
+}
diff --git a/tests/phpunit/includes/RevisionIntegrationTest.php b/tests/phpunit/includes/RevisionIntegrationTest.php
deleted file mode 100644 (file)
index 403983c..0000000
+++ /dev/null
@@ -1,1223 +0,0 @@
-<?php
-
-/**
- * @group ContentHandler
- * @group Database
- *
- * @group medium
- */
-class RevisionIntegrationTest extends MediaWikiTestCase {
-
-       /**
-        * @var WikiPage $testPage
-        */
-       private $testPage;
-
-       public function __construct( $name = null, array $data = [], $dataName = '' ) {
-               parent::__construct( $name, $data, $dataName );
-
-               $this->tablesUsed = array_merge( $this->tablesUsed,
-                       [
-                               'page',
-                               'revision',
-                               'ip_changes',
-                               'text',
-                               'archive',
-
-                               'recentchanges',
-                               'logging',
-
-                               'page_props',
-                               'pagelinks',
-                               'categorylinks',
-                               'langlinks',
-                               'externallinks',
-                               'imagelinks',
-                               'templatelinks',
-                               'iwlinks'
-                       ]
-               );
-       }
-
-       protected function setUp() {
-               global $wgContLang;
-
-               parent::setUp();
-
-               $this->mergeMwGlobalArrayValue(
-                       'wgExtraNamespaces',
-                       [
-                               12312 => 'Dummy',
-                               12313 => 'Dummy_talk',
-                       ]
-               );
-
-               $this->mergeMwGlobalArrayValue(
-                       'wgNamespaceContentModels',
-                       [
-                               12312 => DummyContentForTesting::MODEL_ID,
-                       ]
-               );
-
-               $this->mergeMwGlobalArrayValue(
-                       'wgContentHandlers',
-                       [
-                               DummyContentForTesting::MODEL_ID => 'DummyContentHandlerForTesting',
-                               RevisionTestModifyableContent::MODEL_ID => 'RevisionTestModifyableContentHandler',
-                       ]
-               );
-
-               MWNamespace::clearCaches();
-               // Reset namespace cache
-               $wgContLang->resetNamespaces();
-               if ( !$this->testPage ) {
-                       $this->testPage = WikiPage::factory( Title::newFromText( 'UTPage' ) );
-               }
-       }
-
-       protected function tearDown() {
-               global $wgContLang;
-
-               parent::tearDown();
-
-               MWNamespace::clearCaches();
-               // Reset namespace cache
-               $wgContLang->resetNamespaces();
-       }
-
-       private function makeRevisionWithProps( $props = null ) {
-               if ( $props === null ) {
-                       $props = [];
-               }
-
-               if ( !isset( $props['content'] ) && !isset( $props['text'] ) ) {
-                       $props['text'] = 'Lorem Ipsum';
-               }
-
-               if ( !isset( $props['comment'] ) ) {
-                       $props['comment'] = 'just a test';
-               }
-
-               if ( !isset( $props['page'] ) ) {
-                       $props['page'] = $this->testPage->getId();
-               }
-
-               $rev = new Revision( $props );
-
-               $dbw = wfGetDB( DB_MASTER );
-               $rev->insertOn( $dbw );
-
-               return $rev;
-       }
-
-       /**
-        * @param string $titleString
-        * @param string $text
-        * @param string|null $model
-        *
-        * @return WikiPage
-        */
-       private function createPage( $titleString, $text, $model = null ) {
-               if ( !preg_match( '/:/', $titleString ) &&
-                       ( $model === null || $model === CONTENT_MODEL_WIKITEXT )
-               ) {
-                       $ns = $this->getDefaultWikitextNS();
-                       $titleString = MWNamespace::getCanonicalName( $ns ) . ':' . $titleString;
-               }
-
-               $title = Title::newFromText( $titleString );
-               $wikipage = new WikiPage( $title );
-
-               // Delete the article if it already exists
-               if ( $wikipage->exists() ) {
-                       $wikipage->doDeleteArticle( "done" );
-               }
-
-               $content = ContentHandler::makeContent( $text, $title, $model );
-               $wikipage->doEditContent( $content, __METHOD__, EDIT_NEW );
-
-               return $wikipage;
-       }
-
-       private function assertRevEquals( Revision $orig, Revision $rev = null ) {
-               $this->assertNotNull( $rev, 'missing revision' );
-
-               $this->assertEquals( $orig->getId(), $rev->getId() );
-               $this->assertEquals( $orig->getPage(), $rev->getPage() );
-               $this->assertEquals( $orig->getTimestamp(), $rev->getTimestamp() );
-               $this->assertEquals( $orig->getUser(), $rev->getUser() );
-               $this->assertEquals( $orig->getContentModel(), $rev->getContentModel() );
-               $this->assertEquals( $orig->getContentFormat(), $rev->getContentFormat() );
-               $this->assertEquals( $orig->getSha1(), $rev->getSha1() );
-       }
-
-       /**
-        * @covers Revision::insertOn
-        */
-       public function testInsertOn_success() {
-               $parentId = $this->testPage->getLatest();
-
-               // If an ExternalStore is set don't use it.
-               $this->setMwGlobals( 'wgDefaultExternalStore', false );
-
-               $rev = new Revision( [
-                       'page' => $this->testPage->getId(),
-                       'title' => $this->testPage->getTitle(),
-                       'text' => 'Revision Text',
-                       'comment' => 'Revision comment',
-               ] );
-
-               $revId = $rev->insertOn( wfGetDB( DB_MASTER ) );
-
-               $this->assertInternalType( 'integer', $revId );
-               $this->assertInternalType( 'integer', $rev->getTextId() );
-               $this->assertSame( $revId, $rev->getId() );
-
-               $this->assertSelect(
-                       'text',
-                       [ 'old_id', 'old_text' ],
-                       "old_id = {$rev->getTextId()}",
-                       [ [ strval( $rev->getTextId() ), 'Revision Text' ] ]
-               );
-               $this->assertSelect(
-                       'revision',
-                       [
-                               'rev_id',
-                               'rev_page',
-                               'rev_text_id',
-                               'rev_user',
-                               'rev_minor_edit',
-                               'rev_deleted',
-                               'rev_len',
-                               'rev_parent_id',
-                               'rev_sha1',
-                       ],
-                       "rev_id = {$rev->getId()}",
-                       [ [
-                               strval( $rev->getId() ),
-                               strval( $this->testPage->getId() ),
-                               strval( $rev->getTextId() ),
-                               '0',
-                               '0',
-                               '0',
-                               '13',
-                               strval( $parentId ),
-                               's0ngbdoxagreuf2vjtuxzwdz64n29xm',
-                       ] ]
-               );
-       }
-
-       /**
-        * @covers Revision::insertOn
-        */
-       public function testInsertOn_exceptionOnNoPage() {
-               // If an ExternalStore is set don't use it.
-               $this->setMwGlobals( 'wgDefaultExternalStore', false );
-               $this->setExpectedException(
-                       MWException::class,
-                       "Cannot insert revision: page ID must be nonzero"
-               );
-
-               $rev = new Revision( [] );
-
-               $rev->insertOn( wfGetDB( DB_MASTER ) );
-       }
-
-       /**
-        * @covers Revision::newFromTitle
-        */
-       public function testNewFromTitle_withoutId() {
-               $latestRevId = $this->testPage->getLatest();
-
-               $rev = Revision::newFromTitle( $this->testPage->getTitle() );
-
-               $this->assertTrue( $this->testPage->getTitle()->equals( $rev->getTitle() ) );
-               $this->assertEquals( $latestRevId, $rev->getId() );
-       }
-
-       /**
-        * @covers Revision::newFromTitle
-        */
-       public function testNewFromTitle_withId() {
-               $latestRevId = $this->testPage->getLatest();
-
-               $rev = Revision::newFromTitle( $this->testPage->getTitle(), $latestRevId );
-
-               $this->assertTrue( $this->testPage->getTitle()->equals( $rev->getTitle() ) );
-               $this->assertEquals( $latestRevId, $rev->getId() );
-       }
-
-       /**
-        * @covers Revision::newFromTitle
-        */
-       public function testNewFromTitle_withBadId() {
-               $latestRevId = $this->testPage->getLatest();
-
-               $rev = Revision::newFromTitle( $this->testPage->getTitle(), $latestRevId + 1 );
-
-               $this->assertNull( $rev );
-       }
-
-       /**
-        * @covers Revision::newFromRow
-        */
-       public function testNewFromRow() {
-               $orig = $this->makeRevisionWithProps();
-
-               $dbr = wfGetDB( DB_REPLICA );
-               $revQuery = Revision::getQueryInfo();
-               $res = $dbr->select( $revQuery['tables'], $revQuery['fields'], [ 'rev_id' => $orig->getId() ],
-                  __METHOD__, [], $revQuery['joins'] );
-               $this->assertTrue( is_object( $res ), 'query failed' );
-
-               $row = $res->fetchObject();
-               $res->free();
-
-               $rev = Revision::newFromRow( $row );
-
-               $this->assertRevEquals( $orig, $rev );
-       }
-
-       public function provideNewFromArchiveRow() {
-               yield [
-                       true,
-                       function ( $f ) {
-                               return $f;
-                       },
-               ];
-               yield [
-                       false,
-                       function ( $f ) {
-                               return $f;
-                       },
-               ];
-               yield [
-                       true,
-                       function ( $f ) {
-                               return $f + [ 'ar_namespace', 'ar_title' ];
-                       },
-               ];
-               yield [
-                       false,
-                       function ( $f ) {
-                               return $f + [ 'ar_namespace', 'ar_title' ];
-                       },
-               ];
-               yield [
-                       true,
-                       function ( $f ) {
-                               unset( $f['ar_text_id'] );
-                               return $f;
-                       },
-               ];
-               yield [
-                       false,
-                       function ( $f ) {
-                               unset( $f['ar_text_id'] );
-                               return $f;
-                       },
-               ];
-       }
-
-       /**
-        * @dataProvider provideNewFromArchiveRow
-        * @covers Revision::newFromArchiveRow
-        */
-       public function testNewFromArchiveRow( $contentHandlerUseDB, $selectModifier ) {
-               $this->setMwGlobals( 'wgContentHandlerUseDB', $contentHandlerUseDB );
-
-               $page = $this->createPage(
-                       'RevisionStorageTest_testNewFromArchiveRow',
-                       'Lorem Ipsum',
-                       CONTENT_MODEL_WIKITEXT
-               );
-               $orig = $page->getRevision();
-               $page->doDeleteArticle( 'test Revision::newFromArchiveRow' );
-
-               $dbr = wfGetDB( DB_REPLICA );
-               $arQuery = Revision::getArchiveQueryInfo();
-               $arQuery['fields'] = $selectModifier( $arQuery['fields'] );
-               $res = $dbr->select(
-                       $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ],
-                       __METHOD__, [], $arQuery['joins']
-               );
-               $this->assertTrue( is_object( $res ), 'query failed' );
-
-               $row = $res->fetchObject();
-               $res->free();
-
-               $rev = Revision::newFromArchiveRow( $row );
-
-               $this->assertRevEquals( $orig, $rev );
-       }
-
-       /**
-        * @covers Revision::newFromArchiveRow
-        */
-       public function testNewFromArchiveRowOverrides() {
-               $page = $this->createPage(
-                       'RevisionStorageTest_testNewFromArchiveRow',
-                       'Lorem Ipsum',
-                       CONTENT_MODEL_WIKITEXT
-               );
-               $orig = $page->getRevision();
-               $page->doDeleteArticle( 'test Revision::newFromArchiveRow' );
-
-               $dbr = wfGetDB( DB_REPLICA );
-               $arQuery = Revision::getArchiveQueryInfo();
-               $res = $dbr->select(
-                       $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ],
-                       __METHOD__, [], $arQuery['joins']
-               );
-               $this->assertTrue( is_object( $res ), 'query failed' );
-
-               $row = $res->fetchObject();
-               $res->free();
-
-               $rev = Revision::newFromArchiveRow( $row, [ 'comment' => 'SOMEOVERRIDE' ] );
-
-               $this->assertNotEquals( $orig->getComment(), $rev->getComment() );
-               $this->assertEquals( 'SOMEOVERRIDE', $rev->getComment() );
-       }
-
-       /**
-        * @covers Revision::newFromId
-        */
-       public function testNewFromId() {
-               $orig = $this->testPage->getRevision();
-               $rev = Revision::newFromId( $orig->getId() );
-               $this->assertRevEquals( $orig, $rev );
-       }
-
-       /**
-        * @covers Revision::newFromPageId
-        */
-       public function testNewFromPageId() {
-               $rev = Revision::newFromPageId( $this->testPage->getId() );
-               $this->assertRevEquals(
-                       $this->testPage->getRevision(),
-                       $rev
-               );
-       }
-
-       /**
-        * @covers Revision::newFromPageId
-        */
-       public function testNewFromPageIdWithLatestId() {
-               $rev = Revision::newFromPageId(
-                       $this->testPage->getId(),
-                       $this->testPage->getLatest()
-               );
-               $this->assertRevEquals(
-                       $this->testPage->getRevision(),
-                       $rev
-               );
-       }
-
-       /**
-        * @covers Revision::newFromPageId
-        */
-       public function testNewFromPageIdWithNotLatestId() {
-               $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
-               $rev = Revision::newFromPageId(
-                       $this->testPage->getId(),
-                       $this->testPage->getRevision()->getPrevious()->getId()
-               );
-               $this->assertRevEquals(
-                       $this->testPage->getRevision()->getPrevious(),
-                       $rev
-               );
-       }
-
-       /**
-        * @covers Revision::fetchRevision
-        */
-       public function testFetchRevision() {
-               // Hidden process cache assertion below
-               $this->testPage->getRevision()->getId();
-
-               $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
-               $id = $this->testPage->getRevision()->getId();
-
-               $res = Revision::fetchRevision( $this->testPage->getTitle() );
-
-               # note: order is unspecified
-               $rows = [];
-               while ( ( $row = $res->fetchObject() ) ) {
-                       $rows[$row->rev_id] = $row;
-               }
-
-               $this->assertEquals( 1, count( $rows ), 'expected exactly one revision' );
-               $this->assertArrayHasKey( $id, $rows, 'missing revision with id ' . $id );
-       }
-
-       /**
-        * @covers Revision::getPage
-        */
-       public function testGetPage() {
-               $page = $this->testPage;
-
-               $orig = $this->makeRevisionWithProps( [ 'page' => $page->getId() ] );
-               $rev = Revision::newFromId( $orig->getId() );
-
-               $this->assertEquals( $page->getId(), $rev->getPage() );
-       }
-
-       /**
-        * @covers Revision::isCurrent
-        */
-       public function testIsCurrent() {
-               $rev1 = $this->testPage->getRevision();
-
-               # @todo find out if this should be true
-               # $this->assertTrue( $rev1->isCurrent() );
-
-               $rev1x = Revision::newFromId( $rev1->getId() );
-               $this->assertTrue( $rev1x->isCurrent() );
-
-               $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
-               $rev2 = $this->testPage->getRevision();
-
-               # @todo find out if this should be true
-               # $this->assertTrue( $rev2->isCurrent() );
-
-               $rev1x = Revision::newFromId( $rev1->getId() );
-               $this->assertFalse( $rev1x->isCurrent() );
-
-               $rev2x = Revision::newFromId( $rev2->getId() );
-               $this->assertTrue( $rev2x->isCurrent() );
-       }
-
-       /**
-        * @covers Revision::getPrevious
-        */
-       public function testGetPrevious() {
-               $oldestRevision = $this->testPage->getOldestRevision();
-               $latestRevision = $this->testPage->getLatest();
-
-               $this->assertNull( $oldestRevision->getPrevious() );
-
-               $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
-               $newRevision = $this->testPage->getRevision();
-
-               $this->assertNotNull( $newRevision->getPrevious() );
-               $this->assertEquals( $latestRevision, $newRevision->getPrevious()->getId() );
-       }
-
-       /**
-        * @covers Revision::getNext
-        */
-       public function testGetNext() {
-               $rev1 = $this->testPage->getRevision();
-
-               $this->assertNull( $rev1->getNext() );
-
-               $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
-               $rev2 = $this->testPage->getRevision();
-
-               $this->assertNotNull( $rev1->getNext() );
-               $this->assertEquals( $rev2->getId(), $rev1->getNext()->getId() );
-       }
-
-       /**
-        * @covers Revision::newNullRevision
-        */
-       public function testNewNullRevision() {
-               $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
-               $orig = $this->testPage->getRevision();
-
-               $dbw = wfGetDB( DB_MASTER );
-               $rev = Revision::newNullRevision( $dbw, $this->testPage->getId(), 'a null revision', false );
-
-               $this->assertNotEquals( $orig->getId(), $rev->getId(),
-                       'new null revision should have a different id from the original revision' );
-               $this->assertEquals( $orig->getTextId(), $rev->getTextId(),
-                       'new null revision should have the same text id as the original revision' );
-               $this->assertEquals( __METHOD__, $rev->getContent()->getNativeData() );
-       }
-
-       /**
-        * @covers Revision::insertOn
-        */
-       public function testInsertOn() {
-               $ip = '2600:387:ed7:947e:8c16:a1ad:dd34:1dd7';
-
-               $orig = $this->makeRevisionWithProps( [
-                       'user_text' => $ip
-               ] );
-
-               // Make sure the revision was copied to ip_changes
-               $dbr = wfGetDB( DB_REPLICA );
-               $res = $dbr->select( 'ip_changes', '*', [ 'ipc_rev_id' => $orig->getId() ] );
-               $row = $res->fetchObject();
-
-               $this->assertEquals( IP::toHex( $ip ), $row->ipc_hex );
-               $this->assertEquals( $orig->getTimestamp(), $row->ipc_rev_timestamp );
-       }
-
-       public static function provideUserWasLastToEdit() {
-               yield 'actually the last edit' => [ 3, true ];
-               yield 'not the current edit, but still by this user' => [ 2, true ];
-               yield 'edit by another user' => [ 1, false ];
-               yield 'first edit, by this user, but another user edited in the mean time' => [ 0, false ];
-       }
-
-       /**
-        * @dataProvider provideUserWasLastToEdit
-        */
-       public function testUserWasLastToEdit( $sinceIdx, $expectedLast ) {
-               $userA = User::newFromName( "RevisionStorageTest_userA" );
-               $userB = User::newFromName( "RevisionStorageTest_userB" );
-
-               if ( $userA->getId() === 0 ) {
-                       $userA = User::createNew( $userA->getName() );
-               }
-
-               if ( $userB->getId() === 0 ) {
-                       $userB = User::createNew( $userB->getName() );
-               }
-
-               $ns = $this->getDefaultWikitextNS();
-
-               $dbw = wfGetDB( DB_MASTER );
-               $revisions = [];
-
-               // create revisions -----------------------------
-               $page = WikiPage::factory( Title::newFromText(
-                       'RevisionStorageTest_testUserWasLastToEdit', $ns ) );
-               $page->insertOn( $dbw );
-
-               $revisions[0] = new Revision( [
-                       'page' => $page->getId(),
-                       // we need the title to determine the page's default content model
-                       'title' => $page->getTitle(),
-                       'timestamp' => '20120101000000',
-                       'user' => $userA->getId(),
-                       'text' => 'zero',
-                       'content_model' => CONTENT_MODEL_WIKITEXT,
-                       'summary' => 'edit zero'
-               ] );
-               $revisions[0]->insertOn( $dbw );
-
-               $revisions[1] = new Revision( [
-                       'page' => $page->getId(),
-                       // still need the title, because $page->getId() is 0 (there's no entry in the page table)
-                       'title' => $page->getTitle(),
-                       'timestamp' => '20120101000100',
-                       'user' => $userA->getId(),
-                       'text' => 'one',
-                       'content_model' => CONTENT_MODEL_WIKITEXT,
-                       'summary' => 'edit one'
-               ] );
-               $revisions[1]->insertOn( $dbw );
-
-               $revisions[2] = new Revision( [
-                       'page' => $page->getId(),
-                       'title' => $page->getTitle(),
-                       'timestamp' => '20120101000200',
-                       'user' => $userB->getId(),
-                       'text' => 'two',
-                       'content_model' => CONTENT_MODEL_WIKITEXT,
-                       'summary' => 'edit two'
-               ] );
-               $revisions[2]->insertOn( $dbw );
-
-               $revisions[3] = new Revision( [
-                       'page' => $page->getId(),
-                       'title' => $page->getTitle(),
-                       'timestamp' => '20120101000300',
-                       'user' => $userA->getId(),
-                       'text' => 'three',
-                       'content_model' => CONTENT_MODEL_WIKITEXT,
-                       'summary' => 'edit three'
-               ] );
-               $revisions[3]->insertOn( $dbw );
-
-               $revisions[4] = new Revision( [
-                       'page' => $page->getId(),
-                       'title' => $page->getTitle(),
-                       'timestamp' => '20120101000200',
-                       'user' => $userA->getId(),
-                       'text' => 'zero',
-                       'content_model' => CONTENT_MODEL_WIKITEXT,
-                       'summary' => 'edit four'
-               ] );
-               $revisions[4]->insertOn( $dbw );
-
-               // test it ---------------------------------
-               $since = $revisions[$sinceIdx]->getTimestamp();
-
-               $wasLast = Revision::userWasLastToEdit( $dbw, $page->getId(), $userA->getId(), $since );
-
-               $this->assertEquals( $expectedLast, $wasLast );
-       }
-
-       /**
-        * @param string $text
-        * @param string $title
-        * @param string $model
-        * @param string $format
-        *
-        * @return Revision
-        */
-       private function newTestRevision( $text, $title = "Test",
-               $model = CONTENT_MODEL_WIKITEXT, $format = null
-       ) {
-               if ( is_string( $title ) ) {
-                       $title = Title::newFromText( $title );
-               }
-
-               $content = ContentHandler::makeContent( $text, $title, $model, $format );
-
-               $rev = new Revision(
-                       [
-                               'id' => 42,
-                               'page' => 23,
-                               'title' => $title,
-
-                               'content' => $content,
-                               'length' => $content->getSize(),
-                               'comment' => "testing",
-                               'minor_edit' => false,
-
-                               'content_format' => $format,
-                       ]
-               );
-
-               return $rev;
-       }
-
-       public function provideGetContentModel() {
-               // NOTE: we expect the help namespace to always contain wikitext
-               return [
-                       [ 'hello world', 'Help:Hello', null, null, CONTENT_MODEL_WIKITEXT ],
-                       [ 'hello world', 'User:hello/there.css', null, null, CONTENT_MODEL_CSS ],
-                       [ serialize( 'hello world' ), 'Dummy:Hello', null, null, DummyContentForTesting::MODEL_ID ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideGetContentModel
-        * @covers Revision::getContentModel
-        */
-       public function testGetContentModel( $text, $title, $model, $format, $expectedModel ) {
-               $rev = $this->newTestRevision( $text, $title, $model, $format );
-
-               $this->assertEquals( $expectedModel, $rev->getContentModel() );
-       }
-
-       public function provideGetContentFormat() {
-               // NOTE: we expect the help namespace to always contain wikitext
-               return [
-                       [ 'hello world', 'Help:Hello', null, null, CONTENT_FORMAT_WIKITEXT ],
-                       [ 'hello world', 'Help:Hello', CONTENT_MODEL_CSS, null, CONTENT_FORMAT_CSS ],
-                       [ 'hello world', 'User:hello/there.css', null, null, CONTENT_FORMAT_CSS ],
-                       [ serialize( 'hello world' ), 'Dummy:Hello', null, null, DummyContentForTesting::MODEL_ID ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideGetContentFormat
-        * @covers Revision::getContentFormat
-        */
-       public function testGetContentFormat( $text, $title, $model, $format, $expectedFormat ) {
-               $rev = $this->newTestRevision( $text, $title, $model, $format );
-
-               $this->assertEquals( $expectedFormat, $rev->getContentFormat() );
-       }
-
-       public function provideGetContentHandler() {
-               // NOTE: we expect the help namespace to always contain wikitext
-               return [
-                       [ 'hello world', 'Help:Hello', null, null, 'WikitextContentHandler' ],
-                       [ 'hello world', 'User:hello/there.css', null, null, 'CssContentHandler' ],
-                       [ serialize( 'hello world' ), 'Dummy:Hello', null, null, 'DummyContentHandlerForTesting' ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideGetContentHandler
-        * @covers Revision::getContentHandler
-        */
-       public function testGetContentHandler( $text, $title, $model, $format, $expectedClass ) {
-               $rev = $this->newTestRevision( $text, $title, $model, $format );
-
-               $this->assertEquals( $expectedClass, get_class( $rev->getContentHandler() ) );
-       }
-
-       public function provideGetContent() {
-               // NOTE: we expect the help namespace to always contain wikitext
-               return [
-                       [ 'hello world', 'Help:Hello', null, null, Revision::FOR_PUBLIC, 'hello world' ],
-                       [
-                               serialize( 'hello world' ),
-                               'Hello',
-                               DummyContentForTesting::MODEL_ID,
-                               null,
-                               Revision::FOR_PUBLIC,
-                               serialize( 'hello world' )
-                       ],
-                       [
-                               serialize( 'hello world' ),
-                               'Dummy:Hello',
-                               null,
-                               null,
-                               Revision::FOR_PUBLIC,
-                               serialize( 'hello world' )
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideGetContent
-        * @covers Revision::getContent
-        */
-       public function testGetContent( $text, $title, $model, $format,
-               $audience, $expectedSerialization
-       ) {
-               $rev = $this->newTestRevision( $text, $title, $model, $format );
-               $content = $rev->getContent( $audience );
-
-               $this->assertEquals(
-                       $expectedSerialization,
-                       is_null( $content ) ? null : $content->serialize( $format )
-               );
-       }
-
-       /**
-        * @covers Revision::getContent
-        */
-       public function testGetContent_failure() {
-               $rev = new Revision( [
-                       'page' => $this->testPage->getId(),
-                       'content_model' => $this->testPage->getContentModel(),
-                       'text_id' => 123456789, // not in the test DB
-               ] );
-
-               $this->assertNull( $rev->getContent(),
-                       "getContent() should return null if the revision's text blob could not be loaded." );
-
-               // NOTE: check this twice, once for lazy initialization, and once with the cached value.
-               $this->assertNull( $rev->getContent(),
-                       "getContent() should return null if the revision's text blob could not be loaded." );
-       }
-
-       public function provideGetSize() {
-               return [
-                       [ "hello world.", CONTENT_MODEL_WIKITEXT, 12 ],
-                       [ serialize( "hello world." ), DummyContentForTesting::MODEL_ID, 12 ],
-               ];
-       }
-
-       /**
-        * @covers Revision::getSize
-        * @dataProvider provideGetSize
-        */
-       public function testGetSize( $text, $model, $expected_size ) {
-               $rev = $this->newTestRevision( $text, 'RevisionTest_testGetSize', $model );
-               $this->assertEquals( $expected_size, $rev->getSize() );
-       }
-
-       public function provideGetSha1() {
-               return [
-                       [ "hello world.", CONTENT_MODEL_WIKITEXT, Revision::base36Sha1( "hello world." ) ],
-                       [
-                               serialize( "hello world." ),
-                               DummyContentForTesting::MODEL_ID,
-                               Revision::base36Sha1( serialize( "hello world." ) )
-                       ],
-               ];
-       }
-
-       /**
-        * @covers Revision::getSha1
-        * @dataProvider provideGetSha1
-        */
-       public function testGetSha1( $text, $model, $expected_hash ) {
-               $rev = $this->newTestRevision( $text, 'RevisionTest_testGetSha1', $model );
-               $this->assertEquals( $expected_hash, $rev->getSha1() );
-       }
-
-       /**
-        * Tests whether $rev->getContent() returns a clone when needed.
-        *
-        * @covers Revision::getContent
-        */
-       public function testGetContentClone() {
-               $content = new RevisionTestModifyableContent( "foo" );
-
-               $rev = new Revision(
-                       [
-                               'id' => 42,
-                               'page' => 23,
-                               'title' => Title::newFromText( "testGetContentClone_dummy" ),
-
-                               'content' => $content,
-                               'length' => $content->getSize(),
-                               'comment' => "testing",
-                               'minor_edit' => false,
-                       ]
-               );
-
-               /** @var RevisionTestModifyableContent $content */
-               $content = $rev->getContent( Revision::RAW );
-               $content->setText( "bar" );
-
-               /** @var RevisionTestModifyableContent $content2 */
-               $content2 = $rev->getContent( Revision::RAW );
-               // content is mutable, expect clone
-               $this->assertNotSame( $content, $content2, "expected a clone" );
-               // clone should contain the original text
-               $this->assertEquals( "foo", $content2->getText() );
-
-               $content2->setText( "bla bla" );
-               // clones should be independent
-               $this->assertEquals( "bar", $content->getText() );
-       }
-
-       /**
-        * Tests whether $rev->getContent() returns the same object repeatedly if appropriate.
-        * @covers Revision::getContent
-        */
-       public function testGetContentUncloned() {
-               $rev = $this->newTestRevision( "hello", "testGetContentUncloned_dummy", CONTENT_MODEL_WIKITEXT );
-               $content = $rev->getContent( Revision::RAW );
-               $content2 = $rev->getContent( Revision::RAW );
-
-               // for immutable content like wikitext, this should be the same object
-               $this->assertSame( $content, $content2 );
-       }
-
-       /**
-        * @covers Revision::loadFromId
-        */
-       public function testLoadFromId() {
-               $rev = $this->testPage->getRevision();
-               $this->assertRevEquals(
-                       $rev,
-                       Revision::loadFromId( wfGetDB( DB_MASTER ), $rev->getId() )
-               );
-       }
-
-       /**
-        * @covers Revision::loadFromPageId
-        */
-       public function testLoadFromPageId() {
-               $this->assertRevEquals(
-                       $this->testPage->getRevision(),
-                       Revision::loadFromPageId( wfGetDB( DB_MASTER ), $this->testPage->getId() )
-               );
-       }
-
-       /**
-        * @covers Revision::loadFromPageId
-        */
-       public function testLoadFromPageIdWithLatestRevId() {
-               $this->assertRevEquals(
-                       $this->testPage->getRevision(),
-                       Revision::loadFromPageId(
-                               wfGetDB( DB_MASTER ),
-                               $this->testPage->getId(),
-                               $this->testPage->getLatest()
-                       )
-               );
-       }
-
-       /**
-        * @covers Revision::loadFromPageId
-        */
-       public function testLoadFromPageIdWithNotLatestRevId() {
-               $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
-               $this->assertRevEquals(
-                       $this->testPage->getRevision()->getPrevious(),
-                       Revision::loadFromPageId(
-                               wfGetDB( DB_MASTER ),
-                               $this->testPage->getId(),
-                               $this->testPage->getRevision()->getPrevious()->getId()
-                       )
-               );
-       }
-
-       /**
-        * @covers Revision::loadFromTitle
-        */
-       public function testLoadFromTitle() {
-               $this->assertRevEquals(
-                       $this->testPage->getRevision(),
-                       Revision::loadFromTitle( wfGetDB( DB_MASTER ), $this->testPage->getTitle() )
-               );
-       }
-
-       /**
-        * @covers Revision::loadFromTitle
-        */
-       public function testLoadFromTitleWithLatestRevId() {
-               $this->assertRevEquals(
-                       $this->testPage->getRevision(),
-                       Revision::loadFromTitle(
-                               wfGetDB( DB_MASTER ),
-                               $this->testPage->getTitle(),
-                               $this->testPage->getLatest()
-                       )
-               );
-       }
-
-       /**
-        * @covers Revision::loadFromTitle
-        */
-       public function testLoadFromTitleWithNotLatestRevId() {
-               $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
-               $this->assertRevEquals(
-                       $this->testPage->getRevision()->getPrevious(),
-                       Revision::loadFromTitle(
-                               wfGetDB( DB_MASTER ),
-                               $this->testPage->getTitle(),
-                               $this->testPage->getRevision()->getPrevious()->getId()
-                       )
-               );
-       }
-
-       /**
-        * @covers Revision::loadFromTimestamp()
-        */
-       public function testLoadFromTimestamp() {
-               $this->assertRevEquals(
-                       $this->testPage->getRevision(),
-                       Revision::loadFromTimestamp(
-                               wfGetDB( DB_MASTER ),
-                               $this->testPage->getTitle(),
-                               $this->testPage->getRevision()->getTimestamp()
-                       )
-               );
-       }
-
-       /**
-        * @covers Revision::getParentLengths
-        */
-       public function testGetParentLengths_noRevIds() {
-               $this->assertSame(
-                       [],
-                       Revision::getParentLengths(
-                               wfGetDB( DB_MASTER ),
-                               []
-                       )
-               );
-       }
-
-       /**
-        * @covers Revision::getParentLengths
-        */
-       public function testGetParentLengths_oneRevId() {
-               $text = '831jr091jr0921kr21kr0921kjr0921j09rj1';
-               $textLength = strlen( $text );
-
-               $this->testPage->doEditContent( new WikitextContent( $text ), __METHOD__ );
-               $rev[1] = $this->testPage->getLatest();
-
-               $this->assertSame(
-                       [ $rev[1] => strval( $textLength ) ],
-                       Revision::getParentLengths(
-                               wfGetDB( DB_MASTER ),
-                               [ $rev[1] ]
-                       )
-               );
-       }
-
-       /**
-        * @covers Revision::getParentLengths
-        */
-       public function testGetParentLengths_multipleRevIds() {
-               $textOne = '831jr091jr0921kr21kr0921kjr0921j09rj1';
-               $textOneLength = strlen( $textOne );
-               $textTwo = '831jr091jr092121j09rj1';
-               $textTwoLength = strlen( $textTwo );
-
-               $this->testPage->doEditContent( new WikitextContent( $textOne ), __METHOD__ );
-               $rev[1] = $this->testPage->getLatest();
-               $this->testPage->doEditContent( new WikitextContent( $textTwo ), __METHOD__ );
-               $rev[2] = $this->testPage->getLatest();
-
-               $this->assertSame(
-                       [ $rev[1] => strval( $textOneLength ), $rev[2] => strval( $textTwoLength ) ],
-                       Revision::getParentLengths(
-                               wfGetDB( DB_MASTER ),
-                               [ $rev[1], $rev[2] ]
-                       )
-               );
-       }
-
-       /**
-        * @covers Revision::getTitle
-        */
-       public function testGetTitle_fromExistingRevision() {
-               $this->assertTrue(
-                       $this->testPage->getTitle()->equals(
-                               $this->testPage->getRevision()->getTitle()
-                       )
-               );
-       }
-
-       /**
-        * @covers Revision::getTitle
-        */
-       public function testGetTitle_fromRevisionWhichWillLoadTheTitle() {
-               $rev = new Revision( [ 'id' => $this->testPage->getLatest() ] );
-               $this->assertTrue(
-                       $this->testPage->getTitle()->equals(
-                               $rev->getTitle()
-                       )
-               );
-       }
-
-       /**
-        * @covers Revision::getTitle
-        */
-       public function testGetTitle_forBadRevision() {
-               $rev = new Revision( [] );
-               $this->assertNull( $rev->getTitle() );
-       }
-
-       /**
-        * @covers Revision::isMinor
-        */
-       public function testIsMinor_true() {
-               // Use a sysop to ensure we can mark edits as minor
-               $sysop = $this->getTestSysop()->getUser();
-
-               $this->testPage->doEditContent(
-                       new WikitextContent( __METHOD__ ),
-                       __METHOD__,
-                       EDIT_MINOR,
-                       false,
-                       $sysop
-               );
-               $rev = $this->testPage->getRevision();
-
-               $this->assertSame( true, $rev->isMinor() );
-       }
-
-       /**
-        * @covers Revision::isMinor
-        */
-       public function testIsMinor_false() {
-               $this->testPage->doEditContent(
-                       new WikitextContent( __METHOD__ ),
-                       __METHOD__,
-                       0
-               );
-               $rev = $this->testPage->getRevision();
-
-               $this->assertSame( false, $rev->isMinor() );
-       }
-
-       /**
-        * @covers Revision::getTimestamp
-        */
-       public function testGetTimestamp() {
-               $testTimestamp = wfTimestampNow();
-
-               $this->testPage->doEditContent(
-                       new WikitextContent( __METHOD__ ),
-                       __METHOD__
-               );
-               $rev = $this->testPage->getRevision();
-
-               $this->assertInternalType( 'string', $rev->getTimestamp() );
-               $this->assertTrue( strlen( $rev->getTimestamp() ) == strlen( 'YYYYMMDDHHMMSS' ) );
-               $this->assertContains( substr( $testTimestamp, 0, 10 ), $rev->getTimestamp() );
-       }
-
-       /**
-        * @covers Revision::getUser
-        * @covers Revision::getUserText
-        */
-       public function testGetUserAndText() {
-               $sysop = $this->getTestSysop()->getUser();
-
-               $this->testPage->doEditContent(
-                       new WikitextContent( __METHOD__ ),
-                       __METHOD__,
-                       0,
-                       false,
-                       $sysop
-               );
-               $rev = $this->testPage->getRevision();
-
-               $this->assertSame( $sysop->getId(), $rev->getUser() );
-               $this->assertSame( $sysop->getName(), $rev->getUserText() );
-       }
-
-       /**
-        * @covers Revision::isDeleted
-        */
-       public function testIsDeleted_nothingDeleted() {
-               $rev = $this->testPage->getRevision();
-
-               $this->assertSame( false, $rev->isDeleted( Revision::DELETED_TEXT ) );
-               $this->assertSame( false, $rev->isDeleted( Revision::DELETED_COMMENT ) );
-               $this->assertSame( false, $rev->isDeleted( Revision::DELETED_RESTRICTED ) );
-               $this->assertSame( false, $rev->isDeleted( Revision::DELETED_USER ) );
-       }
-
-       /**
-        * @covers Revision::getVisibility
-        */
-       public function testGetVisibility_nothingDeleted() {
-               $rev = $this->testPage->getRevision();
-
-               $this->assertSame( 0, $rev->getVisibility() );
-       }
-
-       /**
-        * @covers Revision::getComment
-        */
-       public function testGetComment_notDeleted() {
-               $expectedSummary = 'goatlicious summary';
-
-               $this->testPage->doEditContent(
-                       new WikitextContent( __METHOD__ ),
-                       $expectedSummary
-               );
-               $rev = $this->testPage->getRevision();
-
-               $this->assertSame( $expectedSummary, $rev->getComment() );
-       }
-
-       /**
-        * This is a simple blanket test for all simple getters and is methods to provide some
-        * coverage before the split of Revision into multiple classes for MCR work.
-        * @covers Revision::isUnpatrolled
-        */
-       public function testIsUnpatrolled_true() {
-               $rev = $this->testPage->getRevision();
-
-               $this->assertSame( 0, $rev->isUnpatrolled() );
-       }
-
-       /**
-        * This is a simple blanket test for all simple content getters and is methods to provide some
-        * coverage before the split of Revision into multiple classes for MCR work.
-        * @covers Revision::getContent
-        * @covers Revision::getSerializedData
-        * @covers Revision::getContentModel
-        * @covers Revision::getContentFormat
-        * @covers Revision::getContentHandler
-        */
-       public function testSimpleContentGetters() {
-               $expectedText = 'testSimpleContentGetters in Revision. Goats love MCR...';
-               $expectedSummary = 'goatlicious testSimpleContentGetters summary';
-
-               $this->testPage->doEditContent(
-                       new WikitextContent( $expectedText ),
-                       $expectedSummary
-               );
-               $rev = $this->testPage->getRevision();
-
-               $this->assertSame( $expectedText, $rev->getContent()->getNativeData() );
-               $this->assertSame( $expectedText, $rev->getSerializedData() );
-               $this->assertSame( $this->testPage->getContentModel(), $rev->getContentModel() );
-               $this->assertSame( $this->testPage->getContent()->getDefaultFormat(), $rev->getContentFormat() );
-               $this->assertSame( $this->testPage->getContentHandler(), $rev->getContentHandler() );
-       }
-
-}
diff --git a/tests/phpunit/includes/RevisionNoContentHandlerDbTest.php b/tests/phpunit/includes/RevisionNoContentHandlerDbTest.php
new file mode 100644 (file)
index 0000000..c980a48
--- /dev/null
@@ -0,0 +1,14 @@
+<?php
+
+/**
+ * @group Database
+ * @group medium
+ * @group ContentHandler
+ */
+class RevisionNoContentHandlerDbTest extends RevisionDbTestBase {
+
+       protected function getContentHandlerUseDB() {
+               return false;
+       }
+
+}
diff --git a/tests/phpunit/includes/RevisionTest.php b/tests/phpunit/includes/RevisionTest.php
new file mode 100644 (file)
index 0000000..d933d93
--- /dev/null
@@ -0,0 +1,1294 @@
+<?php
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * Test cases in RevisionTest should not interact with the Database.
+ * For test cases that need Database interaction see RevisionDbTestBase.
+ */
+class RevisionTest extends MediaWikiTestCase {
+
+       public function provideConstructFromArray() {
+               yield 'with text' => [
+                       [
+                               'text' => 'hello world.',
+                               'content_model' => CONTENT_MODEL_JAVASCRIPT
+                       ],
+               ];
+               yield 'with content' => [
+                       [
+                               'content' => new JavaScriptContent( 'hellow world.' )
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideConstructFromArray
+        * @covers Revision::__construct
+        * @covers Revision::constructFromRowArray
+        */
+       public function testConstructFromArray( array $rowArray ) {
+               $rev = new Revision( $rowArray );
+               $this->assertNotNull( $rev->getContent(), 'no content object available' );
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContent()->getModel() );
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() );
+       }
+
+       public function provideConstructFromArrayThrowsExceptions() {
+               yield 'content and text_id both not empty' => [
+                       [
+                               'content' => new WikitextContent( 'GOAT' ),
+                               'text_id' => 'someid',
+                               ],
+                       new MWException( "Text already stored in external store (id someid), " .
+                               "can't serialize content object" )
+               ];
+               yield 'with bad content object (class)' => [
+                       [ 'content' => new stdClass() ],
+                       new MWException( '`content` field must contain a Content object.' )
+               ];
+               yield 'with bad content object (string)' => [
+                       [ 'content' => 'ImAGoat' ],
+                       new MWException( '`content` field must contain a Content object.' )
+               ];
+               yield 'bad row format' => [
+                       'imastring, not a row',
+                       new MWException( 'Revision constructor passed invalid row format.' )
+               ];
+       }
+
+       /**
+        * @dataProvider provideConstructFromArrayThrowsExceptions
+        * @covers Revision::__construct
+        * @covers Revision::constructFromRowArray
+        */
+       public function testConstructFromArrayThrowsExceptions( $rowArray, Exception $expectedException ) {
+               $this->setExpectedException(
+                       get_class( $expectedException ),
+                       $expectedException->getMessage(),
+                       $expectedException->getCode()
+               );
+               new Revision( $rowArray );
+       }
+
+       public function provideConstructFromRow() {
+               yield 'Full construction' => [
+                       [
+                               'rev_id' => '2',
+                               'rev_page' => '1',
+                               'rev_text_id' => '2',
+                               'rev_timestamp' => '20171017114835',
+                               'rev_user_text' => '127.0.0.1',
+                               'rev_user' => '0',
+                               'rev_minor_edit' => '0',
+                               'rev_deleted' => '0',
+                               'rev_len' => '46',
+                               'rev_parent_id' => '1',
+                               'rev_sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
+                               'rev_comment_text' => 'Goat Comment!',
+                               'rev_comment_data' => null,
+                               'rev_comment_cid' => null,
+                               'rev_content_format' => 'GOATFORMAT',
+                               'rev_content_model' => 'GOATMODEL',
+                       ],
+                       function ( RevisionTest $testCase, Revision $rev ) {
+                               $testCase->assertSame( 2, $rev->getId() );
+                               $testCase->assertSame( 1, $rev->getPage() );
+                               $testCase->assertSame( 2, $rev->getTextId() );
+                               $testCase->assertSame( '20171017114835', $rev->getTimestamp() );
+                               $testCase->assertSame( '127.0.0.1', $rev->getUserText() );
+                               $testCase->assertSame( 0, $rev->getUser() );
+                               $testCase->assertSame( false, $rev->isMinor() );
+                               $testCase->assertSame( false, $rev->isDeleted( Revision::DELETED_TEXT ) );
+                               $testCase->assertSame( 46, $rev->getSize() );
+                               $testCase->assertSame( 1, $rev->getParentId() );
+                               $testCase->assertSame( 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', $rev->getSha1() );
+                               $testCase->assertSame( 'Goat Comment!', $rev->getComment() );
+                               $testCase->assertSame( 'GOATFORMAT', $rev->getContentFormat() );
+                               $testCase->assertSame( 'GOATMODEL', $rev->getContentModel() );
+                       }
+               ];
+               yield 'null fields' => [
+                       [
+                               'rev_id' => '2',
+                               'rev_page' => '1',
+                               'rev_text_id' => '2',
+                               'rev_timestamp' => '20171017114835',
+                               'rev_user_text' => '127.0.0.1',
+                               'rev_user' => '0',
+                               'rev_minor_edit' => '0',
+                               'rev_deleted' => '0',
+                               'rev_comment_text' => 'Goat Comment!',
+                               'rev_comment_data' => null,
+                               'rev_comment_cid' => null,
+                       ],
+                       function ( RevisionTest $testCase, Revision $rev ) {
+                               $testCase->assertNull( $rev->getSize() );
+                               $testCase->assertNull( $rev->getParentId() );
+                               $testCase->assertNull( $rev->getSha1() );
+                               $testCase->assertSame( 'text/x-wiki', $rev->getContentFormat() );
+                               $testCase->assertSame( 'wikitext', $rev->getContentModel() );
+                       }
+               ];
+       }
+
+       /**
+        * @dataProvider provideConstructFromRow
+        * @covers Revision::__construct
+        * @covers Revision::constructFromDbRowObject
+        */
+       public function testConstructFromRow( array $arrayData, $assertions ) {
+               $row = (object)$arrayData;
+               $rev = new Revision( $row );
+               $assertions( $this, $rev );
+       }
+
+       public function provideGetRevisionText() {
+               yield 'Generic test' => [
+                       'This is a goat of revision text.',
+                       [
+                               'old_flags' => '',
+                               'old_text' => 'This is a goat of revision text.',
+                       ],
+               ];
+       }
+
+       public function provideGetId() {
+               yield [
+                       [],
+                       null
+               ];
+               yield [
+                       [ 'id' => 998 ],
+                       998
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetId
+        * @covers Revision::getId
+        */
+       public function testGetId( $rowArray, $expectedId ) {
+               $rev = new Revision( $rowArray );
+               $this->assertEquals( $expectedId, $rev->getId() );
+       }
+
+       public function provideSetId() {
+               yield [ '123', 123 ];
+               yield [ 456, 456 ];
+       }
+
+       /**
+        * @dataProvider provideSetId
+        * @covers Revision::setId
+        */
+       public function testSetId( $input, $expected ) {
+               $rev = new Revision( [] );
+               $rev->setId( $input );
+               $this->assertSame( $expected, $rev->getId() );
+       }
+
+       public function provideSetUserIdAndName() {
+               yield [ '123', 123, 'GOaT' ];
+               yield [ 456, 456, 'GOaT' ];
+       }
+
+       /**
+        * @dataProvider provideSetUserIdAndName
+        * @covers Revision::setUserIdAndName
+        */
+       public function testSetUserIdAndName( $inputId, $expectedId, $name ) {
+               $rev = new Revision( [] );
+               $rev->setUserIdAndName( $inputId, $name );
+               $this->assertSame( $expectedId, $rev->getUser( Revision::RAW ) );
+               $this->assertEquals( $name, $rev->getUserText( Revision::RAW ) );
+       }
+
+       public function provideGetTextId() {
+               yield [ [], null ];
+               yield [ [ 'text_id' => '123' ], 123 ];
+               yield [ [ 'text_id' => 456 ], 456 ];
+       }
+
+       /**
+        * @dataProvider provideGetTextId
+        * @covers Revision::getTextId()
+        */
+       public function testGetTextId( $rowArray, $expected ) {
+               $rev = new Revision( $rowArray );
+               $this->assertSame( $expected, $rev->getTextId() );
+       }
+
+       public function provideGetParentId() {
+               yield [ [], null ];
+               yield [ [ 'parent_id' => '123' ], 123 ];
+               yield [ [ 'parent_id' => 456 ], 456 ];
+       }
+
+       /**
+        * @dataProvider provideGetParentId
+        * @covers Revision::getParentId()
+        */
+       public function testGetParentId( $rowArray, $expected ) {
+               $rev = new Revision( $rowArray );
+               $this->assertSame( $expected, $rev->getParentId() );
+       }
+
+       /**
+        * @covers Revision::getRevisionText
+        * @dataProvider provideGetRevisionText
+        */
+       public function testGetRevisionText( $expected, $rowData, $prefix = 'old_', $wiki = false ) {
+               $this->assertEquals(
+                       $expected,
+                       Revision::getRevisionText( (object)$rowData, $prefix, $wiki ) );
+       }
+
+       public function provideGetRevisionTextWithZlibExtension() {
+               yield 'Generic gzip test' => [
+                       'This is a small goat of revision text.',
+                       [
+                               'old_flags' => 'gzip',
+                               'old_text' => gzdeflate( 'This is a small goat of revision text.' ),
+                       ],
+               ];
+       }
+
+       /**
+        * @covers Revision::getRevisionText
+        * @dataProvider provideGetRevisionTextWithZlibExtension
+        */
+       public function testGetRevisionWithZlibExtension( $expected, $rowData ) {
+               $this->checkPHPExtension( 'zlib' );
+               $this->testGetRevisionText( $expected, $rowData );
+       }
+
+       public function provideGetRevisionTextWithLegacyEncoding() {
+               yield 'Utf8Native' => [
+                       "Wiki est l'\xc3\xa9cole superieur !",
+                       'iso-8859-1',
+                       [
+                               'old_flags' => 'utf-8',
+                               'old_text' => "Wiki est l'\xc3\xa9cole superieur !",
+                       ]
+               ];
+               yield 'Utf8Legacy' => [
+                       "Wiki est l'\xc3\xa9cole superieur !",
+                       'iso-8859-1',
+                       [
+                               'old_flags' => '',
+                               'old_text' => "Wiki est l'\xe9cole superieur !",
+                       ]
+               ];
+       }
+
+       /**
+        * @covers Revision::getRevisionText
+        * @dataProvider provideGetRevisionTextWithLegacyEncoding
+        */
+       public function testGetRevisionWithLegacyEncoding( $expected, $encoding, $rowData ) {
+               $this->setMwGlobals( 'wgLegacyEncoding', $encoding );
+               $this->testGetRevisionText( $expected, $rowData );
+       }
+
+       public function provideGetRevisionTextWithGzipAndLegacyEncoding() {
+               /**
+                * WARNING!
+                * Do not set the external flag!
+                * Otherwise, getRevisionText will hit the live database (if ExternalStore is enabled)!
+                */
+               yield 'Utf8NativeGzip' => [
+                       "Wiki est l'\xc3\xa9cole superieur !",
+                       'iso-8859-1',
+                       [
+                               'old_flags' => 'gzip,utf-8',
+                               'old_text' => gzdeflate( "Wiki est l'\xc3\xa9cole superieur !" ),
+                       ]
+               ];
+               yield 'Utf8LegacyGzip' => [
+                       "Wiki est l'\xc3\xa9cole superieur !",
+                       'iso-8859-1',
+                       [
+                               'old_flags' => 'gzip',
+                               'old_text' => gzdeflate( "Wiki est l'\xe9cole superieur !" ),
+                       ]
+               ];
+       }
+
+       /**
+        * @covers Revision::getRevisionText
+        * @dataProvider provideGetRevisionTextWithGzipAndLegacyEncoding
+        */
+       public function testGetRevisionWithGzipAndLegacyEncoding( $expected, $encoding, $rowData ) {
+               $this->checkPHPExtension( 'zlib' );
+               $this->setMwGlobals( 'wgLegacyEncoding', $encoding );
+               $this->testGetRevisionText( $expected, $rowData );
+       }
+
+       /**
+        * @covers Revision::compressRevisionText
+        */
+       public function testCompressRevisionTextUtf8() {
+               $row = new stdClass;
+               $row->old_text = "Wiki est l'\xc3\xa9cole superieur !";
+               $row->old_flags = Revision::compressRevisionText( $row->old_text );
+               $this->assertTrue( false !== strpos( $row->old_flags, 'utf-8' ),
+                       "Flags should contain 'utf-8'" );
+               $this->assertFalse( false !== strpos( $row->old_flags, 'gzip' ),
+                       "Flags should not contain 'gzip'" );
+               $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !",
+                       $row->old_text, "Direct check" );
+               $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !",
+                       Revision::getRevisionText( $row ), "getRevisionText" );
+       }
+
+       /**
+        * @covers Revision::compressRevisionText
+        */
+       public function testCompressRevisionTextUtf8Gzip() {
+               $this->checkPHPExtension( 'zlib' );
+               $this->setMwGlobals( 'wgCompressRevisions', true );
+
+               $row = new stdClass;
+               $row->old_text = "Wiki est l'\xc3\xa9cole superieur !";
+               $row->old_flags = Revision::compressRevisionText( $row->old_text );
+               $this->assertTrue( false !== strpos( $row->old_flags, 'utf-8' ),
+                       "Flags should contain 'utf-8'" );
+               $this->assertTrue( false !== strpos( $row->old_flags, 'gzip' ),
+                       "Flags should contain 'gzip'" );
+               $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !",
+                       gzinflate( $row->old_text ), "Direct check" );
+               $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !",
+                       Revision::getRevisionText( $row ), "getRevisionText" );
+       }
+
+       public function provideFetchFromConds() {
+               yield [ 0, [] ];
+               yield [ Revision::READ_LOCKING, [ 'FOR UPDATE' ] ];
+       }
+
+       /**
+        * @dataProvider provideFetchFromConds
+        * @covers Revision::fetchFromConds
+        */
+       public function testFetchFromConds( $flags, array $options ) {
+               $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
+               $conditions = [ 'conditionsArray' ];
+
+               $db = $this->getMock( IDatabase::class );
+               $db->expects( $this->once() )
+                       ->method( 'selectRow' )
+                       ->with(
+                               $this->equalTo( [ 'revision', 'page', 'user' ] ),
+                               // We don't really care about the fields are they come from the selectField methods
+                               $this->isType( 'array' ),
+                               $this->equalTo( $conditions ),
+                               // Method name
+                               $this->equalTo( 'Revision::fetchFromConds' ),
+                               $this->equalTo( $options ),
+                               // We don't really care about the join conds are they come from the joinCond methods
+                               $this->isType( 'array' )
+                       )
+                       ->willReturn( 'RETURNVALUE' );
+
+               $wrapper = TestingAccessWrapper::newFromClass( Revision::class );
+               $result = $wrapper->fetchFromConds( $db, $conditions, $flags );
+
+               $this->assertEquals( 'RETURNVALUE', $result );
+       }
+
+       public function provideDecompressRevisionText() {
+               yield '(no legacy encoding), false in false out' => [ false, false, [], false ];
+               yield '(no legacy encoding), empty in empty out' => [ false, '', [], '' ];
+               yield '(no legacy encoding), empty in empty out' => [ false, 'A', [], 'A' ];
+               yield '(no legacy encoding), string in with gzip flag returns string' => [
+                       // gzip string below generated with gzdeflate( 'AAAABBAAA' )
+                       false, "sttttr\002\022\000", [ 'gzip' ], 'AAAABBAAA',
+               ];
+               yield '(no legacy encoding), string in with object flag returns false' => [
+                       // gzip string below generated with serialize( 'JOJO' )
+                       false, "s:4:\"JOJO\";", [ 'object' ], false,
+               ];
+               yield '(no legacy encoding), serialized object in with object flag returns string' => [
+                       false,
+                       // Using a TitleValue object as it has a getText method (which is needed)
+                       serialize( new TitleValue( 0, 'HHJJDDFF' ) ),
+                       [ 'object' ],
+                       'HHJJDDFF',
+               ];
+               yield '(no legacy encoding), serialized object in with object & gzip flag returns string' => [
+                       false,
+                       // Using a TitleValue object as it has a getText method (which is needed)
+                       gzdeflate( serialize( new TitleValue( 0, '8219JJJ840' ) ) ),
+                       [ 'object', 'gzip' ],
+                       '8219JJJ840',
+               ];
+               yield '(ISO-8859-1 encoding), string in string out' => [
+                       'ISO-8859-1',
+                       iconv( 'utf8', 'ISO-8859-1', "1®Àþ1" ),
+                       [],
+                       '1®Àþ1',
+               ];
+               yield '(ISO-8859-1 encoding), serialized object in with gzip flags returns string' => [
+                       'ISO-8859-1',
+                       gzdeflate( iconv( 'utf8', 'ISO-8859-1', "4®Àþ4" ) ),
+                       [ 'gzip' ],
+                       '4®Àþ4',
+               ];
+               yield '(ISO-8859-1 encoding), serialized object in with object flags returns string' => [
+                       'ISO-8859-1',
+                       serialize( new TitleValue( 0, iconv( 'utf8', 'ISO-8859-1', "3®Àþ3" ) ) ),
+                       [ 'object' ],
+                       '3®Àþ3',
+               ];
+               yield '(ISO-8859-1 encoding), serialized object in with object & gzip flags returns string' => [
+                       'ISO-8859-1',
+                       gzdeflate( serialize( new TitleValue( 0, iconv( 'utf8', 'ISO-8859-1', "2®Àþ2" ) ) ) ),
+                       [ 'gzip', 'object' ],
+                       '2®Àþ2',
+               ];
+       }
+
+       /**
+        * @dataProvider provideDecompressRevisionText
+        * @covers Revision::decompressRevisionText
+        *
+        * @param bool $legacyEncoding
+        * @param mixed $text
+        * @param array $flags
+        * @param mixed $expected
+        */
+       public function testDecompressRevisionText( $legacyEncoding, $text, $flags, $expected ) {
+               $this->setMwGlobals( 'wgLegacyEncoding', $legacyEncoding );
+               $this->setMwGlobals( 'wgLanguageCode', 'en' );
+               $this->assertSame(
+                       $expected,
+                       Revision::decompressRevisionText( $text, $flags )
+               );
+       }
+
+       /**
+        * @covers Revision::getRevisionText
+        */
+       public function testGetRevisionText_returnsFalseWhenNoTextField() {
+               $this->assertFalse( Revision::getRevisionText( new stdClass() ) );
+       }
+
+       public function provideTestGetRevisionText_returnsDecompressedTextFieldWhenNotExternal() {
+               yield 'Just text' => [
+                       (object)[ 'old_text' => 'SomeText' ],
+                       'old_',
+                       'SomeText'
+               ];
+               // gzip string below generated with gzdeflate( 'AAAABBAAA' )
+               yield 'gzip text' => [
+                       (object)[
+                               'old_text' => "sttttr\002\022\000",
+                               'old_flags' => 'gzip'
+                       ],
+                       'old_',
+                       'AAAABBAAA'
+               ];
+               yield 'gzip text and different prefix' => [
+                       (object)[
+                               'jojo_text' => "sttttr\002\022\000",
+                               'jojo_flags' => 'gzip'
+                       ],
+                       'jojo_',
+                       'AAAABBAAA'
+               ];
+       }
+
+       /**
+        * @dataProvider provideTestGetRevisionText_returnsDecompressedTextFieldWhenNotExternal
+        * @covers Revision::getRevisionText
+        */
+       public function testGetRevisionText_returnsDecompressedTextFieldWhenNotExternal(
+               $row,
+               $prefix,
+               $expected
+       ) {
+               $this->assertSame( $expected, Revision::getRevisionText( $row, $prefix ) );
+       }
+
+       public function provideTestGetRevisionText_external_returnsFalseWhenNotEnoughUrlParts() {
+               yield 'Just some text' => [ 'someNonUrlText' ];
+               yield 'No second URL part' => [ 'someProtocol://' ];
+       }
+
+       /**
+        * @dataProvider provideTestGetRevisionText_external_returnsFalseWhenNotEnoughUrlParts
+        * @covers Revision::getRevisionText
+        */
+       public function testGetRevisionText_external_returnsFalseWhenNotEnoughUrlParts(
+               $text
+       ) {
+               $this->assertFalse(
+                       Revision::getRevisionText(
+                               (object)[
+                                       'old_text' => $text,
+                                       'old_flags' => 'external',
+                               ]
+                       )
+               );
+       }
+
+       /**
+        * @covers Revision::getRevisionText
+        */
+       public function testGetRevisionText_external_noOldId() {
+               $this->setService(
+                       'ExternalStoreFactory',
+                       new ExternalStoreFactory( [ 'ForTesting' ] )
+               );
+               $this->assertSame(
+                       'AAAABBAAA',
+                       Revision::getRevisionText(
+                               (object)[
+                                       'old_text' => 'ForTesting://cluster1/12345',
+                                       'old_flags' => 'external,gzip',
+                               ]
+                       )
+               );
+       }
+
+       /**
+        * @covers Revision::getRevisionText
+        */
+       public function testGetRevisionText_external_oldId() {
+               $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
+               $this->setService( 'MainWANObjectCache', $cache );
+               $this->setService(
+                       'ExternalStoreFactory',
+                       new ExternalStoreFactory( [ 'ForTesting' ] )
+               );
+
+               $cacheKey = $cache->makeKey( 'revisiontext', 'textid', '7777' );
+
+               $this->assertSame(
+                       'AAAABBAAA',
+                       Revision::getRevisionText(
+                               (object)[
+                                       'old_text' => 'ForTesting://cluster1/12345',
+                                       'old_flags' => 'external,gzip',
+                                       'old_id' => '7777',
+                               ]
+                       )
+               );
+               $this->assertSame( 'AAAABBAAA', $cache->get( $cacheKey ) );
+       }
+
+       /**
+        * @covers Revision::userJoinCond
+        */
+       public function testUserJoinCond() {
+               $this->hideDeprecated( 'Revision::userJoinCond' );
+               $this->assertEquals(
+                       [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
+                       Revision::userJoinCond()
+               );
+       }
+
+       /**
+        * @covers Revision::pageJoinCond
+        */
+       public function testPageJoinCond() {
+               $this->hideDeprecated( 'Revision::pageJoinCond' );
+               $this->assertEquals(
+                       [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
+                       Revision::pageJoinCond()
+               );
+       }
+
+       public function provideSelectFields() {
+               yield [
+                       true,
+                       [
+                               'rev_id',
+                               'rev_page',
+                               'rev_text_id',
+                               'rev_timestamp',
+                               'rev_user_text',
+                               'rev_user',
+                               'rev_minor_edit',
+                               'rev_deleted',
+                               'rev_len',
+                               'rev_parent_id',
+                               'rev_sha1',
+                               'rev_comment_text' => 'rev_comment',
+                               'rev_comment_data' => 'NULL',
+                               'rev_comment_cid' => 'NULL',
+                               'rev_content_format',
+                               'rev_content_model',
+                       ]
+               ];
+               yield [
+                       false,
+                       [
+                               'rev_id',
+                               'rev_page',
+                               'rev_text_id',
+                               'rev_timestamp',
+                               'rev_user_text',
+                               'rev_user',
+                               'rev_minor_edit',
+                               'rev_deleted',
+                               'rev_len',
+                               'rev_parent_id',
+                               'rev_sha1',
+                               'rev_comment_text' => 'rev_comment',
+                               'rev_comment_data' => 'NULL',
+                               'rev_comment_cid' => 'NULL',
+                       ]
+               ];
+       }
+
+       /**
+        * @dataProvider provideSelectFields
+        * @covers Revision::selectFields
+        * @todo a true unit test would mock CommentStore
+        */
+       public function testSelectFields( $contentHandlerUseDB, $expected ) {
+               $this->hideDeprecated( 'Revision::selectFields' );
+               $this->setMwGlobals( 'wgContentHandlerUseDB', $contentHandlerUseDB );
+               $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
+               $this->assertEquals( $expected, Revision::selectFields() );
+       }
+
+       public function provideSelectArchiveFields() {
+               yield [
+                       true,
+                       [
+                               'ar_id',
+                               'ar_page_id',
+                               'ar_rev_id',
+                               'ar_text',
+                               'ar_text_id',
+                               'ar_timestamp',
+                               'ar_user_text',
+                               'ar_user',
+                               'ar_minor_edit',
+                               'ar_deleted',
+                               'ar_len',
+                               'ar_parent_id',
+                               'ar_sha1',
+                               'ar_comment_text' => 'ar_comment',
+                               'ar_comment_data' => 'NULL',
+                               'ar_comment_cid' => 'NULL',
+                               'ar_content_format',
+                               'ar_content_model',
+                       ]
+               ];
+               yield [
+                       false,
+                       [
+                               'ar_id',
+                               'ar_page_id',
+                               'ar_rev_id',
+                               'ar_text',
+                               'ar_text_id',
+                               'ar_timestamp',
+                               'ar_user_text',
+                               'ar_user',
+                               'ar_minor_edit',
+                               'ar_deleted',
+                               'ar_len',
+                               'ar_parent_id',
+                               'ar_sha1',
+                               'ar_comment_text' => 'ar_comment',
+                               'ar_comment_data' => 'NULL',
+                               'ar_comment_cid' => 'NULL',
+                       ]
+               ];
+       }
+
+       /**
+        * @dataProvider provideSelectArchiveFields
+        * @covers Revision::selectArchiveFields
+        * @todo a true unit test would mock CommentStore
+        */
+       public function testSelectArchiveFields( $contentHandlerUseDB, $expected ) {
+               $this->hideDeprecated( 'Revision::selectArchiveFields' );
+               $this->setMwGlobals( 'wgContentHandlerUseDB', $contentHandlerUseDB );
+               $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
+               $this->assertEquals( $expected, Revision::selectArchiveFields() );
+       }
+
+       /**
+        * @covers Revision::selectTextFields
+        */
+       public function testSelectTextFields() {
+               $this->hideDeprecated( 'Revision::selectTextFields' );
+               $this->assertEquals(
+                       [
+                               'old_text',
+                               'old_flags',
+                       ],
+                       Revision::selectTextFields()
+               );
+       }
+
+       /**
+        * @covers Revision::selectPageFields
+        */
+       public function testSelectPageFields() {
+               $this->hideDeprecated( 'Revision::selectPageFields' );
+               $this->assertEquals(
+                       [
+                               'page_namespace',
+                               'page_title',
+                               'page_id',
+                               'page_latest',
+                               'page_is_redirect',
+                               'page_len',
+                       ],
+                       Revision::selectPageFields()
+               );
+       }
+
+       /**
+        * @covers Revision::selectUserFields
+        */
+       public function testSelectUserFields() {
+               $this->hideDeprecated( 'Revision::selectUserFields' );
+               $this->assertEquals(
+                       [
+                               'user_name',
+                       ],
+                       Revision::selectUserFields()
+               );
+       }
+
+       public function provideGetArchiveQueryInfo() {
+               yield 'wgContentHandlerUseDB false, wgCommentTableSchemaMigrationStage OLD' => [
+                       [
+                               'wgContentHandlerUseDB' => false,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+                       ],
+                       [
+                               'tables' => [ 'archive' ],
+                               'fields' => [
+                                       'ar_id',
+                                       'ar_page_id',
+                                       'ar_rev_id',
+                                       'ar_text',
+                                       'ar_text_id',
+                                       'ar_timestamp',
+                                       'ar_user_text',
+                                       'ar_user',
+                                       'ar_minor_edit',
+                                       'ar_deleted',
+                                       'ar_len',
+                                       'ar_parent_id',
+                                       'ar_sha1',
+                                       'ar_comment_text' => 'ar_comment',
+                                       'ar_comment_data' => 'NULL',
+                                       'ar_comment_cid' => 'NULL',
+                               ],
+                               'joins' => [],
+                       ]
+               ];
+               yield 'wgContentHandlerUseDB true, wgCommentTableSchemaMigrationStage OLD' => [
+                       [
+                               'wgContentHandlerUseDB' => true,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+                       ],
+                       [
+                               'tables' => [ 'archive' ],
+                               'fields' => [
+                                       'ar_id',
+                                       'ar_page_id',
+                                       'ar_rev_id',
+                                       'ar_text',
+                                       'ar_text_id',
+                                       'ar_timestamp',
+                                       'ar_user_text',
+                                       'ar_user',
+                                       'ar_minor_edit',
+                                       'ar_deleted',
+                                       'ar_len',
+                                       'ar_parent_id',
+                                       'ar_sha1',
+                                       'ar_comment_text' => 'ar_comment',
+                                       'ar_comment_data' => 'NULL',
+                                       'ar_comment_cid' => 'NULL',
+                                       'ar_content_format',
+                                       'ar_content_model',
+                               ],
+                               'joins' => [],
+                       ]
+               ];
+               yield 'wgContentHandlerUseDB false, wgCommentTableSchemaMigrationStage WRITE_BOTH' => [
+                       [
+                               'wgContentHandlerUseDB' => false,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
+                       ],
+                       [
+                               'tables' => [
+                                       'archive',
+                                       'comment_ar_comment' => 'comment',
+                               ],
+                               'fields' => [
+                                       'ar_id',
+                                       'ar_page_id',
+                                       'ar_rev_id',
+                                       'ar_text',
+                                       'ar_text_id',
+                                       'ar_timestamp',
+                                       'ar_user_text',
+                                       'ar_user',
+                                       'ar_minor_edit',
+                                       'ar_deleted',
+                                       'ar_len',
+                                       'ar_parent_id',
+                                       'ar_sha1',
+                                       'ar_comment_text' => 'COALESCE( comment_ar_comment.comment_text, ar_comment )',
+                                       'ar_comment_data' => 'comment_ar_comment.comment_data',
+                                       'ar_comment_cid' => 'comment_ar_comment.comment_id',
+                               ],
+                               'joins' => [
+                                       'comment_ar_comment' => [
+                                               'LEFT JOIN',
+                                               'comment_ar_comment.comment_id = ar_comment_id',
+                                       ],
+                               ],
+                       ]
+               ];
+               yield 'wgContentHandlerUseDB false, wgCommentTableSchemaMigrationStage WRITE_NEW' => [
+                       [
+                               'wgContentHandlerUseDB' => false,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_NEW,
+                       ],
+                       [
+                               'tables' => [
+                                       'archive',
+                                       'comment_ar_comment' => 'comment',
+                               ],
+                               'fields' => [
+                                       'ar_id',
+                                       'ar_page_id',
+                                       'ar_rev_id',
+                                       'ar_text',
+                                       'ar_text_id',
+                                       'ar_timestamp',
+                                       'ar_user_text',
+                                       'ar_user',
+                                       'ar_minor_edit',
+                                       'ar_deleted',
+                                       'ar_len',
+                                       'ar_parent_id',
+                                       'ar_sha1',
+                                       'ar_comment_text' => 'COALESCE( comment_ar_comment.comment_text, ar_comment )',
+                                       'ar_comment_data' => 'comment_ar_comment.comment_data',
+                                       'ar_comment_cid' => 'comment_ar_comment.comment_id',
+                               ],
+                               'joins' => [
+                                       'comment_ar_comment' => [
+                                               'LEFT JOIN',
+                                               'comment_ar_comment.comment_id = ar_comment_id',
+                                       ],
+                               ],
+                       ]
+               ];
+               yield 'wgContentHandlerUseDB false, wgCommentTableSchemaMigrationStage NEW' => [
+                       [
+                               'wgContentHandlerUseDB' => false,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_NEW,
+                       ],
+                       [
+                               'tables' => [
+                                       'archive',
+                                       'comment_ar_comment' => 'comment',
+                               ],
+                               'fields' => [
+                                       'ar_id',
+                                       'ar_page_id',
+                                       'ar_rev_id',
+                                       'ar_text',
+                                       'ar_text_id',
+                                       'ar_timestamp',
+                                       'ar_user_text',
+                                       'ar_user',
+                                       'ar_minor_edit',
+                                       'ar_deleted',
+                                       'ar_len',
+                                       'ar_parent_id',
+                                       'ar_sha1',
+                                       'ar_comment_text' => 'comment_ar_comment.comment_text',
+                                       'ar_comment_data' => 'comment_ar_comment.comment_data',
+                                       'ar_comment_cid' => 'comment_ar_comment.comment_id',
+                               ],
+                               'joins' => [
+                                       'comment_ar_comment' => [
+                                               'JOIN',
+                                               'comment_ar_comment.comment_id = ar_comment_id',
+                                       ],
+                               ],
+                       ]
+               ];
+       }
+
+       /**
+        * @covers Revision::getArchiveQueryInfo
+        * @dataProvider provideGetArchiveQueryInfo
+        */
+       public function testGetArchiveQueryInfo( $globals, $expected ) {
+               $this->setMwGlobals( $globals );
+               $this->assertEquals(
+                       $expected,
+                       Revision::getArchiveQueryInfo()
+               );
+       }
+
+       public function provideGetQueryInfo() {
+               yield 'wgContentHandlerUseDB false, wgCommentTableSchemaMigrationStage OLD, opts none' => [
+                       [
+                               'wgContentHandlerUseDB' => false,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+                       ],
+                       [],
+                       [
+                               'tables' => [ 'revision' ],
+                               'fields' => [
+                                       'rev_id',
+                                       'rev_page',
+                                       'rev_text_id',
+                                       'rev_timestamp',
+                                       'rev_user_text',
+                                       'rev_user',
+                                       'rev_minor_edit',
+                                       'rev_deleted',
+                                       'rev_len',
+                                       'rev_parent_id',
+                                       'rev_sha1',
+                                       'rev_comment_text' => 'rev_comment',
+                                       'rev_comment_data' => 'NULL',
+                                       'rev_comment_cid' => 'NULL',
+                               ],
+                               'joins' => [],
+                       ],
+               ];
+               yield 'wgContentHandlerUseDB false, wgCommentTableSchemaMigrationStage OLD, opts page' => [
+                       [
+                               'wgContentHandlerUseDB' => false,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+                       ],
+                       [ 'page' ],
+                       [
+                               'tables' => [ 'revision', 'page' ],
+                               'fields' => [
+                                       'rev_id',
+                                       'rev_page',
+                                       'rev_text_id',
+                                       'rev_timestamp',
+                                       'rev_user_text',
+                                       'rev_user',
+                                       'rev_minor_edit',
+                                       'rev_deleted',
+                                       'rev_len',
+                                       'rev_parent_id',
+                                       'rev_sha1',
+                                       'rev_comment_text' => 'rev_comment',
+                                       'rev_comment_data' => 'NULL',
+                                       'rev_comment_cid' => 'NULL',
+                                       'page_namespace',
+                                       'page_title',
+                                       'page_id',
+                                       'page_latest',
+                                       'page_is_redirect',
+                                       'page_len',
+                               ],
+                               'joins' => [
+                                       'page' => [
+                                               'INNER JOIN',
+                                               [ 'page_id = rev_page' ],
+                                       ],
+                               ],
+                       ],
+               ];
+               yield 'wgContentHandlerUseDB false, wgCommentTableSchemaMigrationStage OLD, opts user' => [
+                       [
+                               'wgContentHandlerUseDB' => false,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+                       ],
+                       [ 'user' ],
+                       [
+                               'tables' => [ 'revision', 'user' ],
+                               'fields' => [
+                                       'rev_id',
+                                       'rev_page',
+                                       'rev_text_id',
+                                       'rev_timestamp',
+                                       'rev_user_text',
+                                       'rev_user',
+                                       'rev_minor_edit',
+                                       'rev_deleted',
+                                       'rev_len',
+                                       'rev_parent_id',
+                                       'rev_sha1',
+                                       'rev_comment_text' => 'rev_comment',
+                                       'rev_comment_data' => 'NULL',
+                                       'rev_comment_cid' => 'NULL',
+                                       'user_name',
+                               ],
+                               'joins' => [
+                                       'user' => [
+                                               'LEFT JOIN',
+                                               [
+                                                       'rev_user != 0',
+                                                       'user_id = rev_user',
+                                               ],
+                                       ],
+                               ],
+                       ],
+               ];
+               yield 'wgContentHandlerUseDB false, wgCommentTableSchemaMigrationStage OLD, opts text' => [
+                       [
+                               'wgContentHandlerUseDB' => false,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+                       ],
+                       [ 'text' ],
+                       [
+                               'tables' => [ 'revision', 'text' ],
+                               'fields' => [
+                                       'rev_id',
+                                       'rev_page',
+                                       'rev_text_id',
+                                       'rev_timestamp',
+                                       'rev_user_text',
+                                       'rev_user',
+                                       'rev_minor_edit',
+                                       'rev_deleted',
+                                       'rev_len',
+                                       'rev_parent_id',
+                                       'rev_sha1',
+                                       'rev_comment_text' => 'rev_comment',
+                                       'rev_comment_data' => 'NULL',
+                                       'rev_comment_cid' => 'NULL',
+                                       'old_text',
+                                       'old_flags',
+                               ],
+                               'joins' => [
+                                       'text' => [
+                                               'INNER JOIN',
+                                               [ 'rev_text_id=old_id' ],
+                                       ],
+                               ],
+                       ],
+               ];
+               yield 'wgContentHandlerUseDB false, wgCommentTableSchemaMigrationStage OLD, opts 3' => [
+                       [
+                               'wgContentHandlerUseDB' => false,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+                       ],
+                       [ 'text', 'page', 'user' ],
+                       [
+                               'tables' => [ 'revision', 'page', 'user', 'text' ],
+                               'fields' => [
+                                       'rev_id',
+                                       'rev_page',
+                                       'rev_text_id',
+                                       'rev_timestamp',
+                                       'rev_user_text',
+                                       'rev_user',
+                                       'rev_minor_edit',
+                                       'rev_deleted',
+                                       'rev_len',
+                                       'rev_parent_id',
+                                       'rev_sha1',
+                                       'rev_comment_text' => 'rev_comment',
+                                       'rev_comment_data' => 'NULL',
+                                       'rev_comment_cid' => 'NULL',
+                                       'page_namespace',
+                                       'page_title',
+                                       'page_id',
+                                       'page_latest',
+                                       'page_is_redirect',
+                                       'page_len',
+                                       'user_name',
+                                       'old_text',
+                                       'old_flags',
+                               ],
+                               'joins' => [
+                                       'page' => [
+                                               'INNER JOIN',
+                                               [ 'page_id = rev_page' ],
+                                       ],
+                                       'user' => [
+                                               'LEFT JOIN',
+                                               [
+                                                       'rev_user != 0',
+                                                       'user_id = rev_user',
+                                               ],
+                                       ],
+                                       'text' => [
+                                               'INNER JOIN',
+                                               [ 'rev_text_id=old_id' ],
+                                       ],
+                               ],
+                       ],
+               ];
+               yield 'wgContentHandlerUseDB true, wgCommentTableSchemaMigrationStage OLD, opts none' => [
+                       [
+                               'wgContentHandlerUseDB' => true,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+                       ],
+                       [],
+                       [
+                               'tables' => [ 'revision' ],
+                               'fields' => [
+                                       'rev_id',
+                                       'rev_page',
+                                       'rev_text_id',
+                                       'rev_timestamp',
+                                       'rev_user_text',
+                                       'rev_user',
+                                       'rev_minor_edit',
+                                       'rev_deleted',
+                                       'rev_len',
+                                       'rev_parent_id',
+                                       'rev_sha1',
+                                       'rev_comment_text' => 'rev_comment',
+                                       'rev_comment_data' => 'NULL',
+                                       'rev_comment_cid' => 'NULL',
+                                       'rev_content_format',
+                                       'rev_content_model',
+                               ],
+                               'joins' => [],
+                       ],
+               ];
+               yield 'wgContentHandlerUseDB false, wgCommentTableSchemaMigrationStage WRITE_BOTH, opts none' => [
+                       [
+                               'wgContentHandlerUseDB' => false,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
+                       ],
+                       [],
+                       [
+                               'tables' => [
+                                       'revision',
+                                       'temp_rev_comment' => 'revision_comment_temp',
+                                       'comment_rev_comment' => 'comment',
+                               ],
+                               'fields' => [
+                                       'rev_id',
+                                       'rev_page',
+                                       'rev_text_id',
+                                       'rev_timestamp',
+                                       'rev_user_text',
+                                       'rev_user',
+                                       'rev_minor_edit',
+                                       'rev_deleted',
+                                       'rev_len',
+                                       'rev_parent_id',
+                                       'rev_sha1',
+                                       'rev_comment_text' => 'COALESCE( comment_rev_comment.comment_text, rev_comment )',
+                                       'rev_comment_data' => 'comment_rev_comment.comment_data',
+                                       'rev_comment_cid' => 'comment_rev_comment.comment_id',
+                               ],
+                               'joins' => [
+                                       'temp_rev_comment' => [
+                                               'LEFT JOIN',
+                                               'temp_rev_comment.revcomment_rev = rev_id',
+                                       ],
+                                       'comment_rev_comment' => [
+                                               'LEFT JOIN',
+                                               'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id',
+                                       ],
+                               ],
+                       ],
+               ];
+               yield 'wgContentHandlerUseDB false, wgCommentTableSchemaMigrationStage WRITE_NEW, opts none' => [
+                       [
+                               'wgContentHandlerUseDB' => false,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_NEW,
+                       ],
+                       [],
+                       [
+                               'tables' => [
+                                       'revision',
+                                       'temp_rev_comment' => 'revision_comment_temp',
+                                       'comment_rev_comment' => 'comment',
+                               ],
+                               'fields' => [
+                                       'rev_id',
+                                       'rev_page',
+                                       'rev_text_id',
+                                       'rev_timestamp',
+                                       'rev_user_text',
+                                       'rev_user',
+                                       'rev_minor_edit',
+                                       'rev_deleted',
+                                       'rev_len',
+                                       'rev_parent_id',
+                                       'rev_sha1',
+                                       'rev_comment_text' => 'COALESCE( comment_rev_comment.comment_text, rev_comment )',
+                                       'rev_comment_data' => 'comment_rev_comment.comment_data',
+                                       'rev_comment_cid' => 'comment_rev_comment.comment_id',
+                               ],
+                               'joins' => [
+                                       'temp_rev_comment' => [
+                                               'LEFT JOIN',
+                                               'temp_rev_comment.revcomment_rev = rev_id',
+                                       ],
+                                       'comment_rev_comment' => [
+                                               'LEFT JOIN',
+                                               'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id',
+                                       ],
+                               ],
+                       ],
+               ];
+               yield 'wgContentHandlerUseDB false, wgCommentTableSchemaMigrationStage NEW, opts none' => [
+                       [
+                               'wgContentHandlerUseDB' => false,
+                               'wgCommentTableSchemaMigrationStage' => MIGRATION_NEW,
+                       ],
+                       [],
+                       [
+                               'tables' => [
+                                       'revision',
+                                       'temp_rev_comment' => 'revision_comment_temp',
+                                       'comment_rev_comment' => 'comment',
+                               ],
+                               'fields' => [
+                                       'rev_id',
+                                       'rev_page',
+                                       'rev_text_id',
+                                       'rev_timestamp',
+                                       'rev_user_text',
+                                       'rev_user',
+                                       'rev_minor_edit',
+                                       'rev_deleted',
+                                       'rev_len',
+                                       'rev_parent_id',
+                                       'rev_sha1',
+                                       'rev_comment_text' => 'comment_rev_comment.comment_text',
+                                       'rev_comment_data' => 'comment_rev_comment.comment_data',
+                                       'rev_comment_cid' => 'comment_rev_comment.comment_id',
+                               ],
+                               'joins' => [
+                                       'temp_rev_comment' => [
+                                               'JOIN',
+                                               'temp_rev_comment.revcomment_rev = rev_id',
+                                       ],
+                                       'comment_rev_comment' => [
+                                               'JOIN',
+                                               'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id',
+                                       ],
+                               ],
+                       ],
+               ];
+       }
+
+       /**
+        * @covers Revision::getQueryInfo
+        * @dataProvider provideGetQueryInfo
+        */
+       public function testGetQueryInfo( $globals, $options, $expected ) {
+               $this->setMwGlobals( $globals );
+               $this->assertEquals(
+                       $expected,
+                       Revision::getQueryInfo( $options )
+               );
+       }
+
+}
diff --git a/tests/phpunit/includes/RevisionUnitTest.php b/tests/phpunit/includes/RevisionUnitTest.php
deleted file mode 100644 (file)
index 7b8d316..0000000
+++ /dev/null
@@ -1,397 +0,0 @@
-<?php
-
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @group ContentHandler
- */
-class RevisionUnitTest extends MediaWikiTestCase {
-
-       public function provideConstructFromArray() {
-               yield 'with text' => [
-                       [
-                               'text' => 'hello world.',
-                               'content_model' => CONTENT_MODEL_JAVASCRIPT
-                       ],
-               ];
-               yield 'with content' => [
-                       [
-                               'content' => new JavaScriptContent( 'hellow world.' )
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideConstructFromArray
-        * @covers Revision::__construct
-        * @covers Revision::constructFromRowArray
-        */
-       public function testConstructFromArray( array $rowArray ) {
-               $rev = new Revision( $rowArray );
-               $this->assertNotNull( $rev->getContent(), 'no content object available' );
-               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContent()->getModel() );
-               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() );
-       }
-
-       public function provideConstructFromArrayThrowsExceptions() {
-               yield 'content and text_id both not empty' => [
-                       [
-                               'content' => new WikitextContent( 'GOAT' ),
-                               'text_id' => 'someid',
-                               ],
-                       new MWException( "Text already stored in external store (id someid), " .
-                               "can't serialize content object" )
-               ];
-               yield 'with bad content object (class)' => [
-                       [ 'content' => new stdClass() ],
-                       new MWException( '`content` field must contain a Content object.' )
-               ];
-               yield 'with bad content object (string)' => [
-                       [ 'content' => 'ImAGoat' ],
-                       new MWException( '`content` field must contain a Content object.' )
-               ];
-               yield 'bad row format' => [
-                       'imastring, not a row',
-                       new MWException( 'Revision constructor passed invalid row format.' )
-               ];
-       }
-
-       /**
-        * @dataProvider provideConstructFromArrayThrowsExceptions
-        * @covers Revision::__construct
-        * @covers Revision::constructFromRowArray
-        */
-       public function testConstructFromArrayThrowsExceptions( $rowArray, Exception $expectedException ) {
-               $this->setExpectedException(
-                       get_class( $expectedException ),
-                       $expectedException->getMessage(),
-                       $expectedException->getCode()
-               );
-               new Revision( $rowArray );
-       }
-
-       public function provideConstructFromRow() {
-               yield 'Full construction' => [
-                       [
-                               'rev_id' => '2',
-                               'rev_page' => '1',
-                               'rev_text_id' => '2',
-                               'rev_timestamp' => '20171017114835',
-                               'rev_user_text' => '127.0.0.1',
-                               'rev_user' => '0',
-                               'rev_minor_edit' => '0',
-                               'rev_deleted' => '0',
-                               'rev_len' => '46',
-                               'rev_parent_id' => '1',
-                               'rev_sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
-                               'rev_comment_text' => 'Goat Comment!',
-                               'rev_comment_data' => null,
-                               'rev_comment_cid' => null,
-                               'rev_content_format' => 'GOATFORMAT',
-                               'rev_content_model' => 'GOATMODEL',
-                       ],
-                       function ( RevisionUnitTest $testCase, Revision $rev ) {
-                               $testCase->assertSame( 2, $rev->getId() );
-                               $testCase->assertSame( 1, $rev->getPage() );
-                               $testCase->assertSame( 2, $rev->getTextId() );
-                               $testCase->assertSame( '20171017114835', $rev->getTimestamp() );
-                               $testCase->assertSame( '127.0.0.1', $rev->getUserText() );
-                               $testCase->assertSame( 0, $rev->getUser() );
-                               $testCase->assertSame( false, $rev->isMinor() );
-                               $testCase->assertSame( false, $rev->isDeleted( Revision::DELETED_TEXT ) );
-                               $testCase->assertSame( 46, $rev->getSize() );
-                               $testCase->assertSame( 1, $rev->getParentId() );
-                               $testCase->assertSame( 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', $rev->getSha1() );
-                               $testCase->assertSame( 'Goat Comment!', $rev->getComment() );
-                               $testCase->assertSame( 'GOATFORMAT', $rev->getContentFormat() );
-                               $testCase->assertSame( 'GOATMODEL', $rev->getContentModel() );
-                       }
-               ];
-               yield 'null fields' => [
-                       [
-                               'rev_id' => '2',
-                               'rev_page' => '1',
-                               'rev_text_id' => '2',
-                               'rev_timestamp' => '20171017114835',
-                               'rev_user_text' => '127.0.0.1',
-                               'rev_user' => '0',
-                               'rev_minor_edit' => '0',
-                               'rev_deleted' => '0',
-                               'rev_comment_text' => 'Goat Comment!',
-                               'rev_comment_data' => null,
-                               'rev_comment_cid' => null,
-                       ],
-                       function ( RevisionUnitTest $testCase, Revision $rev ) {
-                               $testCase->assertNull( $rev->getSize() );
-                               $testCase->assertNull( $rev->getParentId() );
-                               $testCase->assertNull( $rev->getSha1() );
-                               $testCase->assertSame( 'text/x-wiki', $rev->getContentFormat() );
-                               $testCase->assertSame( 'wikitext', $rev->getContentModel() );
-                       }
-               ];
-       }
-
-       /**
-        * @dataProvider provideConstructFromRow
-        * @covers Revision::__construct
-        * @covers Revision::constructFromDbRowObject
-        */
-       public function testConstructFromRow( array $arrayData, $assertions ) {
-               $row = (object)$arrayData;
-               $rev = new Revision( $row );
-               $assertions( $this, $rev );
-       }
-
-       public function provideGetRevisionText() {
-               yield 'Generic test' => [
-                       'This is a goat of revision text.',
-                       [
-                               'old_flags' => '',
-                               'old_text' => 'This is a goat of revision text.',
-                       ],
-               ];
-       }
-
-       public function provideGetId() {
-               yield [
-                       [],
-                       null
-               ];
-               yield [
-                       [ 'id' => 998 ],
-                       998
-               ];
-       }
-
-       /**
-        * @dataProvider provideGetId
-        * @covers Revision::getId
-        */
-       public function testGetId( $rowArray, $expectedId ) {
-               $rev = new Revision( $rowArray );
-               $this->assertEquals( $expectedId, $rev->getId() );
-       }
-
-       public function provideSetId() {
-               yield [ '123', 123 ];
-               yield [ 456, 456 ];
-       }
-
-       /**
-        * @dataProvider provideSetId
-        * @covers Revision::setId
-        */
-       public function testSetId( $input, $expected ) {
-               $rev = new Revision( [] );
-               $rev->setId( $input );
-               $this->assertSame( $expected, $rev->getId() );
-       }
-
-       public function provideSetUserIdAndName() {
-               yield [ '123', 123, 'GOaT' ];
-               yield [ 456, 456, 'GOaT' ];
-       }
-
-       /**
-        * @dataProvider provideSetUserIdAndName
-        * @covers Revision::setUserIdAndName
-        */
-       public function testSetUserIdAndName( $inputId, $expectedId, $name ) {
-               $rev = new Revision( [] );
-               $rev->setUserIdAndName( $inputId, $name );
-               $this->assertSame( $expectedId, $rev->getUser( Revision::RAW ) );
-               $this->assertEquals( $name, $rev->getUserText( Revision::RAW ) );
-       }
-
-       public function provideGetTextId() {
-               yield [ [], null ];
-               yield [ [ 'text_id' => '123' ], 123 ];
-               yield [ [ 'text_id' => 456 ], 456 ];
-       }
-
-       /**
-        * @dataProvider provideGetTextId
-        * @covers Revision::getTextId()
-        */
-       public function testGetTextId( $rowArray, $expected ) {
-               $rev = new Revision( $rowArray );
-               $this->assertSame( $expected, $rev->getTextId() );
-       }
-
-       public function provideGetParentId() {
-               yield [ [], null ];
-               yield [ [ 'parent_id' => '123' ], 123 ];
-               yield [ [ 'parent_id' => 456 ], 456 ];
-       }
-
-       /**
-        * @dataProvider provideGetParentId
-        * @covers Revision::getParentId()
-        */
-       public function testGetParentId( $rowArray, $expected ) {
-               $rev = new Revision( $rowArray );
-               $this->assertSame( $expected, $rev->getParentId() );
-       }
-
-       /**
-        * @covers Revision::getRevisionText
-        * @dataProvider provideGetRevisionText
-        */
-       public function testGetRevisionText( $expected, $rowData, $prefix = 'old_', $wiki = false ) {
-               $this->assertEquals(
-                       $expected,
-                       Revision::getRevisionText( (object)$rowData, $prefix, $wiki ) );
-       }
-
-       public function provideGetRevisionTextWithZlibExtension() {
-               yield 'Generic gzip test' => [
-                       'This is a small goat of revision text.',
-                       [
-                               'old_flags' => 'gzip',
-                               'old_text' => gzdeflate( 'This is a small goat of revision text.' ),
-                       ],
-               ];
-       }
-
-       /**
-        * @covers Revision::getRevisionText
-        * @dataProvider provideGetRevisionTextWithZlibExtension
-        */
-       public function testGetRevisionWithZlibExtension( $expected, $rowData ) {
-               $this->checkPHPExtension( 'zlib' );
-               $this->testGetRevisionText( $expected, $rowData );
-       }
-
-       public function provideGetRevisionTextWithLegacyEncoding() {
-               yield 'Utf8Native' => [
-                       "Wiki est l'\xc3\xa9cole superieur !",
-                       'iso-8859-1',
-                       [
-                               'old_flags' => 'utf-8',
-                               'old_text' => "Wiki est l'\xc3\xa9cole superieur !",
-                       ]
-               ];
-               yield 'Utf8Legacy' => [
-                       "Wiki est l'\xc3\xa9cole superieur !",
-                       'iso-8859-1',
-                       [
-                               'old_flags' => '',
-                               'old_text' => "Wiki est l'\xe9cole superieur !",
-                       ]
-               ];
-       }
-
-       /**
-        * @covers Revision::getRevisionText
-        * @dataProvider provideGetRevisionTextWithLegacyEncoding
-        */
-       public function testGetRevisionWithLegacyEncoding( $expected, $encoding, $rowData ) {
-               $this->setMwGlobals( 'wgLegacyEncoding', $encoding );
-               $this->testGetRevisionText( $expected, $rowData );
-       }
-
-       public function provideGetRevisionTextWithGzipAndLegacyEncoding() {
-               /**
-                * WARNING!
-                * Do not set the external flag!
-                * Otherwise, getRevisionText will hit the live database (if ExternalStore is enabled)!
-                */
-               yield 'Utf8NativeGzip' => [
-                       "Wiki est l'\xc3\xa9cole superieur !",
-                       'iso-8859-1',
-                       [
-                               'old_flags' => 'gzip,utf-8',
-                               'old_text' => gzdeflate( "Wiki est l'\xc3\xa9cole superieur !" ),
-                       ]
-               ];
-               yield 'Utf8LegacyGzip' => [
-                       "Wiki est l'\xc3\xa9cole superieur !",
-                       'iso-8859-1',
-                       [
-                               'old_flags' => 'gzip',
-                               'old_text' => gzdeflate( "Wiki est l'\xe9cole superieur !" ),
-                       ]
-               ];
-       }
-
-       /**
-        * @covers Revision::getRevisionText
-        * @dataProvider provideGetRevisionTextWithGzipAndLegacyEncoding
-        */
-       public function testGetRevisionWithGzipAndLegacyEncoding( $expected, $encoding, $rowData ) {
-               $this->checkPHPExtension( 'zlib' );
-               $this->setMwGlobals( 'wgLegacyEncoding', $encoding );
-               $this->testGetRevisionText( $expected, $rowData );
-       }
-
-       /**
-        * @covers Revision::compressRevisionText
-        */
-       public function testCompressRevisionTextUtf8() {
-               $row = new stdClass;
-               $row->old_text = "Wiki est l'\xc3\xa9cole superieur !";
-               $row->old_flags = Revision::compressRevisionText( $row->old_text );
-               $this->assertTrue( false !== strpos( $row->old_flags, 'utf-8' ),
-                       "Flags should contain 'utf-8'" );
-               $this->assertFalse( false !== strpos( $row->old_flags, 'gzip' ),
-                       "Flags should not contain 'gzip'" );
-               $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !",
-                       $row->old_text, "Direct check" );
-               $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !",
-                       Revision::getRevisionText( $row ), "getRevisionText" );
-       }
-
-       /**
-        * @covers Revision::compressRevisionText
-        */
-       public function testCompressRevisionTextUtf8Gzip() {
-               $this->checkPHPExtension( 'zlib' );
-               $this->setMwGlobals( 'wgCompressRevisions', true );
-
-               $row = new stdClass;
-               $row->old_text = "Wiki est l'\xc3\xa9cole superieur !";
-               $row->old_flags = Revision::compressRevisionText( $row->old_text );
-               $this->assertTrue( false !== strpos( $row->old_flags, 'utf-8' ),
-                       "Flags should contain 'utf-8'" );
-               $this->assertTrue( false !== strpos( $row->old_flags, 'gzip' ),
-                       "Flags should contain 'gzip'" );
-               $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !",
-                       gzinflate( $row->old_text ), "Direct check" );
-               $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !",
-                       Revision::getRevisionText( $row ), "getRevisionText" );
-       }
-
-       public function provideFetchFromConds() {
-               yield [ 0, [] ];
-               yield [ Revision::READ_LOCKING, [ 'FOR UPDATE' ] ];
-       }
-
-       /**
-        * @dataProvider provideFetchFromConds
-        * @covers Revision::fetchFromConds
-        */
-       public function testFetchFromConds( $flags, array $options ) {
-               $conditions = [ 'conditionsArray' ];
-
-               $db = $this->getMock( IDatabase::class );
-               $db->expects( $this->once() )
-                       ->method( 'selectRow' )
-                       ->with(
-                               $this->equalTo( [ 'revision', 'page', 'user' ] ),
-                               // We don't really care about the fields are they come from the selectField methods
-                               $this->isType( 'array' ),
-                               $this->equalTo( $conditions ),
-                               // Method name
-                               $this->equalTo( 'Revision::fetchFromConds' ),
-                               $this->equalTo( $options ),
-                               // We don't really care about the join conds are they come from the joinCond methods
-                               $this->isType( 'array' )
-                       )
-                       ->willReturn( 'RETURNVALUE' );
-
-               $wrapper = TestingAccessWrapper::newFromClass( Revision::class );
-               $result = $wrapper->fetchFromConds( $db, $conditions, $flags );
-
-               $this->assertEquals( 'RETURNVALUE', $result );
-       }
-}
diff --git a/tests/phpunit/includes/SanitizerTest.php b/tests/phpunit/includes/SanitizerTest.php
deleted file mode 100644 (file)
index 6fc25df..0000000
+++ /dev/null
@@ -1,541 +0,0 @@
-<?php
-
-/**
- * @todo Tests covering decodeCharReferences can be refactored into a single
- * method and dataprovider.
- *
- * @group Sanitizer
- */
-class SanitizerTest extends MediaWikiTestCase {
-
-       protected function tearDown() {
-               MWTidy::destroySingleton();
-               parent::tearDown();
-       }
-
-       /**
-        * @covers Sanitizer::decodeCharReferences
-        */
-       public function testDecodeNamedEntities() {
-               $this->assertEquals(
-                       "\xc3\xa9cole",
-                       Sanitizer::decodeCharReferences( '&eacute;cole' ),
-                       'decode named entities'
-               );
-       }
-
-       /**
-        * @covers Sanitizer::decodeCharReferences
-        */
-       public function testDecodeNumericEntities() {
-               $this->assertEquals(
-                       "\xc4\x88io bonas dans l'\xc3\xa9cole!",
-                       Sanitizer::decodeCharReferences( "&#x108;io bonas dans l'&#233;cole!" ),
-                       'decode numeric entities'
-               );
-       }
-
-       /**
-        * @covers Sanitizer::decodeCharReferences
-        */
-       public function testDecodeMixedEntities() {
-               $this->assertEquals(
-                       "\xc4\x88io bonas dans l'\xc3\xa9cole!",
-                       Sanitizer::decodeCharReferences( "&#x108;io bonas dans l'&eacute;cole!" ),
-                       'decode mixed numeric/named entities'
-               );
-       }
-
-       /**
-        * @covers Sanitizer::decodeCharReferences
-        */
-       public function testDecodeMixedComplexEntities() {
-               $this->assertEquals(
-                       "\xc4\x88io bonas dans l'\xc3\xa9cole! (mais pas &#x108;io dans l'&eacute;cole)",
-                       Sanitizer::decodeCharReferences(
-                               "&#x108;io bonas dans l'&eacute;cole! (mais pas &amp;#x108;io dans l'&#38;eacute;cole)"
-                       ),
-                       'decode mixed complex entities'
-               );
-       }
-
-       /**
-        * @covers Sanitizer::decodeCharReferences
-        */
-       public function testInvalidAmpersand() {
-               $this->assertEquals(
-                       'a & b',
-                       Sanitizer::decodeCharReferences( 'a & b' ),
-                       'Invalid ampersand'
-               );
-       }
-
-       /**
-        * @covers Sanitizer::decodeCharReferences
-        */
-       public function testInvalidEntities() {
-               $this->assertEquals(
-                       '&foo;',
-                       Sanitizer::decodeCharReferences( '&foo;' ),
-                       'Invalid named entity'
-               );
-       }
-
-       /**
-        * @covers Sanitizer::decodeCharReferences
-        */
-       public function testInvalidNumberedEntities() {
-               $this->assertEquals(
-                       UtfNormal\Constants::UTF8_REPLACEMENT,
-                       Sanitizer::decodeCharReferences( "&#88888888888888;" ),
-                       'Invalid numbered entity'
-               );
-       }
-
-       /**
-        * @covers Sanitizer::removeHTMLtags
-        * @dataProvider provideHtml5Tags
-        *
-        * @param string $tag Name of an HTML5 element (ie: 'video')
-        * @param bool $escaped Whether sanitizer let the tag in or escape it (ie: '&lt;video&gt;')
-        */
-       public function testRemovehtmltagsOnHtml5Tags( $tag, $escaped ) {
-               MWTidy::setInstance( false );
-
-               if ( $escaped ) {
-                       $this->assertEquals( "&lt;$tag&gt;",
-                               Sanitizer::removeHTMLtags( "<$tag>" )
-                       );
-               } else {
-                       $this->assertEquals( "<$tag></$tag>\n",
-                               Sanitizer::removeHTMLtags( "<$tag>" )
-                       );
-               }
-       }
-
-       /**
-        * Provide HTML5 tags
-        */
-       public static function provideHtml5Tags() {
-               $ESCAPED = true; # We want tag to be escaped
-               $VERBATIM = false; # We want to keep the tag
-               return [
-                       [ 'data', $VERBATIM ],
-                       [ 'mark', $VERBATIM ],
-                       [ 'time', $VERBATIM ],
-                       [ 'video', $ESCAPED ],
-               ];
-       }
-
-       function dataRemoveHTMLtags() {
-               return [
-                       // former testSelfClosingTag
-                       [
-                               '<div>Hello world</div />',
-                               '<div>Hello world</div>',
-                               'Self-closing closing div'
-                       ],
-                       // Make sure special nested HTML5 semantics are not broken
-                       // https://html.spec.whatwg.org/multipage/semantics.html#the-kbd-element
-                       [
-                               '<kbd><kbd>Shift</kbd>+<kbd>F3</kbd></kbd>',
-                               '<kbd><kbd>Shift</kbd>+<kbd>F3</kbd></kbd>',
-                               'Nested <kbd>.'
-                       ],
-                       // https://html.spec.whatwg.org/multipage/semantics.html#the-sub-and-sup-elements
-                       [
-                               '<var>x<sub><var>i</var></sub></var>, <var>y<sub><var>i</var></sub></var>',
-                               '<var>x<sub><var>i</var></sub></var>, <var>y<sub><var>i</var></sub></var>',
-                               'Nested <var>.'
-                       ],
-                       // https://html.spec.whatwg.org/multipage/semantics.html#the-dfn-element
-                       [
-                               '<dfn><abbr title="Garage Door Opener">GDO</abbr></dfn>',
-                               '<dfn><abbr title="Garage Door Opener">GDO</abbr></dfn>',
-                               '<abbr> inside <dfn>',
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider dataRemoveHTMLtags
-        * @covers Sanitizer::removeHTMLtags
-        */
-       public function testRemoveHTMLtags( $input, $output, $msg = null ) {
-               MWTidy::setInstance( false );
-               $this->assertEquals( $output, Sanitizer::removeHTMLtags( $input ), $msg );
-       }
-
-       /**
-        * @dataProvider provideTagAttributesToDecode
-        * @covers Sanitizer::decodeTagAttributes
-        */
-       public function testDecodeTagAttributes( $expected, $attributes, $message = '' ) {
-               $this->assertEquals( $expected,
-                       Sanitizer::decodeTagAttributes( $attributes ),
-                       $message
-               );
-       }
-
-       public static function provideTagAttributesToDecode() {
-               return [
-                       [ [ 'foo' => 'bar' ], 'foo=bar', 'Unquoted attribute' ],
-                       [ [ 'עברית' => 'bar' ], 'עברית=bar', 'Non-Latin attribute' ],
-                       [ [ '६' => 'bar' ], '६=bar', 'Devanagari number' ],
-                       [ [ '搭𨋢' => 'bar' ], '搭𨋢=bar', 'Non-BMP character' ],
-                       [ [], 'ńgh=bar', 'Combining accent is not allowed' ],
-                       [ [ 'foo' => 'bar' ], '    foo   =   bar    ', 'Spaced attribute' ],
-                       [ [ 'foo' => 'bar' ], 'foo="bar"', 'Double-quoted attribute' ],
-                       [ [ 'foo' => 'bar' ], 'foo=\'bar\'', 'Single-quoted attribute' ],
-                       [
-                               [ 'foo' => 'bar', 'baz' => 'foo' ],
-                               'foo=\'bar\'   baz="foo"',
-                               'Several attributes'
-                       ],
-                       [
-                               [ 'foo' => 'bar', 'baz' => 'foo' ],
-                               'foo=\'bar\'   baz="foo"',
-                               'Several attributes'
-                       ],
-                       [
-                               [ 'foo' => 'bar', 'baz' => 'foo' ],
-                               'foo=\'bar\'   baz="foo"',
-                               'Several attributes'
-                       ],
-                       [ [ ':foo' => 'bar' ], ':foo=\'bar\'', 'Leading :' ],
-                       [ [ '_foo' => 'bar' ], '_foo=\'bar\'', 'Leading _' ],
-                       [ [ 'foo' => 'bar' ], 'Foo=\'bar\'', 'Leading capital' ],
-                       [ [ 'foo' => 'BAR' ], 'FOO=BAR', 'Attribute keys are normalized to lowercase' ],
-
-                       # Invalid beginning
-                       [ [], '-foo=bar', 'Leading - is forbidden' ],
-                       [ [], '.foo=bar', 'Leading . is forbidden' ],
-                       [ [ 'foo-bar' => 'bar' ], 'foo-bar=bar', 'A - is allowed inside the attribute' ],
-                       [ [ 'foo-' => 'bar' ], 'foo-=bar', 'A - is allowed inside the attribute' ],
-                       [ [ 'foo.bar' => 'baz' ], 'foo.bar=baz', 'A . is allowed inside the attribute' ],
-                       [ [ 'foo.' => 'baz' ], 'foo.=baz', 'A . is allowed as last character' ],
-                       [ [ 'foo6' => 'baz' ], 'foo6=baz', 'Numbers are allowed' ],
-
-                       # This bit is more relaxed than XML rules, but some extensions use
-                       # it, like ProofreadPage (see T29539)
-                       [ [ '1foo' => 'baz' ], '1foo=baz', 'Leading numbers are allowed' ],
-                       [ [], 'foo$=baz', 'Symbols are not allowed' ],
-                       [ [], 'foo@=baz', 'Symbols are not allowed' ],
-                       [ [], 'foo~=baz', 'Symbols are not allowed' ],
-                       [
-                               [ 'foo' => '1[#^`*%w/(' ],
-                               'foo=1[#^`*%w/(',
-                               'All kind of characters are allowed as values'
-                       ],
-                       [
-                               [ 'foo' => '1[#^`*%\'w/(' ],
-                               'foo="1[#^`*%\'w/("',
-                               'Double quotes are allowed if quoted by single quotes'
-                       ],
-                       [
-                               [ 'foo' => '1[#^`*%"w/(' ],
-                               'foo=\'1[#^`*%"w/(\'',
-                               'Single quotes are allowed if quoted by double quotes'
-                       ],
-                       [ [ 'foo' => '&"' ], 'foo=&amp;&quot;', 'Special chars can be provided as entities' ],
-                       [ [ 'foo' => '&foobar;' ], 'foo=&foobar;', 'Entity-like items are accepted' ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideDeprecatedAttributes
-        * @covers Sanitizer::fixTagAttributes
-        */
-       public function testDeprecatedAttributesUnaltered( $inputAttr, $inputEl, $message = '' ) {
-               $this->assertEquals( " $inputAttr",
-                       Sanitizer::fixTagAttributes( $inputAttr, $inputEl ),
-                       $message
-               );
-       }
-
-       public static function provideDeprecatedAttributes() {
-               /** [ <attribute>, <element>, [message] ] */
-               return [
-                       [ 'clear="left"', 'br' ],
-                       [ 'clear="all"', 'br' ],
-                       [ 'width="100"', 'td' ],
-                       [ 'nowrap="true"', 'td' ],
-                       [ 'nowrap=""', 'td' ],
-                       [ 'align="right"', 'td' ],
-                       [ 'align="center"', 'table' ],
-                       [ 'align="left"', 'tr' ],
-                       [ 'align="center"', 'div' ],
-                       [ 'align="left"', 'h1' ],
-                       [ 'align="left"', 'p' ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideCssCommentsFixtures
-        * @covers Sanitizer::checkCss
-        */
-       public function testCssCommentsChecking( $expected, $css, $message = '' ) {
-               $this->assertEquals( $expected,
-                       Sanitizer::checkCss( $css ),
-                       $message
-               );
-       }
-
-       public static function provideCssCommentsFixtures() {
-               /** [ <expected>, <css>, [message] ] */
-               return [
-                       // Valid comments spanning entire input
-                       [ '/**/', '/**/' ],
-                       [ '/* comment */', '/* comment */' ],
-                       // Weird stuff
-                       [ ' ', '/****/' ],
-                       [ ' ', '/* /* */' ],
-                       [ 'display: block;', "display:/* foo */block;" ],
-                       [ 'display: block;', "display:\\2f\\2a foo \\2a\\2f block;",
-                               'Backslash-escaped comments must be stripped (T30450)' ],
-                       [ '', '/* unfinished comment structure',
-                               'Remove anything after a comment-start token' ],
-                       [ '', "\\2f\\2a unifinished comment'",
-                               'Remove anything after a backslash-escaped comment-start token' ],
-                       [
-                               '/* insecure input */',
-                               'filter: progid:DXImageTransform.Microsoft.AlphaImageLoader'
-                                       . '(src=\'asdf.png\',sizingMethod=\'scale\');'
-                       ],
-                       [
-                               '/* insecure input */',
-                               '-ms-filter: "progid:DXImageTransform.Microsoft.AlphaImageLoader'
-                                       . '(src=\'asdf.png\',sizingMethod=\'scale\')";'
-                       ],
-                       [ '/* insecure input */', 'width: expression(1+1);' ],
-                       [ '/* insecure input */', 'background-image: image(asdf.png);' ],
-                       [ '/* insecure input */', 'background-image: -webkit-image(asdf.png);' ],
-                       [ '/* insecure input */', 'background-image: -moz-image(asdf.png);' ],
-                       [ '/* insecure input */', 'background-image: image-set("asdf.png" 1x, "asdf.png" 2x);' ],
-                       [
-                               '/* insecure input */',
-                               'background-image: -webkit-image-set("asdf.png" 1x, "asdf.png" 2x);'
-                       ],
-                       [
-                               '/* insecure input */',
-                               'background-image: -moz-image-set("asdf.png" 1x, "asdf.png" 2x);'
-                       ],
-                       [ '/* insecure input */', 'foo: attr( title, url );' ],
-                       [ '/* insecure input */', 'foo: attr( title url );' ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideEscapeHtmlAllowEntities
-        * @covers Sanitizer::escapeHtmlAllowEntities
-        */
-       public function testEscapeHtmlAllowEntities( $expected, $html ) {
-               $this->assertEquals(
-                       $expected,
-                       Sanitizer::escapeHtmlAllowEntities( $html )
-               );
-       }
-
-       public static function provideEscapeHtmlAllowEntities() {
-               return [
-                       [ 'foo', 'foo' ],
-                       [ 'a¡b', 'a&#161;b' ],
-                       [ 'foo&#039;bar', "foo'bar" ],
-                       [ '&lt;script&gt;foo&lt;/script&gt;', '<script>foo</script>' ],
-               ];
-       }
-
-       /**
-        * Test Sanitizer::escapeId
-        *
-        * @dataProvider provideEscapeId
-        * @covers Sanitizer::escapeId
-        */
-       public function testEscapeId( $input, $output ) {
-               $this->assertEquals(
-                       $output,
-                       Sanitizer::escapeId( $input, [ 'noninitial', 'legacy' ] )
-               );
-       }
-
-       public static function provideEscapeId() {
-               return [
-                       [ '+', '.2B' ],
-                       [ '&', '.26' ],
-                       [ '=', '.3D' ],
-                       [ ':', ':' ],
-                       [ ';', '.3B' ],
-                       [ '@', '.40' ],
-                       [ '$', '.24' ],
-                       [ '-_.', '-_.' ],
-                       [ '!', '.21' ],
-                       [ '*', '.2A' ],
-                       [ '/', '.2F' ],
-                       [ '[]', '.5B.5D' ],
-                       [ '<>', '.3C.3E' ],
-                       [ '\'', '.27' ],
-                       [ '§', '.C2.A7' ],
-                       [ 'Test:A & B/Here', 'Test:A_.26_B.2FHere' ],
-                       [ 'A&B&amp;C&amp;amp;D&amp;amp;amp;E', 'A.26B.26amp.3BC.26amp.3Bamp.3BD.26amp.3Bamp.3Bamp.3BE' ],
-               ];
-       }
-
-       /**
-        * Test escapeIdReferenceList for consistency with escapeIdForAttribute
-        *
-        * @dataProvider provideEscapeIdReferenceList
-        * @covers Sanitizer::escapeIdReferenceList
-        */
-       public function testEscapeIdReferenceList( $referenceList, $id1, $id2 ) {
-               $this->assertEquals(
-                       Sanitizer::escapeIdReferenceList( $referenceList ),
-                       Sanitizer::escapeIdForAttribute( $id1 )
-                               . ' '
-                               . Sanitizer::escapeIdForAttribute( $id2 )
-               );
-       }
-
-       public static function provideEscapeIdReferenceList() {
-               /** [ <reference list>, <individual id 1>, <individual id 2> ] */
-               return [
-                       [ 'foo bar', 'foo', 'bar' ],
-                       [ '#1 #2', '#1', '#2' ],
-                       [ '+1 +2', '+1', '+2' ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideIsReservedDataAttribute
-        */
-       public function testIsReservedDataAttribute( $attr, $expected ) {
-               $this->assertSame( $expected, Sanitizer::isReservedDataAttribute( $attr ) );
-       }
-
-       public static function provideIsReservedDataAttribute() {
-               return [
-                       [ 'foo', false ],
-                       [ 'data', false ],
-                       [ 'data-foo', false ],
-                       [ 'data-mw', true ],
-                       [ 'data-ooui', true ],
-                       [ 'data-parsoid', true ],
-                       [ 'data-mw-foo', true ],
-                       [ 'data-ooui-foo', true ],
-                       [ 'data-mwfoo', true ], // could be false but this is how it's implemented currently
-               ];
-       }
-
-       /**
-        * @dataProvider provideEscapeIdForStuff
-        *
-        * @covers Sanitizer::escapeIdForAttribute()
-        * @covers Sanitizer::escapeIdForLink()
-        * @covers Sanitizer::escapeIdForExternalInterwiki()
-        * @covers Sanitizer::escapeIdInternal()
-        *
-        * @param string $stuff
-        * @param string[] $config
-        * @param string $id
-        * @param string|false $expected
-        * @param int|null $mode
-        */
-       public function testEscapeIdForStuff( $stuff, array $config, $id, $expected, $mode = null ) {
-               $func = "Sanitizer::escapeIdFor{$stuff}";
-               $iwFlavor = array_pop( $config );
-               $this->setMwGlobals( [
-                       'wgFragmentMode' => $config,
-                       'wgExternalInterwikiFragmentMode' => $iwFlavor,
-               ] );
-               $escaped = call_user_func( $func, $id, $mode );
-               self::assertEquals( $expected, $escaped );
-       }
-
-       public function provideEscapeIdForStuff() {
-               // Test inputs and outputs
-               $text = 'foo тест_#%!\'()[]:<>&&amp;&amp;amp;';
-               $legacyEncoded = 'foo_.D1.82.D0.B5.D1.81.D1.82_.23.25.21.27.28.29.5B.5D:.3C.3E' .
-                       '.26.26amp.3B.26amp.3Bamp.3B';
-               $html5Encoded = 'foo_тест_#%!\'()[]:<>&&amp;&amp;amp;';
-               $html5Experimental = 'foo_тест_!_()[]:<>_amp;_amp;amp;';
-
-               // Settings: last element is $wgExternalInterwikiFragmentMode, the rest is $wgFragmentMode
-               $legacy = [ 'legacy', 'legacy' ];
-               $legacyNew = [ 'legacy', 'html5', 'legacy' ];
-               $newLegacy = [ 'html5', 'legacy', 'legacy' ];
-               $new = [ 'html5', 'legacy' ];
-               $allNew = [ 'html5', 'html5' ];
-               $experimentalLegacy = [ 'html5-legacy', 'legacy', 'legacy' ];
-               $newExperimental = [ 'html5', 'html5-legacy', 'legacy' ];
-
-               return [
-                       // Pure legacy: how MW worked before 2017
-                       [ 'Attribute', $legacy, $text, $legacyEncoded, Sanitizer::ID_PRIMARY ],
-                       [ 'Attribute', $legacy, $text, false, Sanitizer::ID_FALLBACK ],
-                       [ 'Link', $legacy, $text, $legacyEncoded ],
-                       [ 'ExternalInterwiki', $legacy, $text, $legacyEncoded ],
-
-                       // Transition to a new world: legacy links with HTML5 fallback
-                       [ 'Attribute', $legacyNew, $text, $legacyEncoded, Sanitizer::ID_PRIMARY ],
-                       [ 'Attribute', $legacyNew, $text, $html5Encoded, Sanitizer::ID_FALLBACK ],
-                       [ 'Link', $legacyNew, $text, $legacyEncoded ],
-                       [ 'ExternalInterwiki', $legacyNew, $text, $legacyEncoded ],
-
-                       // New world: HTML5 links, legacy fallbacks
-                       [ 'Attribute', $newLegacy, $text, $html5Encoded, Sanitizer::ID_PRIMARY ],
-                       [ 'Attribute', $newLegacy, $text, $legacyEncoded, Sanitizer::ID_FALLBACK ],
-                       [ 'Link', $newLegacy, $text, $html5Encoded ],
-                       [ 'ExternalInterwiki', $newLegacy, $text, $legacyEncoded ],
-
-                       // Distant future: no legacy fallbacks, but still linking to leagacy wikis
-                       [ 'Attribute', $new, $text, $html5Encoded, Sanitizer::ID_PRIMARY ],
-                       [ 'Attribute', $new, $text, false, Sanitizer::ID_FALLBACK ],
-                       [ 'Link', $new, $text, $html5Encoded ],
-                       [ 'ExternalInterwiki', $new, $text, $legacyEncoded ],
-
-                       // Just before the heat death of universe: external interwikis are also HTML5 \m/
-                       [ 'Attribute', $allNew, $text, $html5Encoded, Sanitizer::ID_PRIMARY ],
-                       [ 'Attribute', $allNew, $text, false, Sanitizer::ID_FALLBACK ],
-                       [ 'Link', $allNew, $text, $html5Encoded ],
-                       [ 'ExternalInterwiki', $allNew, $text, $html5Encoded ],
-
-                       // Someone flipped $wgExperimentalHtmlIds on
-                       [ 'Attribute', $experimentalLegacy, $text, $html5Experimental, Sanitizer::ID_PRIMARY ],
-                       [ 'Attribute', $experimentalLegacy, $text, $legacyEncoded, Sanitizer::ID_FALLBACK ],
-                       [ 'Link', $experimentalLegacy, $text, $html5Experimental ],
-                       [ 'ExternalInterwiki', $experimentalLegacy, $text, $legacyEncoded ],
-
-                       // Migration from $wgExperimentalHtmlIds to modern HTML5
-                       [ 'Attribute', $newExperimental, $text, $html5Encoded, Sanitizer::ID_PRIMARY ],
-                       [ 'Attribute', $newExperimental, $text, $html5Experimental, Sanitizer::ID_FALLBACK ],
-                       [ 'Link', $newExperimental, $text, $html5Encoded ],
-                       [ 'ExternalInterwiki', $newExperimental, $text, $legacyEncoded ],
-               ];
-       }
-
-       /**
-        * @expectedException InvalidArgumentException
-        * @covers Sanitizer::escapeIdInternal()
-        */
-       public function testInvalidFragmentThrows() {
-               $this->setMwGlobals( 'wgFragmentMode', [ 'boom!' ] );
-               Sanitizer::escapeIdForAttribute( 'This should throw' );
-       }
-
-       /**
-        * @expectedException UnexpectedValueException
-        * @covers Sanitizer::escapeIdForAttribute()
-        */
-       public function testNoPrimaryFragmentModeThrows() {
-               $this->setMwGlobals( 'wgFragmentMode', [ 666 => 'html5' ] );
-               Sanitizer::escapeIdForAttribute( 'This should throw' );
-       }
-
-       /**
-        * @expectedException UnexpectedValueException
-        * @covers Sanitizer::escapeIdForLink()
-        */
-       public function testNoPrimaryFragmentModeThrows2() {
-               $this->setMwGlobals( 'wgFragmentMode', [ 666 => 'html5' ] );
-               Sanitizer::escapeIdForLink( 'This should throw' );
-       }
-}
diff --git a/tests/phpunit/includes/WatchedItemIntegrationTest.php b/tests/phpunit/includes/WatchedItemIntegrationTest.php
deleted file mode 100644 (file)
index 01e7ecb..0000000
+++ /dev/null
@@ -1,145 +0,0 @@
-<?php
-use MediaWiki\MediaWikiServices;
-
-/**
- * @author Addshore
- *
- * @group Database
- *
- * @covers WatchedItem
- */
-class WatchedItemIntegrationTest extends MediaWikiTestCase {
-
-       public function setUp() {
-               parent::setUp();
-               self::$users['WatchedItemIntegrationTestUser']
-                       = new TestUser( 'WatchedItemIntegrationTestUser' );
-
-               $this->hideDeprecated( 'WatchedItem::fromUserTitle' );
-               $this->hideDeprecated( 'WatchedItem::addWatch' );
-               $this->hideDeprecated( 'WatchedItem::removeWatch' );
-               $this->hideDeprecated( 'WatchedItem::isWatched' );
-               $this->hideDeprecated( 'WatchedItem::duplicateEntries' );
-               $this->hideDeprecated( 'WatchedItem::batchAddWatch' );
-       }
-
-       private function getUser() {
-               return self::$users['WatchedItemIntegrationTestUser']->getUser();
-       }
-
-       public function testWatchAndUnWatchItem() {
-               $user = $this->getUser();
-               $title = Title::newFromText( 'WatchedItemIntegrationTestPage' );
-               // Cleanup after previous tests
-               WatchedItem::fromUserTitle( $user, $title )->removeWatch();
-
-               $this->assertFalse(
-                       WatchedItem::fromUserTitle( $user, $title )->isWatched(),
-                       'Page should not initially be watched'
-               );
-               WatchedItem::fromUserTitle( $user, $title )->addWatch();
-               $this->assertTrue(
-                       WatchedItem::fromUserTitle( $user, $title )->isWatched(),
-                       'Page should be watched'
-               );
-               WatchedItem::fromUserTitle( $user, $title )->removeWatch();
-               $this->assertFalse(
-                       WatchedItem::fromUserTitle( $user, $title )->isWatched(),
-                       'Page should be unwatched'
-               );
-       }
-
-       public function testUpdateAndResetNotificationTimestamp() {
-               $user = $this->getUser();
-               $otherUser = ( new TestUser( 'WatchedItemIntegrationTestUser_otherUser' ) )->getUser();
-               $title = Title::newFromText( 'WatchedItemIntegrationTestPage' );
-               WatchedItem::fromUserTitle( $user, $title )->addWatch();
-               $this->assertNull( WatchedItem::fromUserTitle( $user, $title )->getNotificationTimestamp() );
-
-               EmailNotification::updateWatchlistTimestamp( $otherUser, $title, '20150202010101' );
-               $this->assertEquals(
-                       '20150202010101',
-                       WatchedItem::fromUserTitle( $user, $title )->getNotificationTimestamp()
-               );
-
-               MediaWikiServices::getInstance()->getWatchedItemStore()->resetNotificationTimestamp(
-                       $user, $title
-               );
-               $this->assertNull( WatchedItem::fromUserTitle( $user, $title )->getNotificationTimestamp() );
-       }
-
-       public function testDuplicateAllAssociatedEntries() {
-               $user = $this->getUser();
-               $titleOld = Title::newFromText( 'WatchedItemIntegrationTestPageOld' );
-               $titleNew = Title::newFromText( 'WatchedItemIntegrationTestPageNew' );
-               WatchedItem::fromUserTitle( $user, $titleOld->getSubjectPage() )->addWatch();
-               WatchedItem::fromUserTitle( $user, $titleOld->getTalkPage() )->addWatch();
-               // Cleanup after previous tests
-               WatchedItem::fromUserTitle( $user, $titleNew->getSubjectPage() )->removeWatch();
-               WatchedItem::fromUserTitle( $user, $titleNew->getTalkPage() )->removeWatch();
-
-               WatchedItem::duplicateEntries( $titleOld, $titleNew );
-
-               $this->assertTrue(
-                       WatchedItem::fromUserTitle( $user, $titleOld->getSubjectPage() )->isWatched()
-               );
-               $this->assertTrue(
-                       WatchedItem::fromUserTitle( $user, $titleOld->getTalkPage() )->isWatched()
-               );
-               $this->assertTrue(
-                       WatchedItem::fromUserTitle( $user, $titleNew->getSubjectPage() )->isWatched()
-               );
-               $this->assertTrue(
-                       WatchedItem::fromUserTitle( $user, $titleNew->getTalkPage() )->isWatched()
-               );
-       }
-
-       public function testIsWatched_falseOnNotAllowed() {
-               $user = $this->getUser();
-               $title = Title::newFromText( 'WatchedItemIntegrationTestPage' );
-               WatchedItem::fromUserTitle( $user, $title )->addWatch();
-
-               $this->assertTrue( WatchedItem::fromUserTitle( $user, $title )->isWatched() );
-               $user->mRights = [];
-               $this->assertFalse( WatchedItem::fromUserTitle( $user, $title )->isWatched() );
-       }
-
-       public function testGetNotificationTimestamp_falseOnNotAllowed() {
-               $user = $this->getUser();
-               $title = Title::newFromText( 'WatchedItemIntegrationTestPage' );
-               WatchedItem::fromUserTitle( $user, $title )->addWatch();
-               MediaWikiServices::getInstance()->getWatchedItemStore()->resetNotificationTimestamp(
-                       $user, $title
-               );
-
-               $this->assertEquals(
-                       null,
-                       WatchedItem::fromUserTitle( $user, $title )->getNotificationTimestamp()
-               );
-               $user->mRights = [];
-               $this->assertFalse( WatchedItem::fromUserTitle( $user, $title )->getNotificationTimestamp() );
-       }
-
-       public function testRemoveWatch_falseOnNotAllowed() {
-               $user = $this->getUser();
-               $title = Title::newFromText( 'WatchedItemIntegrationTestPage' );
-               WatchedItem::fromUserTitle( $user, $title )->addWatch();
-
-               $previousRights = $user->mRights;
-               $user->mRights = [];
-               $this->assertFalse( WatchedItem::fromUserTitle( $user, $title )->removeWatch() );
-               $user->mRights = $previousRights;
-               $this->assertTrue( WatchedItem::fromUserTitle( $user, $title )->removeWatch() );
-       }
-
-       public function testGetNotificationTimestamp_falseOnNotWatched() {
-               $user = $this->getUser();
-               $title = Title::newFromText( 'WatchedItemIntegrationTestPage' );
-
-               WatchedItem::fromUserTitle( $user, $title )->removeWatch();
-               $this->assertFalse( WatchedItem::fromUserTitle( $user, $title )->isWatched() );
-
-               $this->assertFalse( WatchedItem::fromUserTitle( $user, $title )->getNotificationTimestamp() );
-       }
-
-}
diff --git a/tests/phpunit/includes/WatchedItemQueryServiceUnitTest.php b/tests/phpunit/includes/WatchedItemQueryServiceUnitTest.php
deleted file mode 100644 (file)
index 62ba5f6..0000000
+++ /dev/null
@@ -1,1676 +0,0 @@
-<?php
-
-use Wikimedia\ScopedCallback;
-use Wikimedia\TestingAccessWrapper;
-
-/**
- * @covers WatchedItemQueryService
- */
-class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase {
-
-       /**
-        * @return PHPUnit_Framework_MockObject_MockObject|Database
-        */
-       private function getMockDb() {
-               $mock = $this->getMockBuilder( Database::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $mock->expects( $this->any() )
-                       ->method( 'makeList' )
-                       ->with(
-                               $this->isType( 'array' ),
-                               $this->isType( 'int' )
-                       )
-                       ->will( $this->returnCallback( function ( $a, $conj ) {
-                               $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR ';
-                               return join( $sqlConj, array_map( function ( $s ) {
-                                       return '(' . $s . ')';
-                               }, $a
-                               ) );
-                       } ) );
-
-               $mock->expects( $this->any() )
-                       ->method( 'addQuotes' )
-                       ->will( $this->returnCallback( function ( $value ) {
-                               return "'$value'";
-                       } ) );
-
-               $mock->expects( $this->any() )
-                       ->method( 'timestamp' )
-                       ->will( $this->returnArgument( 0 ) );
-
-               $mock->expects( $this->any() )
-                       ->method( 'bitAnd' )
-                       ->willReturnCallback( function ( $a, $b ) {
-                               return "($a & $b)";
-                       } );
-
-               return $mock;
-       }
-
-       /**
-        * @param PHPUnit_Framework_MockObject_MockObject|Database $mockDb
-        * @return PHPUnit_Framework_MockObject_MockObject|LoadBalancer
-        */
-       private function getMockLoadBalancer( $mockDb ) {
-               $mock = $this->getMockBuilder( LoadBalancer::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               $mock->expects( $this->any() )
-                       ->method( 'getConnectionRef' )
-                       ->with( DB_REPLICA )
-                       ->will( $this->returnValue( $mockDb ) );
-               return $mock;
-       }
-
-       /**
-        * @param int $id
-        * @return PHPUnit_Framework_MockObject_MockObject|User
-        */
-       private function getMockNonAnonUserWithId( $id ) {
-               $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 ) );
-               return $mock;
-       }
-
-       /**
-        * @param int $id
-        * @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 ) );
-               return $mock;
-       }
-
-       /**
-        * @param int $id
-        * @param string $notAllowedAction
-        * @return PHPUnit_Framework_MockObject_MockObject|User
-        */
-       private function getMockNonAnonUserWithIdAndRestrictedPermissions( $id, $notAllowedAction ) {
-               $mock = $this->getMockNonAnonUserWithId( $id );
-
-               $mock->expects( $this->any() )
-                       ->method( 'isAllowed' )
-                       ->will( $this->returnCallback( function ( $action ) use ( $notAllowedAction ) {
-                               return $action !== $notAllowedAction;
-                       } ) );
-               $mock->expects( $this->any() )
-                       ->method( 'isAllowedAny' )
-                       ->will( $this->returnCallback( function () use ( $notAllowedAction ) {
-                               $actions = func_get_args();
-                               return !in_array( $notAllowedAction, $actions );
-                       } ) );
-
-               return $mock;
-       }
-
-       /**
-        * @param int $id
-        * @return PHPUnit_Framework_MockObject_MockObject|User
-        */
-       private function getMockNonAnonUserWithIdAndNoPatrolRights( $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( false ) );
-               $mock->expects( $this->any() )
-                       ->method( 'useNPPatrol' )
-                       ->will( $this->returnValue( false ) );
-
-               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 ) {
-                       $fakeRow->$valueName = $value;
-               }
-               return $fakeRow;
-       }
-
-       public function testGetWatchedItemsWithRecentChangeInfo() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'select' )
-                       ->with(
-                               [ 'recentchanges', 'watchlist', 'page' ],
-                               [
-                                       'rc_id',
-                                       'rc_namespace',
-                                       'rc_title',
-                                       'rc_timestamp',
-                                       'rc_type',
-                                       'rc_deleted',
-                                       'wl_notificationtimestamp',
-                                       'rc_cur_id',
-                                       'rc_this_oldid',
-                                       'rc_last_oldid',
-                               ],
-                               [
-                                       'wl_user' => 1,
-                                       '(rc_this_oldid=page_latest) OR (rc_type=3)',
-                               ],
-                               $this->isType( 'string' ),
-                               [
-                                       'LIMIT' => 3,
-                               ],
-                               [
-                                       'watchlist' => [
-                                               'INNER JOIN',
-                                               [
-                                                       'wl_namespace=rc_namespace',
-                                                       'wl_title=rc_title'
-                                               ]
-                                       ],
-                                       'page' => [
-                                               'LEFT JOIN',
-                                               'rc_cur_id=page_id',
-                                       ],
-                               ]
-                       )
-                       ->will( $this->returnValue( [
-                               $this->getFakeRow( [
-                                       'rc_id' => 1,
-                                       'rc_namespace' => 0,
-                                       'rc_title' => 'Foo1',
-                                       'rc_timestamp' => '20151212010101',
-                                       'rc_type' => RC_NEW,
-                                       'rc_deleted' => 0,
-                                       'wl_notificationtimestamp' => '20151212010101',
-                               ] ),
-                               $this->getFakeRow( [
-                                       'rc_id' => 2,
-                                       'rc_namespace' => 1,
-                                       'rc_title' => 'Foo2',
-                                       'rc_timestamp' => '20151212010102',
-                                       'rc_type' => RC_NEW,
-                                       'rc_deleted' => 0,
-                                       'wl_notificationtimestamp' => null,
-                               ] ),
-                               $this->getFakeRow( [
-                                       'rc_id' => 3,
-                                       'rc_namespace' => 1,
-                                       'rc_title' => 'Foo3',
-                                       'rc_timestamp' => '20151212010103',
-                                       'rc_type' => RC_NEW,
-                                       'rc_deleted' => 0,
-                                       'wl_notificationtimestamp' => null,
-                               ] ),
-                       ] ) );
-
-               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
-               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
-
-               $startFrom = null;
-               $items = $queryService->getWatchedItemsWithRecentChangeInfo(
-                       $user, [ 'limit' => 2 ], $startFrom
-               );
-
-               $this->assertInternalType( 'array', $items );
-               $this->assertCount( 2, $items );
-
-               foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
-                       $this->assertInstanceOf( WatchedItem::class, $watchedItem );
-                       $this->assertInternalType( 'array', $recentChangeInfo );
-               }
-
-               $this->assertEquals(
-                       new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
-                       $items[0][0]
-               );
-               $this->assertEquals(
-                       [
-                               'rc_id' => 1,
-                               'rc_namespace' => 0,
-                               'rc_title' => 'Foo1',
-                               'rc_timestamp' => '20151212010101',
-                               'rc_type' => RC_NEW,
-                               'rc_deleted' => 0,
-                       ],
-                       $items[0][1]
-               );
-
-               $this->assertEquals(
-                       new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
-                       $items[1][0]
-               );
-               $this->assertEquals(
-                       [
-                               'rc_id' => 2,
-                               'rc_namespace' => 1,
-                               'rc_title' => 'Foo2',
-                               'rc_timestamp' => '20151212010102',
-                               'rc_type' => RC_NEW,
-                               'rc_deleted' => 0,
-                       ],
-                       $items[1][1]
-               );
-
-               $this->assertEquals( [ '20151212010103', 3 ], $startFrom );
-       }
-
-       public function testGetWatchedItemsWithRecentChangeInfo_extension() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'select' )
-                       ->with(
-                               [ 'recentchanges', 'watchlist', 'page', 'extension_dummy_table' ],
-                               [
-                                       'rc_id',
-                                       'rc_namespace',
-                                       'rc_title',
-                                       'rc_timestamp',
-                                       'rc_type',
-                                       'rc_deleted',
-                                       'wl_notificationtimestamp',
-                                       'rc_cur_id',
-                                       'rc_this_oldid',
-                                       'rc_last_oldid',
-                                       'extension_dummy_field',
-                               ],
-                               [
-                                       'wl_user' => 1,
-                                       '(rc_this_oldid=page_latest) OR (rc_type=3)',
-                                       'extension_dummy_cond',
-                               ],
-                               $this->isType( 'string' ),
-                               [
-                                       'extension_dummy_option',
-                               ],
-                               [
-                                       'watchlist' => [
-                                               'INNER JOIN',
-                                               [
-                                                       'wl_namespace=rc_namespace',
-                                                       'wl_title=rc_title'
-                                               ]
-                                       ],
-                                       'page' => [
-                                               'LEFT JOIN',
-                                               'rc_cur_id=page_id',
-                                       ],
-                                       'extension_dummy_join_cond' => [],
-                               ]
-                       )
-                       ->will( $this->returnValue( [
-                               $this->getFakeRow( [
-                                       'rc_id' => 1,
-                                       'rc_namespace' => 0,
-                                       'rc_title' => 'Foo1',
-                                       'rc_timestamp' => '20151212010101',
-                                       'rc_type' => RC_NEW,
-                                       'rc_deleted' => 0,
-                                       'wl_notificationtimestamp' => '20151212010101',
-                               ] ),
-                               $this->getFakeRow( [
-                                       'rc_id' => 2,
-                                       'rc_namespace' => 1,
-                                       'rc_title' => 'Foo2',
-                                       'rc_timestamp' => '20151212010102',
-                                       'rc_type' => RC_NEW,
-                                       'rc_deleted' => 0,
-                                       'wl_notificationtimestamp' => null,
-                               ] ),
-                       ] ) );
-
-               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
-
-               $mockExtension = $this->getMockBuilder( WatchedItemQueryServiceExtension::class )
-                       ->getMock();
-               $mockExtension->expects( $this->once() )
-                       ->method( 'modifyWatchedItemsWithRCInfoQuery' )
-                       ->with(
-                               $this->identicalTo( $user ),
-                               $this->isType( 'array' ),
-                               $this->isInstanceOf( IDatabase::class ),
-                               $this->isType( 'array' ),
-                               $this->isType( 'array' ),
-                               $this->isType( 'array' ),
-                               $this->isType( 'array' ),
-                               $this->isType( 'array' )
-                       )
-                       ->will( $this->returnCallback( function (
-                               $user, $options, $db, &$tables, &$fields, &$conds, &$dbOptions, &$joinConds
-                       ) {
-                               $tables[] = 'extension_dummy_table';
-                               $fields[] = 'extension_dummy_field';
-                               $conds[] = 'extension_dummy_cond';
-                               $dbOptions[] = 'extension_dummy_option';
-                               $joinConds['extension_dummy_join_cond'] = [];
-                       } ) );
-               $mockExtension->expects( $this->once() )
-                       ->method( 'modifyWatchedItemsWithRCInfo' )
-                       ->with(
-                               $this->identicalTo( $user ),
-                               $this->isType( 'array' ),
-                               $this->isInstanceOf( IDatabase::class ),
-                               $this->isType( 'array' ),
-                               $this->anything(),
-                               $this->anything() // Can't test for null here, PHPUnit applies this after the callback
-                       )
-                       ->will( $this->returnCallback( function ( $user, $options, $db, &$items, $res, &$startFrom ) {
-                               foreach ( $items as $i => &$item ) {
-                                       $item[1]['extension_dummy_field'] = $i;
-                               }
-                               unset( $item );
-
-                               $this->assertNull( $startFrom );
-                               $startFrom = [ '20160203123456', 42 ];
-                       } ) );
-
-               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
-               TestingAccessWrapper::newFromObject( $queryService )->extensions = [ $mockExtension ];
-
-               $startFrom = null;
-               $items = $queryService->getWatchedItemsWithRecentChangeInfo(
-                       $user, [], $startFrom
-               );
-
-               $this->assertInternalType( 'array', $items );
-               $this->assertCount( 2, $items );
-
-               foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
-                       $this->assertInstanceOf( WatchedItem::class, $watchedItem );
-                       $this->assertInternalType( 'array', $recentChangeInfo );
-               }
-
-               $this->assertEquals(
-                       new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
-                       $items[0][0]
-               );
-               $this->assertEquals(
-                       [
-                               'rc_id' => 1,
-                               'rc_namespace' => 0,
-                               'rc_title' => 'Foo1',
-                               'rc_timestamp' => '20151212010101',
-                               'rc_type' => RC_NEW,
-                               'rc_deleted' => 0,
-                               'extension_dummy_field' => 0,
-                       ],
-                       $items[0][1]
-               );
-
-               $this->assertEquals(
-                       new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
-                       $items[1][0]
-               );
-               $this->assertEquals(
-                       [
-                               'rc_id' => 2,
-                               'rc_namespace' => 1,
-                               'rc_title' => 'Foo2',
-                               'rc_timestamp' => '20151212010102',
-                               'rc_type' => RC_NEW,
-                               'rc_deleted' => 0,
-                               'extension_dummy_field' => 1,
-                       ],
-                       $items[1][1]
-               );
-
-               $this->assertEquals( [ '20160203123456', 42 ], $startFrom );
-       }
-
-       public function getWatchedItemsWithRecentChangeInfoOptionsProvider() {
-               return [
-                       [
-                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_FLAGS ] ],
-                               null,
-                               [],
-                               [ 'rc_type', 'rc_minor', 'rc_bot' ],
-                               [],
-                               [],
-                               [],
-                       ],
-                       [
-                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_USER ] ],
-                               null,
-                               [],
-                               [ 'rc_user_text' ],
-                               [],
-                               [],
-                               [],
-                       ],
-                       [
-                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_USER_ID ] ],
-                               null,
-                               [],
-                               [ 'rc_user' ],
-                               [],
-                               [],
-                               [],
-                       ],
-                       [
-                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_COMMENT ] ],
-                               null,
-                               [],
-                               [
-                                       'rc_comment_text' => 'rc_comment',
-                                       'rc_comment_data' => 'NULL',
-                                       'rc_comment_cid' => 'NULL',
-                               ],
-                               [],
-                               [],
-                               [],
-                               [ 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD ],
-                       ],
-                       [
-                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_COMMENT ] ],
-                               null,
-                               [ 'comment_rc_comment' => 'comment' ],
-                               [
-                                       'rc_comment_text' => 'COALESCE( comment_rc_comment.comment_text, rc_comment )',
-                                       'rc_comment_data' => 'comment_rc_comment.comment_data',
-                                       'rc_comment_cid' => 'comment_rc_comment.comment_id',
-                               ],
-                               [],
-                               [],
-                               [ 'comment_rc_comment' => [ 'LEFT JOIN', 'comment_rc_comment.comment_id = rc_comment_id' ] ],
-                               [ 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH ],
-                       ],
-                       [
-                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_COMMENT ] ],
-                               null,
-                               [ 'comment_rc_comment' => 'comment' ],
-                               [
-                                       'rc_comment_text' => 'COALESCE( comment_rc_comment.comment_text, rc_comment )',
-                                       'rc_comment_data' => 'comment_rc_comment.comment_data',
-                                       'rc_comment_cid' => 'comment_rc_comment.comment_id',
-                               ],
-                               [],
-                               [],
-                               [ 'comment_rc_comment' => [ 'LEFT JOIN', 'comment_rc_comment.comment_id = rc_comment_id' ] ],
-                               [ 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_NEW ],
-                       ],
-                       [
-                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_COMMENT ] ],
-                               null,
-                               [ 'comment_rc_comment' => 'comment' ],
-                               [
-                                       'rc_comment_text' => 'comment_rc_comment.comment_text',
-                                       'rc_comment_data' => 'comment_rc_comment.comment_data',
-                                       'rc_comment_cid' => 'comment_rc_comment.comment_id',
-                               ],
-                               [],
-                               [],
-                               [ 'comment_rc_comment' => [ 'JOIN', 'comment_rc_comment.comment_id = rc_comment_id' ] ],
-                               [ 'wgCommentTableSchemaMigrationStage' => MIGRATION_NEW ],
-                       ],
-                       [
-                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_PATROL_INFO ] ],
-                               null,
-                               [],
-                               [ 'rc_patrolled', 'rc_log_type' ],
-                               [],
-                               [],
-                               [],
-                       ],
-                       [
-                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_SIZES ] ],
-                               null,
-                               [],
-                               [ 'rc_old_len', 'rc_new_len' ],
-                               [],
-                               [],
-                               [],
-                       ],
-                       [
-                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_LOG_INFO ] ],
-                               null,
-                               [],
-                               [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ],
-                               [],
-                               [],
-                               [],
-                       ],
-                       [
-                               [ 'namespaceIds' => [ 0, 1 ] ],
-                               null,
-                               [],
-                               [],
-                               [ 'wl_namespace' => [ 0, 1 ] ],
-                               [],
-                               [],
-                       ],
-                       [
-                               [ 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ] ],
-                               null,
-                               [],
-                               [],
-                               [ 'wl_namespace' => [ 0, 1 ] ],
-                               [],
-                               [],
-                       ],
-                       [
-                               [ 'rcTypes' => [ RC_EDIT, RC_NEW ] ],
-                               null,
-                               [],
-                               [],
-                               [ 'rc_type' => [ RC_EDIT, RC_NEW ] ],
-                               [],
-                               [],
-                       ],
-                       [
-                               [ 'dir' => WatchedItemQueryService::DIR_OLDER ],
-                               null,
-                               [],
-                               [],
-                               [],
-                               [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
-                               [],
-                       ],
-                       [
-                               [ 'dir' => WatchedItemQueryService::DIR_NEWER ],
-                               null,
-                               [],
-                               [],
-                               [],
-                               [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
-                               [],
-                       ],
-                       [
-                               [ 'dir' => WatchedItemQueryService::DIR_OLDER, 'start' => '20151212010101' ],
-                               null,
-                               [],
-                               [],
-                               [ "rc_timestamp <= '20151212010101'" ],
-                               [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
-                               [],
-                       ],
-                       [
-                               [ 'dir' => WatchedItemQueryService::DIR_OLDER, 'end' => '20151212010101' ],
-                               null,
-                               [],
-                               [],
-                               [ "rc_timestamp >= '20151212010101'" ],
-                               [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
-                               [],
-                       ],
-                       [
-                               [
-                                       'dir' => WatchedItemQueryService::DIR_OLDER,
-                                       'start' => '20151212020101',
-                                       'end' => '20151212010101'
-                               ],
-                               null,
-                               [],
-                               [],
-                               [ "rc_timestamp <= '20151212020101'", "rc_timestamp >= '20151212010101'" ],
-                               [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
-                               [],
-                       ],
-                       [
-                               [ 'dir' => WatchedItemQueryService::DIR_NEWER, 'start' => '20151212010101' ],
-                               null,
-                               [],
-                               [],
-                               [ "rc_timestamp >= '20151212010101'" ],
-                               [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
-                               [],
-                       ],
-                       [
-                               [ 'dir' => WatchedItemQueryService::DIR_NEWER, 'end' => '20151212010101' ],
-                               null,
-                               [],
-                               [],
-                               [ "rc_timestamp <= '20151212010101'" ],
-                               [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
-                               [],
-                       ],
-                       [
-                               [
-                                       'dir' => WatchedItemQueryService::DIR_NEWER,
-                                       'start' => '20151212010101',
-                                       'end' => '20151212020101'
-                               ],
-                               null,
-                               [],
-                               [],
-                               [ "rc_timestamp >= '20151212010101'", "rc_timestamp <= '20151212020101'" ],
-                               [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
-                               [],
-                       ],
-                       [
-                               [ 'limit' => 10 ],
-                               null,
-                               [],
-                               [],
-                               [],
-                               [ 'LIMIT' => 11 ],
-                               [],
-                       ],
-                       [
-                               [ 'limit' => "10; DROP TABLE watchlist;\n--" ],
-                               null,
-                               [],
-                               [],
-                               [],
-                               [ 'LIMIT' => 11 ],
-                               [],
-                       ],
-                       [
-                               [ 'filters' => [ WatchedItemQueryService::FILTER_MINOR ] ],
-                               null,
-                               [],
-                               [],
-                               [ 'rc_minor != 0' ],
-                               [],
-                               [],
-                       ],
-                       [
-                               [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_MINOR ] ],
-                               null,
-                               [],
-                               [],
-                               [ 'rc_minor = 0' ],
-                               [],
-                               [],
-                       ],
-                       [
-                               [ 'filters' => [ WatchedItemQueryService::FILTER_BOT ] ],
-                               null,
-                               [],
-                               [],
-                               [ 'rc_bot != 0' ],
-                               [],
-                               [],
-                       ],
-                       [
-                               [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_BOT ] ],
-                               null,
-                               [],
-                               [],
-                               [ 'rc_bot = 0' ],
-                               [],
-                               [],
-                       ],
-                       [
-                               [ 'filters' => [ WatchedItemQueryService::FILTER_ANON ] ],
-                               null,
-                               [],
-                               [],
-                               [ 'rc_user = 0' ],
-                               [],
-                               [],
-                       ],
-                       [
-                               [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_ANON ] ],
-                               null,
-                               [],
-                               [],
-                               [ 'rc_user != 0' ],
-                               [],
-                               [],
-                       ],
-                       [
-                               [ 'filters' => [ WatchedItemQueryService::FILTER_PATROLLED ] ],
-                               null,
-                               [],
-                               [],
-                               [ 'rc_patrolled != 0' ],
-                               [],
-                               [],
-                       ],
-                       [
-                               [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_PATROLLED ] ],
-                               null,
-                               [],
-                               [],
-                               [ 'rc_patrolled = 0' ],
-                               [],
-                               [],
-                       ],
-                       [
-                               [ 'filters' => [ WatchedItemQueryService::FILTER_UNREAD ] ],
-                               null,
-                               [],
-                               [],
-                               [ 'rc_timestamp >= wl_notificationtimestamp' ],
-                               [],
-                               [],
-                       ],
-                       [
-                               [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_UNREAD ] ],
-                               null,
-                               [],
-                               [],
-                               [ 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp' ],
-                               [],
-                               [],
-                       ],
-                       [
-                               [ 'onlyByUser' => 'SomeOtherUser' ],
-                               null,
-                               [],
-                               [],
-                               [ 'rc_user_text' => 'SomeOtherUser' ],
-                               [],
-                               [],
-                       ],
-                       [
-                               [ 'notByUser' => 'SomeOtherUser' ],
-                               null,
-                               [],
-                               [],
-                               [ "rc_user_text != 'SomeOtherUser'" ],
-                               [],
-                               [],
-                       ],
-                       [
-                               [ 'dir' => WatchedItemQueryService::DIR_OLDER ],
-                               [ '20151212010101', 123 ],
-                               [],
-                               [],
-                               [
-                                       "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
-                               ],
-                               [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
-                               [],
-                       ],
-                       [
-                               [ 'dir' => WatchedItemQueryService::DIR_NEWER ],
-                               [ '20151212010101', 123 ],
-                               [],
-                               [],
-                               [
-                                       "(rc_timestamp > '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id >= 123))"
-                               ],
-                               [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
-                               [],
-                       ],
-                       [
-                               [ 'dir' => WatchedItemQueryService::DIR_OLDER ],
-                               [ '20151212010101', "123; DROP TABLE watchlist;\n--" ],
-                               [],
-                               [],
-                               [
-                                       "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
-                               ],
-                               [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
-                               [],
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider getWatchedItemsWithRecentChangeInfoOptionsProvider
-        */
-       public function testGetWatchedItemsWithRecentChangeInfo_optionsAndEmptyResult(
-               array $options,
-               $startFrom,
-               array $expectedExtraTables,
-               array $expectedExtraFields,
-               array $expectedExtraConds,
-               array $expectedDbOptions,
-               array $expectedExtraJoinConds,
-               array $globals = []
-       ) {
-               // Sigh. This test class doesn't extend MediaWikiTestCase, so we have to reinvent setMwGlobals().
-               if ( $globals ) {
-                       $resetGlobals = [];
-                       foreach ( $globals as $k => $v ) {
-                               $resetGlobals[$k] = $GLOBALS[$k];
-                               $GLOBALS[$k] = $v;
-                       }
-                       $reset = new ScopedCallback( function () use ( $resetGlobals ) {
-                               foreach ( $resetGlobals as $k => $v ) {
-                                       $GLOBALS[$k] = $v;
-                               }
-                       } );
-               }
-
-               $expectedTables = array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables );
-               $expectedFields = array_merge(
-                       [
-                               'rc_id',
-                               'rc_namespace',
-                               'rc_title',
-                               'rc_timestamp',
-                               'rc_type',
-                               'rc_deleted',
-                               'wl_notificationtimestamp',
-
-                               'rc_cur_id',
-                               'rc_this_oldid',
-                               'rc_last_oldid',
-                       ],
-                       $expectedExtraFields
-               );
-               $expectedConds = array_merge(
-                       [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)', ],
-                       $expectedExtraConds
-               );
-               $expectedJoinConds = array_merge(
-                       [
-                               'watchlist' => [
-                                       'INNER JOIN',
-                                       [
-                                               'wl_namespace=rc_namespace',
-                                               'wl_title=rc_title'
-                                       ]
-                               ],
-                               'page' => [
-                                       'LEFT JOIN',
-                                       'rc_cur_id=page_id',
-                               ],
-                       ],
-                       $expectedExtraJoinConds
-               );
-
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'select' )
-                       ->with(
-                               $expectedTables,
-                               $expectedFields,
-                               $expectedConds,
-                               $this->isType( 'string' ),
-                               $expectedDbOptions,
-                               $expectedJoinConds
-                       )
-                       ->will( $this->returnValue( [] ) );
-
-               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
-               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
-
-               $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
-
-               $this->assertEmpty( $items );
-               $this->assertNull( $startFrom );
-       }
-
-       public function filterPatrolledOptionProvider() {
-               return [
-                       [ WatchedItemQueryService::FILTER_PATROLLED ],
-                       [ WatchedItemQueryService::FILTER_NOT_PATROLLED ],
-               ];
-       }
-
-       /**
-        * @dataProvider filterPatrolledOptionProvider
-        */
-       public function testGetWatchedItemsWithRecentChangeInfo_filterPatrolledAndUserWithNoPatrolRights(
-               $filtersOption
-       ) {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'select' )
-                       ->with(
-                               [ 'recentchanges', 'watchlist', 'page' ],
-                               $this->isType( 'array' ),
-                               [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
-                               $this->isType( 'string' ),
-                               $this->isType( 'array' ),
-                               $this->isType( 'array' )
-                       )
-                       ->will( $this->returnValue( [] ) );
-
-               $user = $this->getMockNonAnonUserWithIdAndNoPatrolRights( 1 );
-
-               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
-               $items = $queryService->getWatchedItemsWithRecentChangeInfo(
-                       $user,
-                       [ 'filters' => [ $filtersOption ] ]
-               );
-
-               $this->assertEmpty( $items );
-       }
-
-       public function mysqlIndexOptimizationProvider() {
-               return [
-                       [
-                               'mysql',
-                               [],
-                               [ "rc_timestamp > ''" ],
-                       ],
-                       [
-                               'mysql',
-                               [ 'start' => '20151212010101', 'dir' => WatchedItemQueryService::DIR_OLDER ],
-                               [ "rc_timestamp <= '20151212010101'" ],
-                       ],
-                       [
-                               'mysql',
-                               [ 'end' => '20151212010101', 'dir' => WatchedItemQueryService::DIR_OLDER ],
-                               [ "rc_timestamp >= '20151212010101'" ],
-                       ],
-                       [
-                               'postgres',
-                               [],
-                               [],
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider mysqlIndexOptimizationProvider
-        */
-       public function testGetWatchedItemsWithRecentChangeInfo_mysqlIndexOptimization(
-               $dbType,
-               array $options,
-               array $expectedExtraConds
-       ) {
-               $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
-               $conds = array_merge( $commonConds, $expectedExtraConds );
-
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'select' )
-                       ->with(
-                               [ 'recentchanges', 'watchlist', 'page' ],
-                               $this->isType( 'array' ),
-                               $conds,
-                               $this->isType( 'string' ),
-                               $this->isType( 'array' ),
-                               $this->isType( 'array' )
-                       )
-                       ->will( $this->returnValue( [] ) );
-               $mockDb->expects( $this->any() )
-                       ->method( 'getType' )
-                       ->will( $this->returnValue( $dbType ) );
-
-               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
-               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
-
-               $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
-
-               $this->assertEmpty( $items );
-       }
-
-       public function userPermissionRelatedExtraChecksProvider() {
-               return [
-                       [
-                               [],
-                               'deletedhistory',
-                               [
-                                       '(rc_type != ' . RC_LOG . ') OR ((rc_deleted & ' . LogPage::DELETED_ACTION . ') != ' .
-                                               LogPage::DELETED_ACTION . ')'
-                               ],
-                       ],
-                       [
-                               [],
-                               'suppressrevision',
-                               [
-                                       '(rc_type != ' . RC_LOG . ') OR (' .
-                                               '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
-                                               ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
-                               ],
-                       ],
-                       [
-                               [],
-                               'viewsuppressed',
-                               [
-                                       '(rc_type != ' . RC_LOG . ') OR (' .
-                                               '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
-                                               ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
-                               ],
-                       ],
-                       [
-                               [ 'onlyByUser' => 'SomeOtherUser' ],
-                               'deletedhistory',
-                               [
-                                       'rc_user_text' => 'SomeOtherUser',
-                                       '(rc_deleted & ' . Revision::DELETED_USER . ') != ' . Revision::DELETED_USER,
-                                       '(rc_type != ' . RC_LOG . ') OR ((rc_deleted & ' . LogPage::DELETED_ACTION . ') != ' .
-                                               LogPage::DELETED_ACTION . ')'
-                               ],
-                       ],
-                       [
-                               [ 'onlyByUser' => 'SomeOtherUser' ],
-                               'suppressrevision',
-                               [
-                                       'rc_user_text' => 'SomeOtherUser',
-                                       '(rc_deleted & ' . ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ) . ') != ' .
-                                               ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ),
-                                       '(rc_type != ' . RC_LOG . ') OR (' .
-                                               '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
-                                               ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
-                               ],
-                       ],
-                       [
-                               [ 'onlyByUser' => 'SomeOtherUser' ],
-                               'viewsuppressed',
-                               [
-                                       'rc_user_text' => 'SomeOtherUser',
-                                       '(rc_deleted & ' . ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ) . ') != ' .
-                                               ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ),
-                                       '(rc_type != ' . RC_LOG . ') OR (' .
-                                               '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
-                                               ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
-                               ],
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider userPermissionRelatedExtraChecksProvider
-        */
-       public function testGetWatchedItemsWithRecentChangeInfo_userPermissionRelatedExtraChecks(
-               array $options,
-               $notAllowedAction,
-               array $expectedExtraConds
-       ) {
-               $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
-               $conds = array_merge( $commonConds, $expectedExtraConds );
-
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'select' )
-                       ->with(
-                               [ 'recentchanges', 'watchlist', 'page' ],
-                               $this->isType( 'array' ),
-                               $conds,
-                               $this->isType( 'string' ),
-                               $this->isType( 'array' ),
-                               $this->isType( 'array' )
-                       )
-                       ->will( $this->returnValue( [] ) );
-
-               $user = $this->getMockNonAnonUserWithIdAndRestrictedPermissions( 1, $notAllowedAction );
-
-               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
-               $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
-
-               $this->assertEmpty( $items );
-       }
-
-       public function testGetWatchedItemsWithRecentChangeInfo_allRevisionsOptionAndEmptyResult() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'select' )
-                       ->with(
-                               [ 'recentchanges', 'watchlist' ],
-                               [
-                                       'rc_id',
-                                       'rc_namespace',
-                                       'rc_title',
-                                       'rc_timestamp',
-                                       'rc_type',
-                                       'rc_deleted',
-                                       'wl_notificationtimestamp',
-
-                                       'rc_cur_id',
-                                       'rc_this_oldid',
-                                       'rc_last_oldid',
-                               ],
-                               [ 'wl_user' => 1, ],
-                               $this->isType( 'string' ),
-                               [],
-                               [
-                                       'watchlist' => [
-                                               'INNER JOIN',
-                                               [
-                                                       'wl_namespace=rc_namespace',
-                                                       'wl_title=rc_title'
-                                               ]
-                                       ],
-                               ]
-                       )
-                       ->will( $this->returnValue( [] ) );
-
-               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
-               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
-
-               $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, [ 'allRevisions' => true ] );
-
-               $this->assertEmpty( $items );
-       }
-
-       public function getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider() {
-               return [
-                       [
-                               [ 'rcTypes' => [ 1337 ] ],
-                               null,
-                               'Bad value for parameter $options[\'rcTypes\']',
-                       ],
-                       [
-                               [ 'rcTypes' => [ 'edit' ] ],
-                               null,
-                               'Bad value for parameter $options[\'rcTypes\']',
-                       ],
-                       [
-                               [ 'rcTypes' => [ RC_EDIT, 1337 ] ],
-                               null,
-                               'Bad value for parameter $options[\'rcTypes\']',
-                       ],
-                       [
-                               [ 'dir' => 'foo' ],
-                               null,
-                               'Bad value for parameter $options[\'dir\']',
-                       ],
-                       [
-                               [ 'start' => '20151212010101' ],
-                               null,
-                               'Bad value for parameter $options[\'dir\']: must be provided',
-                       ],
-                       [
-                               [ 'end' => '20151212010101' ],
-                               null,
-                               'Bad value for parameter $options[\'dir\']: must be provided',
-                       ],
-                       [
-                               [],
-                               [ '20151212010101', 123 ],
-                               'Bad value for parameter $options[\'dir\']: must be provided',
-                       ],
-                       [
-                               [ 'dir' => WatchedItemQueryService::DIR_OLDER ],
-                               '20151212010101',
-                               'Bad value for parameter $startFrom: must be a two-element array',
-                       ],
-                       [
-                               [ 'dir' => WatchedItemQueryService::DIR_OLDER ],
-                               [ '20151212010101' ],
-                               'Bad value for parameter $startFrom: must be a two-element array',
-                       ],
-                       [
-                               [ 'dir' => WatchedItemQueryService::DIR_OLDER ],
-                               [ '20151212010101', 123, 'foo' ],
-                               'Bad value for parameter $startFrom: must be a two-element array',
-                       ],
-                       [
-                               [ 'watchlistOwner' => $this->getMockUnrestrictedNonAnonUserWithId( 2 ) ],
-                               null,
-                               'Bad value for parameter $options[\'watchlistOwnerToken\']',
-                       ],
-                       [
-                               [ 'watchlistOwner' => 'Other User', 'watchlistOwnerToken' => 'some-token' ],
-                               null,
-                               'Bad value for parameter $options[\'watchlistOwner\']',
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider
-        */
-       public function testGetWatchedItemsWithRecentChangeInfo_invalidOptions(
-               array $options,
-               $startFrom,
-               $expectedInExceptionMessage
-       ) {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->never() )
-                       ->method( $this->anything() );
-
-               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
-               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
-
-               $this->setExpectedException( InvalidArgumentException::class, $expectedInExceptionMessage );
-               $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
-       }
-
-       public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorOptionAndEmptyResult() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'select' )
-                       ->with(
-                               [ 'recentchanges', 'watchlist', 'page' ],
-                               [
-                                       'rc_id',
-                                       'rc_namespace',
-                                       'rc_title',
-                                       'rc_timestamp',
-                                       'rc_type',
-                                       'rc_deleted',
-                                       'wl_notificationtimestamp',
-                                       'rc_cur_id',
-                               ],
-                               [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
-                               $this->isType( 'string' ),
-                               [],
-                               [
-                                       'watchlist' => [
-                                               'INNER JOIN',
-                                               [
-                                                       'wl_namespace=rc_namespace',
-                                                       'wl_title=rc_title'
-                                               ]
-                                       ],
-                                       'page' => [
-                                               'LEFT JOIN',
-                                               'rc_cur_id=page_id',
-                                       ],
-                               ]
-                       )
-                       ->will( $this->returnValue( [] ) );
-
-               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
-               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
-
-               $items = $queryService->getWatchedItemsWithRecentChangeInfo(
-                       $user,
-                       [ 'usedInGenerator' => true ]
-               );
-
-               $this->assertEmpty( $items );
-       }
-
-       public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorAllRevisionsOptions() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'select' )
-                       ->with(
-                               [ 'recentchanges', 'watchlist' ],
-                               [
-                                       'rc_id',
-                                       'rc_namespace',
-                                       'rc_title',
-                                       'rc_timestamp',
-                                       'rc_type',
-                                       'rc_deleted',
-                                       'wl_notificationtimestamp',
-                                       'rc_this_oldid',
-                               ],
-                               [ 'wl_user' => 1 ],
-                               $this->isType( 'string' ),
-                               [],
-                               [
-                                       'watchlist' => [
-                                               'INNER JOIN',
-                                               [
-                                                       'wl_namespace=rc_namespace',
-                                                       'wl_title=rc_title'
-                                               ]
-                                       ],
-                               ]
-                       )
-                       ->will( $this->returnValue( [] ) );
-
-               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
-               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
-
-               $items = $queryService->getWatchedItemsWithRecentChangeInfo(
-                       $user,
-                       [ 'usedInGenerator' => true, 'allRevisions' => true, ]
-               );
-
-               $this->assertEmpty( $items );
-       }
-
-       public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerOptionAndEmptyResult() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'select' )
-                       ->with(
-                               $this->isType( 'array' ),
-                               $this->isType( 'array' ),
-                               [
-                                       'wl_user' => 2,
-                                       '(rc_this_oldid=page_latest) OR (rc_type=3)',
-                               ],
-                               $this->isType( 'string' ),
-                               $this->isType( 'array' ),
-                               $this->isType( 'array' )
-                       )
-                       ->will( $this->returnValue( [] ) );
-
-               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
-               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
-               $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
-               $otherUser->expects( $this->once() )
-                       ->method( 'getOption' )
-                       ->with( 'watchlisttoken' )
-                       ->willReturn( '0123456789abcdef' );
-
-               $items = $queryService->getWatchedItemsWithRecentChangeInfo(
-                       $user,
-                       [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => '0123456789abcdef' ]
-               );
-
-               $this->assertEmpty( $items );
-       }
-
-       public function invalidWatchlistTokenProvider() {
-               return [
-                       [ 'wrongToken' ],
-                       [ '' ],
-               ];
-       }
-
-       /**
-        * @dataProvider invalidWatchlistTokenProvider
-        */
-       public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerAndInvalidToken( $token ) {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->never() )
-                       ->method( $this->anything() );
-
-               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
-               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
-               $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
-               $otherUser->expects( $this->once() )
-                       ->method( 'getOption' )
-                       ->with( 'watchlisttoken' )
-                       ->willReturn( '0123456789abcdef' );
-
-               $this->setExpectedException( ApiUsageException::class, 'Incorrect watchlist token provided' );
-               $queryService->getWatchedItemsWithRecentChangeInfo(
-                       $user,
-                       [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => $token ]
-               );
-       }
-
-       public function testGetWatchedItemsForUser() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'select' )
-                       ->with(
-                               'watchlist',
-                               [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
-                               [ 'wl_user' => 1 ]
-                       )
-                       ->will( $this->returnValue( [
-                               $this->getFakeRow( [
-                                       'wl_namespace' => 0,
-                                       'wl_title' => 'Foo1',
-                                       'wl_notificationtimestamp' => '20151212010101',
-                               ] ),
-                               $this->getFakeRow( [
-                                       'wl_namespace' => 1,
-                                       'wl_title' => 'Foo2',
-                                       'wl_notificationtimestamp' => null,
-                               ] ),
-                       ] ) );
-
-               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
-               $user = $this->getMockNonAnonUserWithId( 1 );
-
-               $items = $queryService->getWatchedItemsForUser( $user );
-
-               $this->assertInternalType( 'array', $items );
-               $this->assertCount( 2, $items );
-               $this->assertContainsOnlyInstancesOf( WatchedItem::class, $items );
-               $this->assertEquals(
-                       new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
-                       $items[0]
-               );
-               $this->assertEquals(
-                       new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
-                       $items[1]
-               );
-       }
-
-       public function provideGetWatchedItemsForUserOptions() {
-               return [
-                       [
-                               [ 'namespaceIds' => [ 0, 1 ], ],
-                               [ 'wl_namespace' => [ 0, 1 ], ],
-                               []
-                       ],
-                       [
-                               [ 'sort' => WatchedItemQueryService::SORT_ASC, ],
-                               [],
-                               [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
-                       ],
-                       [
-                               [
-                                       'namespaceIds' => [ 0 ],
-                                       'sort' => WatchedItemQueryService::SORT_ASC,
-                               ],
-                               [ 'wl_namespace' => [ 0 ], ],
-                               [ 'ORDER BY' => 'wl_title ASC' ]
-                       ],
-                       [
-                               [ 'limit' => 10 ],
-                               [],
-                               [ 'LIMIT' => 10 ]
-                       ],
-                       [
-                               [
-                                       'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ],
-                                       'limit' => "10; DROP TABLE watchlist;\n--",
-                               ],
-                               [ 'wl_namespace' => [ 0, 1 ], ],
-                               [ 'LIMIT' => 10 ]
-                       ],
-                       [
-                               [ 'filter' => WatchedItemQueryService::FILTER_CHANGED ],
-                               [ 'wl_notificationtimestamp IS NOT NULL' ],
-                               []
-                       ],
-                       [
-                               [ 'filter' => WatchedItemQueryService::FILTER_NOT_CHANGED ],
-                               [ 'wl_notificationtimestamp IS NULL' ],
-                               []
-                       ],
-                       [
-                               [ 'sort' => WatchedItemQueryService::SORT_DESC, ],
-                               [],
-                               [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
-                       ],
-                       [
-                               [
-                                       'namespaceIds' => [ 0 ],
-                                       'sort' => WatchedItemQueryService::SORT_DESC,
-                               ],
-                               [ 'wl_namespace' => [ 0 ], ],
-                               [ 'ORDER BY' => 'wl_title DESC' ]
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideGetWatchedItemsForUserOptions
-        */
-       public function testGetWatchedItemsForUser_optionsAndEmptyResult(
-               array $options,
-               array $expectedConds,
-               array $expectedDbOptions
-       ) {
-               $mockDb = $this->getMockDb();
-               $user = $this->getMockNonAnonUserWithId( 1 );
-
-               $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
-               $mockDb->expects( $this->once() )
-                       ->method( 'select' )
-                       ->with(
-                               'watchlist',
-                               [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
-                               $expectedConds,
-                               $this->isType( 'string' ),
-                               $expectedDbOptions
-                       )
-                       ->will( $this->returnValue( [] ) );
-
-               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
-
-               $items = $queryService->getWatchedItemsForUser( $user, $options );
-               $this->assertEmpty( $items );
-       }
-
-       public function provideGetWatchedItemsForUser_fromUntilStartFromOptions() {
-               return [
-                       [
-                               [
-                                       'from' => new TitleValue( 0, 'SomeDbKey' ),
-                                       'sort' => WatchedItemQueryService::SORT_ASC
-                               ],
-                               [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
-                               [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
-                       ],
-                       [
-                               [
-                                       'from' => new TitleValue( 0, 'SomeDbKey' ),
-                                       'sort' => WatchedItemQueryService::SORT_DESC,
-                               ],
-                               [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
-                               [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
-                       ],
-                       [
-                               [
-                                       'until' => new TitleValue( 0, 'SomeDbKey' ),
-                                       'sort' => WatchedItemQueryService::SORT_ASC
-                               ],
-                               [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
-                               [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
-                       ],
-                       [
-                               [
-                                       'until' => new TitleValue( 0, 'SomeDbKey' ),
-                                       'sort' => WatchedItemQueryService::SORT_DESC
-                               ],
-                               [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
-                               [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
-                       ],
-                       [
-                               [
-                                       'from' => new TitleValue( 0, 'AnotherDbKey' ),
-                                       'until' => new TitleValue( 0, 'SomeOtherDbKey' ),
-                                       'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
-                                       'sort' => WatchedItemQueryService::SORT_ASC
-                               ],
-                               [
-                                       "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
-                                       "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
-                                       "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))",
-                               ],
-                               [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
-                       ],
-                       [
-                               [
-                                       'from' => new TitleValue( 0, 'SomeOtherDbKey' ),
-                                       'until' => new TitleValue( 0, 'AnotherDbKey' ),
-                                       'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
-                                       'sort' => WatchedItemQueryService::SORT_DESC
-                               ],
-                               [
-                                       "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
-                                       "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
-                                       "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))",
-                               ],
-                               [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideGetWatchedItemsForUser_fromUntilStartFromOptions
-        */
-       public function testGetWatchedItemsForUser_fromUntilStartFromOptions(
-               array $options,
-               array $expectedConds,
-               array $expectedDbOptions
-       ) {
-               $user = $this->getMockNonAnonUserWithId( 1 );
-
-               $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
-
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->any() )
-                       ->method( 'addQuotes' )
-                       ->will( $this->returnCallback( function ( $value ) {
-                               return "'$value'";
-                       } ) );
-               $mockDb->expects( $this->any() )
-                       ->method( 'makeList' )
-                       ->with(
-                               $this->isType( 'array' ),
-                               $this->isType( 'int' )
-                       )
-                       ->will( $this->returnCallback( function ( $a, $conj ) {
-                               $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR ';
-                               return join( $sqlConj, array_map( function ( $s ) {
-                                       return '(' . $s . ')';
-                               }, $a
-                               ) );
-                       } ) );
-               $mockDb->expects( $this->once() )
-                       ->method( 'select' )
-                       ->with(
-                               'watchlist',
-                               [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
-                               $expectedConds,
-                               $this->isType( 'string' ),
-                               $expectedDbOptions
-                       )
-                       ->will( $this->returnValue( [] ) );
-
-               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
-
-               $items = $queryService->getWatchedItemsForUser( $user, $options );
-               $this->assertEmpty( $items );
-       }
-
-       public function getWatchedItemsForUserInvalidOptionsProvider() {
-               return [
-                       [
-                               [ 'sort' => 'foo' ],
-                               'Bad value for parameter $options[\'sort\']'
-                       ],
-                       [
-                               [ 'filter' => 'foo' ],
-                               'Bad value for parameter $options[\'filter\']'
-                       ],
-                       [
-                               [ 'from' => new TitleValue( 0, 'SomeDbKey' ), ],
-                               'Bad value for parameter $options[\'sort\']: must be provided'
-                       ],
-                       [
-                               [ 'until' => new TitleValue( 0, 'SomeDbKey' ), ],
-                               'Bad value for parameter $options[\'sort\']: must be provided'
-                       ],
-                       [
-                               [ 'startFrom' => new TitleValue( 0, 'SomeDbKey' ), ],
-                               'Bad value for parameter $options[\'sort\']: must be provided'
-                       ],
-               ];
-       }
-
-       /**
-        * @dataProvider getWatchedItemsForUserInvalidOptionsProvider
-        */
-       public function testGetWatchedItemsForUser_invalidOptionThrowsException(
-               array $options,
-               $expectedInExceptionMessage
-       ) {
-               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $this->getMockDb() ) );
-
-               $this->setExpectedException( InvalidArgumentException::class, $expectedInExceptionMessage );
-               $queryService->getWatchedItemsForUser( $this->getMockNonAnonUserWithId( 1 ), $options );
-       }
-
-       public function testGetWatchedItemsForUser_userNotAllowedToViewWatchlist() {
-               $mockDb = $this->getMockDb();
-
-               $mockDb->expects( $this->never() )
-                       ->method( $this->anything() );
-
-               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
-
-               $items = $queryService->getWatchedItemsForUser( $this->getMockAnonUser() );
-               $this->assertEmpty( $items );
-       }
-
-}
diff --git a/tests/phpunit/includes/WatchedItemStoreIntegrationTest.php b/tests/phpunit/includes/WatchedItemStoreIntegrationTest.php
deleted file mode 100644 (file)
index 61b62aa..0000000
+++ /dev/null
@@ -1,214 +0,0 @@
-<?php
-
-use MediaWiki\MediaWikiServices;
-
-/**
- * @author Addshore
- *
- * @group Database
- *
- * @covers WatchedItemStore
- */
-class WatchedItemStoreIntegrationTest extends MediaWikiTestCase {
-
-       public function setUp() {
-               parent::setUp();
-               self::$users['WatchedItemStoreIntegrationTestUser']
-                       = new TestUser( 'WatchedItemStoreIntegrationTestUser' );
-       }
-
-       private function getUser() {
-               return self::$users['WatchedItemStoreIntegrationTestUser']->getUser();
-       }
-
-       public function testWatchAndUnWatchItem() {
-               $user = $this->getUser();
-               $title = Title::newFromText( 'WatchedItemStoreIntegrationTestPage' );
-               $store = MediaWikiServices::getInstance()->getWatchedItemStore();
-               // Cleanup after previous tests
-               $store->removeWatch( $user, $title );
-               $initialWatchers = $store->countWatchers( $title );
-               $initialUserWatchedItems = $store->countWatchedItems( $user );
-
-               $this->assertFalse(
-                       $store->isWatched( $user, $title ),
-                       'Page should not initially be watched'
-               );
-
-               $store->addWatch( $user, $title );
-               $this->assertTrue(
-                       $store->isWatched( $user, $title ),
-                       'Page should be watched'
-               );
-               $this->assertEquals( $initialUserWatchedItems + 1, $store->countWatchedItems( $user ) );
-               $watchedItemsForUser = $store->getWatchedItemsForUser( $user );
-               $this->assertCount( $initialUserWatchedItems + 1, $watchedItemsForUser );
-               $watchedItemsForUserHasExpectedItem = false;
-               foreach ( $watchedItemsForUser as $watchedItem ) {
-                       if (
-                               $watchedItem->getUser()->equals( $user ) &&
-                               $watchedItem->getLinkTarget() == $title->getTitleValue()
-                       ) {
-                               $watchedItemsForUserHasExpectedItem = true;
-                       }
-               }
-               $this->assertTrue(
-                       $watchedItemsForUserHasExpectedItem,
-                       'getWatchedItemsForUser should contain the page'
-               );
-               $this->assertEquals( $initialWatchers + 1, $store->countWatchers( $title ) );
-               $this->assertEquals(
-                       $initialWatchers + 1,
-                       $store->countWatchersMultiple( [ $title ] )[$title->getNamespace()][$title->getDBkey()]
-               );
-               $this->assertEquals(
-                       [ 0 => [ 'WatchedItemStoreIntegrationTestPage' => $initialWatchers + 1 ] ],
-                       $store->countWatchersMultiple( [ $title ], [ 'minimumWatchers' => $initialWatchers + 1 ] )
-               );
-               $this->assertEquals(
-                       [ 0 => [ 'WatchedItemStoreIntegrationTestPage' => 0 ] ],
-                       $store->countWatchersMultiple( [ $title ], [ 'minimumWatchers' => $initialWatchers + 2 ] )
-               );
-               $this->assertEquals(
-                       [ $title->getNamespace() => [ $title->getDBkey() => null ] ],
-                       $store->getNotificationTimestampsBatch( $user, [ $title ] )
-               );
-
-               $store->removeWatch( $user, $title );
-               $this->assertFalse(
-                       $store->isWatched( $user, $title ),
-                       'Page should be unwatched'
-               );
-               $this->assertEquals( $initialUserWatchedItems, $store->countWatchedItems( $user ) );
-               $watchedItemsForUser = $store->getWatchedItemsForUser( $user );
-               $this->assertCount( $initialUserWatchedItems, $watchedItemsForUser );
-               $watchedItemsForUserHasExpectedItem = false;
-               foreach ( $watchedItemsForUser as $watchedItem ) {
-                       if (
-                               $watchedItem->getUser()->equals( $user ) &&
-                               $watchedItem->getLinkTarget() == $title->getTitleValue()
-                       ) {
-                               $watchedItemsForUserHasExpectedItem = true;
-                       }
-               }
-               $this->assertFalse(
-                       $watchedItemsForUserHasExpectedItem,
-                       'getWatchedItemsForUser should not contain the page'
-               );
-               $this->assertEquals( $initialWatchers, $store->countWatchers( $title ) );
-               $this->assertEquals(
-                       $initialWatchers,
-                       $store->countWatchersMultiple( [ $title ] )[$title->getNamespace()][$title->getDBkey()]
-               );
-               $this->assertEquals(
-                       [ $title->getNamespace() => [ $title->getDBkey() => false ] ],
-                       $store->getNotificationTimestampsBatch( $user, [ $title ] )
-               );
-       }
-
-       public function testUpdateResetAndSetNotificationTimestamp() {
-               $user = $this->getUser();
-               $otherUser = ( new TestUser( 'WatchedItemStoreIntegrationTestUser_otherUser' ) )->getUser();
-               $title = Title::newFromText( 'WatchedItemStoreIntegrationTestPage' );
-               $store = MediaWikiServices::getInstance()->getWatchedItemStore();
-               $store->addWatch( $user, $title );
-               $this->assertNull( $store->loadWatchedItem( $user, $title )->getNotificationTimestamp() );
-               $initialVisitingWatchers = $store->countVisitingWatchers( $title, '20150202020202' );
-               $initialUnreadNotifications = $store->countUnreadNotifications( $user );
-
-               $store->updateNotificationTimestamp( $otherUser, $title, '20150202010101' );
-               $this->assertEquals(
-                       '20150202010101',
-                       $store->loadWatchedItem( $user, $title )->getNotificationTimestamp()
-               );
-               $this->assertEquals(
-                       [ $title->getNamespace() => [ $title->getDBkey() => '20150202010101' ] ],
-                       $store->getNotificationTimestampsBatch( $user, [ $title ] )
-               );
-               $this->assertEquals(
-                       $initialVisitingWatchers - 1,
-                       $store->countVisitingWatchers( $title, '20150202020202' )
-               );
-               $this->assertEquals(
-                       $initialVisitingWatchers - 1,
-                       $store->countVisitingWatchersMultiple(
-                               [ [ $title, '20150202020202' ] ]
-                       )[$title->getNamespace()][$title->getDBkey()]
-               );
-               $this->assertEquals(
-                       $initialUnreadNotifications + 1,
-                       $store->countUnreadNotifications( $user )
-               );
-               $this->assertSame(
-                       true,
-                       $store->countUnreadNotifications( $user, $initialUnreadNotifications + 1 )
-               );
-
-               $this->assertTrue( $store->resetNotificationTimestamp( $user, $title ) );
-               $this->assertNull( $store->getWatchedItem( $user, $title )->getNotificationTimestamp() );
-               $this->assertEquals(
-                       [ $title->getNamespace() => [ $title->getDBkey() => null ] ],
-                       $store->getNotificationTimestampsBatch( $user, [ $title ] )
-               );
-               $this->assertEquals(
-                       $initialVisitingWatchers,
-                       $store->countVisitingWatchers( $title, '20150202020202' )
-               );
-               $this->assertEquals(
-                       $initialVisitingWatchers,
-                       $store->countVisitingWatchersMultiple(
-                               [ [ $title, '20150202020202' ] ]
-                       )[$title->getNamespace()][$title->getDBkey()]
-               );
-               $this->assertEquals(
-                       [ 0 => [ 'WatchedItemStoreIntegrationTestPage' => $initialVisitingWatchers ] ],
-                       $store->countVisitingWatchersMultiple(
-                               [ [ $title, '20150202020202' ] ], $initialVisitingWatchers
-                       )
-               );
-               $this->assertEquals(
-                       [ 0 => [ 'WatchedItemStoreIntegrationTestPage' => 0 ] ],
-                       $store->countVisitingWatchersMultiple(
-                               [ [ $title, '20150202020202' ] ], $initialVisitingWatchers + 1
-                       )
-               );
-
-               // setNotificationTimestampsForUser specifying a title
-               $this->assertTrue(
-                       $store->setNotificationTimestampsForUser( $user, '20200202020202', [ $title ] )
-               );
-               $this->assertEquals(
-                       '20200202020202',
-                       $store->getWatchedItem( $user, $title )->getNotificationTimestamp()
-               );
-
-               // setNotificationTimestampsForUser not specifying a title
-               $this->assertTrue(
-                       $store->setNotificationTimestampsForUser( $user, '20210202020202' )
-               );
-               $this->assertEquals(
-                       '20210202020202',
-                       $store->getWatchedItem( $user, $title )->getNotificationTimestamp()
-               );
-       }
-
-       public function testDuplicateAllAssociatedEntries() {
-               $user = $this->getUser();
-               $titleOld = Title::newFromText( 'WatchedItemStoreIntegrationTestPageOld' );
-               $titleNew = Title::newFromText( 'WatchedItemStoreIntegrationTestPageNew' );
-               $store = MediaWikiServices::getInstance()->getWatchedItemStore();
-               $store->addWatch( $user, $titleOld->getSubjectPage() );
-               $store->addWatch( $user, $titleOld->getTalkPage() );
-               // Cleanup after previous tests
-               $store->removeWatch( $user, $titleNew->getSubjectPage() );
-               $store->removeWatch( $user, $titleNew->getTalkPage() );
-
-               $store->duplicateAllAssociatedEntries( $titleOld, $titleNew );
-
-               $this->assertTrue( $store->isWatched( $user, $titleOld->getSubjectPage() ) );
-               $this->assertTrue( $store->isWatched( $user, $titleOld->getTalkPage() ) );
-               $this->assertTrue( $store->isWatched( $user, $titleNew->getSubjectPage() ) );
-               $this->assertTrue( $store->isWatched( $user, $titleNew->getTalkPage() ) );
-       }
-
-}
diff --git a/tests/phpunit/includes/WatchedItemStoreUnitTest.php b/tests/phpunit/includes/WatchedItemStoreUnitTest.php
deleted file mode 100644 (file)
index 950e220..0000000
+++ /dev/null
@@ -1,2674 +0,0 @@
-<?php
-use MediaWiki\Linker\LinkTarget;
-use Wikimedia\ScopedCallback;
-
-/**
- * @author Addshore
- *
- * @covers WatchedItemStore
- */
-class WatchedItemStoreUnitTest extends MediaWikiTestCase {
-
-       /**
-        * @return PHPUnit_Framework_MockObject_MockObject|IDatabase
-        */
-       private function getMockDb() {
-               return $this->createMock( IDatabase::class );
-       }
-
-       /**
-        * @return PHPUnit_Framework_MockObject_MockObject|LoadBalancer
-        */
-       private function getMockLoadBalancer(
-               $mockDb,
-               $expectedConnectionType = null
-       ) {
-               $mock = $this->getMockBuilder( LoadBalancer::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               if ( $expectedConnectionType !== null ) {
-                       $mock->expects( $this->any() )
-                               ->method( 'getConnectionRef' )
-                               ->with( $expectedConnectionType )
-                               ->will( $this->returnValue( $mockDb ) );
-               } else {
-                       $mock->expects( $this->any() )
-                               ->method( 'getConnectionRef' )
-                               ->will( $this->returnValue( $mockDb ) );
-               }
-               return $mock;
-       }
-
-       /**
-        * @return PHPUnit_Framework_MockObject_MockObject|HashBagOStuff
-        */
-       private function getMockCache() {
-               $mock = $this->getMockBuilder( HashBagOStuff::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               $mock->expects( $this->any() )
-                       ->method( 'makeKey' )
-                       ->will( $this->returnCallback( function () {
-                               return implode( ':', func_get_args() );
-                       } ) );
-               return $mock;
-       }
-
-       /**
-        * @return PHPUnit_Framework_MockObject_MockObject|ReadOnlyMode
-        */
-       private function getMockReadOnlyMode( $readOnly = false ) {
-               $mock = $this->getMockBuilder( ReadOnlyMode::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-               $mock->expects( $this->any() )
-                       ->method( 'isReadOnly' )
-                       ->will( $this->returnValue( $readOnly ) );
-               return $mock;
-       }
-
-       /**
-        * @param int $id
-        * @return PHPUnit_Framework_MockObject_MockObject|User
-        */
-       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 ) );
-               return $mock;
-       }
-
-       /**
-        * @return User
-        */
-       private function getAnonUser() {
-               return User::newFromName( 'Anon_User' );
-       }
-
-       private function getFakeRow( array $rowValues ) {
-               $fakeRow = new stdClass();
-               foreach ( $rowValues as $valueName => $value ) {
-                       $fakeRow->$valueName = $value;
-               }
-               return $fakeRow;
-       }
-
-       private function newWatchedItemStore( LoadBalancer $loadBalancer, HashBagOStuff $cache,
-               ReadOnlyMode $readOnlyMode
-       ) {
-               return new WatchedItemStore(
-                       $loadBalancer,
-                       $cache,
-                       $readOnlyMode
-               );
-       }
-
-       public function testCountWatchedItems() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
-
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->exactly( 1 ) )
-                       ->method( 'selectField' )
-                       ->with(
-                               'watchlist',
-                               'COUNT(*)',
-                               [
-                                       'wl_user' => $user->getId(),
-                               ],
-                               $this->isType( 'string' )
-                       )
-                       ->will( $this->returnValue( 12 ) );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->never() )->method( 'set' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertEquals( 12, $store->countWatchedItems( $user ) );
-       }
-
-       public function testCountWatchers() {
-               $titleValue = new TitleValue( 0, 'SomeDbKey' );
-
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->exactly( 1 ) )
-                       ->method( 'selectField' )
-                       ->with(
-                               'watchlist',
-                               'COUNT(*)',
-                               [
-                                       'wl_namespace' => $titleValue->getNamespace(),
-                                       'wl_title' => $titleValue->getDBkey(),
-                               ],
-                               $this->isType( 'string' )
-                       )
-                       ->will( $this->returnValue( 7 ) );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->never() )->method( 'set' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertEquals( 7, $store->countWatchers( $titleValue ) );
-       }
-
-       public function testCountWatchersMultiple() {
-               $titleValues = [
-                       new TitleValue( 0, 'SomeDbKey' ),
-                       new TitleValue( 0, 'OtherDbKey' ),
-                       new TitleValue( 1, 'AnotherDbKey' ),
-               ];
-
-               $mockDb = $this->getMockDb();
-
-               $dbResult = [
-                       $this->getFakeRow( [ 'wl_title' => 'SomeDbKey', 'wl_namespace' => 0, 'watchers' => 100 ] ),
-                       $this->getFakeRow( [ 'wl_title' => 'OtherDbKey', 'wl_namespace' => 0, 'watchers' => 300 ] ),
-                       $this->getFakeRow( [ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => 1, 'watchers' => 500 ]
-                       ),
-               ];
-               $mockDb->expects( $this->once() )
-                       ->method( 'makeWhereFrom2d' )
-                       ->with(
-                               [ [ 'SomeDbKey' => 1, 'OtherDbKey' => 1 ], [ 'AnotherDbKey' => 1 ] ],
-                               $this->isType( 'string' ),
-                               $this->isType( 'string' )
-                               )
-                       ->will( $this->returnValue( 'makeWhereFrom2d return value' ) );
-               $mockDb->expects( $this->once() )
-                       ->method( 'select' )
-                       ->with(
-                               'watchlist',
-                               [ 'wl_title', 'wl_namespace', 'watchers' => 'COUNT(*)' ],
-                               [ 'makeWhereFrom2d return value' ],
-                               $this->isType( 'string' ),
-                               [
-                                       'GROUP BY' => [ 'wl_namespace', 'wl_title' ],
-                               ]
-                       )
-                       ->will(
-                               $this->returnValue( $dbResult )
-                       );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->never() )->method( 'set' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $expected = [
-                       0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ],
-                       1 => [ 'AnotherDbKey' => 500 ],
-               ];
-               $this->assertEquals( $expected, $store->countWatchersMultiple( $titleValues ) );
-       }
-
-       public function provideIntWithDbUnsafeVersion() {
-               return [
-                       [ 50 ],
-                       [ "50; DROP TABLE watchlist;\n--" ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideIntWithDbUnsafeVersion
-        */
-       public function testCountWatchersMultiple_withMinimumWatchers( $minWatchers ) {
-               $titleValues = [
-                       new TitleValue( 0, 'SomeDbKey' ),
-                       new TitleValue( 0, 'OtherDbKey' ),
-                       new TitleValue( 1, 'AnotherDbKey' ),
-               ];
-
-               $mockDb = $this->getMockDb();
-
-               $dbResult = [
-                       $this->getFakeRow( [ 'wl_title' => 'SomeDbKey', 'wl_namespace' => 0, 'watchers' => 100 ] ),
-                       $this->getFakeRow( [ 'wl_title' => 'OtherDbKey', 'wl_namespace' => 0, 'watchers' => 300 ] ),
-                       $this->getFakeRow( [ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => 1, 'watchers' => 500 ]
-                       ),
-               ];
-               $mockDb->expects( $this->once() )
-                       ->method( 'makeWhereFrom2d' )
-                       ->with(
-                               [ [ 'SomeDbKey' => 1, 'OtherDbKey' => 1 ], [ 'AnotherDbKey' => 1 ] ],
-                               $this->isType( 'string' ),
-                               $this->isType( 'string' )
-                       )
-                       ->will( $this->returnValue( 'makeWhereFrom2d return value' ) );
-               $mockDb->expects( $this->once() )
-                       ->method( 'select' )
-                       ->with(
-                               'watchlist',
-                               [ 'wl_title', 'wl_namespace', 'watchers' => 'COUNT(*)' ],
-                               [ 'makeWhereFrom2d return value' ],
-                               $this->isType( 'string' ),
-                               [
-                                       'GROUP BY' => [ 'wl_namespace', 'wl_title' ],
-                                       'HAVING' => 'COUNT(*) >= 50',
-                               ]
-                       )
-                       ->will(
-                               $this->returnValue( $dbResult )
-                       );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->never() )->method( 'set' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $expected = [
-                       0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ],
-                       1 => [ 'AnotherDbKey' => 500 ],
-               ];
-               $this->assertEquals(
-                       $expected,
-                       $store->countWatchersMultiple( $titleValues, [ 'minimumWatchers' => $minWatchers ] )
-               );
-       }
-
-       public function testCountVisitingWatchers() {
-               $titleValue = new TitleValue( 0, 'SomeDbKey' );
-
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->exactly( 1 ) )
-                       ->method( 'selectField' )
-                       ->with(
-                               'watchlist',
-                               'COUNT(*)',
-                               [
-                                       'wl_namespace' => $titleValue->getNamespace(),
-                                       'wl_title' => $titleValue->getDBkey(),
-                                       'wl_notificationtimestamp >= \'TS111TS\' OR wl_notificationtimestamp IS NULL',
-                               ],
-                               $this->isType( 'string' )
-                       )
-                       ->will( $this->returnValue( 7 ) );
-               $mockDb->expects( $this->exactly( 1 ) )
-                       ->method( 'addQuotes' )
-                       ->will( $this->returnCallback( function ( $value ) {
-                               return "'$value'";
-                       } ) );
-               $mockDb->expects( $this->exactly( 1 ) )
-                       ->method( 'timestamp' )
-                       ->will( $this->returnCallback( function ( $value ) {
-                               return 'TS' . $value . 'TS';
-                       } ) );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'set' );
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertEquals( 7, $store->countVisitingWatchers( $titleValue, '111' ) );
-       }
-
-       public function testCountVisitingWatchersMultiple() {
-               $titleValuesWithThresholds = [
-                       [ new TitleValue( 0, 'SomeDbKey' ), '111' ],
-                       [ new TitleValue( 0, 'OtherDbKey' ), '111' ],
-                       [ new TitleValue( 1, 'AnotherDbKey' ), '123' ],
-               ];
-
-               $dbResult = [
-                       $this->getFakeRow( [ 'wl_title' => 'SomeDbKey', 'wl_namespace' => 0, 'watchers' => 100 ] ),
-                       $this->getFakeRow( [ 'wl_title' => 'OtherDbKey', 'wl_namespace' => 0, 'watchers' => 300 ] ),
-                       $this->getFakeRow( [ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => 1, 'watchers' => 500 ] ),
-               ];
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->exactly( 2 * 3 ) )
-                       ->method( 'addQuotes' )
-                       ->will( $this->returnCallback( function ( $value ) {
-                               return "'$value'";
-                       } ) );
-               $mockDb->expects( $this->exactly( 3 ) )
-                       ->method( 'timestamp' )
-                       ->will( $this->returnCallback( function ( $value ) {
-                               return 'TS' . $value . 'TS';
-                       } ) );
-               $mockDb->expects( $this->any() )
-                       ->method( 'makeList' )
-                       ->with(
-                               $this->isType( 'array' ),
-                               $this->isType( 'int' )
-                       )
-                       ->will( $this->returnCallback( function ( $a, $conj ) {
-                               $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR ';
-                               return join( $sqlConj, array_map( function ( $s ) {
-                                       return '(' . $s . ')';
-                               }, $a
-                               ) );
-                       } ) );
-               $mockDb->expects( $this->never() )
-                       ->method( 'makeWhereFrom2d' );
-
-               $expectedCond =
-                       '((wl_namespace = 0) AND (' .
-                       "(((wl_title = 'SomeDbKey') AND (" .
-                       "(wl_notificationtimestamp >= 'TS111TS') OR (wl_notificationtimestamp IS NULL)" .
-                       ')) OR (' .
-                       "(wl_title = 'OtherDbKey') AND (" .
-                       "(wl_notificationtimestamp >= 'TS111TS') OR (wl_notificationtimestamp IS NULL)" .
-                       '))))' .
-                       ') OR ((wl_namespace = 1) AND (' .
-                       "(((wl_title = 'AnotherDbKey') AND (".
-                       "(wl_notificationtimestamp >= 'TS123TS') OR (wl_notificationtimestamp IS NULL)" .
-                       ')))))';
-               $mockDb->expects( $this->once() )
-                       ->method( 'select' )
-                       ->with(
-                               'watchlist',
-                               [ 'wl_namespace', 'wl_title', 'watchers' => 'COUNT(*)' ],
-                               $expectedCond,
-                               $this->isType( 'string' ),
-                               [
-                                       'GROUP BY' => [ 'wl_namespace', 'wl_title' ],
-                               ]
-                       )
-                       ->will(
-                               $this->returnValue( $dbResult )
-                       );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->never() )->method( 'set' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $expected = [
-                       0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ],
-                       1 => [ 'AnotherDbKey' => 500 ],
-               ];
-               $this->assertEquals(
-                       $expected,
-                       $store->countVisitingWatchersMultiple( $titleValuesWithThresholds )
-               );
-       }
-
-       public function testCountVisitingWatchersMultiple_withMissingTargets() {
-               $titleValuesWithThresholds = [
-                       [ new TitleValue( 0, 'SomeDbKey' ), '111' ],
-                       [ new TitleValue( 0, 'OtherDbKey' ), '111' ],
-                       [ new TitleValue( 1, 'AnotherDbKey' ), '123' ],
-                       [ new TitleValue( 0, 'SomeNotExisitingDbKey' ), null ],
-                       [ new TitleValue( 0, 'OtherNotExisitingDbKey' ), null ],
-               ];
-
-               $dbResult = [
-                       $this->getFakeRow( [ 'wl_title' => 'SomeDbKey', 'wl_namespace' => 0, 'watchers' => 100 ] ),
-                       $this->getFakeRow( [ 'wl_title' => 'OtherDbKey', 'wl_namespace' => 0, 'watchers' => 300 ] ),
-                       $this->getFakeRow( [ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => 1, 'watchers' => 500 ] ),
-                       $this->getFakeRow(
-                               [ 'wl_title' => 'SomeNotExisitingDbKey', 'wl_namespace' => 0, 'watchers' => 100 ]
-                       ),
-                       $this->getFakeRow(
-                               [ 'wl_title' => 'OtherNotExisitingDbKey', 'wl_namespace' => 0, 'watchers' => 200 ]
-                       ),
-               ];
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->exactly( 2 * 3 ) )
-                       ->method( 'addQuotes' )
-                       ->will( $this->returnCallback( function ( $value ) {
-                               return "'$value'";
-                       } ) );
-               $mockDb->expects( $this->exactly( 3 ) )
-                       ->method( 'timestamp' )
-                       ->will( $this->returnCallback( function ( $value ) {
-                               return 'TS' . $value . 'TS';
-                       } ) );
-               $mockDb->expects( $this->any() )
-                       ->method( 'makeList' )
-                       ->with(
-                               $this->isType( 'array' ),
-                               $this->isType( 'int' )
-                       )
-                       ->will( $this->returnCallback( function ( $a, $conj ) {
-                               $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR ';
-                               return join( $sqlConj, array_map( function ( $s ) {
-                                       return '(' . $s . ')';
-                               }, $a
-                               ) );
-                       } ) );
-               $mockDb->expects( $this->once() )
-                       ->method( 'makeWhereFrom2d' )
-                       ->with(
-                               [ [ 'SomeNotExisitingDbKey' => 1, 'OtherNotExisitingDbKey' => 1 ] ],
-                               $this->isType( 'string' ),
-                               $this->isType( 'string' )
-                       )
-                       ->will( $this->returnValue( 'makeWhereFrom2d return value' ) );
-
-               $expectedCond =
-                       '((wl_namespace = 0) AND (' .
-                       "(((wl_title = 'SomeDbKey') AND (" .
-                       "(wl_notificationtimestamp >= 'TS111TS') OR (wl_notificationtimestamp IS NULL)" .
-                       ')) OR (' .
-                       "(wl_title = 'OtherDbKey') AND (" .
-                       "(wl_notificationtimestamp >= 'TS111TS') OR (wl_notificationtimestamp IS NULL)" .
-                       '))))' .
-                       ') OR ((wl_namespace = 1) AND (' .
-                       "(((wl_title = 'AnotherDbKey') AND (".
-                       "(wl_notificationtimestamp >= 'TS123TS') OR (wl_notificationtimestamp IS NULL)" .
-                       '))))' .
-                       ') OR ' .
-                       '(makeWhereFrom2d return value)';
-               $mockDb->expects( $this->once() )
-                       ->method( 'select' )
-                       ->with(
-                               'watchlist',
-                               [ 'wl_namespace', 'wl_title', 'watchers' => 'COUNT(*)' ],
-                               $expectedCond,
-                               $this->isType( 'string' ),
-                               [
-                                       'GROUP BY' => [ 'wl_namespace', 'wl_title' ],
-                               ]
-                       )
-                       ->will(
-                               $this->returnValue( $dbResult )
-                       );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->never() )->method( 'set' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $expected = [
-                       0 => [
-                               'SomeDbKey' => 100, 'OtherDbKey' => 300,
-                               'SomeNotExisitingDbKey' => 100, 'OtherNotExisitingDbKey' => 200
-                       ],
-                       1 => [ 'AnotherDbKey' => 500 ],
-               ];
-               $this->assertEquals(
-                       $expected,
-                       $store->countVisitingWatchersMultiple( $titleValuesWithThresholds )
-               );
-       }
-
-       /**
-        * @dataProvider provideIntWithDbUnsafeVersion
-        */
-       public function testCountVisitingWatchersMultiple_withMinimumWatchers( $minWatchers ) {
-               $titleValuesWithThresholds = [
-                       [ new TitleValue( 0, 'SomeDbKey' ), '111' ],
-                       [ new TitleValue( 0, 'OtherDbKey' ), '111' ],
-                       [ new TitleValue( 1, 'AnotherDbKey' ), '123' ],
-               ];
-
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->any() )
-                       ->method( 'makeList' )
-                       ->will( $this->returnValue( 'makeList return value' ) );
-               $mockDb->expects( $this->once() )
-                       ->method( 'select' )
-                       ->with(
-                               'watchlist',
-                               [ 'wl_namespace', 'wl_title', 'watchers' => 'COUNT(*)' ],
-                               'makeList return value',
-                               $this->isType( 'string' ),
-                               [
-                                       'GROUP BY' => [ 'wl_namespace', 'wl_title' ],
-                                       'HAVING' => 'COUNT(*) >= 50',
-                               ]
-                       )
-                       ->will(
-                               $this->returnValue( [] )
-                       );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->never() )->method( 'set' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $expected = [
-                       0 => [ 'SomeDbKey' => 0, 'OtherDbKey' => 0 ],
-                       1 => [ 'AnotherDbKey' => 0 ],
-               ];
-               $this->assertEquals(
-                       $expected,
-                       $store->countVisitingWatchersMultiple( $titleValuesWithThresholds, $minWatchers )
-               );
-       }
-
-       public function testCountUnreadNotifications() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
-
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->exactly( 1 ) )
-                       ->method( 'selectRowCount' )
-                       ->with(
-                               'watchlist',
-                               '1',
-                               [
-                                       "wl_notificationtimestamp IS NOT NULL",
-                                       'wl_user' => 1,
-                               ],
-                               $this->isType( 'string' )
-                       )
-                       ->will( $this->returnValue( 9 ) );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'set' );
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertEquals( 9, $store->countUnreadNotifications( $user ) );
-       }
-
-       /**
-        * @dataProvider provideIntWithDbUnsafeVersion
-        */
-       public function testCountUnreadNotifications_withUnreadLimit_overLimit( $limit ) {
-               $user = $this->getMockNonAnonUserWithId( 1 );
-
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->exactly( 1 ) )
-                       ->method( 'selectRowCount' )
-                       ->with(
-                               'watchlist',
-                               '1',
-                               [
-                                       "wl_notificationtimestamp IS NOT NULL",
-                                       'wl_user' => 1,
-                               ],
-                               $this->isType( 'string' ),
-                               [ 'LIMIT' => 50 ]
-                       )
-                       ->will( $this->returnValue( 50 ) );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'set' );
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertSame(
-                       true,
-                       $store->countUnreadNotifications( $user, $limit )
-               );
-       }
-
-       /**
-        * @dataProvider provideIntWithDbUnsafeVersion
-        */
-       public function testCountUnreadNotifications_withUnreadLimit_underLimit( $limit ) {
-               $user = $this->getMockNonAnonUserWithId( 1 );
-
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->exactly( 1 ) )
-                       ->method( 'selectRowCount' )
-                       ->with(
-                               'watchlist',
-                               '1',
-                               [
-                                       "wl_notificationtimestamp IS NOT NULL",
-                                       'wl_user' => 1,
-                               ],
-                               $this->isType( 'string' ),
-                               [ 'LIMIT' => 50 ]
-                       )
-                       ->will( $this->returnValue( 9 ) );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'set' );
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertEquals(
-                       9,
-                       $store->countUnreadNotifications( $user, $limit )
-               );
-       }
-
-       public function testDuplicateEntry_nothingToDuplicate() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'select' )
-                       ->with(
-                               'watchlist',
-                               [
-                                       'wl_user',
-                                       'wl_notificationtimestamp',
-                               ],
-                               [
-                                       'wl_namespace' => 0,
-                                       'wl_title' => 'Old_Title',
-                               ],
-                               'WatchedItemStore::duplicateEntry',
-                               [ 'FOR UPDATE' ]
-                       )
-                       ->will( $this->returnValue( new FakeResultWrapper( [] ) ) );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $this->getMockCache(),
-                       $this->getMockReadOnlyMode()
-               );
-
-               $store->duplicateEntry(
-                       Title::newFromText( 'Old_Title' ),
-                       Title::newFromText( 'New_Title' )
-               );
-       }
-
-       public function testDuplicateEntry_somethingToDuplicate() {
-               $fakeRows = [
-                       $this->getFakeRow( [ 'wl_user' => 1, 'wl_notificationtimestamp' => '20151212010101' ] ),
-                       $this->getFakeRow( [ 'wl_user' => 2, 'wl_notificationtimestamp' => null ] ),
-               ];
-
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->at( 0 ) )
-                       ->method( 'select' )
-                       ->with(
-                               'watchlist',
-                               [
-                                       'wl_user',
-                                       'wl_notificationtimestamp',
-                               ],
-                               [
-                                       'wl_namespace' => 0,
-                                       'wl_title' => 'Old_Title',
-                               ]
-                       )
-                       ->will( $this->returnValue( new FakeResultWrapper( $fakeRows ) ) );
-               $mockDb->expects( $this->at( 1 ) )
-                       ->method( 'replace' )
-                       ->with(
-                               'watchlist',
-                               [ [ 'wl_user', 'wl_namespace', 'wl_title' ] ],
-                               [
-                                       [
-                                               'wl_user' => 1,
-                                               'wl_namespace' => 0,
-                                               'wl_title' => 'New_Title',
-                                               'wl_notificationtimestamp' => '20151212010101',
-                                       ],
-                                       [
-                                               'wl_user' => 2,
-                                               'wl_namespace' => 0,
-                                               'wl_title' => 'New_Title',
-                                               'wl_notificationtimestamp' => null,
-                                       ],
-                               ],
-                               $this->isType( 'string' )
-                       );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $store->duplicateEntry(
-                       Title::newFromText( 'Old_Title' ),
-                       Title::newFromText( 'New_Title' )
-               );
-       }
-
-       public function testDuplicateAllAssociatedEntries_nothingToDuplicate() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->at( 0 ) )
-                       ->method( 'select' )
-                       ->with(
-                               'watchlist',
-                               [
-                                       'wl_user',
-                                       'wl_notificationtimestamp',
-                               ],
-                               [
-                                       'wl_namespace' => 0,
-                                       'wl_title' => 'Old_Title',
-                               ]
-                       )
-                       ->will( $this->returnValue( new FakeResultWrapper( [] ) ) );
-               $mockDb->expects( $this->at( 1 ) )
-                       ->method( 'select' )
-                       ->with(
-                               'watchlist',
-                               [
-                                       'wl_user',
-                                       'wl_notificationtimestamp',
-                               ],
-                               [
-                                       'wl_namespace' => 1,
-                                       'wl_title' => 'Old_Title',
-                               ]
-                       )
-                       ->will( $this->returnValue( new FakeResultWrapper( [] ) ) );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $store->duplicateAllAssociatedEntries(
-                       Title::newFromText( 'Old_Title' ),
-                       Title::newFromText( 'New_Title' )
-               );
-       }
-
-       public function provideLinkTargetPairs() {
-               return [
-                       [ Title::newFromText( 'Old_Title' ), Title::newFromText( 'New_Title' ) ],
-                       [ new TitleValue( 0, 'Old_Title' ),  new TitleValue( 0, 'New_Title' ) ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideLinkTargetPairs
-        */
-       public function testDuplicateAllAssociatedEntries_somethingToDuplicate(
-               LinkTarget $oldTarget,
-               LinkTarget $newTarget
-       ) {
-               $fakeRows = [
-                       $this->getFakeRow( [ 'wl_user' => 1, 'wl_notificationtimestamp' => '20151212010101' ] ),
-               ];
-
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->at( 0 ) )
-                       ->method( 'select' )
-                       ->with(
-                               'watchlist',
-                               [
-                                       'wl_user',
-                                       'wl_notificationtimestamp',
-                               ],
-                               [
-                                       'wl_namespace' => $oldTarget->getNamespace(),
-                                       'wl_title' => $oldTarget->getDBkey(),
-                               ]
-                       )
-                       ->will( $this->returnValue( new FakeResultWrapper( $fakeRows ) ) );
-               $mockDb->expects( $this->at( 1 ) )
-                       ->method( 'replace' )
-                       ->with(
-                               'watchlist',
-                               [ [ 'wl_user', 'wl_namespace', 'wl_title' ] ],
-                               [
-                                       [
-                                               'wl_user' => 1,
-                                               'wl_namespace' => $newTarget->getNamespace(),
-                                               'wl_title' => $newTarget->getDBkey(),
-                                               'wl_notificationtimestamp' => '20151212010101',
-                                       ],
-                               ],
-                               $this->isType( 'string' )
-                       );
-               $mockDb->expects( $this->at( 2 ) )
-                       ->method( 'select' )
-                       ->with(
-                               'watchlist',
-                               [
-                                       'wl_user',
-                                       'wl_notificationtimestamp',
-                               ],
-                               [
-                                       'wl_namespace' => $oldTarget->getNamespace() + 1,
-                                       'wl_title' => $oldTarget->getDBkey(),
-                               ]
-                       )
-                       ->will( $this->returnValue( new FakeResultWrapper( $fakeRows ) ) );
-               $mockDb->expects( $this->at( 3 ) )
-                       ->method( 'replace' )
-                       ->with(
-                               'watchlist',
-                               [ [ 'wl_user', 'wl_namespace', 'wl_title' ] ],
-                               [
-                                       [
-                                               'wl_user' => 1,
-                                               'wl_namespace' => $newTarget->getNamespace() + 1,
-                                               'wl_title' => $newTarget->getDBkey(),
-                                               'wl_notificationtimestamp' => '20151212010101',
-                                       ],
-                               ],
-                               $this->isType( 'string' )
-                       );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $store->duplicateAllAssociatedEntries(
-                       $oldTarget,
-                       $newTarget
-               );
-       }
-
-       public function testAddWatch_nonAnonymousUser() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'insert' )
-                       ->with(
-                               'watchlist',
-                               [
-                                       [
-                                               'wl_user' => 1,
-                                               'wl_namespace' => 0,
-                                               'wl_title' => 'Some_Page',
-                                               'wl_notificationtimestamp' => null,
-                                       ]
-                               ]
-                       );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->once() )
-                       ->method( 'delete' )
-                       ->with( '0:Some_Page:1' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $store->addWatch(
-                       $this->getMockNonAnonUserWithId( 1 ),
-                       Title::newFromText( 'Some_Page' )
-               );
-       }
-
-       public function testAddWatch_anonymousUser() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->never() )
-                       ->method( 'insert' );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )
-                       ->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $store->addWatch(
-                       $this->getAnonUser(),
-                       Title::newFromText( 'Some_Page' )
-               );
-       }
-
-       public function testAddWatchBatchForUser_readOnlyDBReturnsFalse() {
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $this->getMockDb() ),
-                       $this->getMockCache(),
-                       $this->getMockReadOnlyMode( true )
-               );
-
-               $this->assertFalse(
-                       $store->addWatchBatchForUser(
-                               $this->getMockNonAnonUserWithId( 1 ),
-                               [ new TitleValue( 0, 'Some_Page' ), new TitleValue( 1, 'Some_Page' ) ]
-                       )
-               );
-       }
-
-       public function testAddWatchBatchForUser_nonAnonymousUser() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'insert' )
-                       ->with(
-                               'watchlist',
-                               [
-                                       [
-                                               'wl_user' => 1,
-                                               'wl_namespace' => 0,
-                                               'wl_title' => 'Some_Page',
-                                               'wl_notificationtimestamp' => null,
-                                       ],
-                                       [
-                                               'wl_user' => 1,
-                                               'wl_namespace' => 1,
-                                               'wl_title' => 'Some_Page',
-                                               'wl_notificationtimestamp' => null,
-                                       ]
-                               ]
-                       );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->exactly( 2 ) )
-                       ->method( 'delete' );
-               $mockCache->expects( $this->at( 1 ) )
-                       ->method( 'delete' )
-                       ->with( '0:Some_Page:1' );
-               $mockCache->expects( $this->at( 3 ) )
-                       ->method( 'delete' )
-                       ->with( '1:Some_Page:1' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $mockUser = $this->getMockNonAnonUserWithId( 1 );
-
-               $this->assertTrue(
-                       $store->addWatchBatchForUser(
-                               $mockUser,
-                               [ new TitleValue( 0, 'Some_Page' ), new TitleValue( 1, 'Some_Page' ) ]
-                       )
-               );
-       }
-
-       public function testAddWatchBatchForUser_anonymousUsersAreSkipped() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->never() )
-                       ->method( 'insert' );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )
-                       ->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertFalse(
-                       $store->addWatchBatchForUser(
-                               $this->getAnonUser(),
-                               [ new TitleValue( 0, 'Other_Page' ) ]
-                       )
-               );
-       }
-
-       public function testAddWatchBatchReturnsTrue_whenGivenEmptyList() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->never() )
-                       ->method( 'insert' );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )
-                       ->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertTrue(
-                       $store->addWatchBatchForUser( $user, [] )
-               );
-       }
-
-       public function testLoadWatchedItem_existingItem() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'selectRow' )
-                       ->with(
-                               'watchlist',
-                               'wl_notificationtimestamp',
-                               [
-                                       'wl_user' => 1,
-                                       'wl_namespace' => 0,
-                                       'wl_title' => 'SomeDbKey',
-                               ]
-                       )
-                       ->will( $this->returnValue(
-                               $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] )
-                       ) );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->once() )
-                       ->method( 'set' )
-                       ->with(
-                               '0:SomeDbKey:1'
-                       );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $watchedItem = $store->loadWatchedItem(
-                       $this->getMockNonAnonUserWithId( 1 ),
-                       new TitleValue( 0, 'SomeDbKey' )
-               );
-               $this->assertInstanceOf( 'WatchedItem', $watchedItem );
-               $this->assertEquals( 1, $watchedItem->getUser()->getId() );
-               $this->assertEquals( 'SomeDbKey', $watchedItem->getLinkTarget()->getDBkey() );
-               $this->assertEquals( 0, $watchedItem->getLinkTarget()->getNamespace() );
-       }
-
-       public function testLoadWatchedItem_noItem() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'selectRow' )
-                       ->with(
-                               'watchlist',
-                               'wl_notificationtimestamp',
-                               [
-                                       'wl_user' => 1,
-                                       'wl_namespace' => 0,
-                                       'wl_title' => 'SomeDbKey',
-                               ]
-                       )
-                       ->will( $this->returnValue( [] ) );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertFalse(
-                       $store->loadWatchedItem(
-                               $this->getMockNonAnonUserWithId( 1 ),
-                               new TitleValue( 0, 'SomeDbKey' )
-                       )
-               );
-       }
-
-       public function testLoadWatchedItem_anonymousUser() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->never() )
-                       ->method( 'selectRow' );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertFalse(
-                       $store->loadWatchedItem(
-                               $this->getAnonUser(),
-                               new TitleValue( 0, 'SomeDbKey' )
-                       )
-               );
-       }
-
-       public function testRemoveWatch_existingItem() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'delete' )
-                       ->with(
-                               'watchlist',
-                               [
-                                       'wl_user' => 1,
-                                       'wl_namespace' => 0,
-                                       'wl_title' => 'SomeDbKey',
-                               ]
-                       );
-               $mockDb->expects( $this->once() )
-                       ->method( 'affectedRows' )
-                       ->will( $this->returnValue( 1 ) );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->once() )
-                       ->method( 'delete' )
-                       ->with( '0:SomeDbKey:1' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertTrue(
-                       $store->removeWatch(
-                               $this->getMockNonAnonUserWithId( 1 ),
-                               new TitleValue( 0, 'SomeDbKey' )
-                       )
-               );
-       }
-
-       public function testRemoveWatch_noItem() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'delete' )
-                       ->with(
-                               'watchlist',
-                               [
-                                       'wl_user' => 1,
-                                       'wl_namespace' => 0,
-                                       'wl_title' => 'SomeDbKey',
-                               ]
-                       );
-               $mockDb->expects( $this->once() )
-                       ->method( 'affectedRows' )
-                       ->will( $this->returnValue( 0 ) );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->once() )
-                       ->method( 'delete' )
-                       ->with( '0:SomeDbKey:1' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertFalse(
-                       $store->removeWatch(
-                               $this->getMockNonAnonUserWithId( 1 ),
-                               new TitleValue( 0, 'SomeDbKey' )
-                       )
-               );
-       }
-
-       public function testRemoveWatch_anonymousUser() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->never() )
-                       ->method( 'delete' );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->never() )
-                       ->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertFalse(
-                       $store->removeWatch(
-                               $this->getAnonUser(),
-                               new TitleValue( 0, 'SomeDbKey' )
-                       )
-               );
-       }
-
-       public function testGetWatchedItem_existingItem() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'selectRow' )
-                       ->with(
-                               'watchlist',
-                               'wl_notificationtimestamp',
-                               [
-                                       'wl_user' => 1,
-                                       'wl_namespace' => 0,
-                                       'wl_title' => 'SomeDbKey',
-                               ]
-                       )
-                       ->will( $this->returnValue(
-                               $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] )
-                       ) );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'delete' );
-               $mockCache->expects( $this->once() )
-                       ->method( 'get' )
-                       ->with(
-                               '0:SomeDbKey:1'
-                       )
-                       ->will( $this->returnValue( null ) );
-               $mockCache->expects( $this->once() )
-                       ->method( 'set' )
-                       ->with(
-                               '0:SomeDbKey:1'
-                       );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $watchedItem = $store->getWatchedItem(
-                       $this->getMockNonAnonUserWithId( 1 ),
-                       new TitleValue( 0, 'SomeDbKey' )
-               );
-               $this->assertInstanceOf( 'WatchedItem', $watchedItem );
-               $this->assertEquals( 1, $watchedItem->getUser()->getId() );
-               $this->assertEquals( 'SomeDbKey', $watchedItem->getLinkTarget()->getDBkey() );
-               $this->assertEquals( 0, $watchedItem->getLinkTarget()->getNamespace() );
-       }
-
-       public function testGetWatchedItem_cachedItem() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->never() )
-                       ->method( 'selectRow' );
-
-               $mockUser = $this->getMockNonAnonUserWithId( 1 );
-               $linkTarget = new TitleValue( 0, 'SomeDbKey' );
-               $cachedItem = new WatchedItem( $mockUser, $linkTarget, '20151212010101' );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'delete' );
-               $mockCache->expects( $this->never() )->method( 'set' );
-               $mockCache->expects( $this->once() )
-                       ->method( 'get' )
-                       ->with(
-                               '0:SomeDbKey:1'
-                       )
-                       ->will( $this->returnValue( $cachedItem ) );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertEquals(
-                       $cachedItem,
-                       $store->getWatchedItem(
-                               $mockUser,
-                               $linkTarget
-                       )
-               );
-       }
-
-       public function testGetWatchedItem_noItem() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'selectRow' )
-                       ->with(
-                               'watchlist',
-                               'wl_notificationtimestamp',
-                               [
-                                       'wl_user' => 1,
-                                       'wl_namespace' => 0,
-                                       'wl_title' => 'SomeDbKey',
-                               ]
-                       )
-                       ->will( $this->returnValue( [] ) );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'set' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-               $mockCache->expects( $this->once() )
-                       ->method( 'get' )
-                       ->with( '0:SomeDbKey:1' )
-                       ->will( $this->returnValue( false ) );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertFalse(
-                       $store->getWatchedItem(
-                               $this->getMockNonAnonUserWithId( 1 ),
-                               new TitleValue( 0, 'SomeDbKey' )
-                       )
-               );
-       }
-
-       public function testGetWatchedItem_anonymousUser() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->never() )
-                       ->method( 'selectRow' );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'set' );
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertFalse(
-                       $store->getWatchedItem(
-                               $this->getAnonUser(),
-                               new TitleValue( 0, 'SomeDbKey' )
-                       )
-               );
-       }
-
-       public function testGetWatchedItemsForUser() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'select' )
-                       ->with(
-                               'watchlist',
-                               [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
-                               [ 'wl_user' => 1 ]
-                       )
-                       ->will( $this->returnValue( [
-                               $this->getFakeRow( [
-                                       'wl_namespace' => 0,
-                                       'wl_title' => 'Foo1',
-                                       'wl_notificationtimestamp' => '20151212010101',
-                               ] ),
-                               $this->getFakeRow( [
-                                       'wl_namespace' => 1,
-                                       'wl_title' => 'Foo2',
-                                       'wl_notificationtimestamp' => null,
-                               ] ),
-                       ] ) );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'delete' );
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->never() )->method( 'set' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-               $user = $this->getMockNonAnonUserWithId( 1 );
-
-               $watchedItems = $store->getWatchedItemsForUser( $user );
-
-               $this->assertInternalType( 'array', $watchedItems );
-               $this->assertCount( 2, $watchedItems );
-               foreach ( $watchedItems as $watchedItem ) {
-                       $this->assertInstanceOf( 'WatchedItem', $watchedItem );
-               }
-               $this->assertEquals(
-                       new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
-                       $watchedItems[0]
-               );
-               $this->assertEquals(
-                       new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
-                       $watchedItems[1]
-               );
-       }
-
-       public function provideDbTypes() {
-               return [
-                       [ false, DB_REPLICA ],
-                       [ true, DB_MASTER ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideDbTypes
-        */
-       public function testGetWatchedItemsForUser_optionsAndEmptyResult( $forWrite, $dbType ) {
-               $mockDb = $this->getMockDb();
-               $mockCache = $this->getMockCache();
-               $mockLoadBalancer = $this->getMockLoadBalancer( $mockDb, $dbType );
-               $user = $this->getMockNonAnonUserWithId( 1 );
-
-               $mockDb->expects( $this->once() )
-                       ->method( 'select' )
-                       ->with(
-                               'watchlist',
-                               [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
-                               [ 'wl_user' => 1 ],
-                               $this->isType( 'string' ),
-                               [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
-                       )
-                       ->will( $this->returnValue( [] ) );
-
-               $store = $this->newWatchedItemStore(
-                       $mockLoadBalancer,
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $watchedItems = $store->getWatchedItemsForUser(
-                       $user,
-                       [ 'forWrite' => $forWrite, 'sort' => WatchedItemStore::SORT_ASC ]
-               );
-               $this->assertEquals( [], $watchedItems );
-       }
-
-       public function testGetWatchedItemsForUser_badSortOptionThrowsException() {
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $this->getMockDb() ),
-                       $this->getMockCache(),
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->setExpectedException( 'InvalidArgumentException' );
-               $store->getWatchedItemsForUser(
-                       $this->getMockNonAnonUserWithId( 1 ),
-                       [ 'sort' => 'foo' ]
-               );
-       }
-
-       public function testIsWatchedItem_existingItem() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'selectRow' )
-                       ->with(
-                               'watchlist',
-                               'wl_notificationtimestamp',
-                               [
-                                       'wl_user' => 1,
-                                       'wl_namespace' => 0,
-                                       'wl_title' => 'SomeDbKey',
-                               ]
-                       )
-                       ->will( $this->returnValue(
-                               $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] )
-                       ) );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'delete' );
-               $mockCache->expects( $this->once() )
-                       ->method( 'get' )
-                       ->with( '0:SomeDbKey:1' )
-                       ->will( $this->returnValue( false ) );
-               $mockCache->expects( $this->once() )
-                       ->method( 'set' )
-                       ->with(
-                               '0:SomeDbKey:1'
-                       );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertTrue(
-                       $store->isWatched(
-                               $this->getMockNonAnonUserWithId( 1 ),
-                               new TitleValue( 0, 'SomeDbKey' )
-                       )
-               );
-       }
-
-       public function testIsWatchedItem_noItem() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'selectRow' )
-                       ->with(
-                               'watchlist',
-                               'wl_notificationtimestamp',
-                               [
-                                       'wl_user' => 1,
-                                       'wl_namespace' => 0,
-                                       'wl_title' => 'SomeDbKey',
-                               ]
-                       )
-                       ->will( $this->returnValue( [] ) );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'set' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-               $mockCache->expects( $this->once() )
-                       ->method( 'get' )
-                       ->with( '0:SomeDbKey:1' )
-                       ->will( $this->returnValue( false ) );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertFalse(
-                       $store->isWatched(
-                               $this->getMockNonAnonUserWithId( 1 ),
-                               new TitleValue( 0, 'SomeDbKey' )
-                       )
-               );
-       }
-
-       public function testIsWatchedItem_anonymousUser() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->never() )
-                       ->method( 'selectRow' );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'set' );
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertFalse(
-                       $store->isWatched(
-                               $this->getAnonUser(),
-                               new TitleValue( 0, 'SomeDbKey' )
-                       )
-               );
-       }
-
-       public function testGetNotificationTimestampsBatch() {
-               $targets = [
-                       new TitleValue( 0, 'SomeDbKey' ),
-                       new TitleValue( 1, 'AnotherDbKey' ),
-               ];
-
-               $mockDb = $this->getMockDb();
-               $dbResult = [
-                       $this->getFakeRow( [
-                               'wl_namespace' => 0,
-                               'wl_title' => 'SomeDbKey',
-                               'wl_notificationtimestamp' => '20151212010101',
-                       ] ),
-                       $this->getFakeRow(
-                               [
-                                       'wl_namespace' => 1,
-                                       'wl_title' => 'AnotherDbKey',
-                                       'wl_notificationtimestamp' => null,
-                               ]
-                       ),
-               ];
-
-               $mockDb->expects( $this->once() )
-                       ->method( 'makeWhereFrom2d' )
-                       ->with(
-                               [ [ 'SomeDbKey' => 1 ], [ 'AnotherDbKey' => 1 ] ],
-                               $this->isType( 'string' ),
-                               $this->isType( 'string' )
-                       )
-                       ->will( $this->returnValue( 'makeWhereFrom2d return value' ) );
-               $mockDb->expects( $this->once() )
-                       ->method( 'select' )
-                       ->with(
-                               'watchlist',
-                               [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
-                               [
-                                       'makeWhereFrom2d return value',
-                                       'wl_user' => 1
-                               ],
-                               $this->isType( 'string' )
-                       )
-                       ->will( $this->returnValue( $dbResult ) );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->exactly( 2 ) )
-                       ->method( 'get' )
-                       ->withConsecutive(
-                               [ '0:SomeDbKey:1' ],
-                               [ '1:AnotherDbKey:1' ]
-                       )
-                       ->will( $this->returnValue( null ) );
-               $mockCache->expects( $this->never() )->method( 'set' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertEquals(
-                       [
-                               0 => [ 'SomeDbKey' => '20151212010101', ],
-                               1 => [ 'AnotherDbKey' => null, ],
-                       ],
-                       $store->getNotificationTimestampsBatch( $this->getMockNonAnonUserWithId( 1 ), $targets )
-               );
-       }
-
-       public function testGetNotificationTimestampsBatch_notWatchedTarget() {
-               $targets = [
-                       new TitleValue( 0, 'OtherDbKey' ),
-               ];
-
-               $mockDb = $this->getMockDb();
-
-               $mockDb->expects( $this->once() )
-                       ->method( 'makeWhereFrom2d' )
-                       ->with(
-                               [ [ 'OtherDbKey' => 1 ] ],
-                               $this->isType( 'string' ),
-                               $this->isType( 'string' )
-                       )
-                       ->will( $this->returnValue( 'makeWhereFrom2d return value' ) );
-               $mockDb->expects( $this->once() )
-                       ->method( 'select' )
-                       ->with(
-                               'watchlist',
-                               [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
-                               [
-                                       'makeWhereFrom2d return value',
-                                       'wl_user' => 1
-                               ],
-                               $this->isType( 'string' )
-                       )
-                       ->will( $this->returnValue( $this->getFakeRow( [] ) ) );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->once() )
-                       ->method( 'get' )
-                       ->with( '0:OtherDbKey:1' )
-                       ->will( $this->returnValue( null ) );
-               $mockCache->expects( $this->never() )->method( 'set' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertEquals(
-                       [
-                               0 => [ 'OtherDbKey' => false, ],
-                       ],
-                       $store->getNotificationTimestampsBatch( $this->getMockNonAnonUserWithId( 1 ), $targets )
-               );
-       }
-
-       public function testGetNotificationTimestampsBatch_cachedItem() {
-               $targets = [
-                       new TitleValue( 0, 'SomeDbKey' ),
-                       new TitleValue( 1, 'AnotherDbKey' ),
-               ];
-
-               $user = $this->getMockNonAnonUserWithId( 1 );
-               $cachedItem = new WatchedItem( $user, $targets[0], '20151212010101' );
-
-               $mockDb = $this->getMockDb();
-
-               $mockDb->expects( $this->once() )
-                       ->method( 'makeWhereFrom2d' )
-                       ->with(
-                               [ 1 => [ 'AnotherDbKey' => 1 ] ],
-                               $this->isType( 'string' ),
-                               $this->isType( 'string' )
-                       )
-                       ->will( $this->returnValue( 'makeWhereFrom2d return value' ) );
-               $mockDb->expects( $this->once() )
-                       ->method( 'select' )
-                       ->with(
-                               'watchlist',
-                               [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
-                               [
-                                       'makeWhereFrom2d return value',
-                                       'wl_user' => 1
-                               ],
-                               $this->isType( 'string' )
-                       )
-                       ->will( $this->returnValue( [
-                               $this->getFakeRow(
-                                       [ 'wl_namespace' => 1, 'wl_title' => 'AnotherDbKey', 'wl_notificationtimestamp' => null, ]
-                               )
-                       ] ) );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->at( 1 ) )
-                       ->method( 'get' )
-                       ->with( '0:SomeDbKey:1' )
-                       ->will( $this->returnValue( $cachedItem ) );
-               $mockCache->expects( $this->at( 3 ) )
-                       ->method( 'get' )
-                       ->with( '1:AnotherDbKey:1' )
-                       ->will( $this->returnValue( null ) );
-               $mockCache->expects( $this->never() )->method( 'set' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertEquals(
-                       [
-                               0 => [ 'SomeDbKey' => '20151212010101', ],
-                               1 => [ 'AnotherDbKey' => null, ],
-                       ],
-                       $store->getNotificationTimestampsBatch( $user, $targets )
-               );
-       }
-
-       public function testGetNotificationTimestampsBatch_allItemsCached() {
-               $targets = [
-                       new TitleValue( 0, 'SomeDbKey' ),
-                       new TitleValue( 1, 'AnotherDbKey' ),
-               ];
-
-               $user = $this->getMockNonAnonUserWithId( 1 );
-               $cachedItems = [
-                       new WatchedItem( $user, $targets[0], '20151212010101' ),
-                       new WatchedItem( $user, $targets[1], null ),
-               ];
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->never() )->method( $this->anything() );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->at( 1 ) )
-                       ->method( 'get' )
-                       ->with( '0:SomeDbKey:1' )
-                       ->will( $this->returnValue( $cachedItems[0] ) );
-               $mockCache->expects( $this->at( 3 ) )
-                       ->method( 'get' )
-                       ->with( '1:AnotherDbKey:1' )
-                       ->will( $this->returnValue( $cachedItems[1] ) );
-               $mockCache->expects( $this->never() )->method( 'set' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertEquals(
-                       [
-                               0 => [ 'SomeDbKey' => '20151212010101', ],
-                               1 => [ 'AnotherDbKey' => null, ],
-                       ],
-                       $store->getNotificationTimestampsBatch( $user, $targets )
-               );
-       }
-
-       public function testGetNotificationTimestampsBatch_anonymousUser() {
-               $targets = [
-                       new TitleValue( 0, 'SomeDbKey' ),
-                       new TitleValue( 1, 'AnotherDbKey' ),
-               ];
-
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->never() )->method( $this->anything() );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( $this->anything() );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertEquals(
-                       [
-                               0 => [ 'SomeDbKey' => false, ],
-                               1 => [ 'AnotherDbKey' => false, ],
-                       ],
-                       $store->getNotificationTimestampsBatch( $this->getAnonUser(), $targets )
-               );
-       }
-
-       public function testResetNotificationTimestamp_anonymousUser() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->never() )
-                       ->method( 'selectRow' );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->never() )->method( 'set' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertFalse(
-                       $store->resetNotificationTimestamp(
-                               $this->getAnonUser(),
-                               Title::newFromText( 'SomeDbKey' )
-                       )
-               );
-       }
-
-       public function testResetNotificationTimestamp_noItem() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'selectRow' )
-                       ->with(
-                               'watchlist',
-                               'wl_notificationtimestamp',
-                               [
-                                       'wl_user' => 1,
-                                       'wl_namespace' => 0,
-                                       'wl_title' => 'SomeDbKey',
-                               ]
-                       )
-                       ->will( $this->returnValue( [] ) );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->never() )->method( 'set' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertFalse(
-                       $store->resetNotificationTimestamp(
-                               $this->getMockNonAnonUserWithId( 1 ),
-                               Title::newFromText( 'SomeDbKey' )
-                       )
-               );
-       }
-
-       public function testResetNotificationTimestamp_item() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
-               $title = Title::newFromText( 'SomeDbKey' );
-
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'selectRow' )
-                       ->with(
-                               'watchlist',
-                               'wl_notificationtimestamp',
-                               [
-                                       'wl_user' => 1,
-                                       'wl_namespace' => 0,
-                                       'wl_title' => 'SomeDbKey',
-                               ]
-                       )
-                       ->will( $this->returnValue(
-                               $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] )
-                       ) );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->once() )
-                       ->method( 'set' )
-                       ->with(
-                               '0:SomeDbKey:1',
-                               $this->isInstanceOf( WatchedItem::class )
-                       );
-               $mockCache->expects( $this->once() )
-                       ->method( 'delete' )
-                       ->with( '0:SomeDbKey:1' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               // Note: This does not actually assert the job is correct
-               $callableCallCounter = 0;
-               $mockCallback = function ( $callable ) use ( &$callableCallCounter ) {
-                       $callableCallCounter++;
-                       $this->assertInternalType( 'callable', $callable );
-               };
-               $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback );
-
-               $this->assertTrue(
-                       $store->resetNotificationTimestamp(
-                               $user,
-                               $title
-                       )
-               );
-               $this->assertEquals( 1, $callableCallCounter );
-
-               ScopedCallback::consume( $scopedOverride );
-       }
-
-       public function testResetNotificationTimestamp_noItemForced() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
-               $title = Title::newFromText( 'SomeDbKey' );
-
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->never() )
-                       ->method( 'selectRow' );
-
-               $mockCache = $this->getMockCache();
-               $mockDb->expects( $this->never() )
-                       ->method( 'get' );
-               $mockDb->expects( $this->never() )
-                       ->method( 'set' );
-               $mockDb->expects( $this->never() )
-                       ->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               // Note: This does not actually assert the job is correct
-               $callableCallCounter = 0;
-               $mockCallback = function ( $callable ) use ( &$callableCallCounter ) {
-                       $callableCallCounter++;
-                       $this->assertInternalType( 'callable', $callable );
-               };
-               $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback );
-
-               $this->assertTrue(
-                       $store->resetNotificationTimestamp(
-                               $user,
-                               $title,
-                               'force'
-                       )
-               );
-               $this->assertEquals( 1, $callableCallCounter );
-
-               ScopedCallback::consume( $scopedOverride );
-       }
-
-       /**
-        * @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(
-               $callback,
-               LinkTarget $expectedTitle,
-               $expectedUserId,
-               callable $notificationTimestampCondition
-       ) {
-               $this->assertInternalType( 'callable', $callback );
-
-               $callbackReflector = new ReflectionFunction( $callback );
-               $vars = $callbackReflector->getStaticVariables();
-               $this->assertArrayHasKey( 'job', $vars );
-               $this->assertInstanceOf( ActivityUpdateJob::class, $vars['job'] );
-
-               /** @var ActivityUpdateJob $job */
-               $job = $vars['job'];
-               $this->assertEquals( $expectedTitle->getDBkey(), $job->getTitle()->getDBkey() );
-               $this->assertEquals( $expectedTitle->getNamespace(), $job->getTitle()->getNamespace() );
-
-               $jobParams = $job->getParams();
-               $this->assertArrayHasKey( 'type', $jobParams );
-               $this->assertEquals( 'updateWatchlistNotification', $jobParams['type'] );
-               $this->assertArrayHasKey( 'userid', $jobParams );
-               $this->assertEquals( $expectedUserId, $jobParams['userid'] );
-               $this->assertArrayHasKey( 'notifTime', $jobParams );
-               $this->assertTrue( $notificationTimestampCondition( $jobParams['notifTime'] ) );
-       }
-
-       public function testResetNotificationTimestamp_oldidSpecifiedLatestRevisionForced() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
-               $oldid = 22;
-               $title = $this->getMockTitle( 'SomeTitle' );
-               $title->expects( $this->once() )
-                       ->method( 'getNextRevisionID' )
-                       ->with( $oldid )
-                       ->will( $this->returnValue( false ) );
-
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->never() )
-                       ->method( 'selectRow' );
-
-               $mockCache = $this->getMockCache();
-               $mockDb->expects( $this->never() )
-                       ->method( 'get' );
-               $mockDb->expects( $this->never() )
-                       ->method( 'set' );
-               $mockDb->expects( $this->never() )
-                       ->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $callableCallCounter = 0;
-               $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback(
-                       function ( $callable ) use ( &$callableCallCounter, $title, $user ) {
-                               $callableCallCounter++;
-                               $this->verifyCallbackJob(
-                                       $callable,
-                                       $title,
-                                       $user->getId(),
-                                       function ( $time ) {
-                                               return $time === null;
-                                       }
-                               );
-                       }
-               );
-
-               $this->assertTrue(
-                       $store->resetNotificationTimestamp(
-                               $user,
-                               $title,
-                               'force',
-                               $oldid
-                       )
-               );
-               $this->assertEquals( 1, $callableCallCounter );
-
-               ScopedCallback::consume( $scopedOverride );
-       }
-
-       public function testResetNotificationTimestamp_oldidSpecifiedNotLatestRevisionForced() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
-               $oldid = 22;
-               $title = $this->getMockTitle( 'SomeDbKey' );
-               $title->expects( $this->once() )
-                       ->method( 'getNextRevisionID' )
-                       ->with( $oldid )
-                       ->will( $this->returnValue( 33 ) );
-
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'selectRow' )
-                       ->with(
-                               'watchlist',
-                               'wl_notificationtimestamp',
-                               [
-                                       'wl_user' => 1,
-                                       'wl_namespace' => 0,
-                                       'wl_title' => 'SomeDbKey',
-                               ]
-                       )
-                       ->will( $this->returnValue(
-                               $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] )
-                       ) );
-
-               $mockCache = $this->getMockCache();
-               $mockDb->expects( $this->never() )
-                       ->method( 'get' );
-               $mockDb->expects( $this->never() )
-                       ->method( 'set' );
-               $mockDb->expects( $this->never() )
-                       ->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $addUpdateCallCounter = 0;
-               $scopedOverrideDeferred = $store->overrideDeferredUpdatesAddCallableUpdateCallback(
-                       function ( $callable ) use ( &$addUpdateCallCounter, $title, $user ) {
-                               $addUpdateCallCounter++;
-                               $this->verifyCallbackJob(
-                                       $callable,
-                                       $title,
-                                       $user->getId(),
-                                       function ( $time ) {
-                                               return $time !== null && $time > '20151212010101';
-                                       }
-                               );
-                       }
-               );
-
-               $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,
-                               $title,
-                               'force',
-                               $oldid
-                       )
-               );
-               $this->assertEquals( 1, $addUpdateCallCounter );
-               $this->assertEquals( 1, $getTimestampCallCounter );
-
-               ScopedCallback::consume( $scopedOverrideDeferred );
-               ScopedCallback::consume( $scopedOverrideRevision );
-       }
-
-       public function testResetNotificationTimestamp_notWatchedPageForced() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
-               $oldid = 22;
-               $title = $this->getMockTitle( 'SomeDbKey' );
-               $title->expects( $this->once() )
-                       ->method( 'getNextRevisionID' )
-                       ->with( $oldid )
-                       ->will( $this->returnValue( 33 ) );
-
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'selectRow' )
-                       ->with(
-                               'watchlist',
-                               'wl_notificationtimestamp',
-                               [
-                                       'wl_user' => 1,
-                                       'wl_namespace' => 0,
-                                       'wl_title' => 'SomeDbKey',
-                               ]
-                       )
-                       ->will( $this->returnValue( false ) );
-
-               $mockCache = $this->getMockCache();
-               $mockDb->expects( $this->never() )
-                       ->method( 'get' );
-               $mockDb->expects( $this->never() )
-                       ->method( 'set' );
-               $mockDb->expects( $this->never() )
-                       ->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $callableCallCounter = 0;
-               $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback(
-                       function ( $callable ) use ( &$callableCallCounter, $title, $user ) {
-                               $callableCallCounter++;
-                               $this->verifyCallbackJob(
-                                       $callable,
-                                       $title,
-                                       $user->getId(),
-                                       function ( $time ) {
-                                               return $time === null;
-                                       }
-                               );
-                       }
-               );
-
-               $this->assertTrue(
-                       $store->resetNotificationTimestamp(
-                               $user,
-                               $title,
-                               'force',
-                               $oldid
-                       )
-               );
-               $this->assertEquals( 1, $callableCallCounter );
-
-               ScopedCallback::consume( $scopedOverride );
-       }
-
-       public function testResetNotificationTimestamp_futureNotificationTimestampForced() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
-               $oldid = 22;
-               $title = $this->getMockTitle( 'SomeDbKey' );
-               $title->expects( $this->once() )
-                       ->method( 'getNextRevisionID' )
-                       ->with( $oldid )
-                       ->will( $this->returnValue( 33 ) );
-
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'selectRow' )
-                       ->with(
-                               'watchlist',
-                               'wl_notificationtimestamp',
-                               [
-                                       'wl_user' => 1,
-                                       'wl_namespace' => 0,
-                                       'wl_title' => 'SomeDbKey',
-                               ]
-                       )
-                       ->will( $this->returnValue(
-                               $this->getFakeRow( [ 'wl_notificationtimestamp' => '30151212010101' ] )
-                       ) );
-
-               $mockCache = $this->getMockCache();
-               $mockDb->expects( $this->never() )
-                       ->method( 'get' );
-               $mockDb->expects( $this->never() )
-                       ->method( 'set' );
-               $mockDb->expects( $this->never() )
-                       ->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $addUpdateCallCounter = 0;
-               $scopedOverrideDeferred = $store->overrideDeferredUpdatesAddCallableUpdateCallback(
-                       function ( $callable ) use ( &$addUpdateCallCounter, $title, $user ) {
-                               $addUpdateCallCounter++;
-                               $this->verifyCallbackJob(
-                                       $callable,
-                                       $title,
-                                       $user->getId(),
-                                       function ( $time ) {
-                                               return $time === '30151212010101';
-                                       }
-                               );
-                       }
-               );
-
-               $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,
-                               $title,
-                               'force',
-                               $oldid
-                       )
-               );
-               $this->assertEquals( 1, $addUpdateCallCounter );
-               $this->assertEquals( 1, $getTimestampCallCounter );
-
-               ScopedCallback::consume( $scopedOverrideDeferred );
-               ScopedCallback::consume( $scopedOverrideRevision );
-       }
-
-       public function testResetNotificationTimestamp_futureNotificationTimestampNotForced() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
-               $oldid = 22;
-               $title = $this->getMockTitle( 'SomeDbKey' );
-               $title->expects( $this->once() )
-                       ->method( 'getNextRevisionID' )
-                       ->with( $oldid )
-                       ->will( $this->returnValue( 33 ) );
-
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'selectRow' )
-                       ->with(
-                               'watchlist',
-                               'wl_notificationtimestamp',
-                               [
-                                       'wl_user' => 1,
-                                       'wl_namespace' => 0,
-                                       'wl_title' => 'SomeDbKey',
-                               ]
-                       )
-                       ->will( $this->returnValue(
-                               $this->getFakeRow( [ 'wl_notificationtimestamp' => '30151212010101' ] )
-                       ) );
-
-               $mockCache = $this->getMockCache();
-               $mockDb->expects( $this->never() )
-                       ->method( 'get' );
-               $mockDb->expects( $this->never() )
-                       ->method( 'set' );
-               $mockDb->expects( $this->never() )
-                       ->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $addUpdateCallCounter = 0;
-               $scopedOverrideDeferred = $store->overrideDeferredUpdatesAddCallableUpdateCallback(
-                       function ( $callable ) use ( &$addUpdateCallCounter, $title, $user ) {
-                               $addUpdateCallCounter++;
-                               $this->verifyCallbackJob(
-                                       $callable,
-                                       $title,
-                                       $user->getId(),
-                                       function ( $time ) {
-                                               return $time === false;
-                                       }
-                               );
-                       }
-               );
-
-               $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,
-                               $title,
-                               '',
-                               $oldid
-                       )
-               );
-               $this->assertEquals( 1, $addUpdateCallCounter );
-               $this->assertEquals( 1, $getTimestampCallCounter );
-
-               ScopedCallback::consume( $scopedOverrideDeferred );
-               ScopedCallback::consume( $scopedOverrideRevision );
-       }
-
-       public function testSetNotificationTimestampsForUser_anonUser() {
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $this->getMockDb() ),
-                       $this->getMockCache(),
-                       $this->getMockReadOnlyMode()
-               );
-               $this->assertFalse( $store->setNotificationTimestampsForUser( $this->getAnonUser(), '' ) );
-       }
-
-       public function testSetNotificationTimestampsForUser_allRows() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
-               $timestamp = '20100101010101';
-
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'update' )
-                       ->with(
-                               'watchlist',
-                               [ 'wl_notificationtimestamp' => 'TS' . $timestamp . 'TS' ],
-                               [ 'wl_user' => 1 ]
-                       )
-                       ->will( $this->returnValue( true ) );
-               $mockDb->expects( $this->exactly( 1 ) )
-                       ->method( 'timestamp' )
-                       ->will( $this->returnCallback( function ( $value ) {
-                               return 'TS' . $value . 'TS';
-                       } ) );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $this->getMockCache(),
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertTrue(
-                       $store->setNotificationTimestampsForUser( $user, $timestamp )
-               );
-       }
-
-       public function testSetNotificationTimestampsForUser_nullTimestamp() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
-               $timestamp = null;
-
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'update' )
-                       ->with(
-                               'watchlist',
-                               [ 'wl_notificationtimestamp' => null ],
-                               [ 'wl_user' => 1 ]
-                       )
-                       ->will( $this->returnValue( true ) );
-               $mockDb->expects( $this->exactly( 0 ) )
-                       ->method( 'timestamp' )
-                       ->will( $this->returnCallback( function ( $value ) {
-                               return 'TS' . $value . 'TS';
-                       } ) );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $this->getMockCache(),
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertTrue(
-                       $store->setNotificationTimestampsForUser( $user, $timestamp )
-               );
-       }
-
-       public function testSetNotificationTimestampsForUser_specificTargets() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
-               $timestamp = '20100101010101';
-               $targets = [ new TitleValue( 0, 'Foo' ), new TitleValue( 0, 'Bar' ) ];
-
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'update' )
-                       ->with(
-                               'watchlist',
-                               [ 'wl_notificationtimestamp' => 'TS' . $timestamp . 'TS' ],
-                               [ 'wl_user' => 1, 0 => 'makeWhereFrom2d return value' ]
-                       )
-                       ->will( $this->returnValue( true ) );
-               $mockDb->expects( $this->exactly( 1 ) )
-                       ->method( 'timestamp' )
-                       ->will( $this->returnCallback( function ( $value ) {
-                               return 'TS' . $value . 'TS';
-                       } ) );
-               $mockDb->expects( $this->once() )
-                       ->method( 'makeWhereFrom2d' )
-                       ->with(
-                               [ [ 'Foo' => 1, 'Bar' => 1 ] ],
-                               $this->isType( 'string' ),
-                               $this->isType( 'string' )
-                       )
-                       ->will( $this->returnValue( 'makeWhereFrom2d return value' ) );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $this->getMockCache(),
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertTrue(
-                       $store->setNotificationTimestampsForUser( $user, $timestamp, $targets )
-               );
-       }
-
-       public function testUpdateNotificationTimestamp_watchersExist() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'selectFieldValues' )
-                       ->with(
-                               'watchlist',
-                               'wl_user',
-                               [
-                                       'wl_user != 1',
-                                       'wl_namespace' => 0,
-                                       'wl_title' => 'SomeDbKey',
-                                       'wl_notificationtimestamp IS NULL'
-                               ]
-                       )
-                       ->will( $this->returnValue( [ '2', '3' ] ) );
-               $mockDb->expects( $this->once() )
-                       ->method( 'update' )
-                       ->with(
-                               'watchlist',
-                               [ 'wl_notificationtimestamp' => null ],
-                               [
-                                       'wl_user' => [ 2, 3 ],
-                                       'wl_namespace' => 0,
-                                       'wl_title' => 'SomeDbKey',
-                               ]
-                       );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'set' );
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $this->assertEquals(
-                       [ 2, 3 ],
-                       $store->updateNotificationTimestamp(
-                               $this->getMockNonAnonUserWithId( 1 ),
-                               new TitleValue( 0, 'SomeDbKey' ),
-                               '20151212010101'
-                       )
-               );
-       }
-
-       public function testUpdateNotificationTimestamp_noWatchers() {
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'selectFieldValues' )
-                       ->with(
-                               'watchlist',
-                               'wl_user',
-                               [
-                                       'wl_user != 1',
-                                       'wl_namespace' => 0,
-                                       'wl_title' => 'SomeDbKey',
-                                       'wl_notificationtimestamp IS NULL'
-                               ]
-                       )
-                       ->will(
-                               $this->returnValue( [] )
-                       );
-               $mockDb->expects( $this->never() )
-                       ->method( 'update' );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->never() )->method( 'set' );
-               $mockCache->expects( $this->never() )->method( 'get' );
-               $mockCache->expects( $this->never() )->method( 'delete' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               $watchers = $store->updateNotificationTimestamp(
-                       $this->getMockNonAnonUserWithId( 1 ),
-                       new TitleValue( 0, 'SomeDbKey' ),
-                       '20151212010101'
-               );
-               $this->assertInternalType( 'array', $watchers );
-               $this->assertEmpty( $watchers );
-       }
-
-       public function testUpdateNotificationTimestamp_clearsCachedItems() {
-               $user = $this->getMockNonAnonUserWithId( 1 );
-               $titleValue = new TitleValue( 0, 'SomeDbKey' );
-
-               $mockDb = $this->getMockDb();
-               $mockDb->expects( $this->once() )
-                       ->method( 'selectRow' )
-                       ->will( $this->returnValue(
-                               $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] )
-                       ) );
-               $mockDb->expects( $this->once() )
-                       ->method( 'selectFieldValues' )
-                       ->will(
-                               $this->returnValue( [ '2', '3' ] )
-                       );
-               $mockDb->expects( $this->once() )
-                       ->method( 'update' );
-
-               $mockCache = $this->getMockCache();
-               $mockCache->expects( $this->once() )
-                       ->method( 'set' )
-                       ->with( '0:SomeDbKey:1', $this->isType( 'object' ) );
-               $mockCache->expects( $this->once() )
-                       ->method( 'get' )
-                       ->with( '0:SomeDbKey:1' );
-               $mockCache->expects( $this->once() )
-                       ->method( 'delete' )
-                       ->with( '0:SomeDbKey:1' );
-
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLoadBalancer( $mockDb ),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
-
-               // This will add the item to the cache
-               $store->getWatchedItem( $user, $titleValue );
-
-               $store->updateNotificationTimestamp(
-                       $this->getMockNonAnonUserWithId( 1 ),
-                       $titleValue,
-                       '20151212010101'
-               );
-       }
-
-}
diff --git a/tests/phpunit/includes/WatchedItemUnitTest.php b/tests/phpunit/includes/WatchedItemUnitTest.php
deleted file mode 100644 (file)
index 8897645..0000000
+++ /dev/null
@@ -1,150 +0,0 @@
-<?php
-use MediaWiki\Linker\LinkTarget;
-
-/**
- * @author Addshore
- *
- * @covers WatchedItem
- */
-class WatchedItemUnitTest extends MediaWikiTestCase {
-
-       /**
-        * @param int $id
-        *
-        * @return PHPUnit_Framework_MockObject_MockObject|User
-        */
-       private function getMockUser( $id ) {
-               $user = $this->createMock( User::class );
-               $user->expects( $this->any() )
-                       ->method( 'getId' )
-                       ->will( $this->returnValue( $id ) );
-               $user->expects( $this->any() )
-                       ->method( 'isAllowed' )
-                       ->will( $this->returnValue( true ) );
-               return $user;
-       }
-
-       public function provideUserTitleTimestamp() {
-               $user = $this->getMockUser( 111 );
-               return [
-                       [ $user, Title::newFromText( 'SomeTitle' ), null ],
-                       [ $user, Title::newFromText( 'SomeTitle' ), '20150101010101' ],
-                       [ $user, new TitleValue( 0, 'TVTitle', 'frag' ), '20150101010101' ],
-               ];
-       }
-
-       /**
-        * @return PHPUnit_Framework_MockObject_MockObject|WatchedItemStore
-        */
-       private function getMockWatchedItemStore() {
-               return $this->getMockBuilder( WatchedItemStore::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-       }
-
-       /**
-        * @dataProvider provideUserTitleTimestamp
-        */
-       public function testConstruction( $user, LinkTarget $linkTarget, $notifTimestamp ) {
-               $item = new WatchedItem( $user, $linkTarget, $notifTimestamp );
-
-               $this->assertSame( $user, $item->getUser() );
-               $this->assertSame( $linkTarget, $item->getLinkTarget() );
-               $this->assertSame( $notifTimestamp, $item->getNotificationTimestamp() );
-
-               // The below tests the internal WatchedItem::getTitle method
-               $this->assertInstanceOf( 'Title', $item->getTitle() );
-               $this->assertSame( $linkTarget->getDBkey(), $item->getTitle()->getDBkey() );
-               $this->assertSame( $linkTarget->getFragment(), $item->getTitle()->getFragment() );
-               $this->assertSame( $linkTarget->getNamespace(), $item->getTitle()->getNamespace() );
-               $this->assertSame( $linkTarget->getText(), $item->getTitle()->getText() );
-       }
-
-       /**
-        * @dataProvider provideUserTitleTimestamp
-        */
-       public function testFromUserTitle( $user, $linkTarget, $timestamp ) {
-               $store = $this->getMockWatchedItemStore();
-               $store->expects( $this->once() )
-                       ->method( 'loadWatchedItem' )
-                       ->with( $user, $linkTarget )
-                       ->will( $this->returnValue( new WatchedItem( $user, $linkTarget, $timestamp ) ) );
-               $this->setService( 'WatchedItemStore', $store );
-
-               $item = WatchedItem::fromUserTitle( $user, $linkTarget, User::IGNORE_USER_RIGHTS );
-
-               $this->assertEquals( $user, $item->getUser() );
-               $this->assertEquals( $linkTarget, $item->getLinkTarget() );
-               $this->assertEquals( $timestamp, $item->getNotificationTimestamp() );
-       }
-
-       public function testAddWatch() {
-               $title = Title::newFromText( 'SomeTitle' );
-               $timestamp = null;
-               $checkRights = 0;
-
-               /** @var User|PHPUnit_Framework_MockObject_MockObject $user */
-               $user = $this->createMock( User::class );
-               $user->expects( $this->once() )
-                       ->method( 'addWatch' )
-                       ->with( $title, $checkRights );
-
-               $item = new WatchedItem( $user, $title, $timestamp, $checkRights );
-               $this->assertTrue( $item->addWatch() );
-       }
-
-       public function testRemoveWatch() {
-               $title = Title::newFromText( 'SomeTitle' );
-               $timestamp = null;
-               $checkRights = 0;
-
-               /** @var User|PHPUnit_Framework_MockObject_MockObject $user */
-               $user = $this->createMock( User::class );
-               $user->expects( $this->once() )
-                       ->method( 'removeWatch' )
-                       ->with( $title, $checkRights );
-
-               $item = new WatchedItem( $user, $title, $timestamp, $checkRights );
-               $this->assertTrue( $item->removeWatch() );
-       }
-
-       public function provideBooleans() {
-               return [
-                       [ true ],
-                       [ false ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideBooleans
-        */
-       public function testIsWatched( $returnValue ) {
-               $title = Title::newFromText( 'SomeTitle' );
-               $timestamp = null;
-               $checkRights = 0;
-
-               /** @var User|PHPUnit_Framework_MockObject_MockObject $user */
-               $user = $this->createMock( User::class );
-               $user->expects( $this->once() )
-                       ->method( 'isWatched' )
-                       ->with( $title, $checkRights )
-                       ->will( $this->returnValue( $returnValue ) );
-
-               $item = new WatchedItem( $user, $title, $timestamp, $checkRights );
-               $this->assertEquals( $returnValue, $item->isWatched() );
-       }
-
-       public function testDuplicateEntries() {
-               $oldTitle = Title::newFromText( 'OldTitle' );
-               $newTitle = Title::newFromText( 'NewTitle' );
-
-               $store = $this->getMockWatchedItemStore();
-               $store->expects( $this->once() )
-                       ->method( 'duplicateAllAssociatedEntries' )
-                       ->with( $oldTitle, $newTitle );
-               $this->setService( 'WatchedItemStore', $store );
-
-               WatchedItem::duplicateEntries( $oldTitle, $newTitle );
-       }
-
-}
index 4a30292..f97dd73 100644 (file)
@@ -3,10 +3,11 @@
 /**
  * @covers Action
  *
- * @author Thiemo Mättig
- *
  * @group Action
  * @group Database
+ *
+ * @licence GNU GPL v2+
+ * @author Thiemo Kreuz
  */
 class ActionTest extends MediaWikiTestCase {
 
index ef70626..7e45f4d 100644 (file)
@@ -278,26 +278,13 @@ class ApiOptionsTest extends MediaWikiLangTestCase {
                $this->mUserMock->expects( $this->never() )
                        ->method( 'resetOptions' );
 
-               $this->mUserMock->expects( $this->at( 2 ) )
-                       ->method( 'getOptions' );
-
-               $this->mUserMock->expects( $this->at( 5 ) )
+               $this->mUserMock->expects( $this->exactly( 3 ) )
                        ->method( 'setOption' )
-                       ->with( $this->equalTo( 'willBeNull' ), $this->identicalTo( null ) );
-
-               $this->mUserMock->expects( $this->at( 6 ) )
-                       ->method( 'getOptions' );
-
-               $this->mUserMock->expects( $this->at( 7 ) )
-                       ->method( 'setOption' )
-                       ->with( $this->equalTo( 'willBeEmpty' ), $this->equalTo( '' ) );
-
-               $this->mUserMock->expects( $this->at( 8 ) )
-                       ->method( 'getOptions' );
-
-               $this->mUserMock->expects( $this->at( 9 ) )
-                       ->method( 'setOption' )
-                       ->with( $this->equalTo( 'willBeHappy' ), $this->equalTo( 'Happy' ) );
+                       ->withConsecutive(
+                               [ $this->equalTo( 'willBeNull' ), $this->identicalTo( null ) ],
+                               [ $this->equalTo( 'willBeEmpty' ), $this->equalTo( '' ) ],
+                               [ $this->equalTo( 'willBeHappy' ), $this->equalTo( 'Happy' ) ]
+                       );
 
                $this->mUserMock->expects( $this->once() )
                        ->method( 'saveSettings' );
@@ -315,19 +302,12 @@ class ApiOptionsTest extends MediaWikiLangTestCase {
                $this->mUserMock->expects( $this->once() )
                        ->method( 'resetOptions' );
 
-               $this->mUserMock->expects( $this->at( 5 ) )
-                       ->method( 'getOptions' );
-
-               $this->mUserMock->expects( $this->at( 6 ) )
-                       ->method( 'setOption' )
-                       ->with( $this->equalTo( 'willBeHappy' ), $this->equalTo( 'Happy' ) );
-
-               $this->mUserMock->expects( $this->at( 7 ) )
-                       ->method( 'getOptions' );
-
-               $this->mUserMock->expects( $this->at( 8 ) )
+               $this->mUserMock->expects( $this->exactly( 2 ) )
                        ->method( 'setOption' )
-                       ->with( $this->equalTo( 'name' ), $this->equalTo( 'value' ) );
+                       ->withConsecutive(
+                               [ $this->equalTo( 'willBeHappy' ), $this->equalTo( 'Happy' ) ],
+                               [ $this->equalTo( 'name' ), $this->equalTo( 'value' ) ]
+                       );
 
                $this->mUserMock->expects( $this->once() )
                        ->method( 'saveSettings' );
@@ -348,21 +328,14 @@ class ApiOptionsTest extends MediaWikiLangTestCase {
                $this->mUserMock->expects( $this->never() )
                        ->method( 'resetOptions' );
 
-               $this->mUserMock->expects( $this->at( 4 ) )
-                       ->method( 'setOption' )
-                       ->with( $this->equalTo( 'testmultiselect-opt1' ), $this->identicalTo( true ) );
-
-               $this->mUserMock->expects( $this->at( 5 ) )
-                       ->method( 'setOption' )
-                       ->with( $this->equalTo( 'testmultiselect-opt2' ), $this->identicalTo( null ) );
-
-               $this->mUserMock->expects( $this->at( 6 ) )
-                       ->method( 'setOption' )
-                       ->with( $this->equalTo( 'testmultiselect-opt3' ), $this->identicalTo( false ) );
-
-               $this->mUserMock->expects( $this->at( 7 ) )
+               $this->mUserMock->expects( $this->exactly( 4 ) )
                        ->method( 'setOption' )
-                       ->with( $this->equalTo( 'testmultiselect-opt4' ), $this->identicalTo( false ) );
+                       ->withConsecutive(
+                               [ $this->equalTo( 'testmultiselect-opt1' ), $this->identicalTo( true ) ],
+                               [ $this->equalTo( 'testmultiselect-opt2' ), $this->identicalTo( null ) ],
+                               [ $this->equalTo( 'testmultiselect-opt3' ), $this->identicalTo( false ) ],
+                               [ $this->equalTo( 'testmultiselect-opt4' ), $this->identicalTo( false ) ]
+                       );
 
                $this->mUserMock->expects( $this->once() )
                        ->method( 'saveSettings' );
index 6970313..82b0f82 100644 (file)
@@ -73,7 +73,7 @@ class AuthPluginPrimaryAuthenticationProviderTest extends \MediaWikiTestCase {
                        );
                $provider = new AuthPluginPrimaryAuthenticationProvider( $plugin );
 
-               \Hooks::run( 'UserGroupsChanged', [ $user, [ 'added' ], [ 'removed' ] ] );
+               \Hooks::run( 'UserGroupsChanged', [ $user, [ 'added' ], [ 'removed' ], false, false, [], [] ] );
        }
 
        public function testOnUserLoggedIn() {
index e0ddb0a..5f37078 100644 (file)
@@ -221,8 +221,12 @@ class LocalPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestCase
                $req->password = 'DoesNotExist';
                $ret = $provider->beginPrimaryAuthentication( $reqs );
                $this->assertEquals(
-                       AuthenticationResponse::newAbstain(),
-                       $provider->beginPrimaryAuthentication( $reqs )
+                       AuthenticationResponse::FAIL,
+                       $ret->status
+               );
+               $this->assertEquals(
+                       'wrongpassword',
+                       $ret->message->getKey()
                );
 
                // Validation failure
diff --git a/tests/phpunit/includes/externalstore/ExternalStoreFactoryTest.php b/tests/phpunit/includes/externalstore/ExternalStoreFactoryTest.php
new file mode 100644 (file)
index 0000000..a0bac63
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * @covers ExternalStoreFactory
+ */
+class ExternalStoreFactoryTest extends PHPUnit_Framework_TestCase {
+
+       public function testExternalStoreFactory_noStores() {
+               $factory = new ExternalStoreFactory( [] );
+               $this->assertFalse( $factory->getStoreObject( 'ForTesting' ) );
+               $this->assertFalse( $factory->getStoreObject( 'foo' ) );
+       }
+
+       public function provideStoreNames() {
+               yield 'Same case as construction' => [ 'ForTesting' ];
+               yield 'All lower case' => [ 'fortesting' ];
+               yield 'All upper case' => [ 'FORTESTING' ];
+               yield 'Mix of cases' => [ 'FOrTEsTInG' ];
+       }
+
+       /**
+        * @dataProvider provideStoreNames
+        */
+       public function testExternalStoreFactory_someStore_protoMatch( $proto ) {
+               $factory = new ExternalStoreFactory( [ 'ForTesting' ] );
+               $store = $factory->getStoreObject( $proto );
+               $this->assertInstanceOf( ExternalStoreForTesting::class, $store );
+       }
+
+       /**
+        * @dataProvider provideStoreNames
+        */
+       public function testExternalStoreFactory_someStore_noProtoMatch( $proto ) {
+               $factory = new ExternalStoreFactory( [ 'SomeOtherClassName' ] );
+               $store = $factory->getStoreObject( $proto );
+               $this->assertFalse( $store );
+       }
+
+}
diff --git a/tests/phpunit/includes/externalstore/ExternalStoreForTesting.php b/tests/phpunit/includes/externalstore/ExternalStoreForTesting.php
new file mode 100644 (file)
index 0000000..50f1e52
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+
+class ExternalStoreForTesting {
+
+       protected $data = [
+               'cluster1' => [
+                       '200' => 'Hello',
+                       '300' => [
+                               'Hello', 'World',
+                       ],
+                       // gzip string below generated with gzdeflate( 'AAAABBAAA' )
+                       '12345' => "sttttr\002\022\000",
+               ],
+       ];
+
+       /**
+        * Fetch data from given URL
+        * @param string $url An url of the form FOO://cluster/id or FOO://cluster/id/itemid.
+        * @return mixed
+        */
+       public function fetchFromURL( $url ) {
+               // Based on ExternalStoreDB
+               $path = explode( '/', $url );
+               $cluster = $path[2];
+               $id = $path[3];
+               if ( isset( $path[4] ) ) {
+                       $itemID = $path[4];
+               } else {
+                       $itemID = false;
+               }
+
+               if ( !isset( $this->data[$cluster][$id] ) ) {
+                       return null;
+               }
+
+               if ( $itemID !== false
+                       && is_array( $this->data[$cluster][$id] )
+                       && isset( $this->data[$cluster][$id][$itemID] )
+               ) {
+                       return $this->data[$cluster][$id][$itemID];
+               }
+
+               return $this->data[$cluster][$id];
+       }
+
+}
index a365c4d..7ca3874 100644 (file)
@@ -1,31 +1,39 @@
 <?php
-/**
- * External Store tests
- */
 
 class ExternalStoreTest extends MediaWikiTestCase {
 
        /**
         * @covers ExternalStore::fetchFromURL
         */
-       public function testExternalFetchFromURL() {
-               $this->setMwGlobals( 'wgExternalStores', false );
+       public function testExternalFetchFromURL_noExternalStores() {
+               $this->setService(
+                       'ExternalStoreFactory',
+                       new ExternalStoreFactory( [] )
+               );
 
                $this->assertFalse(
-                       ExternalStore::fetchFromURL( 'FOO://cluster1/200' ),
+                       ExternalStore::fetchFromURL( 'ForTesting://cluster1/200' ),
                        'Deny if wgExternalStores is not set to a non-empty array'
                );
+       }
 
-               $this->setMwGlobals( 'wgExternalStores', [ 'FOO' ] );
+       /**
+        * @covers ExternalStore::fetchFromURL
+        */
+       public function testExternalFetchFromURL_someExternalStore() {
+               $this->setService(
+                       'ExternalStoreFactory',
+                       new ExternalStoreFactory( [ 'ForTesting' ] )
+               );
 
                $this->assertEquals(
-                       ExternalStore::fetchFromURL( 'FOO://cluster1/200' ),
                        'Hello',
+                       ExternalStore::fetchFromURL( 'ForTesting://cluster1/200' ),
                        'Allow FOO://cluster1/200'
                );
                $this->assertEquals(
-                       ExternalStore::fetchFromURL( 'FOO://cluster1/300/0' ),
                        'Hello',
+                       ExternalStore::fetchFromURL( 'ForTesting://cluster1/300/0' ),
                        'Allow FOO://cluster1/300/0'
                );
                # Assertions for r68900
@@ -43,45 +51,3 @@ class ExternalStoreTest extends MediaWikiTestCase {
                );
        }
 }
-
-class ExternalStoreFOO {
-
-       protected $data = [
-               'cluster1' => [
-                       '200' => 'Hello',
-                       '300' => [
-                               'Hello', 'World',
-                       ],
-               ],
-       ];
-
-       /**
-        * Fetch data from given URL
-        * @param string $url An url of the form FOO://cluster/id or FOO://cluster/id/itemid.
-        * @return mixed
-        */
-       function fetchFromURL( $url ) {
-               // Based on ExternalStoreDB
-               $path = explode( '/', $url );
-               $cluster = $path[2];
-               $id = $path[3];
-               if ( isset( $path[4] ) ) {
-                       $itemID = $path[4];
-               } else {
-                       $itemID = false;
-               }
-
-               if ( !isset( $this->data[$cluster][$id] ) ) {
-                       return null;
-               }
-
-               if ( $itemID !== false
-                       && is_array( $this->data[$cluster][$id] )
-                       && isset( $this->data[$cluster][$id][$itemID] )
-               ) {
-                       return $this->data[$cluster][$id][$itemID];
-               }
-
-               return $this->data[$cluster][$id];
-       }
-}
index 4b03fda..fe7c506 100644 (file)
@@ -6,7 +6,7 @@
  * @group JobQueue
  *
  * @licence GNU GPL v2+
- * @author Thiemo Mättig
+ * @author Thiemo Kreuz
  */
 class JobQueueMemoryTest extends PHPUnit_Framework_TestCase {
 
index 4a986b4..9fd640f 100644 (file)
@@ -5,6 +5,11 @@
  */
 class SvgTest extends MediaWikiMediaTestCase {
 
+       /**
+        * @var SvgHandler
+        */
+       private $handler;
+
        protected function setUp() {
                parent::setUp();
 
@@ -38,4 +43,71 @@ class SvgTest extends MediaWikiMediaTestCase {
                        [ 'Wikimedia-logo.svg', [] ]
                ];
        }
+
+       /**
+        * @param string $userPreferredLanguage
+        * @param array $svgLanguages
+        * @param string $expectedMatch
+        * @dataProvider providerGetMatchedLanguage
+        * @covers SvgHandler::getMatchedLanguage
+        */
+       public function testGetMatchedLanguage( $userPreferredLanguage, $svgLanguages, $expectedMatch ) {
+               $match = $this->handler->getMatchedLanguage( $userPreferredLanguage, $svgLanguages );
+               $this->assertEquals( $expectedMatch, $match );
+       }
+
+       public function providerGetMatchedLanguage() {
+               return [
+                       'no match' => [
+                               'userPreferredLanguage' => 'en',
+                               'svgLanguages' => [ 'de-DE', 'zh', 'ga', 'fr', 'sr-Latn-ME' ],
+                               'expectedMatch' => null,
+                       ],
+                       'no subtags' => [
+                               'userPreferredLanguage' => 'en',
+                               'svgLanguages' => [ 'de', 'zh', 'en', 'fr' ],
+                               'expectedMatch' => 'en',
+                       ],
+                       'user no subtags, svg 1 subtag' => [
+                               'userPreferredLanguage' => 'en',
+                               'svgLanguages' => [ 'de-DE', 'en-GB', 'en-US', 'fr' ],
+                               'expectedMatch' => 'en-GB',
+                       ],
+                       'user no subtags, svg >1 subtag' => [
+                               'userPreferredLanguage' => 'sr',
+                               'svgLanguages' => [ 'de-DE', 'sr-Cyrl-BA', 'sr-Latn-ME', 'en-US', 'fr' ],
+                               'expectedMatch' => 'sr-Cyrl-BA',
+                       ],
+                       'user 1 subtag, svg no subtags' => [
+                               'userPreferredLanguage' => 'en-US',
+                               'svgLanguages' => [ 'de', 'en', 'en', 'fr' ],
+                               'expectedMatch' => null,
+                       ],
+                       'user 1 subtag, svg 1 subtag' => [
+                               'userPreferredLanguage' => 'en-US',
+                               'svgLanguages' => [ 'de-DE', 'en-GB', 'en-US', 'fr' ],
+                               'expectedMatch' => 'en-US',
+                       ],
+                       'user 1 subtag, svg >1 subtag' => [
+                               'userPreferredLanguage' => 'sr-Latn',
+                               'svgLanguages' => [ 'de-DE', 'sr-Cyrl-BA', 'sr-Latn-ME', 'fr' ],
+                               'expectedMatch' => 'sr-Latn-ME',
+                       ],
+                       'user >1 subtag, svg >1 subtag' => [
+                               'userPreferredLanguage' => 'sr-Latn-ME',
+                               'svgLanguages' => [ 'de-DE', 'sr-Cyrl-BA', 'sr-Latn-ME', 'en-US', 'fr' ],
+                               'expectedMatch' => 'sr-Latn-ME',
+                       ],
+                       'user >1 subtag, svg <=1 subtag' => [
+                               'userPreferredLanguage' => 'sr-Latn-ME',
+                               'svgLanguages' => [ 'de-DE', 'sr-Cyrl', 'sr-Latn', 'en-US', 'fr' ],
+                               'expectedMatch' => null,
+                       ],
+                       'ensure case-insensitive' => [
+                               'userPreferredLanguage' => 'sr-latn',
+                               'svgLanguages' => [ 'de-DE', 'sr-Cyrl', 'sr-Latn-ME', 'en-US', 'fr' ],
+                               'expectedMatch' => 'sr-Latn-ME',
+                       ],
+               ];
+       }
 }
index 7814b83..9cb2f94 100644 (file)
@@ -42,7 +42,7 @@ class MemcachedBagOStuffTest extends MediaWikiTestCase {
                );
 
                $this->assertEquals(
-                       'test:##dc89dcb43b28614da27660240af478b5',
+                       'test:BagOStuff-long-key:##dc89dcb43b28614da27660240af478b5',
                        $this->cache->makeKey( '𝕖𝕧𝕖𝕟', '𝕚𝕗', '𝕨𝕖', '𝕄𝔻𝟝', '𝕖𝕒𝕔𝕙',
                                '𝕒𝕣𝕘𝕦𝕞𝕖𝕟𝕥', '𝕥𝕙𝕚𝕤', '𝕜𝕖𝕪', '𝕨𝕠𝕦𝕝𝕕', '𝕤𝕥𝕚𝕝𝕝', '𝕓𝕖', '𝕥𝕠𝕠', '𝕝𝕠𝕟𝕘' )
                );
diff --git a/tests/phpunit/includes/parser/SanitizerTest.php b/tests/phpunit/includes/parser/SanitizerTest.php
new file mode 100644 (file)
index 0000000..d7e72e1
--- /dev/null
@@ -0,0 +1,568 @@
+<?php
+
+/**
+ * @todo Tests covering decodeCharReferences can be refactored into a single
+ * method and dataprovider.
+ *
+ * @group Sanitizer
+ */
+class SanitizerTest extends MediaWikiTestCase {
+
+       protected function tearDown() {
+               MWTidy::destroySingleton();
+               parent::tearDown();
+       }
+
+       /**
+        * @covers Sanitizer::decodeCharReferences
+        */
+       public function testDecodeNamedEntities() {
+               $this->assertEquals(
+                       "\xc3\xa9cole",
+                       Sanitizer::decodeCharReferences( '&eacute;cole' ),
+                       'decode named entities'
+               );
+       }
+
+       /**
+        * @covers Sanitizer::decodeCharReferences
+        */
+       public function testDecodeNumericEntities() {
+               $this->assertEquals(
+                       "\xc4\x88io bonas dans l'\xc3\xa9cole!",
+                       Sanitizer::decodeCharReferences( "&#x108;io bonas dans l'&#233;cole!" ),
+                       'decode numeric entities'
+               );
+       }
+
+       /**
+        * @covers Sanitizer::decodeCharReferences
+        */
+       public function testDecodeMixedEntities() {
+               $this->assertEquals(
+                       "\xc4\x88io bonas dans l'\xc3\xa9cole!",
+                       Sanitizer::decodeCharReferences( "&#x108;io bonas dans l'&eacute;cole!" ),
+                       'decode mixed numeric/named entities'
+               );
+       }
+
+       /**
+        * @covers Sanitizer::decodeCharReferences
+        */
+       public function testDecodeMixedComplexEntities() {
+               $this->assertEquals(
+                       "\xc4\x88io bonas dans l'\xc3\xa9cole! (mais pas &#x108;io dans l'&eacute;cole)",
+                       Sanitizer::decodeCharReferences(
+                               "&#x108;io bonas dans l'&eacute;cole! (mais pas &amp;#x108;io dans l'&#38;eacute;cole)"
+                       ),
+                       'decode mixed complex entities'
+               );
+       }
+
+       /**
+        * @covers Sanitizer::decodeCharReferences
+        */
+       public function testInvalidAmpersand() {
+               $this->assertEquals(
+                       'a & b',
+                       Sanitizer::decodeCharReferences( 'a & b' ),
+                       'Invalid ampersand'
+               );
+       }
+
+       /**
+        * @covers Sanitizer::decodeCharReferences
+        */
+       public function testInvalidEntities() {
+               $this->assertEquals(
+                       '&foo;',
+                       Sanitizer::decodeCharReferences( '&foo;' ),
+                       'Invalid named entity'
+               );
+       }
+
+       /**
+        * @covers Sanitizer::decodeCharReferences
+        */
+       public function testInvalidNumberedEntities() {
+               $this->assertEquals(
+                       UtfNormal\Constants::UTF8_REPLACEMENT,
+                       Sanitizer::decodeCharReferences( "&#88888888888888;" ),
+                       'Invalid numbered entity'
+               );
+       }
+
+       /**
+        * @covers Sanitizer::removeHTMLtags
+        * @dataProvider provideHtml5Tags
+        *
+        * @param string $tag Name of an HTML5 element (ie: 'video')
+        * @param bool $escaped Whether sanitizer let the tag in or escape it (ie: '&lt;video&gt;')
+        */
+       public function testRemovehtmltagsOnHtml5Tags( $tag, $escaped ) {
+               MWTidy::setInstance( false );
+
+               if ( $escaped ) {
+                       $this->assertEquals( "&lt;$tag&gt;",
+                               Sanitizer::removeHTMLtags( "<$tag>" )
+                       );
+               } else {
+                       $this->assertEquals( "<$tag></$tag>\n",
+                               Sanitizer::removeHTMLtags( "<$tag>" )
+                       );
+               }
+       }
+
+       /**
+        * Provide HTML5 tags
+        */
+       public static function provideHtml5Tags() {
+               $ESCAPED = true; # We want tag to be escaped
+               $VERBATIM = false; # We want to keep the tag
+               return [
+                       [ 'data', $VERBATIM ],
+                       [ 'mark', $VERBATIM ],
+                       [ 'time', $VERBATIM ],
+                       [ 'video', $ESCAPED ],
+               ];
+       }
+
+       function dataRemoveHTMLtags() {
+               return [
+                       // former testSelfClosingTag
+                       [
+                               '<div>Hello world</div />',
+                               '<div>Hello world</div>',
+                               'Self-closing closing div'
+                       ],
+                       // Make sure special nested HTML5 semantics are not broken
+                       // https://html.spec.whatwg.org/multipage/semantics.html#the-kbd-element
+                       [
+                               '<kbd><kbd>Shift</kbd>+<kbd>F3</kbd></kbd>',
+                               '<kbd><kbd>Shift</kbd>+<kbd>F3</kbd></kbd>',
+                               'Nested <kbd>.'
+                       ],
+                       // https://html.spec.whatwg.org/multipage/semantics.html#the-sub-and-sup-elements
+                       [
+                               '<var>x<sub><var>i</var></sub></var>, <var>y<sub><var>i</var></sub></var>',
+                               '<var>x<sub><var>i</var></sub></var>, <var>y<sub><var>i</var></sub></var>',
+                               'Nested <var>.'
+                       ],
+                       // https://html.spec.whatwg.org/multipage/semantics.html#the-dfn-element
+                       [
+                               '<dfn><abbr title="Garage Door Opener">GDO</abbr></dfn>',
+                               '<dfn><abbr title="Garage Door Opener">GDO</abbr></dfn>',
+                               '<abbr> inside <dfn>',
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider dataRemoveHTMLtags
+        * @covers Sanitizer::removeHTMLtags
+        */
+       public function testRemoveHTMLtags( $input, $output, $msg = null ) {
+               MWTidy::setInstance( false );
+               $this->assertEquals( $output, Sanitizer::removeHTMLtags( $input ), $msg );
+       }
+
+       /**
+        * @dataProvider provideTagAttributesToDecode
+        * @covers Sanitizer::decodeTagAttributes
+        */
+       public function testDecodeTagAttributes( $expected, $attributes, $message = '' ) {
+               $this->assertEquals( $expected,
+                       Sanitizer::decodeTagAttributes( $attributes ),
+                       $message
+               );
+       }
+
+       public static function provideTagAttributesToDecode() {
+               return [
+                       [ [ 'foo' => 'bar' ], 'foo=bar', 'Unquoted attribute' ],
+                       [ [ 'עברית' => 'bar' ], 'עברית=bar', 'Non-Latin attribute' ],
+                       [ [ '६' => 'bar' ], '६=bar', 'Devanagari number' ],
+                       [ [ '搭𨋢' => 'bar' ], '搭𨋢=bar', 'Non-BMP character' ],
+                       [ [], 'ńgh=bar', 'Combining accent is not allowed' ],
+                       [ [ 'foo' => 'bar' ], '    foo   =   bar    ', 'Spaced attribute' ],
+                       [ [ 'foo' => 'bar' ], 'foo="bar"', 'Double-quoted attribute' ],
+                       [ [ 'foo' => 'bar' ], 'foo=\'bar\'', 'Single-quoted attribute' ],
+                       [
+                               [ 'foo' => 'bar', 'baz' => 'foo' ],
+                               'foo=\'bar\'   baz="foo"',
+                               'Several attributes'
+                       ],
+                       [
+                               [ 'foo' => 'bar', 'baz' => 'foo' ],
+                               'foo=\'bar\'   baz="foo"',
+                               'Several attributes'
+                       ],
+                       [
+                               [ 'foo' => 'bar', 'baz' => 'foo' ],
+                               'foo=\'bar\'   baz="foo"',
+                               'Several attributes'
+                       ],
+                       [ [ ':foo' => 'bar' ], ':foo=\'bar\'', 'Leading :' ],
+                       [ [ '_foo' => 'bar' ], '_foo=\'bar\'', 'Leading _' ],
+                       [ [ 'foo' => 'bar' ], 'Foo=\'bar\'', 'Leading capital' ],
+                       [ [ 'foo' => 'BAR' ], 'FOO=BAR', 'Attribute keys are normalized to lowercase' ],
+
+                       # Invalid beginning
+                       [ [], '-foo=bar', 'Leading - is forbidden' ],
+                       [ [], '.foo=bar', 'Leading . is forbidden' ],
+                       [ [ 'foo-bar' => 'bar' ], 'foo-bar=bar', 'A - is allowed inside the attribute' ],
+                       [ [ 'foo-' => 'bar' ], 'foo-=bar', 'A - is allowed inside the attribute' ],
+                       [ [ 'foo.bar' => 'baz' ], 'foo.bar=baz', 'A . is allowed inside the attribute' ],
+                       [ [ 'foo.' => 'baz' ], 'foo.=baz', 'A . is allowed as last character' ],
+                       [ [ 'foo6' => 'baz' ], 'foo6=baz', 'Numbers are allowed' ],
+
+                       # This bit is more relaxed than XML rules, but some extensions use
+                       # it, like ProofreadPage (see T29539)
+                       [ [ '1foo' => 'baz' ], '1foo=baz', 'Leading numbers are allowed' ],
+                       [ [], 'foo$=baz', 'Symbols are not allowed' ],
+                       [ [], 'foo@=baz', 'Symbols are not allowed' ],
+                       [ [], 'foo~=baz', 'Symbols are not allowed' ],
+                       [
+                               [ 'foo' => '1[#^`*%w/(' ],
+                               'foo=1[#^`*%w/(',
+                               'All kind of characters are allowed as values'
+                       ],
+                       [
+                               [ 'foo' => '1[#^`*%\'w/(' ],
+                               'foo="1[#^`*%\'w/("',
+                               'Double quotes are allowed if quoted by single quotes'
+                       ],
+                       [
+                               [ 'foo' => '1[#^`*%"w/(' ],
+                               'foo=\'1[#^`*%"w/(\'',
+                               'Single quotes are allowed if quoted by double quotes'
+                       ],
+                       [ [ 'foo' => '&"' ], 'foo=&amp;&quot;', 'Special chars can be provided as entities' ],
+                       [ [ 'foo' => '&foobar;' ], 'foo=&foobar;', 'Entity-like items are accepted' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideDeprecatedAttributes
+        * @covers Sanitizer::fixTagAttributes
+        */
+       public function testDeprecatedAttributesUnaltered( $inputAttr, $inputEl, $message = '' ) {
+               $this->assertEquals( " $inputAttr",
+                       Sanitizer::fixTagAttributes( $inputAttr, $inputEl ),
+                       $message
+               );
+       }
+
+       public static function provideDeprecatedAttributes() {
+               /** [ <attribute>, <element>, [message] ] */
+               return [
+                       [ 'clear="left"', 'br' ],
+                       [ 'clear="all"', 'br' ],
+                       [ 'width="100"', 'td' ],
+                       [ 'nowrap="true"', 'td' ],
+                       [ 'nowrap=""', 'td' ],
+                       [ 'align="right"', 'td' ],
+                       [ 'align="center"', 'table' ],
+                       [ 'align="left"', 'tr' ],
+                       [ 'align="center"', 'div' ],
+                       [ 'align="left"', 'h1' ],
+                       [ 'align="left"', 'p' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideCssCommentsFixtures
+        * @covers Sanitizer::checkCss
+        */
+       public function testCssCommentsChecking( $expected, $css, $message = '' ) {
+               $this->assertEquals( $expected,
+                       Sanitizer::checkCss( $css ),
+                       $message
+               );
+       }
+
+       public static function provideCssCommentsFixtures() {
+               /** [ <expected>, <css>, [message] ] */
+               return [
+                       // Valid comments spanning entire input
+                       [ '/**/', '/**/' ],
+                       [ '/* comment */', '/* comment */' ],
+                       // Weird stuff
+                       [ ' ', '/****/' ],
+                       [ ' ', '/* /* */' ],
+                       [ 'display: block;', "display:/* foo */block;" ],
+                       [ 'display: block;', "display:\\2f\\2a foo \\2a\\2f block;",
+                               'Backslash-escaped comments must be stripped (T30450)' ],
+                       [ '', '/* unfinished comment structure',
+                               'Remove anything after a comment-start token' ],
+                       [ '', "\\2f\\2a unifinished comment'",
+                               'Remove anything after a backslash-escaped comment-start token' ],
+                       [
+                               '/* insecure input */',
+                               'filter: progid:DXImageTransform.Microsoft.AlphaImageLoader'
+                                       . '(src=\'asdf.png\',sizingMethod=\'scale\');'
+                       ],
+                       [
+                               '/* insecure input */',
+                               '-ms-filter: "progid:DXImageTransform.Microsoft.AlphaImageLoader'
+                                       . '(src=\'asdf.png\',sizingMethod=\'scale\')";'
+                       ],
+                       [ '/* insecure input */', 'width: expression(1+1);' ],
+                       [ '/* insecure input */', 'background-image: image(asdf.png);' ],
+                       [ '/* insecure input */', 'background-image: -webkit-image(asdf.png);' ],
+                       [ '/* insecure input */', 'background-image: -moz-image(asdf.png);' ],
+                       [ '/* insecure input */', 'background-image: image-set("asdf.png" 1x, "asdf.png" 2x);' ],
+                       [
+                               '/* insecure input */',
+                               'background-image: -webkit-image-set("asdf.png" 1x, "asdf.png" 2x);'
+                       ],
+                       [
+                               '/* insecure input */',
+                               'background-image: -moz-image-set("asdf.png" 1x, "asdf.png" 2x);'
+                       ],
+                       [ '/* insecure input */', 'foo: attr( title, url );' ],
+                       [ '/* insecure input */', 'foo: attr( title url );' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideEscapeHtmlAllowEntities
+        * @covers Sanitizer::escapeHtmlAllowEntities
+        */
+       public function testEscapeHtmlAllowEntities( $expected, $html ) {
+               $this->assertEquals(
+                       $expected,
+                       Sanitizer::escapeHtmlAllowEntities( $html )
+               );
+       }
+
+       public static function provideEscapeHtmlAllowEntities() {
+               return [
+                       [ 'foo', 'foo' ],
+                       [ 'a¡b', 'a&#161;b' ],
+                       [ 'foo&#039;bar', "foo'bar" ],
+                       [ '&lt;script&gt;foo&lt;/script&gt;', '<script>foo</script>' ],
+               ];
+       }
+
+       /**
+        * Test Sanitizer::escapeId
+        *
+        * @dataProvider provideEscapeId
+        * @covers Sanitizer::escapeId
+        */
+       public function testEscapeId( $input, $output ) {
+               $this->assertEquals(
+                       $output,
+                       Sanitizer::escapeId( $input, [ 'noninitial', 'legacy' ] )
+               );
+       }
+
+       public static function provideEscapeId() {
+               return [
+                       [ '+', '.2B' ],
+                       [ '&', '.26' ],
+                       [ '=', '.3D' ],
+                       [ ':', ':' ],
+                       [ ';', '.3B' ],
+                       [ '@', '.40' ],
+                       [ '$', '.24' ],
+                       [ '-_.', '-_.' ],
+                       [ '!', '.21' ],
+                       [ '*', '.2A' ],
+                       [ '/', '.2F' ],
+                       [ '[]', '.5B.5D' ],
+                       [ '<>', '.3C.3E' ],
+                       [ '\'', '.27' ],
+                       [ '§', '.C2.A7' ],
+                       [ 'Test:A & B/Here', 'Test:A_.26_B.2FHere' ],
+                       [ 'A&B&amp;C&amp;amp;D&amp;amp;amp;E', 'A.26B.26amp.3BC.26amp.3Bamp.3BD.26amp.3Bamp.3Bamp.3BE' ],
+               ];
+       }
+
+       /**
+        * Test escapeIdReferenceList for consistency with escapeIdForAttribute
+        *
+        * @dataProvider provideEscapeIdReferenceList
+        * @covers Sanitizer::escapeIdReferenceList
+        */
+       public function testEscapeIdReferenceList( $referenceList, $id1, $id2 ) {
+               $this->assertEquals(
+                       Sanitizer::escapeIdReferenceList( $referenceList ),
+                       Sanitizer::escapeIdForAttribute( $id1 )
+                               . ' '
+                               . Sanitizer::escapeIdForAttribute( $id2 )
+               );
+       }
+
+       public static function provideEscapeIdReferenceList() {
+               /** [ <reference list>, <individual id 1>, <individual id 2> ] */
+               return [
+                       [ 'foo bar', 'foo', 'bar' ],
+                       [ '#1 #2', '#1', '#2' ],
+                       [ '+1 +2', '+1', '+2' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideIsReservedDataAttribute
+        */
+       public function testIsReservedDataAttribute( $attr, $expected ) {
+               $this->assertSame( $expected, Sanitizer::isReservedDataAttribute( $attr ) );
+       }
+
+       public static function provideIsReservedDataAttribute() {
+               return [
+                       [ 'foo', false ],
+                       [ 'data', false ],
+                       [ 'data-foo', false ],
+                       [ 'data-mw', true ],
+                       [ 'data-ooui', true ],
+                       [ 'data-parsoid', true ],
+                       [ 'data-mw-foo', true ],
+                       [ 'data-ooui-foo', true ],
+                       [ 'data-mwfoo', true ], // could be false but this is how it's implemented currently
+               ];
+       }
+
+       /**
+        * @dataProvider provideEscapeIdForStuff
+        *
+        * @covers Sanitizer::escapeIdForAttribute()
+        * @covers Sanitizer::escapeIdForLink()
+        * @covers Sanitizer::escapeIdForExternalInterwiki()
+        * @covers Sanitizer::escapeIdInternal()
+        *
+        * @param string $stuff
+        * @param string[] $config
+        * @param string $id
+        * @param string|false $expected
+        * @param int|null $mode
+        */
+       public function testEscapeIdForStuff( $stuff, array $config, $id, $expected, $mode = null ) {
+               $func = "Sanitizer::escapeIdFor{$stuff}";
+               $iwFlavor = array_pop( $config );
+               $this->setMwGlobals( [
+                       'wgFragmentMode' => $config,
+                       'wgExternalInterwikiFragmentMode' => $iwFlavor,
+               ] );
+               $escaped = call_user_func( $func, $id, $mode );
+               self::assertEquals( $expected, $escaped );
+       }
+
+       public function provideEscapeIdForStuff() {
+               // Test inputs and outputs
+               $text = 'foo тест_#%!\'()[]:<>&&amp;&amp;amp;';
+               $legacyEncoded = 'foo_.D1.82.D0.B5.D1.81.D1.82_.23.25.21.27.28.29.5B.5D:.3C.3E' .
+                       '.26.26amp.3B.26amp.3Bamp.3B';
+               $html5Encoded = 'foo_тест_#%!\'()[]:<>&&amp;&amp;amp;';
+               $html5Experimental = 'foo_тест_!_()[]:<>_amp;_amp;amp;';
+
+               // Settings: last element is $wgExternalInterwikiFragmentMode, the rest is $wgFragmentMode
+               $legacy = [ 'legacy', 'legacy' ];
+               $legacyNew = [ 'legacy', 'html5', 'legacy' ];
+               $newLegacy = [ 'html5', 'legacy', 'legacy' ];
+               $new = [ 'html5', 'legacy' ];
+               $allNew = [ 'html5', 'html5' ];
+               $experimentalLegacy = [ 'html5-legacy', 'legacy', 'legacy' ];
+               $newExperimental = [ 'html5', 'html5-legacy', 'legacy' ];
+
+               return [
+                       // Pure legacy: how MW worked before 2017
+                       [ 'Attribute', $legacy, $text, $legacyEncoded, Sanitizer::ID_PRIMARY ],
+                       [ 'Attribute', $legacy, $text, false, Sanitizer::ID_FALLBACK ],
+                       [ 'Link', $legacy, $text, $legacyEncoded ],
+                       [ 'ExternalInterwiki', $legacy, $text, $legacyEncoded ],
+
+                       // Transition to a new world: legacy links with HTML5 fallback
+                       [ 'Attribute', $legacyNew, $text, $legacyEncoded, Sanitizer::ID_PRIMARY ],
+                       [ 'Attribute', $legacyNew, $text, $html5Encoded, Sanitizer::ID_FALLBACK ],
+                       [ 'Link', $legacyNew, $text, $legacyEncoded ],
+                       [ 'ExternalInterwiki', $legacyNew, $text, $legacyEncoded ],
+
+                       // New world: HTML5 links, legacy fallbacks
+                       [ 'Attribute', $newLegacy, $text, $html5Encoded, Sanitizer::ID_PRIMARY ],
+                       [ 'Attribute', $newLegacy, $text, $legacyEncoded, Sanitizer::ID_FALLBACK ],
+                       [ 'Link', $newLegacy, $text, $html5Encoded ],
+                       [ 'ExternalInterwiki', $newLegacy, $text, $legacyEncoded ],
+
+                       // Distant future: no legacy fallbacks, but still linking to leagacy wikis
+                       [ 'Attribute', $new, $text, $html5Encoded, Sanitizer::ID_PRIMARY ],
+                       [ 'Attribute', $new, $text, false, Sanitizer::ID_FALLBACK ],
+                       [ 'Link', $new, $text, $html5Encoded ],
+                       [ 'ExternalInterwiki', $new, $text, $legacyEncoded ],
+
+                       // Just before the heat death of universe: external interwikis are also HTML5 \m/
+                       [ 'Attribute', $allNew, $text, $html5Encoded, Sanitizer::ID_PRIMARY ],
+                       [ 'Attribute', $allNew, $text, false, Sanitizer::ID_FALLBACK ],
+                       [ 'Link', $allNew, $text, $html5Encoded ],
+                       [ 'ExternalInterwiki', $allNew, $text, $html5Encoded ],
+
+                       // Someone flipped $wgExperimentalHtmlIds on
+                       [ 'Attribute', $experimentalLegacy, $text, $html5Experimental, Sanitizer::ID_PRIMARY ],
+                       [ 'Attribute', $experimentalLegacy, $text, $legacyEncoded, Sanitizer::ID_FALLBACK ],
+                       [ 'Link', $experimentalLegacy, $text, $html5Experimental ],
+                       [ 'ExternalInterwiki', $experimentalLegacy, $text, $legacyEncoded ],
+
+                       // Migration from $wgExperimentalHtmlIds to modern HTML5
+                       [ 'Attribute', $newExperimental, $text, $html5Encoded, Sanitizer::ID_PRIMARY ],
+                       [ 'Attribute', $newExperimental, $text, $html5Experimental, Sanitizer::ID_FALLBACK ],
+                       [ 'Link', $newExperimental, $text, $html5Encoded ],
+                       [ 'ExternalInterwiki', $newExperimental, $text, $legacyEncoded ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideStripAllTags
+        *
+        * @covers Sanitizer::stripAllTags()
+        *
+        * @param string $input
+        * @param string $expected
+        */
+       public function testStripAllTags( $input, $expected ) {
+               $this->assertEquals( $expected, Sanitizer::stripAllTags( $input ) );
+       }
+
+       public function provideStripAllTags() {
+               return [
+                       [ '<p>Foo</p>', 'Foo' ],
+                       [ '<p id="one">Foo</p><p id="two">Bar</p>', 'FooBar' ],
+                       [ "<p>Foo</p>\n<p>Bar</p>", 'Foo Bar' ],
+                       [ '<p>Hello &lt;strong&gt; wor&#x6c;&#100; caf&eacute;</p>', 'Hello <strong> world café' ],
+                       [
+                               '<p><small data-foo=\'bar"&lt;baz>quux\'><a href="./Foo">Bar</a></small> Whee!</p>',
+                               'Bar Whee!'
+                       ],
+                       [ '1<span class="<?php">2</span>3', '123' ],
+                       [ '1<span class="<?">2</span>3', '123' ],
+               ];
+       }
+
+       /**
+        * @expectedException InvalidArgumentException
+        * @covers Sanitizer::escapeIdInternal()
+        */
+       public function testInvalidFragmentThrows() {
+               $this->setMwGlobals( 'wgFragmentMode', [ 'boom!' ] );
+               Sanitizer::escapeIdForAttribute( 'This should throw' );
+       }
+
+       /**
+        * @expectedException UnexpectedValueException
+        * @covers Sanitizer::escapeIdForAttribute()
+        */
+       public function testNoPrimaryFragmentModeThrows() {
+               $this->setMwGlobals( 'wgFragmentMode', [ 666 => 'html5' ] );
+               Sanitizer::escapeIdForAttribute( 'This should throw' );
+       }
+
+       /**
+        * @expectedException UnexpectedValueException
+        * @covers Sanitizer::escapeIdForLink()
+        */
+       public function testNoPrimaryFragmentModeThrows2() {
+               $this->setMwGlobals( 'wgFragmentMode', [ 666 => 'html5' ] );
+               Sanitizer::escapeIdForLink( 'This should throw' );
+       }
+}
index 930bbe4..b1d8c69 100644 (file)
@@ -9,7 +9,7 @@
  * @author Jeroen De Dauw < jeroendedauw@gmail.com >
  * @author Daniel Kinzler
  * @author Addshore
- * @author Thiemo Mättig
+ * @author Thiemo Kreuz
  */
 abstract class SpecialPageTestBase extends MediaWikiTestCase {
 
index 6b16cbf..f980af0 100644 (file)
@@ -252,6 +252,11 @@ class RemexDriverTest extends MediaWikiTestCase {
                        '<table><b>1<p>2</b>3</p>',
                        '<b>1</b><p><b>2</b>3</p><table></table>'
                ],
+               [
+                       'AAA causes reparent of p-wrapped text node (T178632)',
+                       '<i><blockquote>x</i></blockquote>',
+                       '<i></i><blockquote><p><i>x</i></p></blockquote>',
+               ],
        ];
 
        public function provider() {
index 4dbda74..f833554 100644 (file)
@@ -116,4 +116,33 @@ class TitleValueTest extends MediaWikiTestCase {
 
                $this->assertEquals( $text, $title->getText() );
        }
+
+       public function provideTestToString() {
+               yield [
+                       new TitleValue( 0, 'Foo' ),
+                       '0:Foo'
+               ];
+               yield [
+                       new TitleValue( 1, 'Bar_Baz' ),
+                       '1:Bar_Baz'
+               ];
+               yield [
+                       new TitleValue( 9, 'JoJo', 'Frag' ),
+                       '9:JoJo#Frag'
+               ];
+               yield [
+                       new TitleValue( 200, 'tea', 'Fragment', 'wikicode' ),
+                       'wikicode:200:tea#Fragment'
+               ];
+       }
+
+       /**
+        * @dataProvider provideTestToString
+        */
+       public function testToString( TitleValue $value, $expected ) {
+               $this->assertSame(
+                       $expected,
+                       $value->__toString()
+               );
+       }
 }
diff --git a/tests/phpunit/includes/watcheditem/WatchedItemIntegrationTest.php b/tests/phpunit/includes/watcheditem/WatchedItemIntegrationTest.php
new file mode 100644 (file)
index 0000000..01e7ecb
--- /dev/null
@@ -0,0 +1,145 @@
+<?php
+use MediaWiki\MediaWikiServices;
+
+/**
+ * @author Addshore
+ *
+ * @group Database
+ *
+ * @covers WatchedItem
+ */
+class WatchedItemIntegrationTest extends MediaWikiTestCase {
+
+       public function setUp() {
+               parent::setUp();
+               self::$users['WatchedItemIntegrationTestUser']
+                       = new TestUser( 'WatchedItemIntegrationTestUser' );
+
+               $this->hideDeprecated( 'WatchedItem::fromUserTitle' );
+               $this->hideDeprecated( 'WatchedItem::addWatch' );
+               $this->hideDeprecated( 'WatchedItem::removeWatch' );
+               $this->hideDeprecated( 'WatchedItem::isWatched' );
+               $this->hideDeprecated( 'WatchedItem::duplicateEntries' );
+               $this->hideDeprecated( 'WatchedItem::batchAddWatch' );
+       }
+
+       private function getUser() {
+               return self::$users['WatchedItemIntegrationTestUser']->getUser();
+       }
+
+       public function testWatchAndUnWatchItem() {
+               $user = $this->getUser();
+               $title = Title::newFromText( 'WatchedItemIntegrationTestPage' );
+               // Cleanup after previous tests
+               WatchedItem::fromUserTitle( $user, $title )->removeWatch();
+
+               $this->assertFalse(
+                       WatchedItem::fromUserTitle( $user, $title )->isWatched(),
+                       'Page should not initially be watched'
+               );
+               WatchedItem::fromUserTitle( $user, $title )->addWatch();
+               $this->assertTrue(
+                       WatchedItem::fromUserTitle( $user, $title )->isWatched(),
+                       'Page should be watched'
+               );
+               WatchedItem::fromUserTitle( $user, $title )->removeWatch();
+               $this->assertFalse(
+                       WatchedItem::fromUserTitle( $user, $title )->isWatched(),
+                       'Page should be unwatched'
+               );
+       }
+
+       public function testUpdateAndResetNotificationTimestamp() {
+               $user = $this->getUser();
+               $otherUser = ( new TestUser( 'WatchedItemIntegrationTestUser_otherUser' ) )->getUser();
+               $title = Title::newFromText( 'WatchedItemIntegrationTestPage' );
+               WatchedItem::fromUserTitle( $user, $title )->addWatch();
+               $this->assertNull( WatchedItem::fromUserTitle( $user, $title )->getNotificationTimestamp() );
+
+               EmailNotification::updateWatchlistTimestamp( $otherUser, $title, '20150202010101' );
+               $this->assertEquals(
+                       '20150202010101',
+                       WatchedItem::fromUserTitle( $user, $title )->getNotificationTimestamp()
+               );
+
+               MediaWikiServices::getInstance()->getWatchedItemStore()->resetNotificationTimestamp(
+                       $user, $title
+               );
+               $this->assertNull( WatchedItem::fromUserTitle( $user, $title )->getNotificationTimestamp() );
+       }
+
+       public function testDuplicateAllAssociatedEntries() {
+               $user = $this->getUser();
+               $titleOld = Title::newFromText( 'WatchedItemIntegrationTestPageOld' );
+               $titleNew = Title::newFromText( 'WatchedItemIntegrationTestPageNew' );
+               WatchedItem::fromUserTitle( $user, $titleOld->getSubjectPage() )->addWatch();
+               WatchedItem::fromUserTitle( $user, $titleOld->getTalkPage() )->addWatch();
+               // Cleanup after previous tests
+               WatchedItem::fromUserTitle( $user, $titleNew->getSubjectPage() )->removeWatch();
+               WatchedItem::fromUserTitle( $user, $titleNew->getTalkPage() )->removeWatch();
+
+               WatchedItem::duplicateEntries( $titleOld, $titleNew );
+
+               $this->assertTrue(
+                       WatchedItem::fromUserTitle( $user, $titleOld->getSubjectPage() )->isWatched()
+               );
+               $this->assertTrue(
+                       WatchedItem::fromUserTitle( $user, $titleOld->getTalkPage() )->isWatched()
+               );
+               $this->assertTrue(
+                       WatchedItem::fromUserTitle( $user, $titleNew->getSubjectPage() )->isWatched()
+               );
+               $this->assertTrue(
+                       WatchedItem::fromUserTitle( $user, $titleNew->getTalkPage() )->isWatched()
+               );
+       }
+
+       public function testIsWatched_falseOnNotAllowed() {
+               $user = $this->getUser();
+               $title = Title::newFromText( 'WatchedItemIntegrationTestPage' );
+               WatchedItem::fromUserTitle( $user, $title )->addWatch();
+
+               $this->assertTrue( WatchedItem::fromUserTitle( $user, $title )->isWatched() );
+               $user->mRights = [];
+               $this->assertFalse( WatchedItem::fromUserTitle( $user, $title )->isWatched() );
+       }
+
+       public function testGetNotificationTimestamp_falseOnNotAllowed() {
+               $user = $this->getUser();
+               $title = Title::newFromText( 'WatchedItemIntegrationTestPage' );
+               WatchedItem::fromUserTitle( $user, $title )->addWatch();
+               MediaWikiServices::getInstance()->getWatchedItemStore()->resetNotificationTimestamp(
+                       $user, $title
+               );
+
+               $this->assertEquals(
+                       null,
+                       WatchedItem::fromUserTitle( $user, $title )->getNotificationTimestamp()
+               );
+               $user->mRights = [];
+               $this->assertFalse( WatchedItem::fromUserTitle( $user, $title )->getNotificationTimestamp() );
+       }
+
+       public function testRemoveWatch_falseOnNotAllowed() {
+               $user = $this->getUser();
+               $title = Title::newFromText( 'WatchedItemIntegrationTestPage' );
+               WatchedItem::fromUserTitle( $user, $title )->addWatch();
+
+               $previousRights = $user->mRights;
+               $user->mRights = [];
+               $this->assertFalse( WatchedItem::fromUserTitle( $user, $title )->removeWatch() );
+               $user->mRights = $previousRights;
+               $this->assertTrue( WatchedItem::fromUserTitle( $user, $title )->removeWatch() );
+       }
+
+       public function testGetNotificationTimestamp_falseOnNotWatched() {
+               $user = $this->getUser();
+               $title = Title::newFromText( 'WatchedItemIntegrationTestPage' );
+
+               WatchedItem::fromUserTitle( $user, $title )->removeWatch();
+               $this->assertFalse( WatchedItem::fromUserTitle( $user, $title )->isWatched() );
+
+               $this->assertFalse( WatchedItem::fromUserTitle( $user, $title )->getNotificationTimestamp() );
+       }
+
+}
diff --git a/tests/phpunit/includes/watcheditem/WatchedItemQueryServiceUnitTest.php b/tests/phpunit/includes/watcheditem/WatchedItemQueryServiceUnitTest.php
new file mode 100644 (file)
index 0000000..62ba5f6
--- /dev/null
@@ -0,0 +1,1676 @@
+<?php
+
+use Wikimedia\ScopedCallback;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @covers WatchedItemQueryService
+ */
+class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase {
+
+       /**
+        * @return PHPUnit_Framework_MockObject_MockObject|Database
+        */
+       private function getMockDb() {
+               $mock = $this->getMockBuilder( Database::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $mock->expects( $this->any() )
+                       ->method( 'makeList' )
+                       ->with(
+                               $this->isType( 'array' ),
+                               $this->isType( 'int' )
+                       )
+                       ->will( $this->returnCallback( function ( $a, $conj ) {
+                               $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR ';
+                               return join( $sqlConj, array_map( function ( $s ) {
+                                       return '(' . $s . ')';
+                               }, $a
+                               ) );
+                       } ) );
+
+               $mock->expects( $this->any() )
+                       ->method( 'addQuotes' )
+                       ->will( $this->returnCallback( function ( $value ) {
+                               return "'$value'";
+                       } ) );
+
+               $mock->expects( $this->any() )
+                       ->method( 'timestamp' )
+                       ->will( $this->returnArgument( 0 ) );
+
+               $mock->expects( $this->any() )
+                       ->method( 'bitAnd' )
+                       ->willReturnCallback( function ( $a, $b ) {
+                               return "($a & $b)";
+                       } );
+
+               return $mock;
+       }
+
+       /**
+        * @param PHPUnit_Framework_MockObject_MockObject|Database $mockDb
+        * @return PHPUnit_Framework_MockObject_MockObject|LoadBalancer
+        */
+       private function getMockLoadBalancer( $mockDb ) {
+               $mock = $this->getMockBuilder( LoadBalancer::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $mock->expects( $this->any() )
+                       ->method( 'getConnectionRef' )
+                       ->with( DB_REPLICA )
+                       ->will( $this->returnValue( $mockDb ) );
+               return $mock;
+       }
+
+       /**
+        * @param int $id
+        * @return PHPUnit_Framework_MockObject_MockObject|User
+        */
+       private function getMockNonAnonUserWithId( $id ) {
+               $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 ) );
+               return $mock;
+       }
+
+       /**
+        * @param int $id
+        * @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 ) );
+               return $mock;
+       }
+
+       /**
+        * @param int $id
+        * @param string $notAllowedAction
+        * @return PHPUnit_Framework_MockObject_MockObject|User
+        */
+       private function getMockNonAnonUserWithIdAndRestrictedPermissions( $id, $notAllowedAction ) {
+               $mock = $this->getMockNonAnonUserWithId( $id );
+
+               $mock->expects( $this->any() )
+                       ->method( 'isAllowed' )
+                       ->will( $this->returnCallback( function ( $action ) use ( $notAllowedAction ) {
+                               return $action !== $notAllowedAction;
+                       } ) );
+               $mock->expects( $this->any() )
+                       ->method( 'isAllowedAny' )
+                       ->will( $this->returnCallback( function () use ( $notAllowedAction ) {
+                               $actions = func_get_args();
+                               return !in_array( $notAllowedAction, $actions );
+                       } ) );
+
+               return $mock;
+       }
+
+       /**
+        * @param int $id
+        * @return PHPUnit_Framework_MockObject_MockObject|User
+        */
+       private function getMockNonAnonUserWithIdAndNoPatrolRights( $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( false ) );
+               $mock->expects( $this->any() )
+                       ->method( 'useNPPatrol' )
+                       ->will( $this->returnValue( false ) );
+
+               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 ) {
+                       $fakeRow->$valueName = $value;
+               }
+               return $fakeRow;
+       }
+
+       public function testGetWatchedItemsWithRecentChangeInfo() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               [ 'recentchanges', 'watchlist', 'page' ],
+                               [
+                                       'rc_id',
+                                       'rc_namespace',
+                                       'rc_title',
+                                       'rc_timestamp',
+                                       'rc_type',
+                                       'rc_deleted',
+                                       'wl_notificationtimestamp',
+                                       'rc_cur_id',
+                                       'rc_this_oldid',
+                                       'rc_last_oldid',
+                               ],
+                               [
+                                       'wl_user' => 1,
+                                       '(rc_this_oldid=page_latest) OR (rc_type=3)',
+                               ],
+                               $this->isType( 'string' ),
+                               [
+                                       'LIMIT' => 3,
+                               ],
+                               [
+                                       'watchlist' => [
+                                               'INNER JOIN',
+                                               [
+                                                       'wl_namespace=rc_namespace',
+                                                       'wl_title=rc_title'
+                                               ]
+                                       ],
+                                       'page' => [
+                                               'LEFT JOIN',
+                                               'rc_cur_id=page_id',
+                                       ],
+                               ]
+                       )
+                       ->will( $this->returnValue( [
+                               $this->getFakeRow( [
+                                       'rc_id' => 1,
+                                       'rc_namespace' => 0,
+                                       'rc_title' => 'Foo1',
+                                       'rc_timestamp' => '20151212010101',
+                                       'rc_type' => RC_NEW,
+                                       'rc_deleted' => 0,
+                                       'wl_notificationtimestamp' => '20151212010101',
+                               ] ),
+                               $this->getFakeRow( [
+                                       'rc_id' => 2,
+                                       'rc_namespace' => 1,
+                                       'rc_title' => 'Foo2',
+                                       'rc_timestamp' => '20151212010102',
+                                       'rc_type' => RC_NEW,
+                                       'rc_deleted' => 0,
+                                       'wl_notificationtimestamp' => null,
+                               ] ),
+                               $this->getFakeRow( [
+                                       'rc_id' => 3,
+                                       'rc_namespace' => 1,
+                                       'rc_title' => 'Foo3',
+                                       'rc_timestamp' => '20151212010103',
+                                       'rc_type' => RC_NEW,
+                                       'rc_deleted' => 0,
+                                       'wl_notificationtimestamp' => null,
+                               ] ),
+                       ] ) );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
+
+               $startFrom = null;
+               $items = $queryService->getWatchedItemsWithRecentChangeInfo(
+                       $user, [ 'limit' => 2 ], $startFrom
+               );
+
+               $this->assertInternalType( 'array', $items );
+               $this->assertCount( 2, $items );
+
+               foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
+                       $this->assertInstanceOf( WatchedItem::class, $watchedItem );
+                       $this->assertInternalType( 'array', $recentChangeInfo );
+               }
+
+               $this->assertEquals(
+                       new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
+                       $items[0][0]
+               );
+               $this->assertEquals(
+                       [
+                               'rc_id' => 1,
+                               'rc_namespace' => 0,
+                               'rc_title' => 'Foo1',
+                               'rc_timestamp' => '20151212010101',
+                               'rc_type' => RC_NEW,
+                               'rc_deleted' => 0,
+                       ],
+                       $items[0][1]
+               );
+
+               $this->assertEquals(
+                       new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
+                       $items[1][0]
+               );
+               $this->assertEquals(
+                       [
+                               'rc_id' => 2,
+                               'rc_namespace' => 1,
+                               'rc_title' => 'Foo2',
+                               'rc_timestamp' => '20151212010102',
+                               'rc_type' => RC_NEW,
+                               'rc_deleted' => 0,
+                       ],
+                       $items[1][1]
+               );
+
+               $this->assertEquals( [ '20151212010103', 3 ], $startFrom );
+       }
+
+       public function testGetWatchedItemsWithRecentChangeInfo_extension() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               [ 'recentchanges', 'watchlist', 'page', 'extension_dummy_table' ],
+                               [
+                                       'rc_id',
+                                       'rc_namespace',
+                                       'rc_title',
+                                       'rc_timestamp',
+                                       'rc_type',
+                                       'rc_deleted',
+                                       'wl_notificationtimestamp',
+                                       'rc_cur_id',
+                                       'rc_this_oldid',
+                                       'rc_last_oldid',
+                                       'extension_dummy_field',
+                               ],
+                               [
+                                       'wl_user' => 1,
+                                       '(rc_this_oldid=page_latest) OR (rc_type=3)',
+                                       'extension_dummy_cond',
+                               ],
+                               $this->isType( 'string' ),
+                               [
+                                       'extension_dummy_option',
+                               ],
+                               [
+                                       'watchlist' => [
+                                               'INNER JOIN',
+                                               [
+                                                       'wl_namespace=rc_namespace',
+                                                       'wl_title=rc_title'
+                                               ]
+                                       ],
+                                       'page' => [
+                                               'LEFT JOIN',
+                                               'rc_cur_id=page_id',
+                                       ],
+                                       'extension_dummy_join_cond' => [],
+                               ]
+                       )
+                       ->will( $this->returnValue( [
+                               $this->getFakeRow( [
+                                       'rc_id' => 1,
+                                       'rc_namespace' => 0,
+                                       'rc_title' => 'Foo1',
+                                       'rc_timestamp' => '20151212010101',
+                                       'rc_type' => RC_NEW,
+                                       'rc_deleted' => 0,
+                                       'wl_notificationtimestamp' => '20151212010101',
+                               ] ),
+                               $this->getFakeRow( [
+                                       'rc_id' => 2,
+                                       'rc_namespace' => 1,
+                                       'rc_title' => 'Foo2',
+                                       'rc_timestamp' => '20151212010102',
+                                       'rc_type' => RC_NEW,
+                                       'rc_deleted' => 0,
+                                       'wl_notificationtimestamp' => null,
+                               ] ),
+                       ] ) );
+
+               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
+
+               $mockExtension = $this->getMockBuilder( WatchedItemQueryServiceExtension::class )
+                       ->getMock();
+               $mockExtension->expects( $this->once() )
+                       ->method( 'modifyWatchedItemsWithRCInfoQuery' )
+                       ->with(
+                               $this->identicalTo( $user ),
+                               $this->isType( 'array' ),
+                               $this->isInstanceOf( IDatabase::class ),
+                               $this->isType( 'array' ),
+                               $this->isType( 'array' ),
+                               $this->isType( 'array' ),
+                               $this->isType( 'array' ),
+                               $this->isType( 'array' )
+                       )
+                       ->will( $this->returnCallback( function (
+                               $user, $options, $db, &$tables, &$fields, &$conds, &$dbOptions, &$joinConds
+                       ) {
+                               $tables[] = 'extension_dummy_table';
+                               $fields[] = 'extension_dummy_field';
+                               $conds[] = 'extension_dummy_cond';
+                               $dbOptions[] = 'extension_dummy_option';
+                               $joinConds['extension_dummy_join_cond'] = [];
+                       } ) );
+               $mockExtension->expects( $this->once() )
+                       ->method( 'modifyWatchedItemsWithRCInfo' )
+                       ->with(
+                               $this->identicalTo( $user ),
+                               $this->isType( 'array' ),
+                               $this->isInstanceOf( IDatabase::class ),
+                               $this->isType( 'array' ),
+                               $this->anything(),
+                               $this->anything() // Can't test for null here, PHPUnit applies this after the callback
+                       )
+                       ->will( $this->returnCallback( function ( $user, $options, $db, &$items, $res, &$startFrom ) {
+                               foreach ( $items as $i => &$item ) {
+                                       $item[1]['extension_dummy_field'] = $i;
+                               }
+                               unset( $item );
+
+                               $this->assertNull( $startFrom );
+                               $startFrom = [ '20160203123456', 42 ];
+                       } ) );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+               TestingAccessWrapper::newFromObject( $queryService )->extensions = [ $mockExtension ];
+
+               $startFrom = null;
+               $items = $queryService->getWatchedItemsWithRecentChangeInfo(
+                       $user, [], $startFrom
+               );
+
+               $this->assertInternalType( 'array', $items );
+               $this->assertCount( 2, $items );
+
+               foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
+                       $this->assertInstanceOf( WatchedItem::class, $watchedItem );
+                       $this->assertInternalType( 'array', $recentChangeInfo );
+               }
+
+               $this->assertEquals(
+                       new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
+                       $items[0][0]
+               );
+               $this->assertEquals(
+                       [
+                               'rc_id' => 1,
+                               'rc_namespace' => 0,
+                               'rc_title' => 'Foo1',
+                               'rc_timestamp' => '20151212010101',
+                               'rc_type' => RC_NEW,
+                               'rc_deleted' => 0,
+                               'extension_dummy_field' => 0,
+                       ],
+                       $items[0][1]
+               );
+
+               $this->assertEquals(
+                       new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
+                       $items[1][0]
+               );
+               $this->assertEquals(
+                       [
+                               'rc_id' => 2,
+                               'rc_namespace' => 1,
+                               'rc_title' => 'Foo2',
+                               'rc_timestamp' => '20151212010102',
+                               'rc_type' => RC_NEW,
+                               'rc_deleted' => 0,
+                               'extension_dummy_field' => 1,
+                       ],
+                       $items[1][1]
+               );
+
+               $this->assertEquals( [ '20160203123456', 42 ], $startFrom );
+       }
+
+       public function getWatchedItemsWithRecentChangeInfoOptionsProvider() {
+               return [
+                       [
+                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_FLAGS ] ],
+                               null,
+                               [],
+                               [ 'rc_type', 'rc_minor', 'rc_bot' ],
+                               [],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_USER ] ],
+                               null,
+                               [],
+                               [ 'rc_user_text' ],
+                               [],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_USER_ID ] ],
+                               null,
+                               [],
+                               [ 'rc_user' ],
+                               [],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_COMMENT ] ],
+                               null,
+                               [],
+                               [
+                                       'rc_comment_text' => 'rc_comment',
+                                       'rc_comment_data' => 'NULL',
+                                       'rc_comment_cid' => 'NULL',
+                               ],
+                               [],
+                               [],
+                               [],
+                               [ 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD ],
+                       ],
+                       [
+                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_COMMENT ] ],
+                               null,
+                               [ 'comment_rc_comment' => 'comment' ],
+                               [
+                                       'rc_comment_text' => 'COALESCE( comment_rc_comment.comment_text, rc_comment )',
+                                       'rc_comment_data' => 'comment_rc_comment.comment_data',
+                                       'rc_comment_cid' => 'comment_rc_comment.comment_id',
+                               ],
+                               [],
+                               [],
+                               [ 'comment_rc_comment' => [ 'LEFT JOIN', 'comment_rc_comment.comment_id = rc_comment_id' ] ],
+                               [ 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH ],
+                       ],
+                       [
+                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_COMMENT ] ],
+                               null,
+                               [ 'comment_rc_comment' => 'comment' ],
+                               [
+                                       'rc_comment_text' => 'COALESCE( comment_rc_comment.comment_text, rc_comment )',
+                                       'rc_comment_data' => 'comment_rc_comment.comment_data',
+                                       'rc_comment_cid' => 'comment_rc_comment.comment_id',
+                               ],
+                               [],
+                               [],
+                               [ 'comment_rc_comment' => [ 'LEFT JOIN', 'comment_rc_comment.comment_id = rc_comment_id' ] ],
+                               [ 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_NEW ],
+                       ],
+                       [
+                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_COMMENT ] ],
+                               null,
+                               [ 'comment_rc_comment' => 'comment' ],
+                               [
+                                       'rc_comment_text' => 'comment_rc_comment.comment_text',
+                                       'rc_comment_data' => 'comment_rc_comment.comment_data',
+                                       'rc_comment_cid' => 'comment_rc_comment.comment_id',
+                               ],
+                               [],
+                               [],
+                               [ 'comment_rc_comment' => [ 'JOIN', 'comment_rc_comment.comment_id = rc_comment_id' ] ],
+                               [ 'wgCommentTableSchemaMigrationStage' => MIGRATION_NEW ],
+                       ],
+                       [
+                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_PATROL_INFO ] ],
+                               null,
+                               [],
+                               [ 'rc_patrolled', 'rc_log_type' ],
+                               [],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_SIZES ] ],
+                               null,
+                               [],
+                               [ 'rc_old_len', 'rc_new_len' ],
+                               [],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_LOG_INFO ] ],
+                               null,
+                               [],
+                               [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ],
+                               [],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'namespaceIds' => [ 0, 1 ] ],
+                               null,
+                               [],
+                               [],
+                               [ 'wl_namespace' => [ 0, 1 ] ],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ] ],
+                               null,
+                               [],
+                               [],
+                               [ 'wl_namespace' => [ 0, 1 ] ],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'rcTypes' => [ RC_EDIT, RC_NEW ] ],
+                               null,
+                               [],
+                               [],
+                               [ 'rc_type' => [ RC_EDIT, RC_NEW ] ],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'dir' => WatchedItemQueryService::DIR_OLDER ],
+                               null,
+                               [],
+                               [],
+                               [],
+                               [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
+                               [],
+                       ],
+                       [
+                               [ 'dir' => WatchedItemQueryService::DIR_NEWER ],
+                               null,
+                               [],
+                               [],
+                               [],
+                               [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
+                               [],
+                       ],
+                       [
+                               [ 'dir' => WatchedItemQueryService::DIR_OLDER, 'start' => '20151212010101' ],
+                               null,
+                               [],
+                               [],
+                               [ "rc_timestamp <= '20151212010101'" ],
+                               [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
+                               [],
+                       ],
+                       [
+                               [ 'dir' => WatchedItemQueryService::DIR_OLDER, 'end' => '20151212010101' ],
+                               null,
+                               [],
+                               [],
+                               [ "rc_timestamp >= '20151212010101'" ],
+                               [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
+                               [],
+                       ],
+                       [
+                               [
+                                       'dir' => WatchedItemQueryService::DIR_OLDER,
+                                       'start' => '20151212020101',
+                                       'end' => '20151212010101'
+                               ],
+                               null,
+                               [],
+                               [],
+                               [ "rc_timestamp <= '20151212020101'", "rc_timestamp >= '20151212010101'" ],
+                               [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
+                               [],
+                       ],
+                       [
+                               [ 'dir' => WatchedItemQueryService::DIR_NEWER, 'start' => '20151212010101' ],
+                               null,
+                               [],
+                               [],
+                               [ "rc_timestamp >= '20151212010101'" ],
+                               [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
+                               [],
+                       ],
+                       [
+                               [ 'dir' => WatchedItemQueryService::DIR_NEWER, 'end' => '20151212010101' ],
+                               null,
+                               [],
+                               [],
+                               [ "rc_timestamp <= '20151212010101'" ],
+                               [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
+                               [],
+                       ],
+                       [
+                               [
+                                       'dir' => WatchedItemQueryService::DIR_NEWER,
+                                       'start' => '20151212010101',
+                                       'end' => '20151212020101'
+                               ],
+                               null,
+                               [],
+                               [],
+                               [ "rc_timestamp >= '20151212010101'", "rc_timestamp <= '20151212020101'" ],
+                               [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
+                               [],
+                       ],
+                       [
+                               [ 'limit' => 10 ],
+                               null,
+                               [],
+                               [],
+                               [],
+                               [ 'LIMIT' => 11 ],
+                               [],
+                       ],
+                       [
+                               [ 'limit' => "10; DROP TABLE watchlist;\n--" ],
+                               null,
+                               [],
+                               [],
+                               [],
+                               [ 'LIMIT' => 11 ],
+                               [],
+                       ],
+                       [
+                               [ 'filters' => [ WatchedItemQueryService::FILTER_MINOR ] ],
+                               null,
+                               [],
+                               [],
+                               [ 'rc_minor != 0' ],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_MINOR ] ],
+                               null,
+                               [],
+                               [],
+                               [ 'rc_minor = 0' ],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'filters' => [ WatchedItemQueryService::FILTER_BOT ] ],
+                               null,
+                               [],
+                               [],
+                               [ 'rc_bot != 0' ],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_BOT ] ],
+                               null,
+                               [],
+                               [],
+                               [ 'rc_bot = 0' ],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'filters' => [ WatchedItemQueryService::FILTER_ANON ] ],
+                               null,
+                               [],
+                               [],
+                               [ 'rc_user = 0' ],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_ANON ] ],
+                               null,
+                               [],
+                               [],
+                               [ 'rc_user != 0' ],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'filters' => [ WatchedItemQueryService::FILTER_PATROLLED ] ],
+                               null,
+                               [],
+                               [],
+                               [ 'rc_patrolled != 0' ],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_PATROLLED ] ],
+                               null,
+                               [],
+                               [],
+                               [ 'rc_patrolled = 0' ],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'filters' => [ WatchedItemQueryService::FILTER_UNREAD ] ],
+                               null,
+                               [],
+                               [],
+                               [ 'rc_timestamp >= wl_notificationtimestamp' ],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_UNREAD ] ],
+                               null,
+                               [],
+                               [],
+                               [ 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp' ],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'onlyByUser' => 'SomeOtherUser' ],
+                               null,
+                               [],
+                               [],
+                               [ 'rc_user_text' => 'SomeOtherUser' ],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'notByUser' => 'SomeOtherUser' ],
+                               null,
+                               [],
+                               [],
+                               [ "rc_user_text != 'SomeOtherUser'" ],
+                               [],
+                               [],
+                       ],
+                       [
+                               [ 'dir' => WatchedItemQueryService::DIR_OLDER ],
+                               [ '20151212010101', 123 ],
+                               [],
+                               [],
+                               [
+                                       "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
+                               ],
+                               [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
+                               [],
+                       ],
+                       [
+                               [ 'dir' => WatchedItemQueryService::DIR_NEWER ],
+                               [ '20151212010101', 123 ],
+                               [],
+                               [],
+                               [
+                                       "(rc_timestamp > '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id >= 123))"
+                               ],
+                               [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
+                               [],
+                       ],
+                       [
+                               [ 'dir' => WatchedItemQueryService::DIR_OLDER ],
+                               [ '20151212010101', "123; DROP TABLE watchlist;\n--" ],
+                               [],
+                               [],
+                               [
+                                       "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
+                               ],
+                               [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
+                               [],
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider getWatchedItemsWithRecentChangeInfoOptionsProvider
+        */
+       public function testGetWatchedItemsWithRecentChangeInfo_optionsAndEmptyResult(
+               array $options,
+               $startFrom,
+               array $expectedExtraTables,
+               array $expectedExtraFields,
+               array $expectedExtraConds,
+               array $expectedDbOptions,
+               array $expectedExtraJoinConds,
+               array $globals = []
+       ) {
+               // Sigh. This test class doesn't extend MediaWikiTestCase, so we have to reinvent setMwGlobals().
+               if ( $globals ) {
+                       $resetGlobals = [];
+                       foreach ( $globals as $k => $v ) {
+                               $resetGlobals[$k] = $GLOBALS[$k];
+                               $GLOBALS[$k] = $v;
+                       }
+                       $reset = new ScopedCallback( function () use ( $resetGlobals ) {
+                               foreach ( $resetGlobals as $k => $v ) {
+                                       $GLOBALS[$k] = $v;
+                               }
+                       } );
+               }
+
+               $expectedTables = array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables );
+               $expectedFields = array_merge(
+                       [
+                               'rc_id',
+                               'rc_namespace',
+                               'rc_title',
+                               'rc_timestamp',
+                               'rc_type',
+                               'rc_deleted',
+                               'wl_notificationtimestamp',
+
+                               'rc_cur_id',
+                               'rc_this_oldid',
+                               'rc_last_oldid',
+                       ],
+                       $expectedExtraFields
+               );
+               $expectedConds = array_merge(
+                       [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)', ],
+                       $expectedExtraConds
+               );
+               $expectedJoinConds = array_merge(
+                       [
+                               'watchlist' => [
+                                       'INNER JOIN',
+                                       [
+                                               'wl_namespace=rc_namespace',
+                                               'wl_title=rc_title'
+                                       ]
+                               ],
+                               'page' => [
+                                       'LEFT JOIN',
+                                       'rc_cur_id=page_id',
+                               ],
+                       ],
+                       $expectedExtraJoinConds
+               );
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               $expectedTables,
+                               $expectedFields,
+                               $expectedConds,
+                               $this->isType( 'string' ),
+                               $expectedDbOptions,
+                               $expectedJoinConds
+                       )
+                       ->will( $this->returnValue( [] ) );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
+
+               $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
+
+               $this->assertEmpty( $items );
+               $this->assertNull( $startFrom );
+       }
+
+       public function filterPatrolledOptionProvider() {
+               return [
+                       [ WatchedItemQueryService::FILTER_PATROLLED ],
+                       [ WatchedItemQueryService::FILTER_NOT_PATROLLED ],
+               ];
+       }
+
+       /**
+        * @dataProvider filterPatrolledOptionProvider
+        */
+       public function testGetWatchedItemsWithRecentChangeInfo_filterPatrolledAndUserWithNoPatrolRights(
+               $filtersOption
+       ) {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               [ 'recentchanges', 'watchlist', 'page' ],
+                               $this->isType( 'array' ),
+                               [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
+                               $this->isType( 'string' ),
+                               $this->isType( 'array' ),
+                               $this->isType( 'array' )
+                       )
+                       ->will( $this->returnValue( [] ) );
+
+               $user = $this->getMockNonAnonUserWithIdAndNoPatrolRights( 1 );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+               $items = $queryService->getWatchedItemsWithRecentChangeInfo(
+                       $user,
+                       [ 'filters' => [ $filtersOption ] ]
+               );
+
+               $this->assertEmpty( $items );
+       }
+
+       public function mysqlIndexOptimizationProvider() {
+               return [
+                       [
+                               'mysql',
+                               [],
+                               [ "rc_timestamp > ''" ],
+                       ],
+                       [
+                               'mysql',
+                               [ 'start' => '20151212010101', 'dir' => WatchedItemQueryService::DIR_OLDER ],
+                               [ "rc_timestamp <= '20151212010101'" ],
+                       ],
+                       [
+                               'mysql',
+                               [ 'end' => '20151212010101', 'dir' => WatchedItemQueryService::DIR_OLDER ],
+                               [ "rc_timestamp >= '20151212010101'" ],
+                       ],
+                       [
+                               'postgres',
+                               [],
+                               [],
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider mysqlIndexOptimizationProvider
+        */
+       public function testGetWatchedItemsWithRecentChangeInfo_mysqlIndexOptimization(
+               $dbType,
+               array $options,
+               array $expectedExtraConds
+       ) {
+               $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
+               $conds = array_merge( $commonConds, $expectedExtraConds );
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               [ 'recentchanges', 'watchlist', 'page' ],
+                               $this->isType( 'array' ),
+                               $conds,
+                               $this->isType( 'string' ),
+                               $this->isType( 'array' ),
+                               $this->isType( 'array' )
+                       )
+                       ->will( $this->returnValue( [] ) );
+               $mockDb->expects( $this->any() )
+                       ->method( 'getType' )
+                       ->will( $this->returnValue( $dbType ) );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
+
+               $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
+
+               $this->assertEmpty( $items );
+       }
+
+       public function userPermissionRelatedExtraChecksProvider() {
+               return [
+                       [
+                               [],
+                               'deletedhistory',
+                               [
+                                       '(rc_type != ' . RC_LOG . ') OR ((rc_deleted & ' . LogPage::DELETED_ACTION . ') != ' .
+                                               LogPage::DELETED_ACTION . ')'
+                               ],
+                       ],
+                       [
+                               [],
+                               'suppressrevision',
+                               [
+                                       '(rc_type != ' . RC_LOG . ') OR (' .
+                                               '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
+                                               ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
+                               ],
+                       ],
+                       [
+                               [],
+                               'viewsuppressed',
+                               [
+                                       '(rc_type != ' . RC_LOG . ') OR (' .
+                                               '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
+                                               ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
+                               ],
+                       ],
+                       [
+                               [ 'onlyByUser' => 'SomeOtherUser' ],
+                               'deletedhistory',
+                               [
+                                       'rc_user_text' => 'SomeOtherUser',
+                                       '(rc_deleted & ' . Revision::DELETED_USER . ') != ' . Revision::DELETED_USER,
+                                       '(rc_type != ' . RC_LOG . ') OR ((rc_deleted & ' . LogPage::DELETED_ACTION . ') != ' .
+                                               LogPage::DELETED_ACTION . ')'
+                               ],
+                       ],
+                       [
+                               [ 'onlyByUser' => 'SomeOtherUser' ],
+                               'suppressrevision',
+                               [
+                                       'rc_user_text' => 'SomeOtherUser',
+                                       '(rc_deleted & ' . ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ) . ') != ' .
+                                               ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ),
+                                       '(rc_type != ' . RC_LOG . ') OR (' .
+                                               '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
+                                               ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
+                               ],
+                       ],
+                       [
+                               [ 'onlyByUser' => 'SomeOtherUser' ],
+                               'viewsuppressed',
+                               [
+                                       'rc_user_text' => 'SomeOtherUser',
+                                       '(rc_deleted & ' . ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ) . ') != ' .
+                                               ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ),
+                                       '(rc_type != ' . RC_LOG . ') OR (' .
+                                               '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
+                                               ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ')'
+                               ],
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider userPermissionRelatedExtraChecksProvider
+        */
+       public function testGetWatchedItemsWithRecentChangeInfo_userPermissionRelatedExtraChecks(
+               array $options,
+               $notAllowedAction,
+               array $expectedExtraConds
+       ) {
+               $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
+               $conds = array_merge( $commonConds, $expectedExtraConds );
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               [ 'recentchanges', 'watchlist', 'page' ],
+                               $this->isType( 'array' ),
+                               $conds,
+                               $this->isType( 'string' ),
+                               $this->isType( 'array' ),
+                               $this->isType( 'array' )
+                       )
+                       ->will( $this->returnValue( [] ) );
+
+               $user = $this->getMockNonAnonUserWithIdAndRestrictedPermissions( 1, $notAllowedAction );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+               $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
+
+               $this->assertEmpty( $items );
+       }
+
+       public function testGetWatchedItemsWithRecentChangeInfo_allRevisionsOptionAndEmptyResult() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               [ 'recentchanges', 'watchlist' ],
+                               [
+                                       'rc_id',
+                                       'rc_namespace',
+                                       'rc_title',
+                                       'rc_timestamp',
+                                       'rc_type',
+                                       'rc_deleted',
+                                       'wl_notificationtimestamp',
+
+                                       'rc_cur_id',
+                                       'rc_this_oldid',
+                                       'rc_last_oldid',
+                               ],
+                               [ 'wl_user' => 1, ],
+                               $this->isType( 'string' ),
+                               [],
+                               [
+                                       'watchlist' => [
+                                               'INNER JOIN',
+                                               [
+                                                       'wl_namespace=rc_namespace',
+                                                       'wl_title=rc_title'
+                                               ]
+                                       ],
+                               ]
+                       )
+                       ->will( $this->returnValue( [] ) );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
+
+               $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, [ 'allRevisions' => true ] );
+
+               $this->assertEmpty( $items );
+       }
+
+       public function getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider() {
+               return [
+                       [
+                               [ 'rcTypes' => [ 1337 ] ],
+                               null,
+                               'Bad value for parameter $options[\'rcTypes\']',
+                       ],
+                       [
+                               [ 'rcTypes' => [ 'edit' ] ],
+                               null,
+                               'Bad value for parameter $options[\'rcTypes\']',
+                       ],
+                       [
+                               [ 'rcTypes' => [ RC_EDIT, 1337 ] ],
+                               null,
+                               'Bad value for parameter $options[\'rcTypes\']',
+                       ],
+                       [
+                               [ 'dir' => 'foo' ],
+                               null,
+                               'Bad value for parameter $options[\'dir\']',
+                       ],
+                       [
+                               [ 'start' => '20151212010101' ],
+                               null,
+                               'Bad value for parameter $options[\'dir\']: must be provided',
+                       ],
+                       [
+                               [ 'end' => '20151212010101' ],
+                               null,
+                               'Bad value for parameter $options[\'dir\']: must be provided',
+                       ],
+                       [
+                               [],
+                               [ '20151212010101', 123 ],
+                               'Bad value for parameter $options[\'dir\']: must be provided',
+                       ],
+                       [
+                               [ 'dir' => WatchedItemQueryService::DIR_OLDER ],
+                               '20151212010101',
+                               'Bad value for parameter $startFrom: must be a two-element array',
+                       ],
+                       [
+                               [ 'dir' => WatchedItemQueryService::DIR_OLDER ],
+                               [ '20151212010101' ],
+                               'Bad value for parameter $startFrom: must be a two-element array',
+                       ],
+                       [
+                               [ 'dir' => WatchedItemQueryService::DIR_OLDER ],
+                               [ '20151212010101', 123, 'foo' ],
+                               'Bad value for parameter $startFrom: must be a two-element array',
+                       ],
+                       [
+                               [ 'watchlistOwner' => $this->getMockUnrestrictedNonAnonUserWithId( 2 ) ],
+                               null,
+                               'Bad value for parameter $options[\'watchlistOwnerToken\']',
+                       ],
+                       [
+                               [ 'watchlistOwner' => 'Other User', 'watchlistOwnerToken' => 'some-token' ],
+                               null,
+                               'Bad value for parameter $options[\'watchlistOwner\']',
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider
+        */
+       public function testGetWatchedItemsWithRecentChangeInfo_invalidOptions(
+               array $options,
+               $startFrom,
+               $expectedInExceptionMessage
+       ) {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->never() )
+                       ->method( $this->anything() );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
+
+               $this->setExpectedException( InvalidArgumentException::class, $expectedInExceptionMessage );
+               $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
+       }
+
+       public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorOptionAndEmptyResult() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               [ 'recentchanges', 'watchlist', 'page' ],
+                               [
+                                       'rc_id',
+                                       'rc_namespace',
+                                       'rc_title',
+                                       'rc_timestamp',
+                                       'rc_type',
+                                       'rc_deleted',
+                                       'wl_notificationtimestamp',
+                                       'rc_cur_id',
+                               ],
+                               [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
+                               $this->isType( 'string' ),
+                               [],
+                               [
+                                       'watchlist' => [
+                                               'INNER JOIN',
+                                               [
+                                                       'wl_namespace=rc_namespace',
+                                                       'wl_title=rc_title'
+                                               ]
+                                       ],
+                                       'page' => [
+                                               'LEFT JOIN',
+                                               'rc_cur_id=page_id',
+                                       ],
+                               ]
+                       )
+                       ->will( $this->returnValue( [] ) );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
+
+               $items = $queryService->getWatchedItemsWithRecentChangeInfo(
+                       $user,
+                       [ 'usedInGenerator' => true ]
+               );
+
+               $this->assertEmpty( $items );
+       }
+
+       public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorAllRevisionsOptions() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               [ 'recentchanges', 'watchlist' ],
+                               [
+                                       'rc_id',
+                                       'rc_namespace',
+                                       'rc_title',
+                                       'rc_timestamp',
+                                       'rc_type',
+                                       'rc_deleted',
+                                       'wl_notificationtimestamp',
+                                       'rc_this_oldid',
+                               ],
+                               [ 'wl_user' => 1 ],
+                               $this->isType( 'string' ),
+                               [],
+                               [
+                                       'watchlist' => [
+                                               'INNER JOIN',
+                                               [
+                                                       'wl_namespace=rc_namespace',
+                                                       'wl_title=rc_title'
+                                               ]
+                                       ],
+                               ]
+                       )
+                       ->will( $this->returnValue( [] ) );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
+
+               $items = $queryService->getWatchedItemsWithRecentChangeInfo(
+                       $user,
+                       [ 'usedInGenerator' => true, 'allRevisions' => true, ]
+               );
+
+               $this->assertEmpty( $items );
+       }
+
+       public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerOptionAndEmptyResult() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               $this->isType( 'array' ),
+                               $this->isType( 'array' ),
+                               [
+                                       'wl_user' => 2,
+                                       '(rc_this_oldid=page_latest) OR (rc_type=3)',
+                               ],
+                               $this->isType( 'string' ),
+                               $this->isType( 'array' ),
+                               $this->isType( 'array' )
+                       )
+                       ->will( $this->returnValue( [] ) );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
+               $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
+               $otherUser->expects( $this->once() )
+                       ->method( 'getOption' )
+                       ->with( 'watchlisttoken' )
+                       ->willReturn( '0123456789abcdef' );
+
+               $items = $queryService->getWatchedItemsWithRecentChangeInfo(
+                       $user,
+                       [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => '0123456789abcdef' ]
+               );
+
+               $this->assertEmpty( $items );
+       }
+
+       public function invalidWatchlistTokenProvider() {
+               return [
+                       [ 'wrongToken' ],
+                       [ '' ],
+               ];
+       }
+
+       /**
+        * @dataProvider invalidWatchlistTokenProvider
+        */
+       public function testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerAndInvalidToken( $token ) {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->never() )
+                       ->method( $this->anything() );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+               $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 );
+               $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
+               $otherUser->expects( $this->once() )
+                       ->method( 'getOption' )
+                       ->with( 'watchlisttoken' )
+                       ->willReturn( '0123456789abcdef' );
+
+               $this->setExpectedException( ApiUsageException::class, 'Incorrect watchlist token provided' );
+               $queryService->getWatchedItemsWithRecentChangeInfo(
+                       $user,
+                       [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => $token ]
+               );
+       }
+
+       public function testGetWatchedItemsForUser() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               'watchlist',
+                               [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
+                               [ 'wl_user' => 1 ]
+                       )
+                       ->will( $this->returnValue( [
+                               $this->getFakeRow( [
+                                       'wl_namespace' => 0,
+                                       'wl_title' => 'Foo1',
+                                       'wl_notificationtimestamp' => '20151212010101',
+                               ] ),
+                               $this->getFakeRow( [
+                                       'wl_namespace' => 1,
+                                       'wl_title' => 'Foo2',
+                                       'wl_notificationtimestamp' => null,
+                               ] ),
+                       ] ) );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+               $user = $this->getMockNonAnonUserWithId( 1 );
+
+               $items = $queryService->getWatchedItemsForUser( $user );
+
+               $this->assertInternalType( 'array', $items );
+               $this->assertCount( 2, $items );
+               $this->assertContainsOnlyInstancesOf( WatchedItem::class, $items );
+               $this->assertEquals(
+                       new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
+                       $items[0]
+               );
+               $this->assertEquals(
+                       new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
+                       $items[1]
+               );
+       }
+
+       public function provideGetWatchedItemsForUserOptions() {
+               return [
+                       [
+                               [ 'namespaceIds' => [ 0, 1 ], ],
+                               [ 'wl_namespace' => [ 0, 1 ], ],
+                               []
+                       ],
+                       [
+                               [ 'sort' => WatchedItemQueryService::SORT_ASC, ],
+                               [],
+                               [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
+                       ],
+                       [
+                               [
+                                       'namespaceIds' => [ 0 ],
+                                       'sort' => WatchedItemQueryService::SORT_ASC,
+                               ],
+                               [ 'wl_namespace' => [ 0 ], ],
+                               [ 'ORDER BY' => 'wl_title ASC' ]
+                       ],
+                       [
+                               [ 'limit' => 10 ],
+                               [],
+                               [ 'LIMIT' => 10 ]
+                       ],
+                       [
+                               [
+                                       'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ],
+                                       'limit' => "10; DROP TABLE watchlist;\n--",
+                               ],
+                               [ 'wl_namespace' => [ 0, 1 ], ],
+                               [ 'LIMIT' => 10 ]
+                       ],
+                       [
+                               [ 'filter' => WatchedItemQueryService::FILTER_CHANGED ],
+                               [ 'wl_notificationtimestamp IS NOT NULL' ],
+                               []
+                       ],
+                       [
+                               [ 'filter' => WatchedItemQueryService::FILTER_NOT_CHANGED ],
+                               [ 'wl_notificationtimestamp IS NULL' ],
+                               []
+                       ],
+                       [
+                               [ 'sort' => WatchedItemQueryService::SORT_DESC, ],
+                               [],
+                               [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
+                       ],
+                       [
+                               [
+                                       'namespaceIds' => [ 0 ],
+                                       'sort' => WatchedItemQueryService::SORT_DESC,
+                               ],
+                               [ 'wl_namespace' => [ 0 ], ],
+                               [ 'ORDER BY' => 'wl_title DESC' ]
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetWatchedItemsForUserOptions
+        */
+       public function testGetWatchedItemsForUser_optionsAndEmptyResult(
+               array $options,
+               array $expectedConds,
+               array $expectedDbOptions
+       ) {
+               $mockDb = $this->getMockDb();
+               $user = $this->getMockNonAnonUserWithId( 1 );
+
+               $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               'watchlist',
+                               [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
+                               $expectedConds,
+                               $this->isType( 'string' ),
+                               $expectedDbOptions
+                       )
+                       ->will( $this->returnValue( [] ) );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+
+               $items = $queryService->getWatchedItemsForUser( $user, $options );
+               $this->assertEmpty( $items );
+       }
+
+       public function provideGetWatchedItemsForUser_fromUntilStartFromOptions() {
+               return [
+                       [
+                               [
+                                       'from' => new TitleValue( 0, 'SomeDbKey' ),
+                                       'sort' => WatchedItemQueryService::SORT_ASC
+                               ],
+                               [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
+                               [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
+                       ],
+                       [
+                               [
+                                       'from' => new TitleValue( 0, 'SomeDbKey' ),
+                                       'sort' => WatchedItemQueryService::SORT_DESC,
+                               ],
+                               [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
+                               [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
+                       ],
+                       [
+                               [
+                                       'until' => new TitleValue( 0, 'SomeDbKey' ),
+                                       'sort' => WatchedItemQueryService::SORT_ASC
+                               ],
+                               [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
+                               [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
+                       ],
+                       [
+                               [
+                                       'until' => new TitleValue( 0, 'SomeDbKey' ),
+                                       'sort' => WatchedItemQueryService::SORT_DESC
+                               ],
+                               [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
+                               [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
+                       ],
+                       [
+                               [
+                                       'from' => new TitleValue( 0, 'AnotherDbKey' ),
+                                       'until' => new TitleValue( 0, 'SomeOtherDbKey' ),
+                                       'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
+                                       'sort' => WatchedItemQueryService::SORT_ASC
+                               ],
+                               [
+                                       "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
+                                       "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
+                                       "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))",
+                               ],
+                               [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
+                       ],
+                       [
+                               [
+                                       'from' => new TitleValue( 0, 'SomeOtherDbKey' ),
+                                       'until' => new TitleValue( 0, 'AnotherDbKey' ),
+                                       'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
+                                       'sort' => WatchedItemQueryService::SORT_DESC
+                               ],
+                               [
+                                       "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
+                                       "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
+                                       "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))",
+                               ],
+                               [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideGetWatchedItemsForUser_fromUntilStartFromOptions
+        */
+       public function testGetWatchedItemsForUser_fromUntilStartFromOptions(
+               array $options,
+               array $expectedConds,
+               array $expectedDbOptions
+       ) {
+               $user = $this->getMockNonAnonUserWithId( 1 );
+
+               $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->any() )
+                       ->method( 'addQuotes' )
+                       ->will( $this->returnCallback( function ( $value ) {
+                               return "'$value'";
+                       } ) );
+               $mockDb->expects( $this->any() )
+                       ->method( 'makeList' )
+                       ->with(
+                               $this->isType( 'array' ),
+                               $this->isType( 'int' )
+                       )
+                       ->will( $this->returnCallback( function ( $a, $conj ) {
+                               $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR ';
+                               return join( $sqlConj, array_map( function ( $s ) {
+                                       return '(' . $s . ')';
+                               }, $a
+                               ) );
+                       } ) );
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               'watchlist',
+                               [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
+                               $expectedConds,
+                               $this->isType( 'string' ),
+                               $expectedDbOptions
+                       )
+                       ->will( $this->returnValue( [] ) );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+
+               $items = $queryService->getWatchedItemsForUser( $user, $options );
+               $this->assertEmpty( $items );
+       }
+
+       public function getWatchedItemsForUserInvalidOptionsProvider() {
+               return [
+                       [
+                               [ 'sort' => 'foo' ],
+                               'Bad value for parameter $options[\'sort\']'
+                       ],
+                       [
+                               [ 'filter' => 'foo' ],
+                               'Bad value for parameter $options[\'filter\']'
+                       ],
+                       [
+                               [ 'from' => new TitleValue( 0, 'SomeDbKey' ), ],
+                               'Bad value for parameter $options[\'sort\']: must be provided'
+                       ],
+                       [
+                               [ 'until' => new TitleValue( 0, 'SomeDbKey' ), ],
+                               'Bad value for parameter $options[\'sort\']: must be provided'
+                       ],
+                       [
+                               [ 'startFrom' => new TitleValue( 0, 'SomeDbKey' ), ],
+                               'Bad value for parameter $options[\'sort\']: must be provided'
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider getWatchedItemsForUserInvalidOptionsProvider
+        */
+       public function testGetWatchedItemsForUser_invalidOptionThrowsException(
+               array $options,
+               $expectedInExceptionMessage
+       ) {
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $this->getMockDb() ) );
+
+               $this->setExpectedException( InvalidArgumentException::class, $expectedInExceptionMessage );
+               $queryService->getWatchedItemsForUser( $this->getMockNonAnonUserWithId( 1 ), $options );
+       }
+
+       public function testGetWatchedItemsForUser_userNotAllowedToViewWatchlist() {
+               $mockDb = $this->getMockDb();
+
+               $mockDb->expects( $this->never() )
+                       ->method( $this->anything() );
+
+               $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) );
+
+               $items = $queryService->getWatchedItemsForUser( $this->getMockAnonUser() );
+               $this->assertEmpty( $items );
+       }
+
+}
diff --git a/tests/phpunit/includes/watcheditem/WatchedItemStoreIntegrationTest.php b/tests/phpunit/includes/watcheditem/WatchedItemStoreIntegrationTest.php
new file mode 100644 (file)
index 0000000..61b62aa
--- /dev/null
@@ -0,0 +1,214 @@
+<?php
+
+use MediaWiki\MediaWikiServices;
+
+/**
+ * @author Addshore
+ *
+ * @group Database
+ *
+ * @covers WatchedItemStore
+ */
+class WatchedItemStoreIntegrationTest extends MediaWikiTestCase {
+
+       public function setUp() {
+               parent::setUp();
+               self::$users['WatchedItemStoreIntegrationTestUser']
+                       = new TestUser( 'WatchedItemStoreIntegrationTestUser' );
+       }
+
+       private function getUser() {
+               return self::$users['WatchedItemStoreIntegrationTestUser']->getUser();
+       }
+
+       public function testWatchAndUnWatchItem() {
+               $user = $this->getUser();
+               $title = Title::newFromText( 'WatchedItemStoreIntegrationTestPage' );
+               $store = MediaWikiServices::getInstance()->getWatchedItemStore();
+               // Cleanup after previous tests
+               $store->removeWatch( $user, $title );
+               $initialWatchers = $store->countWatchers( $title );
+               $initialUserWatchedItems = $store->countWatchedItems( $user );
+
+               $this->assertFalse(
+                       $store->isWatched( $user, $title ),
+                       'Page should not initially be watched'
+               );
+
+               $store->addWatch( $user, $title );
+               $this->assertTrue(
+                       $store->isWatched( $user, $title ),
+                       'Page should be watched'
+               );
+               $this->assertEquals( $initialUserWatchedItems + 1, $store->countWatchedItems( $user ) );
+               $watchedItemsForUser = $store->getWatchedItemsForUser( $user );
+               $this->assertCount( $initialUserWatchedItems + 1, $watchedItemsForUser );
+               $watchedItemsForUserHasExpectedItem = false;
+               foreach ( $watchedItemsForUser as $watchedItem ) {
+                       if (
+                               $watchedItem->getUser()->equals( $user ) &&
+                               $watchedItem->getLinkTarget() == $title->getTitleValue()
+                       ) {
+                               $watchedItemsForUserHasExpectedItem = true;
+                       }
+               }
+               $this->assertTrue(
+                       $watchedItemsForUserHasExpectedItem,
+                       'getWatchedItemsForUser should contain the page'
+               );
+               $this->assertEquals( $initialWatchers + 1, $store->countWatchers( $title ) );
+               $this->assertEquals(
+                       $initialWatchers + 1,
+                       $store->countWatchersMultiple( [ $title ] )[$title->getNamespace()][$title->getDBkey()]
+               );
+               $this->assertEquals(
+                       [ 0 => [ 'WatchedItemStoreIntegrationTestPage' => $initialWatchers + 1 ] ],
+                       $store->countWatchersMultiple( [ $title ], [ 'minimumWatchers' => $initialWatchers + 1 ] )
+               );
+               $this->assertEquals(
+                       [ 0 => [ 'WatchedItemStoreIntegrationTestPage' => 0 ] ],
+                       $store->countWatchersMultiple( [ $title ], [ 'minimumWatchers' => $initialWatchers + 2 ] )
+               );
+               $this->assertEquals(
+                       [ $title->getNamespace() => [ $title->getDBkey() => null ] ],
+                       $store->getNotificationTimestampsBatch( $user, [ $title ] )
+               );
+
+               $store->removeWatch( $user, $title );
+               $this->assertFalse(
+                       $store->isWatched( $user, $title ),
+                       'Page should be unwatched'
+               );
+               $this->assertEquals( $initialUserWatchedItems, $store->countWatchedItems( $user ) );
+               $watchedItemsForUser = $store->getWatchedItemsForUser( $user );
+               $this->assertCount( $initialUserWatchedItems, $watchedItemsForUser );
+               $watchedItemsForUserHasExpectedItem = false;
+               foreach ( $watchedItemsForUser as $watchedItem ) {
+                       if (
+                               $watchedItem->getUser()->equals( $user ) &&
+                               $watchedItem->getLinkTarget() == $title->getTitleValue()
+                       ) {
+                               $watchedItemsForUserHasExpectedItem = true;
+                       }
+               }
+               $this->assertFalse(
+                       $watchedItemsForUserHasExpectedItem,
+                       'getWatchedItemsForUser should not contain the page'
+               );
+               $this->assertEquals( $initialWatchers, $store->countWatchers( $title ) );
+               $this->assertEquals(
+                       $initialWatchers,
+                       $store->countWatchersMultiple( [ $title ] )[$title->getNamespace()][$title->getDBkey()]
+               );
+               $this->assertEquals(
+                       [ $title->getNamespace() => [ $title->getDBkey() => false ] ],
+                       $store->getNotificationTimestampsBatch( $user, [ $title ] )
+               );
+       }
+
+       public function testUpdateResetAndSetNotificationTimestamp() {
+               $user = $this->getUser();
+               $otherUser = ( new TestUser( 'WatchedItemStoreIntegrationTestUser_otherUser' ) )->getUser();
+               $title = Title::newFromText( 'WatchedItemStoreIntegrationTestPage' );
+               $store = MediaWikiServices::getInstance()->getWatchedItemStore();
+               $store->addWatch( $user, $title );
+               $this->assertNull( $store->loadWatchedItem( $user, $title )->getNotificationTimestamp() );
+               $initialVisitingWatchers = $store->countVisitingWatchers( $title, '20150202020202' );
+               $initialUnreadNotifications = $store->countUnreadNotifications( $user );
+
+               $store->updateNotificationTimestamp( $otherUser, $title, '20150202010101' );
+               $this->assertEquals(
+                       '20150202010101',
+                       $store->loadWatchedItem( $user, $title )->getNotificationTimestamp()
+               );
+               $this->assertEquals(
+                       [ $title->getNamespace() => [ $title->getDBkey() => '20150202010101' ] ],
+                       $store->getNotificationTimestampsBatch( $user, [ $title ] )
+               );
+               $this->assertEquals(
+                       $initialVisitingWatchers - 1,
+                       $store->countVisitingWatchers( $title, '20150202020202' )
+               );
+               $this->assertEquals(
+                       $initialVisitingWatchers - 1,
+                       $store->countVisitingWatchersMultiple(
+                               [ [ $title, '20150202020202' ] ]
+                       )[$title->getNamespace()][$title->getDBkey()]
+               );
+               $this->assertEquals(
+                       $initialUnreadNotifications + 1,
+                       $store->countUnreadNotifications( $user )
+               );
+               $this->assertSame(
+                       true,
+                       $store->countUnreadNotifications( $user, $initialUnreadNotifications + 1 )
+               );
+
+               $this->assertTrue( $store->resetNotificationTimestamp( $user, $title ) );
+               $this->assertNull( $store->getWatchedItem( $user, $title )->getNotificationTimestamp() );
+               $this->assertEquals(
+                       [ $title->getNamespace() => [ $title->getDBkey() => null ] ],
+                       $store->getNotificationTimestampsBatch( $user, [ $title ] )
+               );
+               $this->assertEquals(
+                       $initialVisitingWatchers,
+                       $store->countVisitingWatchers( $title, '20150202020202' )
+               );
+               $this->assertEquals(
+                       $initialVisitingWatchers,
+                       $store->countVisitingWatchersMultiple(
+                               [ [ $title, '20150202020202' ] ]
+                       )[$title->getNamespace()][$title->getDBkey()]
+               );
+               $this->assertEquals(
+                       [ 0 => [ 'WatchedItemStoreIntegrationTestPage' => $initialVisitingWatchers ] ],
+                       $store->countVisitingWatchersMultiple(
+                               [ [ $title, '20150202020202' ] ], $initialVisitingWatchers
+                       )
+               );
+               $this->assertEquals(
+                       [ 0 => [ 'WatchedItemStoreIntegrationTestPage' => 0 ] ],
+                       $store->countVisitingWatchersMultiple(
+                               [ [ $title, '20150202020202' ] ], $initialVisitingWatchers + 1
+                       )
+               );
+
+               // setNotificationTimestampsForUser specifying a title
+               $this->assertTrue(
+                       $store->setNotificationTimestampsForUser( $user, '20200202020202', [ $title ] )
+               );
+               $this->assertEquals(
+                       '20200202020202',
+                       $store->getWatchedItem( $user, $title )->getNotificationTimestamp()
+               );
+
+               // setNotificationTimestampsForUser not specifying a title
+               $this->assertTrue(
+                       $store->setNotificationTimestampsForUser( $user, '20210202020202' )
+               );
+               $this->assertEquals(
+                       '20210202020202',
+                       $store->getWatchedItem( $user, $title )->getNotificationTimestamp()
+               );
+       }
+
+       public function testDuplicateAllAssociatedEntries() {
+               $user = $this->getUser();
+               $titleOld = Title::newFromText( 'WatchedItemStoreIntegrationTestPageOld' );
+               $titleNew = Title::newFromText( 'WatchedItemStoreIntegrationTestPageNew' );
+               $store = MediaWikiServices::getInstance()->getWatchedItemStore();
+               $store->addWatch( $user, $titleOld->getSubjectPage() );
+               $store->addWatch( $user, $titleOld->getTalkPage() );
+               // Cleanup after previous tests
+               $store->removeWatch( $user, $titleNew->getSubjectPage() );
+               $store->removeWatch( $user, $titleNew->getTalkPage() );
+
+               $store->duplicateAllAssociatedEntries( $titleOld, $titleNew );
+
+               $this->assertTrue( $store->isWatched( $user, $titleOld->getSubjectPage() ) );
+               $this->assertTrue( $store->isWatched( $user, $titleOld->getTalkPage() ) );
+               $this->assertTrue( $store->isWatched( $user, $titleNew->getSubjectPage() ) );
+               $this->assertTrue( $store->isWatched( $user, $titleNew->getTalkPage() ) );
+       }
+
+}
diff --git a/tests/phpunit/includes/watcheditem/WatchedItemStoreUnitTest.php b/tests/phpunit/includes/watcheditem/WatchedItemStoreUnitTest.php
new file mode 100644 (file)
index 0000000..43b4fe9
--- /dev/null
@@ -0,0 +1,2675 @@
+<?php
+use MediaWiki\Linker\LinkTarget;
+use Wikimedia\Rdbms\LoadBalancer;
+use Wikimedia\ScopedCallback;
+
+/**
+ * @author Addshore
+ *
+ * @covers WatchedItemStore
+ */
+class WatchedItemStoreUnitTest extends MediaWikiTestCase {
+
+       /**
+        * @return PHPUnit_Framework_MockObject_MockObject|IDatabase
+        */
+       private function getMockDb() {
+               return $this->createMock( IDatabase::class );
+       }
+
+       /**
+        * @return PHPUnit_Framework_MockObject_MockObject|LoadBalancer
+        */
+       private function getMockLoadBalancer(
+               $mockDb,
+               $expectedConnectionType = null
+       ) {
+               $mock = $this->getMockBuilder( LoadBalancer::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               if ( $expectedConnectionType !== null ) {
+                       $mock->expects( $this->any() )
+                               ->method( 'getConnectionRef' )
+                               ->with( $expectedConnectionType )
+                               ->will( $this->returnValue( $mockDb ) );
+               } else {
+                       $mock->expects( $this->any() )
+                               ->method( 'getConnectionRef' )
+                               ->will( $this->returnValue( $mockDb ) );
+               }
+               return $mock;
+       }
+
+       /**
+        * @return PHPUnit_Framework_MockObject_MockObject|HashBagOStuff
+        */
+       private function getMockCache() {
+               $mock = $this->getMockBuilder( HashBagOStuff::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $mock->expects( $this->any() )
+                       ->method( 'makeKey' )
+                       ->will( $this->returnCallback( function () {
+                               return implode( ':', func_get_args() );
+                       } ) );
+               return $mock;
+       }
+
+       /**
+        * @return PHPUnit_Framework_MockObject_MockObject|ReadOnlyMode
+        */
+       private function getMockReadOnlyMode( $readOnly = false ) {
+               $mock = $this->getMockBuilder( ReadOnlyMode::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+               $mock->expects( $this->any() )
+                       ->method( 'isReadOnly' )
+                       ->will( $this->returnValue( $readOnly ) );
+               return $mock;
+       }
+
+       /**
+        * @param int $id
+        * @return PHPUnit_Framework_MockObject_MockObject|User
+        */
+       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 ) );
+               return $mock;
+       }
+
+       /**
+        * @return User
+        */
+       private function getAnonUser() {
+               return User::newFromName( 'Anon_User' );
+       }
+
+       private function getFakeRow( array $rowValues ) {
+               $fakeRow = new stdClass();
+               foreach ( $rowValues as $valueName => $value ) {
+                       $fakeRow->$valueName = $value;
+               }
+               return $fakeRow;
+       }
+
+       private function newWatchedItemStore( LoadBalancer $loadBalancer, HashBagOStuff $cache,
+               ReadOnlyMode $readOnlyMode
+       ) {
+               return new WatchedItemStore(
+                       $loadBalancer,
+                       $cache,
+                       $readOnlyMode
+               );
+       }
+
+       public function testCountWatchedItems() {
+               $user = $this->getMockNonAnonUserWithId( 1 );
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->exactly( 1 ) )
+                       ->method( 'selectField' )
+                       ->with(
+                               'watchlist',
+                               'COUNT(*)',
+                               [
+                                       'wl_user' => $user->getId(),
+                               ],
+                               $this->isType( 'string' )
+                       )
+                       ->will( $this->returnValue( 12 ) );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->never() )->method( 'set' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertEquals( 12, $store->countWatchedItems( $user ) );
+       }
+
+       public function testCountWatchers() {
+               $titleValue = new TitleValue( 0, 'SomeDbKey' );
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->exactly( 1 ) )
+                       ->method( 'selectField' )
+                       ->with(
+                               'watchlist',
+                               'COUNT(*)',
+                               [
+                                       'wl_namespace' => $titleValue->getNamespace(),
+                                       'wl_title' => $titleValue->getDBkey(),
+                               ],
+                               $this->isType( 'string' )
+                       )
+                       ->will( $this->returnValue( 7 ) );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->never() )->method( 'set' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertEquals( 7, $store->countWatchers( $titleValue ) );
+       }
+
+       public function testCountWatchersMultiple() {
+               $titleValues = [
+                       new TitleValue( 0, 'SomeDbKey' ),
+                       new TitleValue( 0, 'OtherDbKey' ),
+                       new TitleValue( 1, 'AnotherDbKey' ),
+               ];
+
+               $mockDb = $this->getMockDb();
+
+               $dbResult = [
+                       $this->getFakeRow( [ 'wl_title' => 'SomeDbKey', 'wl_namespace' => 0, 'watchers' => 100 ] ),
+                       $this->getFakeRow( [ 'wl_title' => 'OtherDbKey', 'wl_namespace' => 0, 'watchers' => 300 ] ),
+                       $this->getFakeRow( [ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => 1, 'watchers' => 500 ]
+                       ),
+               ];
+               $mockDb->expects( $this->once() )
+                       ->method( 'makeWhereFrom2d' )
+                       ->with(
+                               [ [ 'SomeDbKey' => 1, 'OtherDbKey' => 1 ], [ 'AnotherDbKey' => 1 ] ],
+                               $this->isType( 'string' ),
+                               $this->isType( 'string' )
+                               )
+                       ->will( $this->returnValue( 'makeWhereFrom2d return value' ) );
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               'watchlist',
+                               [ 'wl_title', 'wl_namespace', 'watchers' => 'COUNT(*)' ],
+                               [ 'makeWhereFrom2d return value' ],
+                               $this->isType( 'string' ),
+                               [
+                                       'GROUP BY' => [ 'wl_namespace', 'wl_title' ],
+                               ]
+                       )
+                       ->will(
+                               $this->returnValue( $dbResult )
+                       );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->never() )->method( 'set' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $expected = [
+                       0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ],
+                       1 => [ 'AnotherDbKey' => 500 ],
+               ];
+               $this->assertEquals( $expected, $store->countWatchersMultiple( $titleValues ) );
+       }
+
+       public function provideIntWithDbUnsafeVersion() {
+               return [
+                       [ 50 ],
+                       [ "50; DROP TABLE watchlist;\n--" ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideIntWithDbUnsafeVersion
+        */
+       public function testCountWatchersMultiple_withMinimumWatchers( $minWatchers ) {
+               $titleValues = [
+                       new TitleValue( 0, 'SomeDbKey' ),
+                       new TitleValue( 0, 'OtherDbKey' ),
+                       new TitleValue( 1, 'AnotherDbKey' ),
+               ];
+
+               $mockDb = $this->getMockDb();
+
+               $dbResult = [
+                       $this->getFakeRow( [ 'wl_title' => 'SomeDbKey', 'wl_namespace' => 0, 'watchers' => 100 ] ),
+                       $this->getFakeRow( [ 'wl_title' => 'OtherDbKey', 'wl_namespace' => 0, 'watchers' => 300 ] ),
+                       $this->getFakeRow( [ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => 1, 'watchers' => 500 ]
+                       ),
+               ];
+               $mockDb->expects( $this->once() )
+                       ->method( 'makeWhereFrom2d' )
+                       ->with(
+                               [ [ 'SomeDbKey' => 1, 'OtherDbKey' => 1 ], [ 'AnotherDbKey' => 1 ] ],
+                               $this->isType( 'string' ),
+                               $this->isType( 'string' )
+                       )
+                       ->will( $this->returnValue( 'makeWhereFrom2d return value' ) );
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               'watchlist',
+                               [ 'wl_title', 'wl_namespace', 'watchers' => 'COUNT(*)' ],
+                               [ 'makeWhereFrom2d return value' ],
+                               $this->isType( 'string' ),
+                               [
+                                       'GROUP BY' => [ 'wl_namespace', 'wl_title' ],
+                                       'HAVING' => 'COUNT(*) >= 50',
+                               ]
+                       )
+                       ->will(
+                               $this->returnValue( $dbResult )
+                       );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->never() )->method( 'set' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $expected = [
+                       0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ],
+                       1 => [ 'AnotherDbKey' => 500 ],
+               ];
+               $this->assertEquals(
+                       $expected,
+                       $store->countWatchersMultiple( $titleValues, [ 'minimumWatchers' => $minWatchers ] )
+               );
+       }
+
+       public function testCountVisitingWatchers() {
+               $titleValue = new TitleValue( 0, 'SomeDbKey' );
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->exactly( 1 ) )
+                       ->method( 'selectField' )
+                       ->with(
+                               'watchlist',
+                               'COUNT(*)',
+                               [
+                                       'wl_namespace' => $titleValue->getNamespace(),
+                                       'wl_title' => $titleValue->getDBkey(),
+                                       'wl_notificationtimestamp >= \'TS111TS\' OR wl_notificationtimestamp IS NULL',
+                               ],
+                               $this->isType( 'string' )
+                       )
+                       ->will( $this->returnValue( 7 ) );
+               $mockDb->expects( $this->exactly( 1 ) )
+                       ->method( 'addQuotes' )
+                       ->will( $this->returnCallback( function ( $value ) {
+                               return "'$value'";
+                       } ) );
+               $mockDb->expects( $this->exactly( 1 ) )
+                       ->method( 'timestamp' )
+                       ->will( $this->returnCallback( function ( $value ) {
+                               return 'TS' . $value . 'TS';
+                       } ) );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'set' );
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertEquals( 7, $store->countVisitingWatchers( $titleValue, '111' ) );
+       }
+
+       public function testCountVisitingWatchersMultiple() {
+               $titleValuesWithThresholds = [
+                       [ new TitleValue( 0, 'SomeDbKey' ), '111' ],
+                       [ new TitleValue( 0, 'OtherDbKey' ), '111' ],
+                       [ new TitleValue( 1, 'AnotherDbKey' ), '123' ],
+               ];
+
+               $dbResult = [
+                       $this->getFakeRow( [ 'wl_title' => 'SomeDbKey', 'wl_namespace' => 0, 'watchers' => 100 ] ),
+                       $this->getFakeRow( [ 'wl_title' => 'OtherDbKey', 'wl_namespace' => 0, 'watchers' => 300 ] ),
+                       $this->getFakeRow( [ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => 1, 'watchers' => 500 ] ),
+               ];
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->exactly( 2 * 3 ) )
+                       ->method( 'addQuotes' )
+                       ->will( $this->returnCallback( function ( $value ) {
+                               return "'$value'";
+                       } ) );
+               $mockDb->expects( $this->exactly( 3 ) )
+                       ->method( 'timestamp' )
+                       ->will( $this->returnCallback( function ( $value ) {
+                               return 'TS' . $value . 'TS';
+                       } ) );
+               $mockDb->expects( $this->any() )
+                       ->method( 'makeList' )
+                       ->with(
+                               $this->isType( 'array' ),
+                               $this->isType( 'int' )
+                       )
+                       ->will( $this->returnCallback( function ( $a, $conj ) {
+                               $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR ';
+                               return join( $sqlConj, array_map( function ( $s ) {
+                                       return '(' . $s . ')';
+                               }, $a
+                               ) );
+                       } ) );
+               $mockDb->expects( $this->never() )
+                       ->method( 'makeWhereFrom2d' );
+
+               $expectedCond =
+                       '((wl_namespace = 0) AND (' .
+                       "(((wl_title = 'SomeDbKey') AND (" .
+                       "(wl_notificationtimestamp >= 'TS111TS') OR (wl_notificationtimestamp IS NULL)" .
+                       ')) OR (' .
+                       "(wl_title = 'OtherDbKey') AND (" .
+                       "(wl_notificationtimestamp >= 'TS111TS') OR (wl_notificationtimestamp IS NULL)" .
+                       '))))' .
+                       ') OR ((wl_namespace = 1) AND (' .
+                       "(((wl_title = 'AnotherDbKey') AND (".
+                       "(wl_notificationtimestamp >= 'TS123TS') OR (wl_notificationtimestamp IS NULL)" .
+                       ')))))';
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               'watchlist',
+                               [ 'wl_namespace', 'wl_title', 'watchers' => 'COUNT(*)' ],
+                               $expectedCond,
+                               $this->isType( 'string' ),
+                               [
+                                       'GROUP BY' => [ 'wl_namespace', 'wl_title' ],
+                               ]
+                       )
+                       ->will(
+                               $this->returnValue( $dbResult )
+                       );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->never() )->method( 'set' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $expected = [
+                       0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ],
+                       1 => [ 'AnotherDbKey' => 500 ],
+               ];
+               $this->assertEquals(
+                       $expected,
+                       $store->countVisitingWatchersMultiple( $titleValuesWithThresholds )
+               );
+       }
+
+       public function testCountVisitingWatchersMultiple_withMissingTargets() {
+               $titleValuesWithThresholds = [
+                       [ new TitleValue( 0, 'SomeDbKey' ), '111' ],
+                       [ new TitleValue( 0, 'OtherDbKey' ), '111' ],
+                       [ new TitleValue( 1, 'AnotherDbKey' ), '123' ],
+                       [ new TitleValue( 0, 'SomeNotExisitingDbKey' ), null ],
+                       [ new TitleValue( 0, 'OtherNotExisitingDbKey' ), null ],
+               ];
+
+               $dbResult = [
+                       $this->getFakeRow( [ 'wl_title' => 'SomeDbKey', 'wl_namespace' => 0, 'watchers' => 100 ] ),
+                       $this->getFakeRow( [ 'wl_title' => 'OtherDbKey', 'wl_namespace' => 0, 'watchers' => 300 ] ),
+                       $this->getFakeRow( [ 'wl_title' => 'AnotherDbKey', 'wl_namespace' => 1, 'watchers' => 500 ] ),
+                       $this->getFakeRow(
+                               [ 'wl_title' => 'SomeNotExisitingDbKey', 'wl_namespace' => 0, 'watchers' => 100 ]
+                       ),
+                       $this->getFakeRow(
+                               [ 'wl_title' => 'OtherNotExisitingDbKey', 'wl_namespace' => 0, 'watchers' => 200 ]
+                       ),
+               ];
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->exactly( 2 * 3 ) )
+                       ->method( 'addQuotes' )
+                       ->will( $this->returnCallback( function ( $value ) {
+                               return "'$value'";
+                       } ) );
+               $mockDb->expects( $this->exactly( 3 ) )
+                       ->method( 'timestamp' )
+                       ->will( $this->returnCallback( function ( $value ) {
+                               return 'TS' . $value . 'TS';
+                       } ) );
+               $mockDb->expects( $this->any() )
+                       ->method( 'makeList' )
+                       ->with(
+                               $this->isType( 'array' ),
+                               $this->isType( 'int' )
+                       )
+                       ->will( $this->returnCallback( function ( $a, $conj ) {
+                               $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR ';
+                               return join( $sqlConj, array_map( function ( $s ) {
+                                       return '(' . $s . ')';
+                               }, $a
+                               ) );
+                       } ) );
+               $mockDb->expects( $this->once() )
+                       ->method( 'makeWhereFrom2d' )
+                       ->with(
+                               [ [ 'SomeNotExisitingDbKey' => 1, 'OtherNotExisitingDbKey' => 1 ] ],
+                               $this->isType( 'string' ),
+                               $this->isType( 'string' )
+                       )
+                       ->will( $this->returnValue( 'makeWhereFrom2d return value' ) );
+
+               $expectedCond =
+                       '((wl_namespace = 0) AND (' .
+                       "(((wl_title = 'SomeDbKey') AND (" .
+                       "(wl_notificationtimestamp >= 'TS111TS') OR (wl_notificationtimestamp IS NULL)" .
+                       ')) OR (' .
+                       "(wl_title = 'OtherDbKey') AND (" .
+                       "(wl_notificationtimestamp >= 'TS111TS') OR (wl_notificationtimestamp IS NULL)" .
+                       '))))' .
+                       ') OR ((wl_namespace = 1) AND (' .
+                       "(((wl_title = 'AnotherDbKey') AND (".
+                       "(wl_notificationtimestamp >= 'TS123TS') OR (wl_notificationtimestamp IS NULL)" .
+                       '))))' .
+                       ') OR ' .
+                       '(makeWhereFrom2d return value)';
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               'watchlist',
+                               [ 'wl_namespace', 'wl_title', 'watchers' => 'COUNT(*)' ],
+                               $expectedCond,
+                               $this->isType( 'string' ),
+                               [
+                                       'GROUP BY' => [ 'wl_namespace', 'wl_title' ],
+                               ]
+                       )
+                       ->will(
+                               $this->returnValue( $dbResult )
+                       );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->never() )->method( 'set' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $expected = [
+                       0 => [
+                               'SomeDbKey' => 100, 'OtherDbKey' => 300,
+                               'SomeNotExisitingDbKey' => 100, 'OtherNotExisitingDbKey' => 200
+                       ],
+                       1 => [ 'AnotherDbKey' => 500 ],
+               ];
+               $this->assertEquals(
+                       $expected,
+                       $store->countVisitingWatchersMultiple( $titleValuesWithThresholds )
+               );
+       }
+
+       /**
+        * @dataProvider provideIntWithDbUnsafeVersion
+        */
+       public function testCountVisitingWatchersMultiple_withMinimumWatchers( $minWatchers ) {
+               $titleValuesWithThresholds = [
+                       [ new TitleValue( 0, 'SomeDbKey' ), '111' ],
+                       [ new TitleValue( 0, 'OtherDbKey' ), '111' ],
+                       [ new TitleValue( 1, 'AnotherDbKey' ), '123' ],
+               ];
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->any() )
+                       ->method( 'makeList' )
+                       ->will( $this->returnValue( 'makeList return value' ) );
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               'watchlist',
+                               [ 'wl_namespace', 'wl_title', 'watchers' => 'COUNT(*)' ],
+                               'makeList return value',
+                               $this->isType( 'string' ),
+                               [
+                                       'GROUP BY' => [ 'wl_namespace', 'wl_title' ],
+                                       'HAVING' => 'COUNT(*) >= 50',
+                               ]
+                       )
+                       ->will(
+                               $this->returnValue( [] )
+                       );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->never() )->method( 'set' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $expected = [
+                       0 => [ 'SomeDbKey' => 0, 'OtherDbKey' => 0 ],
+                       1 => [ 'AnotherDbKey' => 0 ],
+               ];
+               $this->assertEquals(
+                       $expected,
+                       $store->countVisitingWatchersMultiple( $titleValuesWithThresholds, $minWatchers )
+               );
+       }
+
+       public function testCountUnreadNotifications() {
+               $user = $this->getMockNonAnonUserWithId( 1 );
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->exactly( 1 ) )
+                       ->method( 'selectRowCount' )
+                       ->with(
+                               'watchlist',
+                               '1',
+                               [
+                                       "wl_notificationtimestamp IS NOT NULL",
+                                       'wl_user' => 1,
+                               ],
+                               $this->isType( 'string' )
+                       )
+                       ->will( $this->returnValue( 9 ) );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'set' );
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertEquals( 9, $store->countUnreadNotifications( $user ) );
+       }
+
+       /**
+        * @dataProvider provideIntWithDbUnsafeVersion
+        */
+       public function testCountUnreadNotifications_withUnreadLimit_overLimit( $limit ) {
+               $user = $this->getMockNonAnonUserWithId( 1 );
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->exactly( 1 ) )
+                       ->method( 'selectRowCount' )
+                       ->with(
+                               'watchlist',
+                               '1',
+                               [
+                                       "wl_notificationtimestamp IS NOT NULL",
+                                       'wl_user' => 1,
+                               ],
+                               $this->isType( 'string' ),
+                               [ 'LIMIT' => 50 ]
+                       )
+                       ->will( $this->returnValue( 50 ) );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'set' );
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertSame(
+                       true,
+                       $store->countUnreadNotifications( $user, $limit )
+               );
+       }
+
+       /**
+        * @dataProvider provideIntWithDbUnsafeVersion
+        */
+       public function testCountUnreadNotifications_withUnreadLimit_underLimit( $limit ) {
+               $user = $this->getMockNonAnonUserWithId( 1 );
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->exactly( 1 ) )
+                       ->method( 'selectRowCount' )
+                       ->with(
+                               'watchlist',
+                               '1',
+                               [
+                                       "wl_notificationtimestamp IS NOT NULL",
+                                       'wl_user' => 1,
+                               ],
+                               $this->isType( 'string' ),
+                               [ 'LIMIT' => 50 ]
+                       )
+                       ->will( $this->returnValue( 9 ) );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'set' );
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertEquals(
+                       9,
+                       $store->countUnreadNotifications( $user, $limit )
+               );
+       }
+
+       public function testDuplicateEntry_nothingToDuplicate() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               'watchlist',
+                               [
+                                       'wl_user',
+                                       'wl_notificationtimestamp',
+                               ],
+                               [
+                                       'wl_namespace' => 0,
+                                       'wl_title' => 'Old_Title',
+                               ],
+                               'WatchedItemStore::duplicateEntry',
+                               [ 'FOR UPDATE' ]
+                       )
+                       ->will( $this->returnValue( new FakeResultWrapper( [] ) ) );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $this->getMockCache(),
+                       $this->getMockReadOnlyMode()
+               );
+
+               $store->duplicateEntry(
+                       Title::newFromText( 'Old_Title' ),
+                       Title::newFromText( 'New_Title' )
+               );
+       }
+
+       public function testDuplicateEntry_somethingToDuplicate() {
+               $fakeRows = [
+                       $this->getFakeRow( [ 'wl_user' => 1, 'wl_notificationtimestamp' => '20151212010101' ] ),
+                       $this->getFakeRow( [ 'wl_user' => 2, 'wl_notificationtimestamp' => null ] ),
+               ];
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->at( 0 ) )
+                       ->method( 'select' )
+                       ->with(
+                               'watchlist',
+                               [
+                                       'wl_user',
+                                       'wl_notificationtimestamp',
+                               ],
+                               [
+                                       'wl_namespace' => 0,
+                                       'wl_title' => 'Old_Title',
+                               ]
+                       )
+                       ->will( $this->returnValue( new FakeResultWrapper( $fakeRows ) ) );
+               $mockDb->expects( $this->at( 1 ) )
+                       ->method( 'replace' )
+                       ->with(
+                               'watchlist',
+                               [ [ 'wl_user', 'wl_namespace', 'wl_title' ] ],
+                               [
+                                       [
+                                               'wl_user' => 1,
+                                               'wl_namespace' => 0,
+                                               'wl_title' => 'New_Title',
+                                               'wl_notificationtimestamp' => '20151212010101',
+                                       ],
+                                       [
+                                               'wl_user' => 2,
+                                               'wl_namespace' => 0,
+                                               'wl_title' => 'New_Title',
+                                               'wl_notificationtimestamp' => null,
+                                       ],
+                               ],
+                               $this->isType( 'string' )
+                       );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $store->duplicateEntry(
+                       Title::newFromText( 'Old_Title' ),
+                       Title::newFromText( 'New_Title' )
+               );
+       }
+
+       public function testDuplicateAllAssociatedEntries_nothingToDuplicate() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->at( 0 ) )
+                       ->method( 'select' )
+                       ->with(
+                               'watchlist',
+                               [
+                                       'wl_user',
+                                       'wl_notificationtimestamp',
+                               ],
+                               [
+                                       'wl_namespace' => 0,
+                                       'wl_title' => 'Old_Title',
+                               ]
+                       )
+                       ->will( $this->returnValue( new FakeResultWrapper( [] ) ) );
+               $mockDb->expects( $this->at( 1 ) )
+                       ->method( 'select' )
+                       ->with(
+                               'watchlist',
+                               [
+                                       'wl_user',
+                                       'wl_notificationtimestamp',
+                               ],
+                               [
+                                       'wl_namespace' => 1,
+                                       'wl_title' => 'Old_Title',
+                               ]
+                       )
+                       ->will( $this->returnValue( new FakeResultWrapper( [] ) ) );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $store->duplicateAllAssociatedEntries(
+                       Title::newFromText( 'Old_Title' ),
+                       Title::newFromText( 'New_Title' )
+               );
+       }
+
+       public function provideLinkTargetPairs() {
+               return [
+                       [ Title::newFromText( 'Old_Title' ), Title::newFromText( 'New_Title' ) ],
+                       [ new TitleValue( 0, 'Old_Title' ),  new TitleValue( 0, 'New_Title' ) ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideLinkTargetPairs
+        */
+       public function testDuplicateAllAssociatedEntries_somethingToDuplicate(
+               LinkTarget $oldTarget,
+               LinkTarget $newTarget
+       ) {
+               $fakeRows = [
+                       $this->getFakeRow( [ 'wl_user' => 1, 'wl_notificationtimestamp' => '20151212010101' ] ),
+               ];
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->at( 0 ) )
+                       ->method( 'select' )
+                       ->with(
+                               'watchlist',
+                               [
+                                       'wl_user',
+                                       'wl_notificationtimestamp',
+                               ],
+                               [
+                                       'wl_namespace' => $oldTarget->getNamespace(),
+                                       'wl_title' => $oldTarget->getDBkey(),
+                               ]
+                       )
+                       ->will( $this->returnValue( new FakeResultWrapper( $fakeRows ) ) );
+               $mockDb->expects( $this->at( 1 ) )
+                       ->method( 'replace' )
+                       ->with(
+                               'watchlist',
+                               [ [ 'wl_user', 'wl_namespace', 'wl_title' ] ],
+                               [
+                                       [
+                                               'wl_user' => 1,
+                                               'wl_namespace' => $newTarget->getNamespace(),
+                                               'wl_title' => $newTarget->getDBkey(),
+                                               'wl_notificationtimestamp' => '20151212010101',
+                                       ],
+                               ],
+                               $this->isType( 'string' )
+                       );
+               $mockDb->expects( $this->at( 2 ) )
+                       ->method( 'select' )
+                       ->with(
+                               'watchlist',
+                               [
+                                       'wl_user',
+                                       'wl_notificationtimestamp',
+                               ],
+                               [
+                                       'wl_namespace' => $oldTarget->getNamespace() + 1,
+                                       'wl_title' => $oldTarget->getDBkey(),
+                               ]
+                       )
+                       ->will( $this->returnValue( new FakeResultWrapper( $fakeRows ) ) );
+               $mockDb->expects( $this->at( 3 ) )
+                       ->method( 'replace' )
+                       ->with(
+                               'watchlist',
+                               [ [ 'wl_user', 'wl_namespace', 'wl_title' ] ],
+                               [
+                                       [
+                                               'wl_user' => 1,
+                                               'wl_namespace' => $newTarget->getNamespace() + 1,
+                                               'wl_title' => $newTarget->getDBkey(),
+                                               'wl_notificationtimestamp' => '20151212010101',
+                                       ],
+                               ],
+                               $this->isType( 'string' )
+                       );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $store->duplicateAllAssociatedEntries(
+                       $oldTarget,
+                       $newTarget
+               );
+       }
+
+       public function testAddWatch_nonAnonymousUser() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'insert' )
+                       ->with(
+                               'watchlist',
+                               [
+                                       [
+                                               'wl_user' => 1,
+                                               'wl_namespace' => 0,
+                                               'wl_title' => 'Some_Page',
+                                               'wl_notificationtimestamp' => null,
+                                       ]
+                               ]
+                       );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->once() )
+                       ->method( 'delete' )
+                       ->with( '0:Some_Page:1' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $store->addWatch(
+                       $this->getMockNonAnonUserWithId( 1 ),
+                       Title::newFromText( 'Some_Page' )
+               );
+       }
+
+       public function testAddWatch_anonymousUser() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->never() )
+                       ->method( 'insert' );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )
+                       ->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $store->addWatch(
+                       $this->getAnonUser(),
+                       Title::newFromText( 'Some_Page' )
+               );
+       }
+
+       public function testAddWatchBatchForUser_readOnlyDBReturnsFalse() {
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $this->getMockDb() ),
+                       $this->getMockCache(),
+                       $this->getMockReadOnlyMode( true )
+               );
+
+               $this->assertFalse(
+                       $store->addWatchBatchForUser(
+                               $this->getMockNonAnonUserWithId( 1 ),
+                               [ new TitleValue( 0, 'Some_Page' ), new TitleValue( 1, 'Some_Page' ) ]
+                       )
+               );
+       }
+
+       public function testAddWatchBatchForUser_nonAnonymousUser() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'insert' )
+                       ->with(
+                               'watchlist',
+                               [
+                                       [
+                                               'wl_user' => 1,
+                                               'wl_namespace' => 0,
+                                               'wl_title' => 'Some_Page',
+                                               'wl_notificationtimestamp' => null,
+                                       ],
+                                       [
+                                               'wl_user' => 1,
+                                               'wl_namespace' => 1,
+                                               'wl_title' => 'Some_Page',
+                                               'wl_notificationtimestamp' => null,
+                                       ]
+                               ]
+                       );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->exactly( 2 ) )
+                       ->method( 'delete' );
+               $mockCache->expects( $this->at( 1 ) )
+                       ->method( 'delete' )
+                       ->with( '0:Some_Page:1' );
+               $mockCache->expects( $this->at( 3 ) )
+                       ->method( 'delete' )
+                       ->with( '1:Some_Page:1' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $mockUser = $this->getMockNonAnonUserWithId( 1 );
+
+               $this->assertTrue(
+                       $store->addWatchBatchForUser(
+                               $mockUser,
+                               [ new TitleValue( 0, 'Some_Page' ), new TitleValue( 1, 'Some_Page' ) ]
+                       )
+               );
+       }
+
+       public function testAddWatchBatchForUser_anonymousUsersAreSkipped() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->never() )
+                       ->method( 'insert' );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )
+                       ->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertFalse(
+                       $store->addWatchBatchForUser(
+                               $this->getAnonUser(),
+                               [ new TitleValue( 0, 'Other_Page' ) ]
+                       )
+               );
+       }
+
+       public function testAddWatchBatchReturnsTrue_whenGivenEmptyList() {
+               $user = $this->getMockNonAnonUserWithId( 1 );
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->never() )
+                       ->method( 'insert' );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )
+                       ->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertTrue(
+                       $store->addWatchBatchForUser( $user, [] )
+               );
+       }
+
+       public function testLoadWatchedItem_existingItem() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'selectRow' )
+                       ->with(
+                               'watchlist',
+                               'wl_notificationtimestamp',
+                               [
+                                       'wl_user' => 1,
+                                       'wl_namespace' => 0,
+                                       'wl_title' => 'SomeDbKey',
+                               ]
+                       )
+                       ->will( $this->returnValue(
+                               $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] )
+                       ) );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->once() )
+                       ->method( 'set' )
+                       ->with(
+                               '0:SomeDbKey:1'
+                       );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $watchedItem = $store->loadWatchedItem(
+                       $this->getMockNonAnonUserWithId( 1 ),
+                       new TitleValue( 0, 'SomeDbKey' )
+               );
+               $this->assertInstanceOf( 'WatchedItem', $watchedItem );
+               $this->assertEquals( 1, $watchedItem->getUser()->getId() );
+               $this->assertEquals( 'SomeDbKey', $watchedItem->getLinkTarget()->getDBkey() );
+               $this->assertEquals( 0, $watchedItem->getLinkTarget()->getNamespace() );
+       }
+
+       public function testLoadWatchedItem_noItem() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'selectRow' )
+                       ->with(
+                               'watchlist',
+                               'wl_notificationtimestamp',
+                               [
+                                       'wl_user' => 1,
+                                       'wl_namespace' => 0,
+                                       'wl_title' => 'SomeDbKey',
+                               ]
+                       )
+                       ->will( $this->returnValue( [] ) );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertFalse(
+                       $store->loadWatchedItem(
+                               $this->getMockNonAnonUserWithId( 1 ),
+                               new TitleValue( 0, 'SomeDbKey' )
+                       )
+               );
+       }
+
+       public function testLoadWatchedItem_anonymousUser() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->never() )
+                       ->method( 'selectRow' );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertFalse(
+                       $store->loadWatchedItem(
+                               $this->getAnonUser(),
+                               new TitleValue( 0, 'SomeDbKey' )
+                       )
+               );
+       }
+
+       public function testRemoveWatch_existingItem() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'delete' )
+                       ->with(
+                               'watchlist',
+                               [
+                                       'wl_user' => 1,
+                                       'wl_namespace' => 0,
+                                       'wl_title' => 'SomeDbKey',
+                               ]
+                       );
+               $mockDb->expects( $this->once() )
+                       ->method( 'affectedRows' )
+                       ->will( $this->returnValue( 1 ) );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->once() )
+                       ->method( 'delete' )
+                       ->with( '0:SomeDbKey:1' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertTrue(
+                       $store->removeWatch(
+                               $this->getMockNonAnonUserWithId( 1 ),
+                               new TitleValue( 0, 'SomeDbKey' )
+                       )
+               );
+       }
+
+       public function testRemoveWatch_noItem() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'delete' )
+                       ->with(
+                               'watchlist',
+                               [
+                                       'wl_user' => 1,
+                                       'wl_namespace' => 0,
+                                       'wl_title' => 'SomeDbKey',
+                               ]
+                       );
+               $mockDb->expects( $this->once() )
+                       ->method( 'affectedRows' )
+                       ->will( $this->returnValue( 0 ) );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->once() )
+                       ->method( 'delete' )
+                       ->with( '0:SomeDbKey:1' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertFalse(
+                       $store->removeWatch(
+                               $this->getMockNonAnonUserWithId( 1 ),
+                               new TitleValue( 0, 'SomeDbKey' )
+                       )
+               );
+       }
+
+       public function testRemoveWatch_anonymousUser() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->never() )
+                       ->method( 'delete' );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->never() )
+                       ->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertFalse(
+                       $store->removeWatch(
+                               $this->getAnonUser(),
+                               new TitleValue( 0, 'SomeDbKey' )
+                       )
+               );
+       }
+
+       public function testGetWatchedItem_existingItem() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'selectRow' )
+                       ->with(
+                               'watchlist',
+                               'wl_notificationtimestamp',
+                               [
+                                       'wl_user' => 1,
+                                       'wl_namespace' => 0,
+                                       'wl_title' => 'SomeDbKey',
+                               ]
+                       )
+                       ->will( $this->returnValue(
+                               $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] )
+                       ) );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'delete' );
+               $mockCache->expects( $this->once() )
+                       ->method( 'get' )
+                       ->with(
+                               '0:SomeDbKey:1'
+                       )
+                       ->will( $this->returnValue( null ) );
+               $mockCache->expects( $this->once() )
+                       ->method( 'set' )
+                       ->with(
+                               '0:SomeDbKey:1'
+                       );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $watchedItem = $store->getWatchedItem(
+                       $this->getMockNonAnonUserWithId( 1 ),
+                       new TitleValue( 0, 'SomeDbKey' )
+               );
+               $this->assertInstanceOf( 'WatchedItem', $watchedItem );
+               $this->assertEquals( 1, $watchedItem->getUser()->getId() );
+               $this->assertEquals( 'SomeDbKey', $watchedItem->getLinkTarget()->getDBkey() );
+               $this->assertEquals( 0, $watchedItem->getLinkTarget()->getNamespace() );
+       }
+
+       public function testGetWatchedItem_cachedItem() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->never() )
+                       ->method( 'selectRow' );
+
+               $mockUser = $this->getMockNonAnonUserWithId( 1 );
+               $linkTarget = new TitleValue( 0, 'SomeDbKey' );
+               $cachedItem = new WatchedItem( $mockUser, $linkTarget, '20151212010101' );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'delete' );
+               $mockCache->expects( $this->never() )->method( 'set' );
+               $mockCache->expects( $this->once() )
+                       ->method( 'get' )
+                       ->with(
+                               '0:SomeDbKey:1'
+                       )
+                       ->will( $this->returnValue( $cachedItem ) );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertEquals(
+                       $cachedItem,
+                       $store->getWatchedItem(
+                               $mockUser,
+                               $linkTarget
+                       )
+               );
+       }
+
+       public function testGetWatchedItem_noItem() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'selectRow' )
+                       ->with(
+                               'watchlist',
+                               'wl_notificationtimestamp',
+                               [
+                                       'wl_user' => 1,
+                                       'wl_namespace' => 0,
+                                       'wl_title' => 'SomeDbKey',
+                               ]
+                       )
+                       ->will( $this->returnValue( [] ) );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'set' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+               $mockCache->expects( $this->once() )
+                       ->method( 'get' )
+                       ->with( '0:SomeDbKey:1' )
+                       ->will( $this->returnValue( false ) );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertFalse(
+                       $store->getWatchedItem(
+                               $this->getMockNonAnonUserWithId( 1 ),
+                               new TitleValue( 0, 'SomeDbKey' )
+                       )
+               );
+       }
+
+       public function testGetWatchedItem_anonymousUser() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->never() )
+                       ->method( 'selectRow' );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'set' );
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertFalse(
+                       $store->getWatchedItem(
+                               $this->getAnonUser(),
+                               new TitleValue( 0, 'SomeDbKey' )
+                       )
+               );
+       }
+
+       public function testGetWatchedItemsForUser() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               'watchlist',
+                               [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
+                               [ 'wl_user' => 1 ]
+                       )
+                       ->will( $this->returnValue( [
+                               $this->getFakeRow( [
+                                       'wl_namespace' => 0,
+                                       'wl_title' => 'Foo1',
+                                       'wl_notificationtimestamp' => '20151212010101',
+                               ] ),
+                               $this->getFakeRow( [
+                                       'wl_namespace' => 1,
+                                       'wl_title' => 'Foo2',
+                                       'wl_notificationtimestamp' => null,
+                               ] ),
+                       ] ) );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'delete' );
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->never() )->method( 'set' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+               $user = $this->getMockNonAnonUserWithId( 1 );
+
+               $watchedItems = $store->getWatchedItemsForUser( $user );
+
+               $this->assertInternalType( 'array', $watchedItems );
+               $this->assertCount( 2, $watchedItems );
+               foreach ( $watchedItems as $watchedItem ) {
+                       $this->assertInstanceOf( 'WatchedItem', $watchedItem );
+               }
+               $this->assertEquals(
+                       new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
+                       $watchedItems[0]
+               );
+               $this->assertEquals(
+                       new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
+                       $watchedItems[1]
+               );
+       }
+
+       public function provideDbTypes() {
+               return [
+                       [ false, DB_REPLICA ],
+                       [ true, DB_MASTER ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideDbTypes
+        */
+       public function testGetWatchedItemsForUser_optionsAndEmptyResult( $forWrite, $dbType ) {
+               $mockDb = $this->getMockDb();
+               $mockCache = $this->getMockCache();
+               $mockLoadBalancer = $this->getMockLoadBalancer( $mockDb, $dbType );
+               $user = $this->getMockNonAnonUserWithId( 1 );
+
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               'watchlist',
+                               [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
+                               [ 'wl_user' => 1 ],
+                               $this->isType( 'string' ),
+                               [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
+                       )
+                       ->will( $this->returnValue( [] ) );
+
+               $store = $this->newWatchedItemStore(
+                       $mockLoadBalancer,
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $watchedItems = $store->getWatchedItemsForUser(
+                       $user,
+                       [ 'forWrite' => $forWrite, 'sort' => WatchedItemStore::SORT_ASC ]
+               );
+               $this->assertEquals( [], $watchedItems );
+       }
+
+       public function testGetWatchedItemsForUser_badSortOptionThrowsException() {
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $this->getMockDb() ),
+                       $this->getMockCache(),
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->setExpectedException( 'InvalidArgumentException' );
+               $store->getWatchedItemsForUser(
+                       $this->getMockNonAnonUserWithId( 1 ),
+                       [ 'sort' => 'foo' ]
+               );
+       }
+
+       public function testIsWatchedItem_existingItem() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'selectRow' )
+                       ->with(
+                               'watchlist',
+                               'wl_notificationtimestamp',
+                               [
+                                       'wl_user' => 1,
+                                       'wl_namespace' => 0,
+                                       'wl_title' => 'SomeDbKey',
+                               ]
+                       )
+                       ->will( $this->returnValue(
+                               $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] )
+                       ) );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'delete' );
+               $mockCache->expects( $this->once() )
+                       ->method( 'get' )
+                       ->with( '0:SomeDbKey:1' )
+                       ->will( $this->returnValue( false ) );
+               $mockCache->expects( $this->once() )
+                       ->method( 'set' )
+                       ->with(
+                               '0:SomeDbKey:1'
+                       );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertTrue(
+                       $store->isWatched(
+                               $this->getMockNonAnonUserWithId( 1 ),
+                               new TitleValue( 0, 'SomeDbKey' )
+                       )
+               );
+       }
+
+       public function testIsWatchedItem_noItem() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'selectRow' )
+                       ->with(
+                               'watchlist',
+                               'wl_notificationtimestamp',
+                               [
+                                       'wl_user' => 1,
+                                       'wl_namespace' => 0,
+                                       'wl_title' => 'SomeDbKey',
+                               ]
+                       )
+                       ->will( $this->returnValue( [] ) );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'set' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+               $mockCache->expects( $this->once() )
+                       ->method( 'get' )
+                       ->with( '0:SomeDbKey:1' )
+                       ->will( $this->returnValue( false ) );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertFalse(
+                       $store->isWatched(
+                               $this->getMockNonAnonUserWithId( 1 ),
+                               new TitleValue( 0, 'SomeDbKey' )
+                       )
+               );
+       }
+
+       public function testIsWatchedItem_anonymousUser() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->never() )
+                       ->method( 'selectRow' );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'set' );
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertFalse(
+                       $store->isWatched(
+                               $this->getAnonUser(),
+                               new TitleValue( 0, 'SomeDbKey' )
+                       )
+               );
+       }
+
+       public function testGetNotificationTimestampsBatch() {
+               $targets = [
+                       new TitleValue( 0, 'SomeDbKey' ),
+                       new TitleValue( 1, 'AnotherDbKey' ),
+               ];
+
+               $mockDb = $this->getMockDb();
+               $dbResult = [
+                       $this->getFakeRow( [
+                               'wl_namespace' => 0,
+                               'wl_title' => 'SomeDbKey',
+                               'wl_notificationtimestamp' => '20151212010101',
+                       ] ),
+                       $this->getFakeRow(
+                               [
+                                       'wl_namespace' => 1,
+                                       'wl_title' => 'AnotherDbKey',
+                                       'wl_notificationtimestamp' => null,
+                               ]
+                       ),
+               ];
+
+               $mockDb->expects( $this->once() )
+                       ->method( 'makeWhereFrom2d' )
+                       ->with(
+                               [ [ 'SomeDbKey' => 1 ], [ 'AnotherDbKey' => 1 ] ],
+                               $this->isType( 'string' ),
+                               $this->isType( 'string' )
+                       )
+                       ->will( $this->returnValue( 'makeWhereFrom2d return value' ) );
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               'watchlist',
+                               [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
+                               [
+                                       'makeWhereFrom2d return value',
+                                       'wl_user' => 1
+                               ],
+                               $this->isType( 'string' )
+                       )
+                       ->will( $this->returnValue( $dbResult ) );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->exactly( 2 ) )
+                       ->method( 'get' )
+                       ->withConsecutive(
+                               [ '0:SomeDbKey:1' ],
+                               [ '1:AnotherDbKey:1' ]
+                       )
+                       ->will( $this->returnValue( null ) );
+               $mockCache->expects( $this->never() )->method( 'set' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertEquals(
+                       [
+                               0 => [ 'SomeDbKey' => '20151212010101', ],
+                               1 => [ 'AnotherDbKey' => null, ],
+                       ],
+                       $store->getNotificationTimestampsBatch( $this->getMockNonAnonUserWithId( 1 ), $targets )
+               );
+       }
+
+       public function testGetNotificationTimestampsBatch_notWatchedTarget() {
+               $targets = [
+                       new TitleValue( 0, 'OtherDbKey' ),
+               ];
+
+               $mockDb = $this->getMockDb();
+
+               $mockDb->expects( $this->once() )
+                       ->method( 'makeWhereFrom2d' )
+                       ->with(
+                               [ [ 'OtherDbKey' => 1 ] ],
+                               $this->isType( 'string' ),
+                               $this->isType( 'string' )
+                       )
+                       ->will( $this->returnValue( 'makeWhereFrom2d return value' ) );
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               'watchlist',
+                               [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
+                               [
+                                       'makeWhereFrom2d return value',
+                                       'wl_user' => 1
+                               ],
+                               $this->isType( 'string' )
+                       )
+                       ->will( $this->returnValue( $this->getFakeRow( [] ) ) );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->once() )
+                       ->method( 'get' )
+                       ->with( '0:OtherDbKey:1' )
+                       ->will( $this->returnValue( null ) );
+               $mockCache->expects( $this->never() )->method( 'set' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertEquals(
+                       [
+                               0 => [ 'OtherDbKey' => false, ],
+                       ],
+                       $store->getNotificationTimestampsBatch( $this->getMockNonAnonUserWithId( 1 ), $targets )
+               );
+       }
+
+       public function testGetNotificationTimestampsBatch_cachedItem() {
+               $targets = [
+                       new TitleValue( 0, 'SomeDbKey' ),
+                       new TitleValue( 1, 'AnotherDbKey' ),
+               ];
+
+               $user = $this->getMockNonAnonUserWithId( 1 );
+               $cachedItem = new WatchedItem( $user, $targets[0], '20151212010101' );
+
+               $mockDb = $this->getMockDb();
+
+               $mockDb->expects( $this->once() )
+                       ->method( 'makeWhereFrom2d' )
+                       ->with(
+                               [ 1 => [ 'AnotherDbKey' => 1 ] ],
+                               $this->isType( 'string' ),
+                               $this->isType( 'string' )
+                       )
+                       ->will( $this->returnValue( 'makeWhereFrom2d return value' ) );
+               $mockDb->expects( $this->once() )
+                       ->method( 'select' )
+                       ->with(
+                               'watchlist',
+                               [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
+                               [
+                                       'makeWhereFrom2d return value',
+                                       'wl_user' => 1
+                               ],
+                               $this->isType( 'string' )
+                       )
+                       ->will( $this->returnValue( [
+                               $this->getFakeRow(
+                                       [ 'wl_namespace' => 1, 'wl_title' => 'AnotherDbKey', 'wl_notificationtimestamp' => null, ]
+                               )
+                       ] ) );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->at( 1 ) )
+                       ->method( 'get' )
+                       ->with( '0:SomeDbKey:1' )
+                       ->will( $this->returnValue( $cachedItem ) );
+               $mockCache->expects( $this->at( 3 ) )
+                       ->method( 'get' )
+                       ->with( '1:AnotherDbKey:1' )
+                       ->will( $this->returnValue( null ) );
+               $mockCache->expects( $this->never() )->method( 'set' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertEquals(
+                       [
+                               0 => [ 'SomeDbKey' => '20151212010101', ],
+                               1 => [ 'AnotherDbKey' => null, ],
+                       ],
+                       $store->getNotificationTimestampsBatch( $user, $targets )
+               );
+       }
+
+       public function testGetNotificationTimestampsBatch_allItemsCached() {
+               $targets = [
+                       new TitleValue( 0, 'SomeDbKey' ),
+                       new TitleValue( 1, 'AnotherDbKey' ),
+               ];
+
+               $user = $this->getMockNonAnonUserWithId( 1 );
+               $cachedItems = [
+                       new WatchedItem( $user, $targets[0], '20151212010101' ),
+                       new WatchedItem( $user, $targets[1], null ),
+               ];
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->never() )->method( $this->anything() );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->at( 1 ) )
+                       ->method( 'get' )
+                       ->with( '0:SomeDbKey:1' )
+                       ->will( $this->returnValue( $cachedItems[0] ) );
+               $mockCache->expects( $this->at( 3 ) )
+                       ->method( 'get' )
+                       ->with( '1:AnotherDbKey:1' )
+                       ->will( $this->returnValue( $cachedItems[1] ) );
+               $mockCache->expects( $this->never() )->method( 'set' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertEquals(
+                       [
+                               0 => [ 'SomeDbKey' => '20151212010101', ],
+                               1 => [ 'AnotherDbKey' => null, ],
+                       ],
+                       $store->getNotificationTimestampsBatch( $user, $targets )
+               );
+       }
+
+       public function testGetNotificationTimestampsBatch_anonymousUser() {
+               $targets = [
+                       new TitleValue( 0, 'SomeDbKey' ),
+                       new TitleValue( 1, 'AnotherDbKey' ),
+               ];
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->never() )->method( $this->anything() );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( $this->anything() );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertEquals(
+                       [
+                               0 => [ 'SomeDbKey' => false, ],
+                               1 => [ 'AnotherDbKey' => false, ],
+                       ],
+                       $store->getNotificationTimestampsBatch( $this->getAnonUser(), $targets )
+               );
+       }
+
+       public function testResetNotificationTimestamp_anonymousUser() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->never() )
+                       ->method( 'selectRow' );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->never() )->method( 'set' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertFalse(
+                       $store->resetNotificationTimestamp(
+                               $this->getAnonUser(),
+                               Title::newFromText( 'SomeDbKey' )
+                       )
+               );
+       }
+
+       public function testResetNotificationTimestamp_noItem() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'selectRow' )
+                       ->with(
+                               'watchlist',
+                               'wl_notificationtimestamp',
+                               [
+                                       'wl_user' => 1,
+                                       'wl_namespace' => 0,
+                                       'wl_title' => 'SomeDbKey',
+                               ]
+                       )
+                       ->will( $this->returnValue( [] ) );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->never() )->method( 'set' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertFalse(
+                       $store->resetNotificationTimestamp(
+                               $this->getMockNonAnonUserWithId( 1 ),
+                               Title::newFromText( 'SomeDbKey' )
+                       )
+               );
+       }
+
+       public function testResetNotificationTimestamp_item() {
+               $user = $this->getMockNonAnonUserWithId( 1 );
+               $title = Title::newFromText( 'SomeDbKey' );
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'selectRow' )
+                       ->with(
+                               'watchlist',
+                               'wl_notificationtimestamp',
+                               [
+                                       'wl_user' => 1,
+                                       'wl_namespace' => 0,
+                                       'wl_title' => 'SomeDbKey',
+                               ]
+                       )
+                       ->will( $this->returnValue(
+                               $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] )
+                       ) );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->once() )
+                       ->method( 'set' )
+                       ->with(
+                               '0:SomeDbKey:1',
+                               $this->isInstanceOf( WatchedItem::class )
+                       );
+               $mockCache->expects( $this->once() )
+                       ->method( 'delete' )
+                       ->with( '0:SomeDbKey:1' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               // Note: This does not actually assert the job is correct
+               $callableCallCounter = 0;
+               $mockCallback = function ( $callable ) use ( &$callableCallCounter ) {
+                       $callableCallCounter++;
+                       $this->assertInternalType( 'callable', $callable );
+               };
+               $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback );
+
+               $this->assertTrue(
+                       $store->resetNotificationTimestamp(
+                               $user,
+                               $title
+                       )
+               );
+               $this->assertEquals( 1, $callableCallCounter );
+
+               ScopedCallback::consume( $scopedOverride );
+       }
+
+       public function testResetNotificationTimestamp_noItemForced() {
+               $user = $this->getMockNonAnonUserWithId( 1 );
+               $title = Title::newFromText( 'SomeDbKey' );
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->never() )
+                       ->method( 'selectRow' );
+
+               $mockCache = $this->getMockCache();
+               $mockDb->expects( $this->never() )
+                       ->method( 'get' );
+               $mockDb->expects( $this->never() )
+                       ->method( 'set' );
+               $mockDb->expects( $this->never() )
+                       ->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               // Note: This does not actually assert the job is correct
+               $callableCallCounter = 0;
+               $mockCallback = function ( $callable ) use ( &$callableCallCounter ) {
+                       $callableCallCounter++;
+                       $this->assertInternalType( 'callable', $callable );
+               };
+               $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback );
+
+               $this->assertTrue(
+                       $store->resetNotificationTimestamp(
+                               $user,
+                               $title,
+                               'force'
+                       )
+               );
+               $this->assertEquals( 1, $callableCallCounter );
+
+               ScopedCallback::consume( $scopedOverride );
+       }
+
+       /**
+        * @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(
+               $callback,
+               LinkTarget $expectedTitle,
+               $expectedUserId,
+               callable $notificationTimestampCondition
+       ) {
+               $this->assertInternalType( 'callable', $callback );
+
+               $callbackReflector = new ReflectionFunction( $callback );
+               $vars = $callbackReflector->getStaticVariables();
+               $this->assertArrayHasKey( 'job', $vars );
+               $this->assertInstanceOf( ActivityUpdateJob::class, $vars['job'] );
+
+               /** @var ActivityUpdateJob $job */
+               $job = $vars['job'];
+               $this->assertEquals( $expectedTitle->getDBkey(), $job->getTitle()->getDBkey() );
+               $this->assertEquals( $expectedTitle->getNamespace(), $job->getTitle()->getNamespace() );
+
+               $jobParams = $job->getParams();
+               $this->assertArrayHasKey( 'type', $jobParams );
+               $this->assertEquals( 'updateWatchlistNotification', $jobParams['type'] );
+               $this->assertArrayHasKey( 'userid', $jobParams );
+               $this->assertEquals( $expectedUserId, $jobParams['userid'] );
+               $this->assertArrayHasKey( 'notifTime', $jobParams );
+               $this->assertTrue( $notificationTimestampCondition( $jobParams['notifTime'] ) );
+       }
+
+       public function testResetNotificationTimestamp_oldidSpecifiedLatestRevisionForced() {
+               $user = $this->getMockNonAnonUserWithId( 1 );
+               $oldid = 22;
+               $title = $this->getMockTitle( 'SomeTitle' );
+               $title->expects( $this->once() )
+                       ->method( 'getNextRevisionID' )
+                       ->with( $oldid )
+                       ->will( $this->returnValue( false ) );
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->never() )
+                       ->method( 'selectRow' );
+
+               $mockCache = $this->getMockCache();
+               $mockDb->expects( $this->never() )
+                       ->method( 'get' );
+               $mockDb->expects( $this->never() )
+                       ->method( 'set' );
+               $mockDb->expects( $this->never() )
+                       ->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $callableCallCounter = 0;
+               $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback(
+                       function ( $callable ) use ( &$callableCallCounter, $title, $user ) {
+                               $callableCallCounter++;
+                               $this->verifyCallbackJob(
+                                       $callable,
+                                       $title,
+                                       $user->getId(),
+                                       function ( $time ) {
+                                               return $time === null;
+                                       }
+                               );
+                       }
+               );
+
+               $this->assertTrue(
+                       $store->resetNotificationTimestamp(
+                               $user,
+                               $title,
+                               'force',
+                               $oldid
+                       )
+               );
+               $this->assertEquals( 1, $callableCallCounter );
+
+               ScopedCallback::consume( $scopedOverride );
+       }
+
+       public function testResetNotificationTimestamp_oldidSpecifiedNotLatestRevisionForced() {
+               $user = $this->getMockNonAnonUserWithId( 1 );
+               $oldid = 22;
+               $title = $this->getMockTitle( 'SomeDbKey' );
+               $title->expects( $this->once() )
+                       ->method( 'getNextRevisionID' )
+                       ->with( $oldid )
+                       ->will( $this->returnValue( 33 ) );
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'selectRow' )
+                       ->with(
+                               'watchlist',
+                               'wl_notificationtimestamp',
+                               [
+                                       'wl_user' => 1,
+                                       'wl_namespace' => 0,
+                                       'wl_title' => 'SomeDbKey',
+                               ]
+                       )
+                       ->will( $this->returnValue(
+                               $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] )
+                       ) );
+
+               $mockCache = $this->getMockCache();
+               $mockDb->expects( $this->never() )
+                       ->method( 'get' );
+               $mockDb->expects( $this->never() )
+                       ->method( 'set' );
+               $mockDb->expects( $this->never() )
+                       ->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $addUpdateCallCounter = 0;
+               $scopedOverrideDeferred = $store->overrideDeferredUpdatesAddCallableUpdateCallback(
+                       function ( $callable ) use ( &$addUpdateCallCounter, $title, $user ) {
+                               $addUpdateCallCounter++;
+                               $this->verifyCallbackJob(
+                                       $callable,
+                                       $title,
+                                       $user->getId(),
+                                       function ( $time ) {
+                                               return $time !== null && $time > '20151212010101';
+                                       }
+                               );
+                       }
+               );
+
+               $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,
+                               $title,
+                               'force',
+                               $oldid
+                       )
+               );
+               $this->assertEquals( 1, $addUpdateCallCounter );
+               $this->assertEquals( 1, $getTimestampCallCounter );
+
+               ScopedCallback::consume( $scopedOverrideDeferred );
+               ScopedCallback::consume( $scopedOverrideRevision );
+       }
+
+       public function testResetNotificationTimestamp_notWatchedPageForced() {
+               $user = $this->getMockNonAnonUserWithId( 1 );
+               $oldid = 22;
+               $title = $this->getMockTitle( 'SomeDbKey' );
+               $title->expects( $this->once() )
+                       ->method( 'getNextRevisionID' )
+                       ->with( $oldid )
+                       ->will( $this->returnValue( 33 ) );
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'selectRow' )
+                       ->with(
+                               'watchlist',
+                               'wl_notificationtimestamp',
+                               [
+                                       'wl_user' => 1,
+                                       'wl_namespace' => 0,
+                                       'wl_title' => 'SomeDbKey',
+                               ]
+                       )
+                       ->will( $this->returnValue( false ) );
+
+               $mockCache = $this->getMockCache();
+               $mockDb->expects( $this->never() )
+                       ->method( 'get' );
+               $mockDb->expects( $this->never() )
+                       ->method( 'set' );
+               $mockDb->expects( $this->never() )
+                       ->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $callableCallCounter = 0;
+               $scopedOverride = $store->overrideDeferredUpdatesAddCallableUpdateCallback(
+                       function ( $callable ) use ( &$callableCallCounter, $title, $user ) {
+                               $callableCallCounter++;
+                               $this->verifyCallbackJob(
+                                       $callable,
+                                       $title,
+                                       $user->getId(),
+                                       function ( $time ) {
+                                               return $time === null;
+                                       }
+                               );
+                       }
+               );
+
+               $this->assertTrue(
+                       $store->resetNotificationTimestamp(
+                               $user,
+                               $title,
+                               'force',
+                               $oldid
+                       )
+               );
+               $this->assertEquals( 1, $callableCallCounter );
+
+               ScopedCallback::consume( $scopedOverride );
+       }
+
+       public function testResetNotificationTimestamp_futureNotificationTimestampForced() {
+               $user = $this->getMockNonAnonUserWithId( 1 );
+               $oldid = 22;
+               $title = $this->getMockTitle( 'SomeDbKey' );
+               $title->expects( $this->once() )
+                       ->method( 'getNextRevisionID' )
+                       ->with( $oldid )
+                       ->will( $this->returnValue( 33 ) );
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'selectRow' )
+                       ->with(
+                               'watchlist',
+                               'wl_notificationtimestamp',
+                               [
+                                       'wl_user' => 1,
+                                       'wl_namespace' => 0,
+                                       'wl_title' => 'SomeDbKey',
+                               ]
+                       )
+                       ->will( $this->returnValue(
+                               $this->getFakeRow( [ 'wl_notificationtimestamp' => '30151212010101' ] )
+                       ) );
+
+               $mockCache = $this->getMockCache();
+               $mockDb->expects( $this->never() )
+                       ->method( 'get' );
+               $mockDb->expects( $this->never() )
+                       ->method( 'set' );
+               $mockDb->expects( $this->never() )
+                       ->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $addUpdateCallCounter = 0;
+               $scopedOverrideDeferred = $store->overrideDeferredUpdatesAddCallableUpdateCallback(
+                       function ( $callable ) use ( &$addUpdateCallCounter, $title, $user ) {
+                               $addUpdateCallCounter++;
+                               $this->verifyCallbackJob(
+                                       $callable,
+                                       $title,
+                                       $user->getId(),
+                                       function ( $time ) {
+                                               return $time === '30151212010101';
+                                       }
+                               );
+                       }
+               );
+
+               $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,
+                               $title,
+                               'force',
+                               $oldid
+                       )
+               );
+               $this->assertEquals( 1, $addUpdateCallCounter );
+               $this->assertEquals( 1, $getTimestampCallCounter );
+
+               ScopedCallback::consume( $scopedOverrideDeferred );
+               ScopedCallback::consume( $scopedOverrideRevision );
+       }
+
+       public function testResetNotificationTimestamp_futureNotificationTimestampNotForced() {
+               $user = $this->getMockNonAnonUserWithId( 1 );
+               $oldid = 22;
+               $title = $this->getMockTitle( 'SomeDbKey' );
+               $title->expects( $this->once() )
+                       ->method( 'getNextRevisionID' )
+                       ->with( $oldid )
+                       ->will( $this->returnValue( 33 ) );
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'selectRow' )
+                       ->with(
+                               'watchlist',
+                               'wl_notificationtimestamp',
+                               [
+                                       'wl_user' => 1,
+                                       'wl_namespace' => 0,
+                                       'wl_title' => 'SomeDbKey',
+                               ]
+                       )
+                       ->will( $this->returnValue(
+                               $this->getFakeRow( [ 'wl_notificationtimestamp' => '30151212010101' ] )
+                       ) );
+
+               $mockCache = $this->getMockCache();
+               $mockDb->expects( $this->never() )
+                       ->method( 'get' );
+               $mockDb->expects( $this->never() )
+                       ->method( 'set' );
+               $mockDb->expects( $this->never() )
+                       ->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $addUpdateCallCounter = 0;
+               $scopedOverrideDeferred = $store->overrideDeferredUpdatesAddCallableUpdateCallback(
+                       function ( $callable ) use ( &$addUpdateCallCounter, $title, $user ) {
+                               $addUpdateCallCounter++;
+                               $this->verifyCallbackJob(
+                                       $callable,
+                                       $title,
+                                       $user->getId(),
+                                       function ( $time ) {
+                                               return $time === false;
+                                       }
+                               );
+                       }
+               );
+
+               $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,
+                               $title,
+                               '',
+                               $oldid
+                       )
+               );
+               $this->assertEquals( 1, $addUpdateCallCounter );
+               $this->assertEquals( 1, $getTimestampCallCounter );
+
+               ScopedCallback::consume( $scopedOverrideDeferred );
+               ScopedCallback::consume( $scopedOverrideRevision );
+       }
+
+       public function testSetNotificationTimestampsForUser_anonUser() {
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $this->getMockDb() ),
+                       $this->getMockCache(),
+                       $this->getMockReadOnlyMode()
+               );
+               $this->assertFalse( $store->setNotificationTimestampsForUser( $this->getAnonUser(), '' ) );
+       }
+
+       public function testSetNotificationTimestampsForUser_allRows() {
+               $user = $this->getMockNonAnonUserWithId( 1 );
+               $timestamp = '20100101010101';
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'update' )
+                       ->with(
+                               'watchlist',
+                               [ 'wl_notificationtimestamp' => 'TS' . $timestamp . 'TS' ],
+                               [ 'wl_user' => 1 ]
+                       )
+                       ->will( $this->returnValue( true ) );
+               $mockDb->expects( $this->exactly( 1 ) )
+                       ->method( 'timestamp' )
+                       ->will( $this->returnCallback( function ( $value ) {
+                               return 'TS' . $value . 'TS';
+                       } ) );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $this->getMockCache(),
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertTrue(
+                       $store->setNotificationTimestampsForUser( $user, $timestamp )
+               );
+       }
+
+       public function testSetNotificationTimestampsForUser_nullTimestamp() {
+               $user = $this->getMockNonAnonUserWithId( 1 );
+               $timestamp = null;
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'update' )
+                       ->with(
+                               'watchlist',
+                               [ 'wl_notificationtimestamp' => null ],
+                               [ 'wl_user' => 1 ]
+                       )
+                       ->will( $this->returnValue( true ) );
+               $mockDb->expects( $this->exactly( 0 ) )
+                       ->method( 'timestamp' )
+                       ->will( $this->returnCallback( function ( $value ) {
+                               return 'TS' . $value . 'TS';
+                       } ) );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $this->getMockCache(),
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertTrue(
+                       $store->setNotificationTimestampsForUser( $user, $timestamp )
+               );
+       }
+
+       public function testSetNotificationTimestampsForUser_specificTargets() {
+               $user = $this->getMockNonAnonUserWithId( 1 );
+               $timestamp = '20100101010101';
+               $targets = [ new TitleValue( 0, 'Foo' ), new TitleValue( 0, 'Bar' ) ];
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'update' )
+                       ->with(
+                               'watchlist',
+                               [ 'wl_notificationtimestamp' => 'TS' . $timestamp . 'TS' ],
+                               [ 'wl_user' => 1, 0 => 'makeWhereFrom2d return value' ]
+                       )
+                       ->will( $this->returnValue( true ) );
+               $mockDb->expects( $this->exactly( 1 ) )
+                       ->method( 'timestamp' )
+                       ->will( $this->returnCallback( function ( $value ) {
+                               return 'TS' . $value . 'TS';
+                       } ) );
+               $mockDb->expects( $this->once() )
+                       ->method( 'makeWhereFrom2d' )
+                       ->with(
+                               [ [ 'Foo' => 1, 'Bar' => 1 ] ],
+                               $this->isType( 'string' ),
+                               $this->isType( 'string' )
+                       )
+                       ->will( $this->returnValue( 'makeWhereFrom2d return value' ) );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $this->getMockCache(),
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertTrue(
+                       $store->setNotificationTimestampsForUser( $user, $timestamp, $targets )
+               );
+       }
+
+       public function testUpdateNotificationTimestamp_watchersExist() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'selectFieldValues' )
+                       ->with(
+                               'watchlist',
+                               'wl_user',
+                               [
+                                       'wl_user != 1',
+                                       'wl_namespace' => 0,
+                                       'wl_title' => 'SomeDbKey',
+                                       'wl_notificationtimestamp IS NULL'
+                               ]
+                       )
+                       ->will( $this->returnValue( [ '2', '3' ] ) );
+               $mockDb->expects( $this->once() )
+                       ->method( 'update' )
+                       ->with(
+                               'watchlist',
+                               [ 'wl_notificationtimestamp' => null ],
+                               [
+                                       'wl_user' => [ 2, 3 ],
+                                       'wl_namespace' => 0,
+                                       'wl_title' => 'SomeDbKey',
+                               ]
+                       );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'set' );
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $this->assertEquals(
+                       [ 2, 3 ],
+                       $store->updateNotificationTimestamp(
+                               $this->getMockNonAnonUserWithId( 1 ),
+                               new TitleValue( 0, 'SomeDbKey' ),
+                               '20151212010101'
+                       )
+               );
+       }
+
+       public function testUpdateNotificationTimestamp_noWatchers() {
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'selectFieldValues' )
+                       ->with(
+                               'watchlist',
+                               'wl_user',
+                               [
+                                       'wl_user != 1',
+                                       'wl_namespace' => 0,
+                                       'wl_title' => 'SomeDbKey',
+                                       'wl_notificationtimestamp IS NULL'
+                               ]
+                       )
+                       ->will(
+                               $this->returnValue( [] )
+                       );
+               $mockDb->expects( $this->never() )
+                       ->method( 'update' );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->never() )->method( 'set' );
+               $mockCache->expects( $this->never() )->method( 'get' );
+               $mockCache->expects( $this->never() )->method( 'delete' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               $watchers = $store->updateNotificationTimestamp(
+                       $this->getMockNonAnonUserWithId( 1 ),
+                       new TitleValue( 0, 'SomeDbKey' ),
+                       '20151212010101'
+               );
+               $this->assertInternalType( 'array', $watchers );
+               $this->assertEmpty( $watchers );
+       }
+
+       public function testUpdateNotificationTimestamp_clearsCachedItems() {
+               $user = $this->getMockNonAnonUserWithId( 1 );
+               $titleValue = new TitleValue( 0, 'SomeDbKey' );
+
+               $mockDb = $this->getMockDb();
+               $mockDb->expects( $this->once() )
+                       ->method( 'selectRow' )
+                       ->will( $this->returnValue(
+                               $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] )
+                       ) );
+               $mockDb->expects( $this->once() )
+                       ->method( 'selectFieldValues' )
+                       ->will(
+                               $this->returnValue( [ '2', '3' ] )
+                       );
+               $mockDb->expects( $this->once() )
+                       ->method( 'update' );
+
+               $mockCache = $this->getMockCache();
+               $mockCache->expects( $this->once() )
+                       ->method( 'set' )
+                       ->with( '0:SomeDbKey:1', $this->isType( 'object' ) );
+               $mockCache->expects( $this->once() )
+                       ->method( 'get' )
+                       ->with( '0:SomeDbKey:1' );
+               $mockCache->expects( $this->once() )
+                       ->method( 'delete' )
+                       ->with( '0:SomeDbKey:1' );
+
+               $store = $this->newWatchedItemStore(
+                       $this->getMockLoadBalancer( $mockDb ),
+                       $mockCache,
+                       $this->getMockReadOnlyMode()
+               );
+
+               // This will add the item to the cache
+               $store->getWatchedItem( $user, $titleValue );
+
+               $store->updateNotificationTimestamp(
+                       $this->getMockNonAnonUserWithId( 1 ),
+                       $titleValue,
+                       '20151212010101'
+               );
+       }
+
+}
diff --git a/tests/phpunit/includes/watcheditem/WatchedItemUnitTest.php b/tests/phpunit/includes/watcheditem/WatchedItemUnitTest.php
new file mode 100644 (file)
index 0000000..8897645
--- /dev/null
@@ -0,0 +1,150 @@
+<?php
+use MediaWiki\Linker\LinkTarget;
+
+/**
+ * @author Addshore
+ *
+ * @covers WatchedItem
+ */
+class WatchedItemUnitTest extends MediaWikiTestCase {
+
+       /**
+        * @param int $id
+        *
+        * @return PHPUnit_Framework_MockObject_MockObject|User
+        */
+       private function getMockUser( $id ) {
+               $user = $this->createMock( User::class );
+               $user->expects( $this->any() )
+                       ->method( 'getId' )
+                       ->will( $this->returnValue( $id ) );
+               $user->expects( $this->any() )
+                       ->method( 'isAllowed' )
+                       ->will( $this->returnValue( true ) );
+               return $user;
+       }
+
+       public function provideUserTitleTimestamp() {
+               $user = $this->getMockUser( 111 );
+               return [
+                       [ $user, Title::newFromText( 'SomeTitle' ), null ],
+                       [ $user, Title::newFromText( 'SomeTitle' ), '20150101010101' ],
+                       [ $user, new TitleValue( 0, 'TVTitle', 'frag' ), '20150101010101' ],
+               ];
+       }
+
+       /**
+        * @return PHPUnit_Framework_MockObject_MockObject|WatchedItemStore
+        */
+       private function getMockWatchedItemStore() {
+               return $this->getMockBuilder( WatchedItemStore::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+       }
+
+       /**
+        * @dataProvider provideUserTitleTimestamp
+        */
+       public function testConstruction( $user, LinkTarget $linkTarget, $notifTimestamp ) {
+               $item = new WatchedItem( $user, $linkTarget, $notifTimestamp );
+
+               $this->assertSame( $user, $item->getUser() );
+               $this->assertSame( $linkTarget, $item->getLinkTarget() );
+               $this->assertSame( $notifTimestamp, $item->getNotificationTimestamp() );
+
+               // The below tests the internal WatchedItem::getTitle method
+               $this->assertInstanceOf( 'Title', $item->getTitle() );
+               $this->assertSame( $linkTarget->getDBkey(), $item->getTitle()->getDBkey() );
+               $this->assertSame( $linkTarget->getFragment(), $item->getTitle()->getFragment() );
+               $this->assertSame( $linkTarget->getNamespace(), $item->getTitle()->getNamespace() );
+               $this->assertSame( $linkTarget->getText(), $item->getTitle()->getText() );
+       }
+
+       /**
+        * @dataProvider provideUserTitleTimestamp
+        */
+       public function testFromUserTitle( $user, $linkTarget, $timestamp ) {
+               $store = $this->getMockWatchedItemStore();
+               $store->expects( $this->once() )
+                       ->method( 'loadWatchedItem' )
+                       ->with( $user, $linkTarget )
+                       ->will( $this->returnValue( new WatchedItem( $user, $linkTarget, $timestamp ) ) );
+               $this->setService( 'WatchedItemStore', $store );
+
+               $item = WatchedItem::fromUserTitle( $user, $linkTarget, User::IGNORE_USER_RIGHTS );
+
+               $this->assertEquals( $user, $item->getUser() );
+               $this->assertEquals( $linkTarget, $item->getLinkTarget() );
+               $this->assertEquals( $timestamp, $item->getNotificationTimestamp() );
+       }
+
+       public function testAddWatch() {
+               $title = Title::newFromText( 'SomeTitle' );
+               $timestamp = null;
+               $checkRights = 0;
+
+               /** @var User|PHPUnit_Framework_MockObject_MockObject $user */
+               $user = $this->createMock( User::class );
+               $user->expects( $this->once() )
+                       ->method( 'addWatch' )
+                       ->with( $title, $checkRights );
+
+               $item = new WatchedItem( $user, $title, $timestamp, $checkRights );
+               $this->assertTrue( $item->addWatch() );
+       }
+
+       public function testRemoveWatch() {
+               $title = Title::newFromText( 'SomeTitle' );
+               $timestamp = null;
+               $checkRights = 0;
+
+               /** @var User|PHPUnit_Framework_MockObject_MockObject $user */
+               $user = $this->createMock( User::class );
+               $user->expects( $this->once() )
+                       ->method( 'removeWatch' )
+                       ->with( $title, $checkRights );
+
+               $item = new WatchedItem( $user, $title, $timestamp, $checkRights );
+               $this->assertTrue( $item->removeWatch() );
+       }
+
+       public function provideBooleans() {
+               return [
+                       [ true ],
+                       [ false ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideBooleans
+        */
+       public function testIsWatched( $returnValue ) {
+               $title = Title::newFromText( 'SomeTitle' );
+               $timestamp = null;
+               $checkRights = 0;
+
+               /** @var User|PHPUnit_Framework_MockObject_MockObject $user */
+               $user = $this->createMock( User::class );
+               $user->expects( $this->once() )
+                       ->method( 'isWatched' )
+                       ->with( $title, $checkRights )
+                       ->will( $this->returnValue( $returnValue ) );
+
+               $item = new WatchedItem( $user, $title, $timestamp, $checkRights );
+               $this->assertEquals( $returnValue, $item->isWatched() );
+       }
+
+       public function testDuplicateEntries() {
+               $oldTitle = Title::newFromText( 'OldTitle' );
+               $newTitle = Title::newFromText( 'NewTitle' );
+
+               $store = $this->getMockWatchedItemStore();
+               $store->expects( $this->once() )
+                       ->method( 'duplicateAllAssociatedEntries' )
+                       ->with( $oldTitle, $newTitle );
+               $this->setService( 'WatchedItemStore', $store );
+
+               WatchedItem::duplicateEntries( $oldTitle, $newTitle );
+       }
+
+}
index 0da03df..950d2df 100644 (file)
@@ -6,7 +6,7 @@
  * @group Language
  *
  * @license GPL-2.0+
- * @author Thiemo Mättig
+ * @author Thiemo Kreuz
  */
 class LanguageCodeTest extends PHPUnit_Framework_TestCase {
 
index 81184aa..fc2ed33 100644 (file)
@@ -157,6 +157,25 @@ class LanguageConverterTest extends MediaWikiLangTestCase {
                $wgRequest->setVal( 'variant', null );
                $this->assertEquals( 'tg', $this->lc->getPreferredVariant() );
        }
+
+       /**
+        * Test exhausting pcre.backtrack_limit
+        */
+       public function testAutoConvertT124404() {
+               $testString = '';
+               for ( $i = 0; $i < 1000; $i++ ) {
+                       $testString .= 'xxx xxx xxx';
+               }
+               $testString .= "\n<big id='в'></big>";
+               $old = ini_set( 'pcre.backtrack_limit', 200 );
+               $result = $this->lc->autoConvert( $testString, 'tg-latn' );
+               ini_set( 'pcre.backtrack_limit', $old );
+               // The в in the id attribute should not get converted to a v
+               $this->assertFalse(
+                       strpos( $result, 'v' ),
+                       "в converted to v despite being in attribue"
+               );
+       }
 }
 
 /**
diff --git a/tests/phpunit/languages/classes/LanguageCrhTest.php b/tests/phpunit/languages/classes/LanguageCrhTest.php
new file mode 100644 (file)
index 0000000..f34288c
--- /dev/null
@@ -0,0 +1,72 @@
+<?php
+
+class LanguageCrhTest extends LanguageClassesTestCase {
+       /**
+        * @dataProvider provideAutoConvertToAllVariants
+        * @covers Language::autoConvertToAllVariants
+        */
+       public function testAutoConvertToAllVariants( $result, $value ) {
+               $this->assertEquals( $result, $this->getLang()->autoConvertToAllVariants( $value ) );
+       }
+
+       public static function provideAutoConvertToAllVariants() {
+               return [
+                       [ // general words, covering more of the alphabet
+                               [
+                                       'crh'      => 'рузгярнынъ ruzgârnıñ Париж Parij',
+                                       'crh-cyrl' => 'рузгярнынъ рузгярнынъ Париж Париж',
+                                       'crh-latn' => 'ruzgârnıñ ruzgârnıñ Parij Parij',
+                               ],
+                               'рузгярнынъ ruzgârnıñ Париж Parij'
+                       ],
+                       [ // general words, covering more of the alphabet
+                               [
+                                       'crh'      => 'чёкюч çöküç элифбени elifbeni полициясы politsiyası',
+                                       'crh-cyrl' => 'чёкюч чёкюч элифбени элифбени полициясы полициясы',
+                                       'crh-latn' => 'çöküç çöküç elifbeni elifbeni politsiyası politsiyası',
+                               ],
+                               'чёкюч çöküç элифбени elifbeni полициясы politsiyası'
+                       ],
+                       [ // general words, covering more of the alphabet
+                               [
+                                       'crh'      => 'хусусында hususında акъшамларны aqşamlarnı опькеленюв öpkelenüv',
+                                       'crh-cyrl' => 'хусусында хусусында акъшамларны акъшамларны опькеленюв опькеленюв',
+                                       'crh-latn' => 'hususında hususında aqşamlarnı aqşamlarnı öpkelenüv öpkelenüv',
+                               ],
+                               'хусусында hususında акъшамларны aqşamlarnı опькеленюв öpkelenüv'
+                       ],
+                       [ // general words, covering more of the alphabet
+                               [
+                                       'crh'      => 'кулюмсиреди külümsiredi айтмайджагъым aytmaycağım козьяшсыз közyaşsız',
+                                       'crh-cyrl' => 'кулюмсиреди кулюмсиреди айтмайджагъым айтмайджагъым козьяшсыз козьяшсыз',
+                                       'crh-latn' => 'külümsiredi külümsiredi aytmaycağım aytmaycağım közyaşsız közyaşsız',
+                               ],
+                               'кулюмсиреди külümsiredi айтмайджагъым aytmaycağım козьяшсыз közyaşsız'
+                       ],
+                       [ // exception words
+                               [
+                                       'crh'      => 'инструменталь instrumental гургуль gürgül тюшюнмемек tüşünmemek',
+                                       'crh-cyrl' => 'инструменталь инструменталь гургуль гургуль тюшюнмемек тюшюнмемек',
+                                       'crh-latn' => 'instrumental instrumental gürgül gürgül tüşünmemek tüşünmemek',
+                               ],
+                               'инструменталь instrumental гургуль gürgül тюшюнмемек tüşünmemek'
+                       ],
+                       [ // multi part words
+                               [
+                                       'crh'      => 'эки юз eki yüz',
+                                       'crh-cyrl' => 'эки юз эки юз',
+                                       'crh-latn' => 'eki yüz eki yüz',
+                               ],
+                               'эки юз eki yüz'
+                       ],
+                       [ // ALL CAPS, made up acronyms
+                               [
+                                       'crh'      => 'ÑAB QIC ĞUK COT НЪАБ КЪЫДж ГЪУК ДЖОТ CA ДЖА',
+                                       'crh-cyrl' => 'НЪАБ КЪЫДж ГЪУК ДЖОТ НЪАБ КЪЫДж ГЪУК ДЖОТ ДЖА ДЖА',
+                                       'crh-latn' => 'ÑAB QIC ĞUK COT ÑAB QIC ĞUK COT CA CA',
+                               ],
+                               'ÑAB QIC ĞUK COT НЪАБ КЪЫДж ГЪУК ДЖОТ CA ДЖА'
+                       ],
+               ];
+       }
+}
index 2bc9c21..534af86 100644 (file)
                                { name: 'filter5', cssClass: 'filter5class' },
                                { name: 'filter6' } // Not supporting highlights
                        ]
-               }, {
-                       name: 'group4',
-                       title: 'Group 4',
-                       type: 'boolean',
-                       isSticky: true,
-                       filters: [
-                               { name: 'stickyFilter7', cssClass: 'filter7class' },
-                               { name: 'stickyFilter8', cssClass: 'filter8class' }
-                       ]
                } ],
                minimalDefaultParams = {
                        filter1: '1',
                        { urlversion: '2', filter2: '1', group3: 'filter5', foo: 'bar' },
                        'Model state is reflected in the updated URI with existing uri params'
                );
-
-               // Update the model with sticky filter
-               filtersModel.toggleFiltersSelected( {
-                       group4__stickyFilter7: true
-               } );
-
-               assert.deepEqual(
-                       ( uriProcessor.getUpdatedUri( {} ) ).query,
-                       { urlversion: '2', filter2: '1', group3: 'filter5' },
-                       'Sticky parameters are not reflected in the URI query'
-               );
        } );
 
        QUnit.test( 'updateModelBasedOnQuery', function ( assert ) {
index 98b87fe..890fe5b 100644 (file)
@@ -3,8 +3,8 @@ const Page = require( './page' );
 
 class PreferencesPage extends Page {
 
-       get realName() { return browser.element( '#mw-input-wprealname' ); }
-       get save() { return browser.element( '#prefcontrol' ); }
+       get realName() { return browser.element( '#mw-input-wprealname .oo-ui-inputWidget-input' ); }
+       get save() { return browser.element( '#prefcontrol .oo-ui-buttonElement-button' ); }
 
        open() {
                super.open( 'Special:Preferences' );