Merge "Move Title::isNamespaceProtected() to PermissionManager."
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 28 Aug 2019 09:19:14 +0000 (09:19 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 28 Aug 2019 09:19:14 +0000 (09:19 +0000)
232 files changed:
RELEASE-NOTES-1.34
autoload.php
docs/database.txt
docs/hooks.txt
docs/memcached.txt
includes/Category.php
includes/DefaultSettings.php
includes/ForkController.php
includes/Linker.php
includes/MediaWikiServices.php
includes/OutputPage.php
includes/Rest/EntryPoint.php
includes/Revision/RevisionRenderer.php
includes/ServiceWiring.php
includes/Setup.php
includes/Storage/NameTableStore.php
includes/Title.php
includes/WebRequest.php
includes/WebStart.php
includes/WikiMap.php
includes/actions/InfoAction.php
includes/actions/pagers/HistoryPager.php
includes/api/ApiHelp.php
includes/api/ApiMain.php
includes/api/ApiPageSet.php
includes/api/ApiQueryAllDeletedRevisions.php
includes/api/ApiQueryImageInfo.php
includes/api/i18n/ar.json
includes/api/i18n/en.json
includes/api/i18n/fr.json
includes/api/i18n/qqq.json
includes/api/i18n/sv.json
includes/api/i18n/zh-hans.json
includes/api/i18n/zh-hant.json
includes/auth/Throttler.php
includes/block/BlockManager.php
includes/cache/FileCacheBase.php
includes/cache/MessageCache.php
includes/cache/localisation/LocalisationCache.php
includes/changes/RecentChange.php
includes/db/MWLBFactory.php
includes/db/ORAField.php [deleted file]
includes/db/ORAResult.php [deleted file]
includes/filebackend/FileBackendGroup.php
includes/filebackend/lockmanager/LockManagerGroup.php
includes/filebackend/lockmanager/LockManagerGroupFactory.php [new file with mode: 0644]
includes/gallery/ImageGalleryBase.php
includes/installer/Installer.php
includes/installer/SqliteInstaller.php
includes/installer/i18n/da.json
includes/installer/i18n/id.json
includes/installer/i18n/ro.json
includes/installer/i18n/sh.json
includes/jobqueue/JobQueueDB.php
includes/jobqueue/JobRunner.php
includes/language/ConverterRule.php [new file with mode: 0644]
includes/language/LanguageCode.php
includes/language/LanguageNameUtils.php [deleted file]
includes/libs/Xhprof.php
includes/libs/filebackend/FSFileBackend.php
includes/libs/filebackend/FileBackend.php
includes/libs/filebackend/FileBackendMultiWrite.php
includes/libs/filebackend/FileBackendStore.php
includes/libs/filebackend/FileOpBatch.php
includes/libs/filebackend/HTTPFileStreamer.php
includes/libs/filebackend/MemoryFileBackend.php
includes/libs/filebackend/SwiftFileBackend.php
includes/libs/filebackend/fileop/FileOp.php
includes/libs/filebackend/fileop/StoreFileOp.php
includes/libs/filebackend/fsfile/FSFile.php
includes/libs/filebackend/fsfile/TempFSFile.php
includes/libs/mime/XmlTypeCheck.php
includes/libs/objectcache/APCBagOStuff.php
includes/libs/objectcache/APCUBagOStuff.php
includes/libs/objectcache/BagOStuff.php
includes/libs/objectcache/CachedBagOStuff.php
includes/libs/objectcache/EmptyBagOStuff.php
includes/libs/objectcache/HashBagOStuff.php
includes/libs/objectcache/MediumSpecificBagOStuff.php
includes/libs/objectcache/MemcachedClient.php [deleted file]
includes/libs/objectcache/MemcachedPeclBagOStuff.php
includes/libs/objectcache/MemcachedPhpBagOStuff.php
includes/libs/objectcache/MultiWriteBagOStuff.php
includes/libs/objectcache/RESTBagOStuff.php
includes/libs/objectcache/RedisBagOStuff.php
includes/libs/objectcache/ReplicatedBagOStuff.php
includes/libs/objectcache/WinCacheBagOStuff.php
includes/libs/objectcache/utils/MemcachedClient.php [new file with mode: 0644]
includes/libs/objectcache/wancache/WANObjectCache.php
includes/libs/rdbms/ChronologyProtector.php
includes/libs/rdbms/database/Database.php
includes/libs/rdbms/database/DatabaseMysqlBase.php
includes/libs/rdbms/database/DatabaseSqlite.php
includes/libs/rdbms/database/IDatabase.php
includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php [deleted file]
includes/libs/rdbms/encasing/MssqlBlob.php [deleted file]
includes/libs/rdbms/field/MssqlField.php [deleted file]
includes/libs/rdbms/lbfactory/LBFactory.php
includes/libs/rdbms/loadbalancer/ILoadBalancer.php
includes/libs/rdbms/loadbalancer/LoadBalancer.php
includes/mail/EmailNotification.php
includes/media/FormatMetadata.php
includes/objectcache/SqlBagOStuff.php
includes/pager/IndexPager.php
includes/pager/TablePager.php
includes/parser/CoreParserFunctions.php
includes/parser/PPFrame_DOM.php
includes/parser/PPFrame_Hash.php
includes/parser/ParserCache.php
includes/password/PasswordPolicyChecks.php
includes/preferences/DefaultPreferencesFactory.php
includes/profiler/Profiler.php
includes/rcfeed/IRCColourfulRCFeedFormatter.php
includes/resourceloader/ResourceLoader.php
includes/resourceloader/ResourceLoaderContext.php
includes/resourceloader/ResourceLoaderStartUpModule.php
includes/search/RevisionSearchResult.php [new file with mode: 0644]
includes/search/RevisionSearchResultTrait.php [new file with mode: 0644]
includes/search/SearchResult.php
includes/search/SearchResultTrait.php [new file with mode: 0644]
includes/search/SqlSearchResult.php
includes/skins/Skin.php
includes/skins/SkinTemplate.php
includes/specialpage/ChangesListSpecialPage.php
includes/specials/SpecialContributions.php
includes/specials/SpecialNewSection.php
includes/specials/SpecialNewimages.php
includes/specials/SpecialRecentChanges.php
includes/specials/pagers/AllMessagesTablePager.php
includes/specials/pagers/BlockListPager.php
includes/specials/pagers/CategoryPager.php
includes/specials/pagers/ContribsPager.php
includes/specials/pagers/DeletedContribsPager.php
includes/specials/pagers/ImageListPager.php
includes/specials/pagers/NewFilesPager.php
includes/specials/pagers/ProtectedPagesPager.php
includes/user/User.php
languages/ConverterRule.php [deleted file]
languages/Language.php
languages/data/Names.php
languages/i18n/ban.json
languages/i18n/bcc.json
languages/i18n/bjn.json
languages/i18n/bn.json
languages/i18n/bs.json
languages/i18n/cs.json
languages/i18n/da.json
languages/i18n/el.json
languages/i18n/en.json
languages/i18n/es.json
languages/i18n/exif/en.json
languages/i18n/exif/qqq.json
languages/i18n/exif/ro.json
languages/i18n/exif/szl.json
languages/i18n/exif/tt-cyrl.json
languages/i18n/exif/zh-hans.json
languages/i18n/fa.json
languages/i18n/fi.json
languages/i18n/fr.json
languages/i18n/he.json
languages/i18n/id.json
languages/i18n/ig.json
languages/i18n/io.json
languages/i18n/it.json
languages/i18n/ko.json
languages/i18n/min.json
languages/i18n/ml.json
languages/i18n/nap.json
languages/i18n/nl.json
languages/i18n/nqo.json
languages/i18n/nys.json
languages/i18n/pl.json
languages/i18n/qqq.json
languages/i18n/ru.json
languages/i18n/sd.json
languages/i18n/sh.json
languages/i18n/sv.json
languages/i18n/te.json
languages/i18n/tr.json
languages/i18n/uk.json
languages/i18n/zh-hans.json
languages/i18n/zh-hant.json
languages/i18n/zh-hk.json
maintenance/namespaceDupes.php
maintenance/populateLogSearch.php
maintenance/rebuildLocalisationCache.php
maintenance/rebuildtextindex.php
maintenance/sql.php
maintenance/sqlite.php
maintenance/tables.sql
maintenance/update.php
package-lock.json
package.json
resources/lib/foreign-resources.yaml
resources/lib/jquery/jquery-3.3.1.patch
resources/lib/jquery/jquery.migrate-3.0.1.patch [new file with mode: 0644]
resources/src/mediawiki.Uri/Uri.js
resources/src/mediawiki.widgets/mw.widgets.TitleWidget.js
resources/src/startup/mediawiki.js
tests/common/TestsAutoLoader.php
tests/parser/ParserTestRunner.php
tests/parser/parserTests.txt
tests/phpunit/MediaWikiIntegrationTestCase.php
tests/phpunit/includes/OutputPageTest.php
tests/phpunit/includes/Rest/EntryPointTest.php [new file with mode: 0644]
tests/phpunit/includes/Storage/NameTableStoreTest.php
tests/phpunit/includes/TitlePermissionTest.php
tests/phpunit/includes/api/ApiQuerySearchTest.php
tests/phpunit/includes/api/ApiQuerySiteinfoTest.php
tests/phpunit/includes/cache/LocalisationCacheTest.php
tests/phpunit/includes/db/DatabaseTestHelper.php
tests/phpunit/includes/filebackend/lockmanager/LockManagerGroupIntegrationTest.php
tests/phpunit/includes/language/ConverterRuleTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/objectcache/BagOStuffTest.php
tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php
tests/phpunit/includes/logging/LogFormatterTest.php
tests/phpunit/includes/password/PasswordPolicyChecksTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderContextTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderStartUpModuleTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderTest.php
tests/phpunit/includes/search/SearchResultTest.php [deleted file]
tests/phpunit/includes/search/SearchResultTraitTest.php [new file with mode: 0644]
tests/phpunit/languages/LanguageTest.php
tests/phpunit/mocks/search/MockSearchResult.php
tests/phpunit/unit/includes/Rest/EntryPointTest.php [deleted file]
tests/phpunit/unit/includes/filebackend/lockmanager/LockManagerGroupFactoryTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/filebackend/lockmanager/LockManagerGroupTest.php [new file with mode: 0644]
tests/phpunit/unit/includes/language/LanguageNameUtilsTest.php [deleted file]
tests/phpunit/unit/includes/language/LanguageNameUtilsTestTrait.php [deleted file]
tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js
tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js
tests/selenium/wdio.conf.js

index 8730484..932122d 100644 (file)
@@ -75,6 +75,8 @@ For notes on 1.33.x and older releases, see HISTORY.
 * $wgDebugPrintHttpHeaders - The default of including HTTP headers in the
   debug log channel is no longer configurable. The debug log itself remains
   configurable via $wgDebugLogFile.
+* $wgMsgCacheExpiry - The MessageCache uses 24 hours as the expiry for values
+  stored in WANObjectCache. This is no longer configurable.
 * $wgPasswordSalt – This setting, used for migrating exceptionally old, insecure
   password setups and deprecated since 1.24, is now removed.
 * $wgDBOracleDRCP - If you must use persistent connections, set DBO_PERSISTENT
@@ -88,6 +90,10 @@ For notes on 1.33.x and older releases, see HISTORY.
   ([[Special:NewSection/Test]] redirects to creating a new section in "Test").
   Otherwise, it displays a basic interface to allow the end user to specify
   the target manually.
+* (T220447) Special:Contributions/newbies has been removed for performance and
+  usefulness reasons. Use Special:RecentChanges?userExpLevel=newcomer instead.
+* Special:NewFiles/newbies has been removed for performance and usefulness
+  reasons. Use Special:RecentChanges?userExpLevel=newcomer&namespace=6 instead.
 
 === New developer features in 1.34 ===
 * The ImgAuthModifyHeaders hook was added to img_auth.php to allow modification
@@ -354,8 +360,6 @@ because of Phabricator reports.
 * The UserIsBlockedFrom hook is only called if a block is found first, and
   should only be used to unblock a blocked user.
 * …
-* Language::$dataCache has been removed (without prior deprecation, for
-  practical reasons). Use MediaWikiServices instead to get a LocalisationCache.
 
 === Deprecations in 1.34 ===
 * The MWNamespace class is deprecated. Use NamespaceInfo.
@@ -411,6 +415,9 @@ because of Phabricator reports.
 * ResourceLoaderContext::getConfig and ResourceLoaderContext::getLogger have
   been deprecated. Inside ResourceLoaderModule subclasses, use the local methods
   instead. Elsewhere, use the methods from the ResourceLoader class.
+* The Profiler::setTemplated and Profiler::getTemplated methods have been
+  deprecated. Use Profiler::setAllowOutput and Profiler::getAllowOutput
+  instead.
 * The Preprocessor_DOM implementation has been deprecated.  It will be
   removed in a future release.  Use the Preprocessor_Hash implementation
   instead.
@@ -431,6 +438,8 @@ because of Phabricator reports.
   engines.
 * Skin::escapeSearchLink() is deprecated. Use Skin::getSearchLink() or the skin
   template option 'searchaction' instead.
+* Skin::getRevisionId() and Skin::isRevisionCurrent() have been deprecated.
+  Use OutputPage::getRevisionId() and OutputPage::isRevisionCurrent() instead.
 * LoadBalancer::haveIndex() and LoadBalancer::isNonZeroLoad() have
   been deprecated.
 * FileBackend::getWikiId() has been deprecated.
@@ -463,12 +472,10 @@ because of Phabricator reports.
 * Constructing MovePage directly is deprecated. Use MovePageFactory.
 * TempFSFile::factory() has been deprecated. Use TempFSFileFactory instead.
 * wfIsBadImage() is deprecated. Use the BadFileLookup service instead.
-* Language::getLocalisationCache() is deprecated. Use MediaWikiServices.
-* The following Language methods are deprecated: isSupportedLanguage,
-  isValidCode, isValidBuiltInCode, isKnownLanguageTag, fetchLanguageNames,
-  fetchLanguageName, getFileName, getMessagesFileName, getJsonMessagesFileName.
-  Use the new LanguageNameUtils class instead. (Note that fetchLanguageName(s)
-  are called getLanguageName(s) in the new class.)
+* Building a new SearchResult is hard-deprecated, always call
+  SearchResult::newFromTitle(). This class is being refactored into an abstract
+  class. If you extend this class please be sure to override all its methods
+  or extend RevisionSearchResult.
 
 === Other changes in 1.34 ===
 * …
index 9b35c1b..35c9b0a 100644 (file)
@@ -316,7 +316,7 @@ $wgAutoloadLocalClasses = [
        'ConvertExtensionToRegistration' => __DIR__ . '/maintenance/convertExtensionToRegistration.php',
        'ConvertLinks' => __DIR__ . '/maintenance/convertLinks.php',
        'ConvertUserOptions' => __DIR__ . '/maintenance/convertUserOptions.php',
-       'ConverterRule' => __DIR__ . '/languages/ConverterRule.php',
+       'ConverterRule' => __DIR__ . '/includes/language/ConverterRule.php',
        'Cookie' => __DIR__ . '/includes/libs/Cookie.php',
        'CookieJar' => __DIR__ . '/includes/libs/CookieJar.php',
        'CopyFileBackend' => __DIR__ . '/maintenance/copyFileBackend.php',
@@ -882,6 +882,7 @@ $wgAutoloadLocalClasses = [
        'MediaWiki\\Diff\\ComplexityException' => __DIR__ . '/includes/diff/ComplexityException.php',
        'MediaWiki\\Diff\\WordAccumulator' => __DIR__ . '/includes/diff/WordAccumulator.php',
        'MediaWiki\\FileBackend\\FSFile\\TempFSFileFactory' => __DIR__ . '/includes/libs/filebackend/fsfile/TempFSFileFactory.php',
+       'MediaWiki\\FileBackend\\LockManager\\LockManagerGroupFactory' => __DIR__ . '/includes/filebackend/lockmanager/LockManagerGroupFactory.php',
        'MediaWiki\\HeaderCallback' => __DIR__ . '/includes/HeaderCallback.php',
        'MediaWiki\\Http\\HttpRequestFactory' => __DIR__ . '/includes/http/HttpRequestFactory.php',
        'MediaWiki\\Installer\\InstallException' => __DIR__ . '/includes/installer/InstallException.php',
@@ -892,7 +893,6 @@ $wgAutoloadLocalClasses = [
        '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\\Languages\\LanguageNameUtils' => __DIR__ . '/includes/language/LanguageNameUtils.php',
        'MediaWiki\\Logger\\ConsoleLogger' => __DIR__ . '/includes/debug/logger/ConsoleLogger.php',
        'MediaWiki\\Logger\\ConsoleSpi' => __DIR__ . '/includes/debug/logger/ConsoleSpi.php',
        'MediaWiki\\Logger\\LegacyLogger' => __DIR__ . '/includes/debug/logger/LegacyLogger.php',
@@ -975,7 +975,7 @@ $wgAutoloadLocalClasses = [
        'MediumSpecificBagOStuff' => __DIR__ . '/includes/libs/objectcache/MediumSpecificBagOStuff.php',
        'MemcLockManager' => __DIR__ . '/includes/libs/lockmanager/MemcLockManager.php',
        'MemcachedBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedBagOStuff.php',
-       'MemcachedClient' => __DIR__ . '/includes/libs/objectcache/MemcachedClient.php',
+       'MemcachedClient' => __DIR__ . '/includes/libs/objectcache/utils/MemcachedClient.php',
        'MemcachedPeclBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedPeclBagOStuff.php',
        'MemcachedPhpBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedPhpBagOStuff.php',
        'MemoizedCallable' => __DIR__ . '/includes/libs/MemoizedCallable.php',
@@ -1045,8 +1045,6 @@ $wgAutoloadLocalClasses = [
        'NullStatsdDataFactory' => __DIR__ . '/includes/libs/stats/NullStatsdDataFactory.php',
        'NumericUppercaseCollation' => __DIR__ . '/includes/collation/NumericUppercaseCollation.php',
        'OOUIHTMLForm' => __DIR__ . '/includes/htmlform/OOUIHTMLForm.php',
-       'ORAField' => __DIR__ . '/includes/db/ORAField.php',
-       'ORAResult' => __DIR__ . '/includes/db/ORAResult.php',
        'ObjectCache' => __DIR__ . '/includes/objectcache/ObjectCache.php',
        'OldChangesList' => __DIR__ . '/includes/changes/OldChangesList.php',
        'OldLocalFile' => __DIR__ . '/includes/filerepo/file/OldLocalFile.php',
@@ -1288,6 +1286,8 @@ $wgAutoloadLocalClasses = [
        'RevisionItemBase' => __DIR__ . '/includes/revisionlist/RevisionItemBase.php',
        'RevisionList' => __DIR__ . '/includes/revisionlist/RevisionList.php',
        'RevisionListBase' => __DIR__ . '/includes/revisionlist/RevisionListBase.php',
+       'RevisionSearchResult' => __DIR__ . '/includes/search/RevisionSearchResult.php',
+       'RevisionSearchResultTrait' => __DIR__ . '/includes/search/RevisionSearchResultTrait.php',
        'RiffExtractor' => __DIR__ . '/includes/libs/RiffExtractor.php',
        'RightsLogFormatter' => __DIR__ . '/includes/logging/RightsLogFormatter.php',
        'RollbackAction' => __DIR__ . '/includes/actions/RollbackAction.php',
@@ -1319,6 +1319,7 @@ $wgAutoloadLocalClasses = [
        'SearchResult' => __DIR__ . '/includes/search/SearchResult.php',
        'SearchResultSet' => __DIR__ . '/includes/search/SearchResultSet.php',
        'SearchResultSetTrait' => __DIR__ . '/includes/search/SearchResultSetTrait.php',
+       'SearchResultTrait' => __DIR__ . '/includes/search/SearchResultTrait.php',
        'SearchSqlite' => __DIR__ . '/includes/search/SearchSqlite.php',
        'SearchSuggestion' => __DIR__ . '/includes/search/SearchSuggestion.php',
        'SearchSuggestionSet' => __DIR__ . '/includes/search/SearchSuggestionSet.php',
@@ -1684,9 +1685,6 @@ $wgAutoloadLocalClasses = [
        'Wikimedia\\Rdbms\\LoadMonitorMySQL' => __DIR__ . '/includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php',
        'Wikimedia\\Rdbms\\LoadMonitorNull' => __DIR__ . '/includes/libs/rdbms/loadmonitor/LoadMonitorNull.php',
        'Wikimedia\\Rdbms\\MaintainableDBConnRef' => __DIR__ . '/includes/libs/rdbms/database/MaintainableDBConnRef.php',
-       'Wikimedia\\Rdbms\\MssqlBlob' => __DIR__ . '/includes/libs/rdbms/encasing/MssqlBlob.php',
-       'Wikimedia\\Rdbms\\MssqlField' => __DIR__ . '/includes/libs/rdbms/field/MssqlField.php',
-       'Wikimedia\\Rdbms\\MssqlResultWrapper' => __DIR__ . '/includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php',
        'Wikimedia\\Rdbms\\MySQLField' => __DIR__ . '/includes/libs/rdbms/field/MySQLField.php',
        'Wikimedia\\Rdbms\\MySQLMasterPos' => __DIR__ . '/includes/libs/rdbms/database/position/MySQLMasterPos.php',
        'Wikimedia\\Rdbms\\NextSequenceValue' => __DIR__ . '/includes/libs/rdbms/database/utils/NextSequenceValue.php',
index 6e88d68..c09dd38 100644 (file)
@@ -176,8 +176,6 @@ MediaWiki does support the following other DBMSs to varying degrees.
 
 * PostgreSQL
 * SQLite
-* Oracle
-* MSSQL
 
 More information can be found about each of these databases (known issues,
 level of support, extra configuration) in the "databases" subdirectory in
index 719c60f..b7ea02c 100644 (file)
@@ -3504,6 +3504,12 @@ processing.
 &$archive: PageArchive object
 $title: Title object of the page that we're about to undelete
 
+'UndeletePageToolLinks': Add one or more links to edit page subtitle when a page
+has been previously deleted.
+$context: IContextSource (object)
+$linkRenderer: LinkRenderer instance
+&$links: Array of HTML strings
+
 'UndeleteShowRevision': Called when showing a revision in Special:Undelete.
 $title: title object related to the revision
 $rev: revision (object) that will be viewed
index ba325fe..d0a6e3d 100644 (file)
@@ -131,13 +131,7 @@ Localisation:
        cleared by: Language::loadLocalisation()
 
 Message Cache:
-       backend: $wgMessageCacheType
-       key: $wgDBname:messages, $wgDBname:messages-hash, $wgDBname:messages-status
-       ex: wikidb:messages, wikidb:messages-hash, wikidb:messages-status
-       stores: an array where the keys are DB keys and the values are messages
-       set in: wfMessage(), Article::editUpdates() and Title::moveTo()
-       expiry: $wgMsgCacheExpiry
-       cleared by: nothing
+       See MessageCache.php.
 
 Newtalk:
        key: $wgDBname:newtalk:ip:$ip
index 34ac0e1..229958a 100644 (file)
@@ -340,7 +340,7 @@ class Category {
                $dbw->lockForUpdate( 'category', [ 'cat_title' => $this->mName ], __METHOD__ );
 
                // Lock all the `categorylinks` records and gaps for this category;
-               // this is a separate query due to postgres/oracle limitations
+               // this is a separate query due to postgres limitations
                $dbw->selectRowCount(
                        [ 'categorylinks', 'page' ],
                        '*',
index 8341dac..93a5919 100644 (file)
@@ -788,10 +788,6 @@ $wgFileBackends = [];
  * See LockManager::__construct() for more details.
  * Additional parameters are specific to the lock manager class used.
  * These settings should be global to all wikis.
- *
- * When using DBLockManager, the 'dbsByBucket' map can reference 'localDBMaster' as
- * a peer database in each bucket. This will result in an extra connection to the domain
- * that the LockManager services, which must also be a valid wiki ID.
  */
 $wgLockManagers = [];
 
@@ -2636,8 +2632,6 @@ $wgLocalisationCacheConf = [
        'store' => 'detect',
        'storeClass' => false,
        'storeDirectory' => false,
-       'storeServer' => [],
-       'forceRecache' => false,
        'manualRecache' => false,
 ];
 
@@ -3120,11 +3114,6 @@ $wgTranslateNumerals = true;
  */
 $wgUseDatabaseMessages = true;
 
-/**
- * Expiry time for the message cache key
- */
-$wgMsgCacheExpiry = 86400;
-
 /**
  * Maximum entry size in the message cache, in bytes
  */
@@ -6844,6 +6833,8 @@ $wgRCLinkLimits = [ 50, 100, 250, 500 ];
 /**
  * List of Days options to list in the Special:Recentchanges and
  * Special:Recentchangeslinked pages.
+ *
+ * @see ChangesListSpecialPage::getLinkDays
  */
 $wgRCLinkDays = [ 1, 3, 7, 14, 30 ];
 
index 85f3a7d..af06a88 100644 (file)
@@ -154,7 +154,6 @@ class ForkController {
                // Don't share DB, storage, or memcached connections
                MediaWikiServices::resetChildProcessServices();
                FileBackendGroup::destroySingleton();
-               LockManagerGroup::destroySingletons();
                JobQueueGroup::destroySingletons();
                ObjectCache::clear();
                RedisConnectionPool::destroySingletons();
index 47be8a2..03d2516 100644 (file)
@@ -688,35 +688,37 @@ class Linker {
                if ( $label == '' ) {
                        $label = $title->getPrefixedText();
                }
-               $encLabel = htmlspecialchars( $label );
                $currentExists = $time
                        && MediaWikiServices::getInstance()->getRepoGroup()->findFile( $title ) !== false;
 
                if ( ( $wgUploadMissingFileUrl || $wgUploadNavigationUrl || $wgEnableUploads )
                        && !$currentExists
                ) {
-                       $redir = RepoGroup::singleton()->getLocalRepo()->checkRedirect( $title );
-
-                       if ( $redir ) {
-                               // We already know it's a redirect, so mark it
-                               // accordingly
+                       if ( RepoGroup::singleton()->getLocalRepo()->checkRedirect( $title ) ) {
+                               // We already know it's a redirect, so mark it accordingly
                                return self::link(
                                        $title,
-                                       $encLabel,
+                                       htmlspecialchars( $label ),
                                        [ 'class' => 'mw-redirect' ],
                                        wfCgiToArray( $query ),
                                        [ 'known', 'noclasses' ]
                                );
                        }
 
-                       $href = self::getUploadUrl( $title, $query );
-
-                       return '<a href="' . htmlspecialchars( $href ) . '" class="new" title="' .
-                               htmlspecialchars( $title->getPrefixedText(), ENT_QUOTES ) . '">' .
-                               $encLabel . '</a>';
+                       return Html::element( 'a', [
+                                       'href' => self::getUploadUrl( $title, $query ),
+                                       'class' => 'new',
+                                       'title' => $title->getPrefixedText()
+                               ], $label );
                }
 
-               return self::link( $title, $encLabel, [], wfCgiToArray( $query ), [ 'known', 'noclasses' ] );
+               return self::link(
+                       $title,
+                       htmlspecialchars( $label ),
+                       [],
+                       wfCgiToArray( $query ),
+                       [ 'known', 'noclasses' ]
+               );
        }
 
        /**
@@ -1322,7 +1324,7 @@ class Linker {
                                        $services->getNamespaceInfo()->getCanonicalName( NS_MEDIA ), '/' );
                                $medians .= '|';
                                $medians .= preg_quote(
-                                       MediaWikiServices::getInstance()->getContentLanguage()->getNsText( NS_MEDIA ),
+                                       $services->getContentLanguage()->getNsText( NS_MEDIA ),
                                        '/'
                                ) . '):';
 
@@ -1359,7 +1361,7 @@ class Linker {
                                        }
                                        if ( $match[1] !== false && $match[1] !== '' ) {
                                                if ( preg_match(
-                                                       MediaWikiServices::getInstance()->getContentLanguage()->linkTrail(),
+                                                       $services->getContentLanguage()->linkTrail(),
                                                        $match[3],
                                                        $submatch
                                                ) ) {
@@ -1375,7 +1377,7 @@ class Linker {
 
                                                Title::newFromText( $linkTarget );
                                                try {
-                                                       $target = MediaWikiServices::getInstance()->getTitleParser()->
+                                                       $target = $services->getTitleParser()->
                                                                parseTitle( $linkTarget );
 
                                                        if ( $target->getText() == '' && !$target->isExternal()
index e926c32..3b80e58 100644 (file)
@@ -14,12 +14,11 @@ use GlobalVarConfig;
 use Hooks;
 use IBufferingStatsdDataFactory;
 use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
-use LocalisationCache;
 use MediaWiki\Block\BlockManager;
 use MediaWiki\Block\BlockRestrictionStore;
 use MediaWiki\FileBackend\FSFile\TempFSFileFactory;
+use MediaWiki\FileBackend\LockManager\LockManagerGroupFactory;
 use MediaWiki\Http\HttpRequestFactory;
-use MediaWiki\Languages\LanguageNameUtils;
 use MediaWiki\Page\MovePageFactory;
 use MediaWiki\Permissions\PermissionManager;
 use MediaWiki\Preferences\PreferencesFactory;
@@ -625,14 +624,6 @@ class MediaWikiServices extends ServiceContainer {
                return $this->getService( 'InterwikiLookup' );
        }
 
-       /**
-        * @since 1.34
-        * @return LanguageNameUtils
-        */
-       public function getLanguageNameUtils() {
-               return $this->getService( 'LanguageNameUtils' );
-       }
-
        /**
         * @since 1.28
         * @return LinkCache
@@ -660,14 +651,6 @@ class MediaWikiServices extends ServiceContainer {
                return $this->getService( 'LinkRendererFactory' );
        }
 
-       /**
-        * @since 1.34
-        * @return LocalisationCache
-        */
-       public function getLocalisationCache() : LocalisationCache {
-               return $this->getService( 'LocalisationCache' );
-       }
-
        /**
         * @since 1.28
         * @return \BagOStuff
@@ -676,6 +659,14 @@ class MediaWikiServices extends ServiceContainer {
                return $this->getService( 'LocalServerObjectCache' );
        }
 
+       /**
+        * @since 1.34
+        * @return LockManagerGroupFactory
+        */
+       public function getLockManagerGroupFactory() : LockManagerGroupFactory {
+               return $this->getService( 'LockManagerGroupFactory' );
+       }
+
        /**
         * @since 1.32
         * @return MagicWordFactory
index f8b7502..b2ca53a 100644 (file)
@@ -1661,6 +1661,16 @@ class OutputPage extends ContextSource {
                return $this->mRevisionId;
        }
 
+       /**
+        * Whether the revision displayed is the latest revision of the page
+        *
+        * @since 1.34
+        * @return bool
+        */
+       public function isRevisionCurrent() {
+               return $this->mRevisionId == 0 || $this->mRevisionId == $this->getTitle()->getLatestRevID();
+       }
+
        /**
         * Set the timestamp of the revision which will be displayed. This is used
         * to avoid a extra DB call in Skin::lastModified().
@@ -3217,7 +3227,7 @@ class OutputPage extends ContextSource {
 
                $title = $this->getTitle();
                $ns = $title->getNamespace();
-               $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
+               $nsInfo = $services->getNamespaceInfo();
                $canonicalNamespace = $nsInfo->exists( $ns )
                        ? $nsInfo->getCanonicalName( $ns )
                        : $title->getNsText();
index ae01ae4..a4959d1 100644 (file)
@@ -3,6 +3,7 @@
 namespace MediaWiki\Rest;
 
 use ExtensionRegistry;
+use MediaWiki;
 use MediaWiki\MediaWikiServices;
 use MediaWiki\Rest\BasicAccess\MWBasicAuthorizer;
 use RequestContext;
@@ -16,6 +17,8 @@ class EntryPoint {
        private $webResponse;
        /** @var Router */
        private $router;
+       /** @var RequestContext */
+       private $context;
 
        public static function main() {
                // URL safety checks
@@ -24,10 +27,12 @@ class EntryPoint {
                        return;
                }
 
+               $context = RequestContext::getMain();
+
                // Set $wgTitle and the title in RequestContext, as in api.php
                global $wgTitle;
                $wgTitle = Title::makeTitle( NS_SPECIAL, 'Badtitle/rest.php' );
-               RequestContext::getMain()->setTitle( $wgTitle );
+               $context->setTitle( $wgTitle );
 
                $services = MediaWikiServices::getInstance();
                $conf = $services->getMainConfig();
@@ -42,7 +47,7 @@ class EntryPoint {
                        'cookiePrefix' => $conf->get( 'CookiePrefix' )
                ] );
 
-               $authorizer = new MWBasicAuthorizer( RequestContext::getMain()->getUser(),
+               $authorizer = new MWBasicAuthorizer( $context->getUser(),
                        $services->getPermissionManager() );
 
                global $IP;
@@ -56,21 +61,24 @@ class EntryPoint {
                );
 
                $entryPoint = new self(
+                       $context,
                        $request,
                        $wgRequest->response(),
                        $router );
                $entryPoint->execute();
        }
 
-       public function __construct( RequestInterface $request, WebResponse $webResponse,
-               Router $router
+       public function __construct( RequestContext $context, RequestInterface $request,
+               WebResponse $webResponse, Router $router
        ) {
+               $this->context = $context;
                $this->request = $request;
                $this->webResponse = $webResponse;
                $this->router = $router;
        }
 
        public function execute() {
+               ob_start();
                $response = $this->router->execute( $this->request );
 
                $this->webResponse->header(
@@ -91,10 +99,13 @@ class EntryPoint {
                }
 
                // Clear all errors that might have been displayed if display_errors=On
-               ob_clean();
+               ob_end_clean();
 
                $stream = $response->getBody();
                $stream->rewind();
+
+               MediaWiki::preOutputCommit( $this->context );
+
                if ( $stream instanceof CopyableStreamInterface ) {
                        $stream->copyToStream( fopen( 'php://output', 'w' ) );
                } else {
@@ -106,5 +117,8 @@ class EntryPoint {
                                echo $buffer;
                        }
                }
+
+               $mw = new MediaWiki;
+               $mw->doPostOutputShutdown( 'fast' );
        }
 }
index ca4bb73..3c3b6a9 100644 (file)
@@ -164,13 +164,9 @@ class RevisionRenderer {
        }
 
        private function getSpeculativeRevId( $dbIndex ) {
-               // Use a fresh master connection in order to see the latest data, by avoiding
+               // Use a separate master connection in order to see the latest data, by avoiding
                // stale data from REPEATABLE-READ snapshots.
-               // HACK: But don't use a fresh connection in unit tests, since it would not have
-               // the fake tables. This should be handled by the LoadBalancer!
-               $flags = defined( 'MW_PHPUNIT_TEST' ) || $dbIndex === DB_REPLICA
-                       ? 0
-                       : ILoadBalancer::CONN_TRX_AUTOCOMMIT;
+               $flags = ILoadBalancer::CONN_TRX_AUTOCOMMIT;
 
                $db = $this->loadBalancer->getConnectionRef( $dbIndex, [], $this->dbDomain, $flags );
 
@@ -183,13 +179,9 @@ class RevisionRenderer {
        }
 
        private function getSpeculativePageId( $dbIndex ) {
-               // Use a fresh master connection in order to see the latest data, by avoiding
+               // Use a separate master connection in order to see the latest data, by avoiding
                // stale data from REPEATABLE-READ snapshots.
-               // HACK: But don't use a fresh connection in unit tests, since it would not have
-               // the fake tables. This should be handled by the LoadBalancer!
-               $flags = defined( 'MW_PHPUNIT_TEST' ) || $dbIndex === DB_REPLICA
-                       ? 0
-                       : ILoadBalancer::CONN_TRX_AUTOCOMMIT;
+               $flags = ILoadBalancer::CONN_TRX_AUTOCOMMIT;
 
                $db = $this->loadBalancer->getConnectionRef( $dbIndex, [], $this->dbDomain, $flags );
 
index 21a66cd..1acd038 100644 (file)
@@ -45,10 +45,10 @@ use MediaWiki\Block\BlockRestrictionStore;
 use MediaWiki\Config\ConfigRepository;
 use MediaWiki\Config\ServiceOptions;
 use MediaWiki\FileBackend\FSFile\TempFSFileFactory;
+use MediaWiki\FileBackend\LockManager\LockManagerGroupFactory;
 use MediaWiki\Http\HttpRequestFactory;
 use MediaWiki\Interwiki\ClassicInterwikiLookup;
 use MediaWiki\Interwiki\InterwikiLookup;
-use MediaWiki\Languages\LanguageNameUtils;
 use MediaWiki\Linker\LinkRenderer;
 use MediaWiki\Linker\LinkRendererFactory;
 use MediaWiki\Logger\LoggerFactory;
@@ -257,13 +257,6 @@ return [
                );
        },
 
-       'LanguageNameUtils' => function ( MediaWikiServices $services ) : LanguageNameUtils {
-               return new LanguageNameUtils( new ServiceOptions(
-                       LanguageNameUtils::$constructorOptions,
-                       $services->getMainConfig()
-               ) );
-       },
-
        'LinkCache' => function ( MediaWikiServices $services ) : LinkCache {
                return new LinkCache(
                        $services->getTitleFormatter(),
@@ -290,56 +283,6 @@ return [
                );
        },
 
-       'LocalisationCache' => function ( MediaWikiServices $services ) : LocalisationCache {
-               $conf = $services->getMainConfig()->get( 'LocalisationCacheConf' );
-
-               $logger = LoggerFactory::getInstance( 'localisation' );
-
-               // Figure out what class to use for the LCStore
-               $storeArg = [];
-               $storeArg['directory'] =
-                       $conf['storeDirectory'] ?? $services->getMainConfig()->get( 'CacheDirectory' );
-
-               if ( !empty( $conf['storeClass'] ) ) {
-                       $storeClass = $conf['storeClass'];
-               } elseif ( $conf['store'] === 'files' || $conf['store'] === 'file' ||
-                       ( $conf['store'] === 'detect' && $storeArg['directory'] )
-               ) {
-                       $storeClass = LCStoreCDB::class;
-               } elseif ( $conf['store'] === 'db' || $conf['store'] === 'detect' ) {
-                       $storeClass = LCStoreDB::class;
-                       $storeArg['server'] = $conf['storeServer'] ?? [];
-               } elseif ( $conf['store'] === 'array' ) {
-                       $storeClass = LCStoreStaticArray::class;
-               } else {
-                       throw new MWException(
-                               'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.'
-                       );
-               }
-               $logger->debug( "LocalisationCache: using store $storeClass" );
-
-               return new $conf['class'](
-                       new ServiceOptions(
-                               LocalisationCache::$constructorOptions,
-                               // Two of the options are stored in $wgLocalisationCacheConf
-                               $conf,
-                               // In case someone set that config variable and didn't reset all keys, set defaults.
-                               [
-                                       'forceRecache' => false,
-                                       'manualRecache' => false,
-                               ],
-                               // Some other options come from config itself
-                               $services->getMainConfig()
-                       ),
-                       new $storeClass( $storeArg ),
-                       $logger,
-                       [ function () use ( $services ) {
-                               $services->getResourceLoader()->getMessageBlobStore()->clear();
-                       } ],
-                       $services->getLanguageNameUtils()
-               );
-       },
-
        'LocalServerObjectCache' => function ( MediaWikiServices $services ) : BagOStuff {
                $config = $services->getMainConfig();
                $cacheId = \ObjectCache::detectLocalServerCache();
@@ -347,6 +290,14 @@ return [
                return \ObjectCache::newFromParams( $config->get( 'ObjectCaches' )[$cacheId] );
        },
 
+       'LockManagerGroupFactory' => function ( MediaWikiServices $services ) : LockManagerGroupFactory {
+               return new LockManagerGroupFactory(
+                       WikiMap::getCurrentWikiDbDomain()->getId(),
+                       $services->getMainConfig()->get( 'LockManagers' ),
+                       $services->getDBLoadBalancerFactory()
+               );
+       },
+
        'MagicWordFactory' => function ( MediaWikiServices $services ) : MagicWordFactory {
                return new MagicWordFactory( $services->getContentLanguage() );
        },
@@ -403,7 +354,6 @@ return [
                                ? $services->getLocalServerObjectCache()
                                : new EmptyBagOStuff(),
                        $mainConfig->get( 'UseDatabaseMessages' ),
-                       $mainConfig->get( 'MsgCacheExpiry' ),
                        $services->getContentLanguage()
                );
        },
index 201e1a9..2267800 100644 (file)
@@ -96,7 +96,7 @@ if ( !interface_exists( 'Psr\Log\LoggerInterface' ) ) {
 // Install a header callback
 MediaWiki\HeaderCallback::register();
 
-// Set the encoding used by reading HTTP input, writing HTTP output.
+// Set the encoding used by PHP for reading HTTP input, and writing output.
 // This is also the default for mbstring functions.
 mb_internal_encoding( 'UTF-8' );
 
@@ -128,9 +128,6 @@ if ( defined( 'MW_SETUP_CALLBACK' ) ) {
  * Main setup
  */
 
-$fname = 'Setup.php';
-$ps_setup = Profiler::instance()->scopedProfileIn( $fname );
-
 // Load queued extensions
 ExtensionRegistry::getInstance()->loadFromQueue();
 // Don't let any other extensions load
@@ -141,8 +138,6 @@ putenv( "LC_ALL=$wgShellLocale" );
 setlocale( LC_ALL, $wgShellLocale );
 
 // Set various default paths sensibly...
-$ps_default = Profiler::instance()->scopedProfileIn( $fname . '-defaults' );
-
 if ( $wgScript === false ) {
        $wgScript = "$wgScriptPath/index.php";
 }
@@ -368,19 +363,6 @@ foreach ( $wgForeignFileRepos as &$repo ) {
 unset( $repo ); // no global pollution; destroy reference
 
 $rcMaxAgeDays = $wgRCMaxAge / ( 3600 * 24 );
-if ( $wgRCFilterByAge ) {
-       // Trim down $wgRCLinkDays so that it only lists links which are valid
-       // as determined by $wgRCMaxAge.
-       // Note that we allow 1 link higher than the max for things like 56 days but a 60 day link.
-       sort( $wgRCLinkDays );
-
-       foreach ( $wgRCLinkDays as $i => $days ) {
-               if ( $days >= $rcMaxAgeDays ) {
-                       array_splice( $wgRCLinkDays, $i + 1 );
-                       break;
-               }
-       }
-}
 // Ensure that default user options are not invalid, since that breaks Special:Preferences
 $wgDefaultUserOptions['rcdays'] = min(
        $wgDefaultUserOptions['rcdays'],
@@ -638,8 +620,6 @@ if ( defined( 'MW_NO_SESSION' ) ) {
        $wgPHPSessionHandling = MW_NO_SESSION === 'warn' ? 'warn' : 'disable';
 }
 
-Profiler::instance()->scopedProfileOut( $ps_default );
-
 // Disable MWDebug for command line mode, this prevents MWDebug from eating up
 // all the memory from logging SQL queries on maintenance scripts
 global $wgCommandLineMode;
@@ -669,8 +649,6 @@ foreach ( [ 'wgArticlePath', 'wgVariantArticlePath' ] as $varName ) {
        }
 }
 
-$ps_default2 = Profiler::instance()->scopedProfileIn( $fname . '-defaults2' );
-
 if ( $wgCanonicalServer === false ) {
        $wgCanonicalServer = wfExpandUrl( $wgServer, PROTO_HTTP );
 }
@@ -740,10 +718,6 @@ if ( $wgSharedDB && $wgSharedTables ) {
        );
 }
 
-Profiler::instance()->scopedProfileOut( $ps_default2 );
-
-$ps_misc = Profiler::instance()->scopedProfileIn( $fname . '-misc' );
-
 // Raise the memory limit if it's too low
 // Note, this makes use of wfDebug, and thus should not be before
 // MWDebug::init() is called.
@@ -823,13 +797,9 @@ wfDebugLog( 'caches',
        ', session: ' . get_class( ObjectCache::getInstance( $wgSessionCacheType ) )
 );
 
-Profiler::instance()->scopedProfileOut( $ps_misc );
-
 // Most of the config is out, some might want to run hooks here.
 Hooks::run( 'SetupAfterCache' );
 
-$ps_globals = Profiler::instance()->scopedProfileIn( $fname . '-globals' );
-
 /**
  * @var Language $wgContLang
  * @deprecated since 1.32, use the ContentLanguage service directly
@@ -939,9 +909,6 @@ $wgParser = new StubObject( 'wgParser', function () {
  */
 $wgTitle = null;
 
-Profiler::instance()->scopedProfileOut( $ps_globals );
-$ps_extensions = Profiler::instance()->scopedProfileIn( $fname . '-extensions' );
-
 // Extension setup functions
 // Entries should be added to this variable during the inclusion
 // of the extension file. This allows the extension to perform
@@ -974,6 +941,3 @@ if ( !$wgCommandLineMode ) {
 }
 
 $wgFullyInitialised = true;
-
-Profiler::instance()->scopedProfileOut( $ps_extensions );
-Profiler::instance()->scopedProfileOut( $ps_setup );
index 5ef0304..88f301a 100644 (file)
@@ -111,7 +111,7 @@ class NameTableStore {
         * @return IDatabase
         */
        private function getDBConnection( $index, $flags = 0 ) {
-               return $this->loadBalancer->getConnection( $index, [], $this->domain, $flags );
+               return $this->loadBalancer->getConnectionRef( $index, [], $this->domain, $flags );
        }
 
        /**
@@ -160,10 +160,7 @@ class NameTableStore {
                        if ( $id === null ) {
                                // RACE: $name was already in the db, probably just inserted, so load from master.
                                // Use DBO_TRX to avoid missing inserts due to other threads or REPEATABLE-READs.
-                               // ...but not during unit tests, because we need the fake DB tables of the default
-                               // connection.
-                               $connFlags = defined( 'MW_PHPUNIT_TEST' ) ? 0 : ILoadBalancer::CONN_TRX_AUTOCOMMIT;
-                               $table = $this->reloadMap( $connFlags );
+                               $table = $this->reloadMap( ILoadBalancer::CONN_TRX_AUTOCOMMIT );
 
                                $searchResult = array_search( $name, $table, true );
                                if ( $searchResult === false ) {
index 0d07791..96f196f 100644 (file)
@@ -3185,7 +3185,7 @@ class Title implements LinkTarget, IDBAccessObject {
        public static function capitalize( $text, $ns = NS_MAIN ) {
                $services = MediaWikiServices::getInstance();
                if ( $services->getNamespaceInfo()->isCapitalized( $ns ) ) {
-                       return MediaWikiServices::getInstance()->getContentLanguage()->ucfirst( $text );
+                       return $services->getContentLanguage()->ucfirst( $text );
                } else {
                        return $text;
                }
index 6593e49..defe07e 100644 (file)
@@ -395,18 +395,25 @@ class WebRequest {
                # https://www.php.net/variables.external#language.variables.external.dot-in-names
                # Work around PHP *feature* to avoid *bugs* elsewhere.
                $name = strtr( $name, '.', '_' );
-               if ( isset( $arr[$name] ) ) {
-                       $data = $arr[$name];
+
+               if ( !isset( $arr[$name] ) ) {
+                       return $default;
+               }
+
+               $data = $arr[$name];
+               # Optimisation: Skip UTF-8 normalization and legacy transcoding for simple ASCII strings.
+               $isAsciiStr = ( is_string( $data ) && preg_match( '/[^\x20-\x7E]/', $data ) === 0 );
+               if ( !$isAsciiStr ) {
                        if ( isset( $_GET[$name] ) && is_string( $data ) ) {
                                # Check for alternate/legacy character encoding.
-                               $contLang = MediaWikiServices::getInstance()->getContentLanguage();
-                               $data = $contLang->checkTitleEncoding( $data );
+                               $data = MediaWikiServices::getInstance()
+                                       ->getContentLanguage()
+                                       ->checkTitleEncoding( $data );
                        }
                        $data = $this->normalizeUnicode( $data );
-                       return $data;
-               } else {
-                       return $default;
                }
+
+               return $data;
        }
 
        /**
index c83fdea..9573091 100644 (file)
@@ -91,17 +91,20 @@ if ( !defined( 'MW_API' ) &&
        header( 'Cache-Control: no-cache' );
        header( 'Content-Type: text/html; charset=utf-8' );
        HttpStatus::header( 400 );
-       $error = wfMessage( 'nonwrite-api-promise-error' )->escaped();
-       $content = <<<EOT
+       $errorHtml = wfMessage( 'nonwrite-api-promise-error' )
+               ->useDatabase( false )
+               ->inContentLanguage()
+               ->escaped();
+       $content = <<<HTML
 <!DOCTYPE html>
 <html>
 <head><meta charset="UTF-8" /></head>
 <body>
-$error
+$errorHtml
 </body>
 </html>
 
-EOT;
+HTML;
        header( 'Content-Length: ' . strlen( $content ) );
        echo $content;
        die();
index f2641f4..0363877 100644 (file)
@@ -260,7 +260,6 @@ class WikiMap {
         *
         * @see $wgDBmwschema
         * @see PostgresInstaller
-        * @see MssqlInstaller
         *
         * @param string|DatabaseDomain $domain
         * @return string
index 15cee94..7bcfc88 100644 (file)
@@ -280,7 +280,7 @@ class InfoAction extends FormlessAction {
                // Language in which the page content is (supposed to be) written
                $pageLang = $title->getPageLanguage()->getCode();
 
-               $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
+               $permissionManager = $services->getPermissionManager();
 
                $pageLangHtml = $pageLang . ' - ' .
                        Language::fetchLanguageName( $pageLang, $lang->getCode() );
index 14f76bc..fd2fbd0 100644 (file)
@@ -422,7 +422,7 @@ class HistoryPager extends ReverseChronologicalPager {
                                $undoTooltip = $latest
                                        ? [ 'title' => $this->msg( 'tooltip-undo' )->text() ]
                                        : [];
-                               $undolink = MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink(
+                               $undolink = $this->getLinkRenderer()->makeKnownLink(
                                        $this->getTitle(),
                                        $this->msg( 'editundo' )->text(),
                                        $undoTooltip,
@@ -502,7 +502,7 @@ class HistoryPager extends ReverseChronologicalPager {
                ) {
                        return $cur;
                } else {
-                       return MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink(
+                       return $this->getLinkRenderer()->makeKnownLink(
                                $this->getTitle(),
                                new HtmlArmor( $cur ),
                                [],
@@ -531,7 +531,7 @@ class HistoryPager extends ReverseChronologicalPager {
                        return $last;
                }
 
-               $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
+               $linkRenderer = $this->getLinkRenderer();
                if ( $next === 'unknown' ) {
                        # Next row probably exists but is unknown, use an oldid=prev link
                        return $linkRenderer->makeKnownLink(
index 988957b..2627715 100644 (file)
@@ -66,6 +66,23 @@ class ApiHelp extends ApiBase {
                        ApiResult::setSubelementsList( $data, 'help' );
                        $result->addValue( null, $this->getModuleName(), $data );
                } else {
+                       // Show any errors at the top of the HTML
+                       $transform = [
+                               'Types' => [ 'AssocAsObject' => true ],
+                               'Strip' => 'all',
+                       ];
+                       $errors = array_filter( [
+                               'errors' => $this->getResult()->getResultData( [ 'errors' ], $transform ),
+                               'warnings' => $this->getResult()->getResultData( [ 'warnings' ], $transform ),
+                       ] );
+                       if ( $errors ) {
+                               $json = FormatJson::encode( $errors, true, FormatJson::UTF8_OK );
+                               // Escape any "--", some parsers might interpret that as end-of-comment.
+                               // The above already escaped any "<" and ">".
+                               $json = str_replace( '--', '-\u002D', $json );
+                               $html = "<!-- API warnings and errors:\n$json\n-->\n$html";
+                       }
+
                        $result->reset();
                        $result->addValue( null, 'text', $html, ApiResult::NO_SIZE_CHECK );
                        $result->addValue( null, 'mime', 'text/html', ApiResult::NO_SIZE_CHECK );
index 6577c7b..641aa9f 100644 (file)
@@ -1539,6 +1539,12 @@ class ApiMain extends ApiBase {
                        $this->dieWithErrorOrDebug( [ 'apierror-mustbeposted', $this->mAction ] );
                }
 
+               if ( $request->wasPosted() && !$request->getHeader( 'Content-Type' ) ) {
+                       $this->addDeprecation(
+                               'apiwarn-deprecation-post-without-content-type', 'post-without-content-type'
+                       );
+               }
+
                // See if custom printer is used
                $this->mPrinter = $module->getCustomPrinter();
                if ( is_null( $this->mPrinter ) ) {
index 1b58865..c604322 100644 (file)
@@ -1253,7 +1253,7 @@ class ApiPageSet extends ApiBase {
 
                        // Need gender information
                        if (
-                               MediaWikiServices::getInstance()->getNamespaceInfo()->
+                               $services->getNamespaceInfo()->
                                        hasGenderDistinction( $titleObj->getNamespace() )
                        ) {
                                $usernames[] = $titleObj->getText();
index d713b3a..7d6d342 100644 (file)
@@ -134,7 +134,7 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase {
                        $this->addJoinConds(
                                [ 'change_tag' => [ 'JOIN', [ 'ar_rev_id=ct_rev_id' ] ] ]
                        );
-                       $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
+                       $changeTagDefStore = $services->getChangeTagDefStore();
                        try {
                                $this->addWhereFld( 'ct_tag_id', $changeTagDefStore->getId( $params['tag'] ) );
                        } catch ( NameTableAccessException $exception ) {
index b97ab3c..5e737c3 100644 (file)
@@ -63,7 +63,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
                                $this->dieWithError( [ 'apierror-bad-badfilecontexttitle', $p ], 'invalid-title' );
                        }
                } else {
-                       $badFileContextTitle = false;
+                       $badFileContextTitle = null;
                }
 
                $pageIds = $this->getPageSet()->getGoodAndMissingTitlesByNamespace();
index af97236..dc9c516 100644 (file)
        "apiwarn-deprecation-missingparam": "نظرا لعدم تحديد <var>$1</var>; تم استخدام تنسيق قديم للإخراج، تم إيقاف هذا التنسيق، وسيتم دائما استخدام التنسيق الجديد في المستقبل.",
        "apiwarn-deprecation-parameter": "تم إيقاف الوسيط <var>$1</var>.",
        "apiwarn-deprecation-parse-headitems": "تم إيقاف <kbd>prop=headitems</kbd> منذ ميدياويكي 1.28; استخدم <kbd>prop=headhtml</kbd> عند إنشاء مستندات HTML جديدة، أو <kbd>prop=modules|jsconfigvars</kbd> عند تحديث مستند من جانب العميل.",
+       "apiwarn-deprecation-post-without-content-type": "تم تقديم طلب POST بدون عنوان <code>Content-Type</code>، هذا لا يعمل بشكل موثوق.",
        "apiwarn-deprecation-purge-get": "تم إيقاف استخدام <kbd>action=purge</kbd> عبر GET; استخدم POST بدلا من ذلك.",
        "apiwarn-deprecation-withreplacement": "تم إيقاف <kbd>$1</kbd>; الرجاء استخدام <kbd>$2</kbd> بدلا من ذلك.",
        "apiwarn-difftohidden": "لا يمكنك إجراء مقارنة مع r$1: المحتوى مخفي.",
index 6625863..8b42a07 100644 (file)
        "apiwarn-deprecation-missingparam": "Because <var>$1</var> was not specified, a legacy format has been used for the output. This format is deprecated, and in the future the new format will always be used.",
        "apiwarn-deprecation-parameter": "The parameter <var>$1</var> has been deprecated.",
        "apiwarn-deprecation-parse-headitems": "<kbd>prop=headitems</kbd> is deprecated since MediaWiki 1.28. Use <kbd>prop=headhtml</kbd> when creating new HTML documents, or <kbd>prop=modules|jsconfigvars</kbd> when updating a document client-side.",
+       "apiwarn-deprecation-post-without-content-type": "A POST request was made without a <code>Content-Type</code> header. This does not work reliably.",
        "apiwarn-deprecation-purge-get": "Use of <kbd>action=purge</kbd> via GET is deprecated. Use POST instead.",
        "apiwarn-deprecation-withreplacement": "<kbd>$1</kbd> has been deprecated. Please use <kbd>$2</kbd> instead.",
        "apiwarn-difftohidden": "Couldn't diff to r$1: content is hidden.",
index 591bf31..b72d519 100644 (file)
        "apiwarn-deprecation-missingparam": "Comme <var>$1</var> n’a pas été spécifié, un format ancien a été utilisé pour la sortie. Ce format est obsolète, et dans le futur, le nouveau format sera toujours utilisé.",
        "apiwarn-deprecation-parameter": "Le paramètre <var>$1</var> est désuet.",
        "apiwarn-deprecation-parse-headitems": "<kbd>prop=headitems</kbd> est désuet depuis MédiaWiki 1.28. Utilisez <kbd>prop=headhtml</kbd> lors de la création de nouveaux documents HTML, ou <kbd>prop=modules|jsconfigvars</kbd> lors de la mise à jour d’un document côté client.",
+       "apiwarn-deprecation-post-without-content-type": "Une requête POST a été faite sans entête <code>Content-Type</code>. Cela ne fonctionne pas de façon fiable.",
        "apiwarn-deprecation-purge-get": "L’utilisation de <kbd>action=purge</kbd> via un GET est désuète. Utiliser POST à la place.",
        "apiwarn-deprecation-withreplacement": "<kbd>$1</kbd> est désuet. Veuillez utiliser <kbd>$2</kbd> à la place.",
        "apiwarn-difftohidden": "Impossible de faire un diff avec r$1 : le contenu est masqué.",
index d5de23f..87f056b 100644 (file)
        "apiwarn-deprecation-missingparam": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
        "apiwarn-deprecation-parameter": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.",
        "apiwarn-deprecation-parse-headitems": "{{doc-apierror}}",
+       "apiwarn-deprecation-post-without-content-type": "{{doc-apierror}}",
        "apiwarn-deprecation-purge-get": "{{doc-apierror}}",
        "apiwarn-deprecation-withreplacement": "{{doc-apierror}}\n\nParameters:\n* $1 - Query string fragment that is deprecated, e.g. \"action=tokens\".\n* $2 - Query string fragment to use instead, e.g. \"action=tokens\".",
        "apiwarn-difftohidden": "{{doc-apierror}}\n\nParameters:\n* $1 - Revision ID number.\n\n\"r\" is short for \"revision\". You may translate it.",
index 75c41fc..ea84a53 100644 (file)
@@ -49,6 +49,8 @@
        "apihelp-block-param-reblock": "Skriv över befintlig blockering om användaren redan är blockerad.",
        "apihelp-block-param-watchuser": "Bevaka användarens eller IP-adressens användarsida och diskussionssida",
        "apihelp-block-param-tags": "Ändra märken att tillämpa i blockloggens post.",
+       "apihelp-block-param-pagerestrictions": "Lista över titlar att blockera användaren från att redigera. Gäller endast när <var>partial</var> är \"true\".",
+       "apihelp-block-param-namespacerestrictions": "Lista över namnrymds-ID:n att blockera användaren från att redigera. Gäller endast när <var>partial</var> är \"true\".",
        "apihelp-block-example-ip-simple": "Blockera IP-adressen <kbd>192.0.2.5</kbd> i tre dagar med motivationen <kbd>First strike</kbd>",
        "apihelp-block-example-user-complex": "Blockera användare <kbd>Vandal</kbd> på obegränsad tid med motivationen <kbd>Vandalism</kbd>, och förhindra kontoskapande och e-post.",
        "apihelp-changeauthenticationdata-summary": "Ändra autentiseringsdata för aktuell användare.",
        "apihelp-query+blocks-paramvalue-prop-expiry": "Lägger till en tidsstämpel för när blockeringen går ut.",
        "apihelp-query+blocks-paramvalue-prop-reason": "Lägger till de skäl som angetts för blockeringen.",
        "apihelp-query+blocks-paramvalue-prop-range": "Lägger till intervallet av IP-adresser som berörs av blockeringen.",
+       "apihelp-query+blocks-paramvalue-prop-restrictions": "Lägger till partiella blockeringsbegränsningar om blockeringen inte gäller för hela webbplatsen.",
        "apihelp-query+blocks-example-simple": "Lista blockeringar.",
        "apihelp-query+blocks-example-users": "Lista blockeringar av användarna <kbd>Alice</kbd> och <kbd>Bob</kbd>.",
        "apihelp-query+categories-summary": "Lista alla kategorier sidorna tillhör.",
        "apierror-unknownformat": "Okänt format \"$1\".",
        "apiwarn-compare-no-next": "Sidversion $2 är den senaste sidversionen av $1, det finns ingen sidversion för <kbd>torelative=next</kbd> att jämföra med.",
        "apiwarn-compare-no-prev": "Sidversionen $2 är den tidigaste sidversion för $1, det finns ingen sidversion för <kbd>torelative=prev</kbd> att jämföra med.",
+       "apiwarn-deprecation-post-without-content-type": "En POST-begäran gjordes utan en <code>Content-Type</code> i sidhuvudet. Det fungerar inte ordentligt.",
        "api-feed-error-title": "Fel ($1)"
 }
index 83e8314..7e08e77 100644 (file)
@@ -30,7 +30,8 @@
                        "科劳",
                        "SolidBlock",
                        "神樂坂秀吉",
-                       "94rain"
+                       "94rain",
+                       "予弦"
                ]
        },
        "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|文档]]\n* [[mw:Special:MyLanguage/API:FAQ|常见问题]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api 邮件列表]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API公告]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R 程序错误与功能请求]\n</div>\n<strong>状态信息:</strong>MediaWiki API是一个成熟稳定的,不断受到支持和改进的界面。尽管我们尽力避免,但偶尔也需要作出重大更新;请订阅[https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ mediawiki-api-announce 邮件列表]以便获得更新通知。\n\n<strong>错误请求:</strong>当API收到错误请求时,HTTP header将会返回一个包含\"MediaWiki-API-Error\"的值,随后header的值与error code将会送回并设置为相同的值。详细信息请参阅[[mw:Special:MyLanguage/API:Errors_and_warnings|API:错误与警告]]。\n\n<p class=\"mw-apisandbox-link\"><strong>测试中:</strong>测试API请求的易用性,请参见[[Special:ApiSandbox]]。</p>",
@@ -64,6 +65,9 @@
        "apihelp-block-param-reblock": "如果该用户已被封禁,则覆盖已有的封禁。",
        "apihelp-block-param-watchuser": "监视用户或该 IP 的用户页和讨论页。",
        "apihelp-block-param-tags": "要在封禁日志中应用到实体的更改标签。",
+       "apihelp-block-param-partial": "封禁用户于特定页面或名字空间而不是整个站点。",
+       "apihelp-block-param-pagerestrictions": "阻止用户编辑的标题列表。仅在<var>partial</var>设置为true时适用。",
+       "apihelp-block-param-namespacerestrictions": "用于阻止用户编辑的名字空间ID列表。仅在<var>partial</var>设置为true时适用。",
        "apihelp-block-example-ip-simple": "封禁IP地址<kbd>192.0.2.5</kbd>三天,原因<kbd>First strike</kbd>。",
        "apihelp-block-example-user-complex": "无限期封禁用户<kbd>Vandal</kbd>,原因<kbd>Vandalism</kbd>,并阻止新账户创建和电子邮件发送。",
        "apihelp-changeauthenticationdata-summary": "更改当前用户的身份验证数据。",
index b7c60ed..14a7717 100644 (file)
        "apiwarn-deprecation-missingparam": "因為未指定 <var>$1</var>,輸出內容使用到過去舊有的格式。該格式已棄用,並且往後都只會使用新格式。",
        "apiwarn-deprecation-parameter": "參數 <var>$1</var> 已棄用。",
        "apiwarn-deprecation-parse-headitems": "<kbd>prop=headitems</kbd> 自 MediaWiki 的 1.28 版本後已被棄用。當建立新 HTML 文件時請使用 <kbd>prop=headhtml</kbd>,或是當更新文件客戶端時請使用 <kbd>prop=modules|jsconfigvars</kbd>。",
+       "apiwarn-deprecation-post-without-content-type": "POST 請求不需要 <code>Content-Type</code> 標頭,這會無法可靠運作。",
        "apiwarn-deprecation-purge-get": "透過 GET 方式使用的 <kbd>action=purge</kbd> 已棄用,請以 POST 替代。",
        "apiwarn-deprecation-withreplacement": "<kbd>$1</kbd> 已棄用,請改用 <kbd>$2</kbd>。",
        "apiwarn-difftohidden": "無法對 r$1 比較差異:內容被隱蔵。",
index e6d9bd8..9d0175a 100644 (file)
@@ -127,14 +127,16 @@ class Throttler implements LoggerAwareInterface {
                                continue;
                        }
 
-                       $throttleKey = $this->cache->makeGlobalKey( 'throttler', $this->type, $index, $ipKey, $userKey );
+                       $throttleKey = $this->cache->makeGlobalKey(
+                               'throttler',
+                               $this->type,
+                               $index,
+                               $ipKey,
+                               $userKey
+                       );
                        $throttleCount = $this->cache->get( $throttleKey );
-
-                       if ( !$throttleCount ) { // counter not started yet
-                               $this->cache->add( $throttleKey, 1, $expiry );
-                       } elseif ( $throttleCount < $count ) { // throttle limited not yet reached
-                               $this->cache->incr( $throttleKey );
-                       } else { // throttled
+                       if ( $throttleCount && $throttleCount >= $count ) {
+                               // Throttle limited reached
                                $this->logRejection( [
                                        'throttle' => $this->type,
                                        'index' => $index,
@@ -147,13 +149,12 @@ class Throttler implements LoggerAwareInterface {
                                        // @codeCoverageIgnoreEnd
                                ] );
 
-                               return [
-                                       'throttleIndex' => $index,
-                                       'count' => $count,
-                                       'wait' => $expiry,
-                               ];
+                               return [ 'throttleIndex' => $index, 'count' => $count, 'wait' => $expiry ];
+                       } else {
+                               $this->cache->incrWithInit( $throttleKey, $expiry, 1 );
                        }
                }
+
                return false;
        }
 
index 03043e1..83b59c7 100644 (file)
@@ -104,6 +104,7 @@ class BlockManager {
         */
        public function getUserBlock( User $user, $fromReplica ) {
                $isAnon = $user->getId() === 0;
+               $fromMaster = !$fromReplica;
 
                // TODO: If $user is the current user, we should use the current request. Otherwise,
                // we should not look for XFF or cookie blocks.
@@ -127,7 +128,7 @@ class BlockManager {
                // User/IP blocking
                // After this, $blocks is an array of blocks or an empty array
                // TODO: remove dependency on DatabaseBlock
-               $blocks = DatabaseBlock::newListFromTarget( $user, $ip, !$fromReplica );
+               $blocks = DatabaseBlock::newListFromTarget( $user, $ip, $fromMaster );
 
                // Cookie blocking
                $cookieBlock = $this->getBlockFromCookieValue( $user, $request );
@@ -164,7 +165,7 @@ class BlockManager {
                        $xff = array_map( 'trim', explode( ',', $xff ) );
                        $xff = array_diff( $xff, [ $ip ] );
                        // TODO: remove dependency on DatabaseBlock
-                       $xffblocks = DatabaseBlock::getBlocksForIPList( $xff, $isAnon, !$fromReplica );
+                       $xffblocks = DatabaseBlock::getBlocksForIPList( $xff, $isAnon, $fromMaster );
                        $blocks = array_merge( $blocks, $xffblocks );
                }
 
@@ -256,7 +257,7 @@ class BlockManager {
                        $block = DatabaseBlock::newFromID( $blockCookieId );
                        if (
                                $block instanceof DatabaseBlock &&
-                               $this->shouldApplyCookieBlock( $block, $user->isAnon() )
+                               $this->shouldApplyCookieBlock( $block, !$user->isRegistered() )
                        ) {
                                return $block;
                        }
index ce5a019..fb42539 100644 (file)
@@ -230,31 +230,26 @@ abstract class FileCacheBase {
         */
        public function incrMissesRecent( WebRequest $request ) {
                if ( mt_rand( 0, self::MISS_FACTOR - 1 ) == 0 ) {
-                       $cache = ObjectCache::getLocalClusterInstance();
                        # Get a large IP range that should include the user  even if that
                        # person's IP address changes
                        $ip = $request->getIP();
                        if ( !IP::isValid( $ip ) ) {
                                return;
                        }
+
                        $ip = IP::isIPv6( $ip )
                                ? IP::sanitizeRange( "$ip/32" )
                                : IP::sanitizeRange( "$ip/16" );
 
                        # Bail out if a request already came from this range...
+                       $cache = ObjectCache::getLocalClusterInstance();
                        $key = $cache->makeKey( static::class, 'attempt', $this->mType, $this->mKey, $ip );
-                       if ( $cache->get( $key ) ) {
+                       if ( !$cache->add( $key, 1, self::MISS_TTL_SEC ) ) {
                                return; // possibly the same user
                        }
-                       $cache->set( $key, 1, self::MISS_TTL_SEC );
 
                        # Increment the number of cache misses...
-                       $key = $this->cacheMissKey( $cache );
-                       if ( $cache->get( $key ) === false ) {
-                               $cache->set( $key, 1, self::MISS_TTL_SEC );
-                       } else {
-                               $cache->incr( $key );
-                       }
+                       $cache->incrWithInit( $this->cacheMissKey( $cache ), self::MISS_TTL_SEC );
                }
        }
 
index 93fdb16..dfbc50a 100644 (file)
@@ -45,6 +45,12 @@ class MessageCache {
        /** How long memcached locks last */
        const LOCK_TTL = 30;
 
+       /**
+        * Lifetime for cache, for keys stored in $wanCache, in seconds.
+        * @var int
+        */
+       const WAN_TTL = IExpiringStore::TTL_DAY;
+
        /**
         * Process cache of loaded messages that are defined in MediaWiki namespace
         *
@@ -70,12 +76,6 @@ class MessageCache {
         */
        protected $mDisable;
 
-       /**
-        * Lifetime for cache, used by object caching.
-        * Set on construction, see __construct().
-        */
-       protected $mExpiry;
-
        /**
         * Message cache has its own parser which it uses to transform messages
         * @var ParserOptions
@@ -137,7 +137,6 @@ class MessageCache {
         * @param BagOStuff $clusterCache
         * @param BagOStuff $serverCache
         * @param bool $useDB Whether to look for message overrides (e.g. MediaWiki: pages)
-        * @param int $expiry Lifetime for cache. @see $mExpiry.
         * @param Language|null $contLang Content language of site
         */
        public function __construct(
@@ -145,7 +144,6 @@ class MessageCache {
                BagOStuff $clusterCache,
                BagOStuff $serverCache,
                $useDB,
-               $expiry,
                Language $contLang = null
        ) {
                $this->wanCache = $wanCache;
@@ -155,7 +153,6 @@ class MessageCache {
                $this->cache = new MapCacheLRU( 5 ); // limit size for sanity
 
                $this->mDisable = !$useDB;
-               $this->mExpiry = $expiry;
                $this->contLang = $contLang ?? MediaWikiServices::getInstance()->getContentLanguage();
        }
 
@@ -504,6 +501,21 @@ class MessageCache {
                // Set the text for small software-defined messages in the main cache map
                $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
                $revQuery = $revisionStore->getQueryInfo( [ 'page', 'user' ] );
+
+               // T231196: MySQL/MariaDB (10.1.37) can sometimes irrationally decide that querying `actor` then
+               // `revision` then `page` is somehow better than starting with `page`. Tell it not to reorder the
+               // query (and also reorder it ourselves because as generated by RevisionStore it'll have
+               // `revision` first rather than `page`).
+               $revQuery['joins']['revision'] = $revQuery['joins']['page'];
+               unset( $revQuery['joins']['page'] );
+               // It isn't actually necesssary to reorder $revQuery['tables'] as Database does the right thing
+               // when join conditions are given for all joins, but Gergő is wary of relying on that so pull
+               // `page` to the start.
+               $revQuery['tables'] = array_merge(
+                       [ 'page' ],
+                       array_diff( $revQuery['tables'], [ 'page' ] )
+               );
+
                $res = $dbr->select(
                        $revQuery['tables'],
                        $revQuery['fields'],
@@ -512,7 +524,7 @@ class MessageCache {
                                'page_latest = rev_id' // get the latest revision only
                        ] ),
                        __METHOD__ . "($code)-small",
-                       [],
+                       [ 'STRAIGHT_JOIN' ],
                        $revQuery['joins']
                );
                foreach ( $res as $row ) {
@@ -551,7 +563,7 @@ class MessageCache {
                # messages larger than $wgMaxMsgCacheEntrySize, since those are only
                # stored and fetched from memcache.
                $cache['HASH'] = md5( serialize( $cache ) );
-               $cache['EXPIRY'] = wfTimestamp( TS_MW, time() + $this->mExpiry );
+               $cache['EXPIRY'] = wfTimestamp( TS_MW, time() + self::WAN_TTL );
                unset( $cache['EXCESSIVE'] ); // only needed for hash
 
                return $cache;
@@ -666,7 +678,7 @@ class MessageCache {
                        $this->wanCache->set(
                                $this->bigMessageCacheKey( $cache['HASH'], $title ),
                                ' ' . $newTextByTitle[$title],
-                               $this->mExpiry
+                               self::WAN_TTL
                        );
                }
                // Mark this cache as definitely being "latest" (non-volatile) so
@@ -1090,11 +1102,11 @@ class MessageCache {
                $fname = __METHOD__;
                return $this->srvCache->getWithSetCallback(
                        $this->srvCache->makeKey( 'messages-big', $hash, $dbKey ),
-                       IExpiringStore::TTL_MINUTE,
+                       BagOStuff::TTL_HOUR,
                        function () use ( $code, $dbKey, $hash, $fname ) {
                                return $this->wanCache->getWithSetCallback(
                                        $this->bigMessageCacheKey( $hash, $dbKey ),
-                                       $this->mExpiry,
+                                       self::WAN_TTL,
                                        function ( $oldValue, &$ttl, &$setOpts ) use ( $dbKey, $code, $fname ) {
                                                // Try loading the message from the database
                                                $dbr = wfGetDB( DB_REPLICA );
index fb4675e..ffc7cd0 100644 (file)
 
 use CLDRPluralRuleParser\Evaluator;
 use CLDRPluralRuleParser\Error as CLDRPluralRuleError;
-use MediaWiki\Config\ServiceOptions;
-use MediaWiki\Languages\LanguageNameUtils;
-use Psr\Log\LoggerInterface;
+use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
 
 /**
  * Class for caching the contents of localisation files, Messages*.php
  * and *.i18n.php.
  *
- * An instance of this class is available using MediaWikiServices.
+ * An instance of this class is available using Language::getLocalisationCache().
  *
  * The values retrieved from here are merged, containing items from extension
  * files, core messages files and the language fallback sequence (e.g. zh-cn ->
@@ -41,8 +40,8 @@ use Psr\Log\LoggerInterface;
 class LocalisationCache {
        const VERSION = 4;
 
-       /** @var ServiceOptions */
-       private $options;
+       /** Configuration associative array */
+       private $conf;
 
        /**
         * True if recaching should only be done on an explicit call to recache().
@@ -51,6 +50,11 @@ class LocalisationCache {
         */
        private $manualRecache = false;
 
+       /**
+        * True to treat all files as expired until they are regenerated by this object.
+        */
+       private $forceRecache = false;
+
        /**
         * The cache data. 3-d array, where the first key is the language code,
         * the second key is the item key e.g. 'messages', and the third key is
@@ -67,16 +71,10 @@ class LocalisationCache {
        private $store;
 
        /**
-        * @var LoggerInterface
+        * @var \Psr\Log\LoggerInterface
         */
        private $logger;
 
-       /** @var callable[] See comment for parameter in constructor */
-       private $clearStoreCallbacks;
-
-       /** @var LanguageNameUtils */
-       private $langNameUtils;
-
        /**
         * A 2-d associative array, code/key, where presence indicates that the item
         * is loaded. Value arbitrary.
@@ -190,52 +188,60 @@ class LocalisationCache {
 
        private $mergeableKeys = null;
 
-       /**
-        * @todo Make this a const when HHVM support is dropped (T192166)
-        *
-        * @var array
-        * @since 1.34
-        */
-       public static $constructorOptions = [
-               // True to treat all files as expired until they are regenerated by this object.
-               'forceRecache',
-               'manualRecache',
-               'ExtensionMessagesFiles',
-               'MessagesDirs',
-       ];
-
        /**
         * For constructor parameters, see the documentation in DefaultSettings.php
         * for $wgLocalisationCacheConf.
         *
-        * Do not construct this directly. Use MediaWikiServices.
-        *
-        * @param ServiceOptions $options
-        * @param LCStore $store What backend to use for storage
-        * @param LoggerInterface $logger
-        * @param callable[] $clearStoreCallbacks To be called whenever the cache is cleared. Can be
-        *   used to clear other caches that depend on this one, such as ResourceLoader's
-        *   MessageBlobStore.
-        * @param LanguageNameUtils $langNameUtils
+        * @param array $conf
         * @throws MWException
         */
-       function __construct(
-               ServiceOptions $options,
-               LCStore $store,
-               LoggerInterface $logger,
-               array $clearStoreCallbacks,
-               LanguageNameUtils $langNameUtils
-       ) {
-               $options->assertRequiredOptions( self::$constructorOptions );
-
-               $this->options = $options;
-               $this->store = $store;
-               $this->logger = $logger;
-               $this->clearStoreCallbacks = $clearStoreCallbacks;
-               $this->langNameUtils = $langNameUtils;
-
-               // Keep this separate from $this->options so it can be mutable
-               $this->manualRecache = $options->get( 'manualRecache' );
+       function __construct( $conf ) {
+               global $wgCacheDirectory;
+
+               $this->conf = $conf;
+               $this->logger = LoggerFactory::getInstance( 'localisation' );
+
+               $directory = !empty( $conf['storeDirectory'] ) ? $conf['storeDirectory'] : $wgCacheDirectory;
+               $storeArg = [];
+               $storeArg['directory'] = $directory;
+
+               if ( !empty( $conf['storeClass'] ) ) {
+                       $storeClass = $conf['storeClass'];
+               } else {
+                       switch ( $conf['store'] ) {
+                               case 'files':
+                               case 'file':
+                                       $storeClass = LCStoreCDB::class;
+                                       break;
+                               case 'db':
+                                       $storeClass = LCStoreDB::class;
+                                       $storeArg['server'] = $conf['storeServer'] ?? [];
+                                       break;
+                               case 'array':
+                                       $storeClass = LCStoreStaticArray::class;
+                                       break;
+                               case 'detect':
+                                       if ( $directory ) {
+                                               $storeClass = LCStoreCDB::class;
+                                       } else {
+                                               $storeClass = LCStoreDB::class;
+                                               $storeArg['server'] = $conf['storeServer'] ?? [];
+                                       }
+                                       break;
+                               default:
+                                       throw new MWException(
+                                               'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.'
+                                       );
+                       }
+               }
+               $this->logger->debug( static::class . ": using store $storeClass" );
+
+               $this->store = new $storeClass( $storeArg );
+               foreach ( [ 'manualRecache', 'forceRecache' ] as $var ) {
+                       if ( isset( $conf[$var] ) ) {
+                               $this->$var = $conf[$var];
+                       }
+               }
        }
 
        /**
@@ -400,7 +406,7 @@ class LocalisationCache {
         * @return bool
         */
        public function isExpired( $code ) {
-               if ( $this->options->get( 'forceRecache' ) && !isset( $this->recachedLangs[$code] ) ) {
+               if ( $this->forceRecache && !isset( $this->recachedLangs[$code] ) ) {
                        $this->logger->debug( __METHOD__ . "($code): forced reload" );
 
                        return true;
@@ -445,7 +451,7 @@ class LocalisationCache {
                $this->initialisedLangs[$code] = true;
 
                # If the code is of the wrong form for a Messages*.php file, do a shallow fallback
-               if ( !$this->langNameUtils->isValidBuiltInCode( $code ) ) {
+               if ( !Language::isValidBuiltInCode( $code ) ) {
                        $this->initShallowFallback( $code, 'en' );
 
                        return;
@@ -453,7 +459,7 @@ class LocalisationCache {
 
                # Recache the data if necessary
                if ( !$this->manualRecache && $this->isExpired( $code ) ) {
-                       if ( $this->langNameUtils->isSupportedLanguage( $code ) ) {
+                       if ( Language::isSupportedLanguage( $code ) ) {
                                $this->recache( $code );
                        } elseif ( $code === 'en' ) {
                                throw new MWException( 'MessagesEn.php is missing.' );
@@ -691,7 +697,7 @@ class LocalisationCache {
                global $IP;
 
                // This reads in the PHP i18n file with non-messages l10n data
-               $fileName = $this->langNameUtils->getMessagesFileName( $code );
+               $fileName = Language::getMessagesFileName( $code );
                if ( !file_exists( $fileName ) ) {
                        $data = [];
                } else {
@@ -798,12 +804,14 @@ class LocalisationCache {
        public function getMessagesDirs() {
                global $IP;
 
+               $config = MediaWikiServices::getInstance()->getMainConfig();
+               $messagesDirs = $config->get( 'MessagesDirs' );
                return [
                        'core' => "$IP/languages/i18n",
                        'exif' => "$IP/languages/i18n/exif",
                        'api' => "$IP/includes/api/i18n",
                        'oojs-ui' => "$IP/resources/lib/ooui/i18n",
-               ] + $this->options->get( 'MessagesDirs' );
+               ] + $messagesDirs;
        }
 
        /**
@@ -813,6 +821,8 @@ class LocalisationCache {
         * @throws MWException
         */
        public function recache( $code ) {
+               global $wgExtensionMessagesFiles;
+
                if ( !$code ) {
                        throw new MWException( "Invalid language code requested" );
                }
@@ -864,7 +874,7 @@ class LocalisationCache {
 
                # Load non-JSON localisation data for extensions
                $extensionData = array_fill_keys( $codeSequence, $initialData );
-               foreach ( $this->options->get( 'ExtensionMessagesFiles' ) as $extension => $fileName ) {
+               foreach ( $wgExtensionMessagesFiles as $extension => $fileName ) {
                        if ( isset( $messageDirs[$extension] ) ) {
                                # This extension has JSON message data; skip the PHP shim
                                continue;
@@ -1028,9 +1038,8 @@ class LocalisationCache {
                # HACK: If using a null (i.e. disabled) storage backend, we
                # can't write to the MessageBlobStore either
                if ( !$this->store instanceof LCStoreNull ) {
-                       foreach ( $this->clearStoreCallbacks as $callback ) {
-                               $callback();
-                       }
+                       $blobStore = MediaWikiServices::getInstance()->getResourceLoader()->getMessageBlobStore();
+                       $blobStore->clear();
                }
        }
 
@@ -1091,4 +1100,5 @@ class LocalisationCache {
                $this->store = new LCStoreNull;
                $this->manualRecache = false;
        }
+
 }
index c3b4728..0c6a3d1 100644 (file)
@@ -391,7 +391,7 @@ class RecentChange implements Taggable {
                }
 
                # If our database is strict about IP addresses, use NULL instead of an empty string
-               $strictIPs = in_array( $dbw->getType(), [ 'oracle', 'postgres' ] ); // legacy
+               $strictIPs = $dbw->getType() === 'postgres'; // legacy
                if ( $strictIPs && $this->mAttribs['rc_ip'] == '' ) {
                        unset( $this->mAttribs['rc_ip'] );
                }
index 0c17840..80eb2f7 100644 (file)
@@ -168,7 +168,7 @@ abstract class MWLBFactory {
         * @return array
         */
        private static function getDbTypesWithSchemas() {
-               return [ 'postgres', 'mssql' ];
+               return [ 'postgres' ];
        }
 
        /**
@@ -193,16 +193,6 @@ abstract class MWLBFactory {
                                // Work around the reserved word usage in MediaWiki schema
                                'keywordTableMap' => [ 'user' => 'mwuser', 'text' => 'pagecontent' ]
                        ];
-               } elseif ( $server['type'] === 'oracle' ) {
-                       $server += [
-                               // Work around the reserved word usage in MediaWiki schema
-                               'keywordTableMap' => [ 'user' => 'mwuser', 'text' => 'pagecontent' ]
-                       ];
-               } elseif ( $server['type'] === 'mssql' ) {
-                       $server += [
-                               'port' => $options->get( 'DBport' ),
-                               'useWindowsAuth' => $options->get( 'DBWindowsAuthentication' )
-                       ];
                }
 
                if ( in_array( $server['type'], self::getDbTypesWithSchemas(), true ) ) {
diff --git a/includes/db/ORAField.php b/includes/db/ORAField.php
deleted file mode 100644 (file)
index df31000..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-
-use Wikimedia\Rdbms\Field;
-
-class ORAField implements Field {
-       private $name, $tablename, $default, $max_length, $nullable,
-               $is_pk, $is_unique, $is_multiple, $is_key, $type;
-
-       function __construct( $info ) {
-               $this->name = $info['column_name'];
-               $this->tablename = $info['table_name'];
-               $this->default = $info['data_default'];
-               $this->max_length = $info['data_length'];
-               $this->nullable = $info['not_null'];
-               $this->is_pk = isset( $info['prim'] ) && $info['prim'] == 1 ? 1 : 0;
-               $this->is_unique = isset( $info['uniq'] ) && $info['uniq'] == 1 ? 1 : 0;
-               $this->is_multiple = isset( $info['nonuniq'] ) && $info['nonuniq'] == 1 ? 1 : 0;
-               $this->is_key = ( $this->is_pk || $this->is_unique || $this->is_multiple );
-               $this->type = $info['data_type'];
-       }
-
-       function name() {
-               return $this->name;
-       }
-
-       function tableName() {
-               return $this->tablename;
-       }
-
-       function defaultValue() {
-               return $this->default;
-       }
-
-       function maxLength() {
-               return $this->max_length;
-       }
-
-       function isNullable() {
-               return $this->nullable;
-       }
-
-       function isKey() {
-               return $this->is_key;
-       }
-
-       function isMultipleKey() {
-               return $this->is_multiple;
-       }
-
-       function type() {
-               return $this->type;
-       }
-}
diff --git a/includes/db/ORAResult.php b/includes/db/ORAResult.php
deleted file mode 100644 (file)
index aafd386..0000000
+++ /dev/null
@@ -1,110 +0,0 @@
-<?php
-
-use Wikimedia\Rdbms\IDatabase;
-
-/**
- * The oci8 extension is fairly weak and doesn't support oci_num_rows, among
- * other things. We use a wrapper class to handle that and other
- * Oracle-specific bits, like converting column names back to lowercase.
- * @ingroup Database
- */
-class ORAResult {
-       private $rows;
-       private $cursor;
-       private $nrows;
-
-       private $columns = [];
-
-       private function array_unique_md( $array_in ) {
-               $array_out = [];
-               $array_hashes = [];
-
-               foreach ( $array_in as $item ) {
-                       $hash = md5( serialize( $item ) );
-                       if ( !isset( $array_hashes[$hash] ) ) {
-                               $array_hashes[$hash] = $hash;
-                               $array_out[] = $item;
-                       }
-               }
-
-               return $array_out;
-       }
-
-       /**
-        * @param IDatabase &$db
-        * @param resource $stmt A valid OCI statement identifier
-        * @param bool $unique
-        */
-       function __construct( &$db, $stmt, $unique = false ) {
-               $this->db =& $db;
-
-               $this->nrows = oci_fetch_all( $stmt, $this->rows, 0, -1, OCI_FETCHSTATEMENT_BY_ROW | OCI_NUM );
-               if ( $this->nrows === false ) {
-                       $e = oci_error( $stmt );
-                       $db->reportQueryError( $e['message'], $e['code'], '', __METHOD__ );
-                       $this->free();
-
-                       return;
-               }
-
-               if ( $unique ) {
-                       $this->rows = $this->array_unique_md( $this->rows );
-                       $this->nrows = count( $this->rows );
-               }
-
-               if ( $this->nrows > 0 ) {
-                       foreach ( $this->rows[0] as $k => $v ) {
-                               $this->columns[$k] = strtolower( oci_field_name( $stmt, $k + 1 ) );
-                       }
-               }
-
-               $this->cursor = 0;
-               oci_free_statement( $stmt );
-       }
-
-       public function free() {
-               unset( $this->db );
-       }
-
-       public function seek( $row ) {
-               $this->cursor = min( $row, $this->nrows );
-       }
-
-       public function numRows() {
-               return $this->nrows;
-       }
-
-       public function numFields() {
-               return count( $this->columns );
-       }
-
-       public function fetchObject() {
-               if ( $this->cursor >= $this->nrows ) {
-                       return false;
-               }
-               $row = $this->rows[$this->cursor++];
-               $ret = new stdClass();
-               foreach ( $row as $k => $v ) {
-                       $lc = $this->columns[$k];
-                       $ret->$lc = $v;
-               }
-
-               return $ret;
-       }
-
-       public function fetchRow() {
-               if ( $this->cursor >= $this->nrows ) {
-                       return false;
-               }
-
-               $row = $this->rows[$this->cursor++];
-               $ret = [];
-               foreach ( $row as $k => $v ) {
-                       $lc = $this->columns[$k];
-                       $ret[$lc] = $v;
-                       $ret[$k] = $v;
-               }
-
-               return $ret;
-       }
-}
index db7141f..8b31f05 100644 (file)
@@ -186,7 +186,7 @@ class FileBackendGroup {
                                'mimeCallback' => [ $this, 'guessMimeInternal' ],
                                'obResetFunc' => 'wfResetOutputBuffers',
                                'streamMimeFunc' => [ StreamFile::class, 'contentTypeFromPath' ],
-                               'tmpFileFactory' => MediaWikiServices::getInstance()->getTempFSFileFactory(),
+                               'tmpFileFactory' => $services->getTempFSFileFactory(),
                                'statusWrapper' => [ Status::class, 'wrap' ],
                                'wanCache' => $services->getMainWANObjectCache(),
                                'srvCache' => ObjectCache::getLocalServerInstance( 'hash' ),
index 957af3e..9b43164 100644 (file)
@@ -22,6 +22,7 @@
  */
 use MediaWiki\MediaWikiServices;
 use MediaWiki\Logger\LoggerFactory;
+use Wikimedia\Rdbms\LBFactory;
 
 /**
  * Class to handle file lock manager registration
@@ -30,62 +31,27 @@ use MediaWiki\Logger\LoggerFactory;
  * @since 1.19
  */
 class LockManagerGroup {
-       /** @var LockManagerGroup[] (domain => LockManagerGroup) */
-       protected static $instances = [];
+       /** @var string domain (usually wiki ID) */
+       protected $domain;
 
-       protected $domain; // string; domain (usually wiki ID)
+       /** @var LBFactory */
+       protected $lbFactory;
 
        /** @var array Array of (name => ('class' => ..., 'config' => ..., 'instance' => ...)) */
        protected $managers = [];
 
        /**
+        * Do not call this directly. Use LockManagerGroupFactory.
+        *
         * @param string $domain Domain (usually wiki ID)
+        * @param array[] $lockManagerConfigs In format of $wgLockManagers
+        * @param LBFactory $lbFactory
         */
-       protected function __construct( $domain ) {
+       public function __construct( $domain, array $lockManagerConfigs, LBFactory $lbFactory ) {
                $this->domain = $domain;
-       }
-
-       /**
-        * @param bool|string $domain Domain (usually wiki ID). Default: false.
-        * @return LockManagerGroup
-        */
-       public static function singleton( $domain = false ) {
-               if ( $domain === false ) {
-                       $domain = WikiMap::getCurrentWikiDbDomain()->getId();
-               }
-
-               if ( !isset( self::$instances[$domain] ) ) {
-                       self::$instances[$domain] = new self( $domain );
-                       self::$instances[$domain]->initFromGlobals();
-               }
-
-               return self::$instances[$domain];
-       }
-
-       /**
-        * Destroy the singleton instances
-        */
-       public static function destroySingletons() {
-               self::$instances = [];
-       }
-
-       /**
-        * Register lock managers from the global variables
-        */
-       protected function initFromGlobals() {
-               global $wgLockManagers;
-
-               $this->register( $wgLockManagers );
-       }
+               $this->lbFactory = $lbFactory;
 
-       /**
-        * Register an array of file lock manager configurations
-        *
-        * @param array $configs
-        * @throws Exception
-        */
-       protected function register( array $configs ) {
-               foreach ( $configs as $config ) {
+               foreach ( $lockManagerConfigs as $config ) {
                        $config['domain'] = $this->domain;
                        if ( !isset( $config['name'] ) ) {
                                throw new Exception( "Cannot register a lock manager with no name." );
@@ -104,6 +70,26 @@ class LockManagerGroup {
                }
        }
 
+       /**
+        * @deprecated since 1.34, use LockManagerGroupFactory
+        *
+        * @param bool|string $domain Domain (usually wiki ID). Default: false.
+        * @return LockManagerGroup
+        */
+       public static function singleton( $domain = false ) {
+               return MediaWikiServices::getInstance()->getLockManagerGroupFactory()
+                       ->getLockManagerGroup( $domain );
+       }
+
+       /**
+        * Destroy the singleton instances
+        *
+        * @deprecated since 1.34, use resetServiceForTesting() on LockManagerGroupFactory
+        */
+       public static function destroySingletons() {
+               MediaWikiServices::getInstance()->resetServiceForTesting( 'LockManagerGroupFactory' );
+       }
+
        /**
         * Get the lock manager object with a given name
         *
@@ -118,21 +104,10 @@ class LockManagerGroup {
                // Lazy-load the actual lock manager instance
                if ( !isset( $this->managers[$name]['instance'] ) ) {
                        $class = $this->managers[$name]['class'];
+                       '@phan-var string $class';
                        $config = $this->managers[$name]['config'];
-                       if ( $class === DBLockManager::class ) {
-                               $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
-                               $lb = $lbFactory->getMainLB( $config['domain'] );
-                               $config['dbServers']['localDBMaster'] = $lb->getLazyConnectionRef(
-                                       DB_MASTER,
-                                       [],
-                                       $config['domain'],
-                                       $lb::CONN_TRX_AUTOCOMMIT
-                               );
-                               $config['srvCache'] = ObjectCache::getLocalServerInstance( 'hash' );
-                       }
                        $config['logger'] = LoggerFactory::getInstance( 'LockManager' );
 
-                       // @phan-suppress-next-line PhanTypeInstantiateAbstract
                        $this->managers[$name]['instance'] = new $class( $config );
                }
 
@@ -159,6 +134,8 @@ class LockManagerGroup {
         * Get the default lock manager configured for the site.
         * Returns NullLockManager if no lock manager could be found.
         *
+        * XXX This looks unused, should we just get rid of it?
+        *
         * @return LockManager
         */
        public function getDefault() {
@@ -172,6 +149,8 @@ class LockManagerGroup {
         * or at least some other effective configured lock manager.
         * Throws an exception if no lock manager could be found.
         *
+        * XXX This looks unused, should we just get rid of it?
+        *
         * @return LockManager
         * @throws Exception
         */
diff --git a/includes/filebackend/lockmanager/LockManagerGroupFactory.php b/includes/filebackend/lockmanager/LockManagerGroupFactory.php
new file mode 100644 (file)
index 0000000..9c8d4bb
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+
+namespace MediaWiki\FileBackend\LockManager;
+
+use LockManagerGroup;
+use Wikimedia\Rdbms\LBFactory;
+
+/**
+ * Service to construct LockManagerGroups.
+ */
+class LockManagerGroupFactory {
+       /** @var string */
+       private $defaultDomain;
+
+       /** @var array */
+       private $lockManagerConfigs;
+
+       /** @var LBFactory */
+       private $lbFactory;
+
+       /** @var LockManagerGroup[] (domain => LockManagerGroup) */
+       private $instances = [];
+
+       /**
+        * Do not call directly, use MediaWikiServices.
+        *
+        * @param string $defaultDomain
+        * @param array $lockManagerConfigs In format of $wgLockManagers
+        * @param LBFactory $lbFactory
+        */
+       public function __construct( $defaultDomain, array $lockManagerConfigs, LBFactory $lbFactory ) {
+               $this->defaultDomain = $defaultDomain;
+               $this->lockManagerConfigs = $lockManagerConfigs;
+               $this->lbFactory = $lbFactory;
+       }
+
+       /**
+        * @param string|null|false $domain Domain (usually wiki ID). false for the default (normally
+        *   the current wiki's domain).
+        * @return LockManagerGroup
+        */
+       public function getLockManagerGroup( $domain = false ) : LockManagerGroup {
+               if ( $domain === false || $domain === null ) {
+                       $domain = $this->defaultDomain;
+               }
+
+               if ( !isset( $this->instances[$domain] ) ) {
+                       $this->instances[$domain] =
+                               new LockManagerGroup( $domain, $this->lockManagerConfigs, $this->lbFactory );
+               }
+
+               return $this->instances[$domain];
+       }
+}
index 06e1271..991ef79 100644 (file)
@@ -80,10 +80,10 @@ abstract class ImageGalleryBase extends ContextSource {
        public $mParser;
 
        /**
-        * @var Title Contextual title, used when images are being screened against
+        * @var Title|null Contextual title, used when images are being screened against
         *   the bad image list
         */
-       protected $contextTitle = false;
+       protected $contextTitle = null;
 
        /** @var array */
        protected $mAttribs = [];
@@ -363,7 +363,7 @@ abstract class ImageGalleryBase extends ContextSource {
        /**
         * Set the contextual title
         *
-        * @param Title $title Contextual title
+        * @param Title|null $title Contextual title
         */
        public function setContextTitle( $title ) {
                $this->contextTitle = $title;
@@ -372,12 +372,10 @@ abstract class ImageGalleryBase extends ContextSource {
        /**
         * Get the contextual title, if applicable
         *
-        * @return Title|bool Title or false
+        * @return Title|null
         */
        public function getContextTitle() {
-               return is_object( $this->contextTitle ) && $this->contextTitle instanceof Title
-                       ? $this->contextTitle
-                       : false;
+               return $this->contextTitle;
        }
 
        /**
index 414222b..de15456 100644 (file)
@@ -412,17 +412,14 @@ abstract class Installer {
                // This will be overridden in the web installer with the user-specified language
                RequestContext::getMain()->setLanguage( 'en' );
 
+               // Disable the i18n cache
+               // TODO: manage LocalisationCache singleton in MediaWikiServices
+               Language::getLocalisationCache()->disableBackend();
+
                // Disable all global services, since we don't have any configuration yet!
                MediaWikiServices::disableStorageBackend();
 
                $mwServices = MediaWikiServices::getInstance();
-
-               // Disable i18n cache
-               $mwServices->getLocalisationCache()->disableBackend();
-
-               // Clear language cache so the old i18n cache doesn't sneak back in
-               Language::clearCaches();
-
                // Disable object cache (otherwise CACHE_ANYTHING will try CACHE_DB and
                // SqlBagOStuff will then throw since we just disabled wfGetDB)
                $wgObjectCaches = $mwServices->getMainConfig()->get( 'ObjectCaches' );
index 7c39ded..01bb30e 100644 (file)
@@ -242,27 +242,6 @@ class SqliteInstaller extends DatabaseInstaller {
                $this->setVar( 'wgDBpassword', '' );
                $this->setupSchemaVars();
 
-               # Create the global cache DB
-               try {
-                       $conn = Database::factory(
-                               'sqlite', [ 'dbname' => 'wikicache', 'dbDirectory' => $dir ] );
-                       # @todo: don't duplicate objectcache definition, though it's very simple
-                       $sql =
-<<<EOT
-       CREATE TABLE IF NOT EXISTS objectcache (
-               keyname BLOB NOT NULL default '' PRIMARY KEY,
-               value BLOB,
-               exptime TEXT
-       )
-EOT;
-                       $conn->query( $sql );
-                       $conn->query( "CREATE INDEX IF NOT EXISTS exptime ON objectcache (exptime)" );
-                       $conn->query( "PRAGMA journal_mode=WAL" ); // this is permanent
-                       $conn->close();
-               } catch ( DBConnectionError $e ) {
-                       return Status::newFatal( 'config-sqlite-connection-error', $e->getMessage() );
-               }
-
                # Create the l10n cache DB
                try {
                        $conn = Database::factory(
index 42d7bf0..498fd7c 100644 (file)
        "config-install-step-failed": "mislykkedes",
        "config-install-extensions": "Inkluderer udvidelser",
        "config-install-database": "Opsætter database",
+       "config-install-user": "Opretter databasebruger",
        "config-install-user-alreadyexists": "Brugeren \"$1\" findes allerede",
        "config-install-user-create-failed": "Oprettelse af brugeren \"$1\" mislykkedes: $2",
        "config-install-tables": "Opretter tabeller",
        "config-install-keys": "Genererer hemmelige nøgler",
        "config-install-mainpage-exists": "Forsiden findes allerede, springer over",
        "config-install-mainpage-failed": "Kunne ikke indsætte forside: $1",
+       "config-install-db-success": "Databasen blev sat op",
        "config-help": "hjælp",
        "config-help-tooltip": "klik for at udvide",
        "config-nofile": "Filen \"$1\" kunne ikke blive fundet. Er den blevet slettet?",
index d0b43ad..a14bef1 100644 (file)
        "config-restart": "Ya, nyalakan ulang",
        "config-welcome": "=== Pengecekan lingkungan ===\nPengecekan dasar kini akan dilakukan untuk melihat apakah lingkungan ini memadai untuk instalasi MediaWiki.\nIngatlah untuk menyertakan informasi ini jika Anda mencari bantuan tentang cara menyelesaikan instalasi.",
        "config-welcome-section-copyright": "=== Hak cipta dan persyaratan ===\n\n$1\n\nProgram ini adalah perangkat lunak bebas; Anda dapat mendistribusikan dan/atau memodifikasinya di bawah persyaratan GNU General Public License seperti yang diterbitkan oleh Free Software Foundation; baik versi 2 lisensi, atau (sesuai pilihan Anda) versi yang lebih baru.\n\nProgram ini didistribusikan dengan harapan bahwa itu akan berguna, tetapi <strong>tanpa jaminan apa pun</strong>; bahkan tanpa jaminan tersirat untuk <strong>dapat diperjualbelikan</strong> atau <strong>sesuai untuk tujuan tertentu</strong>.\nLihat GNU General Public License untuk lebih jelasnya.\n\nAnda seharusnya telah menerima [$2 salinan dari GNU General Public License] bersama dengan program ini; jika tidak, kirimkan surat untuk Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, atau [https://www.gnu.org/copyleft/gpl.html baca versi daring].",
-       "config-sidebar": "* [https://www.mediawiki.org/wiki/MediaWiki/id Situs MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents/id Pedoman Pengguna]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents/id Pedoman Administrator]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ/id FAQ]\n----\n* <doclink href=Readme>Read me</doclink>\n* <doclink href=ReleaseNotes>Release notes</doclink>\n* <doclink href=Copying>Copying</doclink>\n* <doclink href=UpgradeDoc>Upgrading</doclink>",
+       "config-sidebar": "* [https://www.mediawiki.org Halaman depan MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Panduan Pengguna]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Panduan Pengurus]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Pertanyaan yang sering ditanyakan]",
+       "config-sidebar-readme": "Pelajari selengkapnya",
+       "config-sidebar-relnotes": "Catatan rilis",
+       "config-sidebar-license": "Menyalin",
+       "config-sidebar-upgrade": "Memperbarui",
        "config-env-good": "Kondisi telah diperiksa.\nAnda dapat menginstal MediaWiki.",
        "config-env-bad": "Kondisi telah diperiksa.\nAnda tidak dapat menginstal MediaWiki.",
        "config-env-php": "PHP $1 diinstal.",
        "config-env-hhvm": "HHVM $1 telah dipasang.",
-       "config-unicode-using-intl": "Menggunakan [https://pecl.php.net/intl ekstensi PECL intl] untuk normalisasi Unicode.",
+       "config-unicode-using-intl": "Menggunakan [https://php.net/manual/en/book.intl.php ekstensi internasional PHP] untuk normalisasi Unicode.",
        "config-unicode-pure-php-warning": "<strong>Peringatan:</strong> [https://pecl.php.net/intl intl Ekstensi PECL] tidak tersedia untuk menangani normalisasi Unicode, dikembalikan untuk melambatkan implementasi PHP asli.\nApabila Anda menjalankan situs dengan lalu-lintas tinggi, Anda harus membaca [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations normalisasi Unicode].",
        "config-unicode-update-warning": "<strong>Peringatan:</strong> Versi terinstal dari pembungkus normalisasi Unicode menggunakan versi lama pustaka [http://site.icu-project.org/ proyek ICU].\nAnda harus [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations meningkatkan versinya] jika ingin menggunakan Unicode.",
        "config-no-db": "Pengandar basis data yang sesuai tidak ditemukan! Anda perlu menginstal pengandar basis data untuk PHP.\n{{PLURAL:$2|Jenis|Jenis}} basis data yang didukung: $1.\n\nJika Anda mengompilasi PHP sendiri, ubahlah konfigurasinya dengan mengaktifkan klien basis data, misalnya menggunakan <code>./configure --with-mysqli</code>.\nJika Anda menginstal PHP dari paket Debian atau Ubuntu, maka Anda juga perlu menginstal seperti paket <code>php-mysql</code>.",
@@ -95,7 +99,7 @@
        "config-db-host": "Inang basis data:",
        "config-db-host-help": "Jika server basis data Anda berada di server yang berbeda, masukkan nama inang atau alamat IP di sini.\n\nJika Anda menggunakan inang web bersama, penyedia inang Anda harus memberikan nama inang yang benar di dokumentasi mereka.\n\nJika Anda menginstal pada server Windows dan menggunakan MySQL, \"localhost\" mungkin tidak dapat digunakan sebagai nama server. Jika demikian, coba \"127.0.0.1\" untuk alamat IP lokal.\n\nJika Anda menggunakan PostgreSQL, biarkan field ini kosong untuk menghubungkan lewat soket Unix.",
        "config-db-wiki-settings": "Identifikasi wiki ini",
-       "config-db-name": "Nama basis data:",
+       "config-db-name": "Nama basis data (tanpa tanda hubung):",
        "config-db-name-help": "Pilih nama yang mengidentifikasikan wiki Anda.\nNama tersebut tidak boleh mengandung spasi.\n\nJika Anda menggunakan inang web bersama, penyedia inang Anda dapat memberikan Anda nama basis data khusus untuk digunakan atau mengizinkan Anda membuat basis data melalui panel kontrol.",
        "config-db-install-account": "Akun pengguna untuk instalasi",
        "config-db-username": "Nama pengguna basis data:",
        "config-db-account-lock": "Gunakan nama pengguna dan kata sandi yang sama selama operasi normal",
        "config-db-wiki-account": "Akun pengguna untuk operasi normal",
        "config-db-wiki-help": "Masukkan nama pengguna dan sandi yang akan digunakan untuk terhubung ke basis data wiki selama operasi normal.\nJika akun tidak ada, akun instalasi memiliki hak yang memadai, akun pengguna ini akan dibuat dengan hak akses minimum yang diperlukan untuk mengoperasikan wiki.",
-       "config-db-prefix": "Prefiks tabel basis data:",
+       "config-db-prefix": "Prefiks tabel basis data (tanpa tanda hubung):",
        "config-db-prefix-help": "Jika Anda perlu berbagi satu basis data di antara beberapa wiki, atau antara MediaWiki dan aplikasi web lain, Anda dapat memilih untuk menambahkan prefiks terhadap semua nama tabel demi menghindari konflik.\nJangan gunakan spasi.\n\nPrefiks ini biasanya dibiarkan kosong.",
        "config-mysql-old": "MySQL $1 atau versi terbaru diperlukan, Anda menggunakan $2.",
        "config-db-port": "Porta basis data:",
-       "config-db-schema": "Skema untuk MediaWiki",
+       "config-db-schema": "Skema untuk MediaWiki (tanpa tanda hubung):",
        "config-db-schema-help": "Skema ini biasanya berjalan baik.\nUbah hanya jika Anda tahu Anda perlu mengubahnya.",
        "config-pg-test-error": "Tidak dapat terhubung ke basis data <strong>$1</strong>: $2",
        "config-sqlite-dir": "Direktori data SQLite:",
        "config-sqlite-dir-help": "SQLite menyimpan semua data dalam satu berkas.\n\nDirektori yang Anda berikan harus dapat ditulisi oleh server web selama instalasi.\n\nDirektori itu '''tidak''' boleh dapat diakses melalui web, inilah sebabnya kami tidak menempatkannya bersama dengan berkas PHP lain.\n\nPenginstal akan membuat berkas <code>.htaccess</code> bersamaan dengan itu, tetapi jika gagal, orang dapat memperoleh akses ke basis data mentah Anda.\nItu termasuk data mentah pengguna (alamat surel, hash sandi) serta revisi yang dihapus dan data lainnya yang dibatasi pada wiki.\n\nPertimbangkan untuk menempatkan basis data di tempat lain, misalnya di <code>/var/lib/mediawiki/yourwiki</code>.",
-       "config-type-mysql": "MySQL (atau yang kompatibel)",
+       "config-type-mysql": "MariaDB, MySQL, atau yang kompatibel",
        "config-type-postgres": "PostgreSQL",
        "config-type-sqlite": "SQLite",
        "config-support-info": "MediaWiki mendukung sistem basis data berikut:\n\n$1\n\nJika Anda tidak melihat sistem basis data yang Anda gunakan tercantum di bawah ini, ikuti petunjuk terkait di atas untuk mengaktifkan dukungan.",
        "config-dbsupport-mysql": "* [{{int:version-db-mysql-url}} MySQL] adalah target utama MediaWiki dan memiliki dukungan terbaik. MediaWiki juga berjalan dengan [{{int:version-db-mariadb-url}} MariaDB] dan [{{int:version-db-percona-url}} Server Percona], yang kompatibel dengan MySQL. ([https://www.php.net/manual/en/mysql.installation.php Cara mengompilasi PHP dengan dukungan MySQL])",
        "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] adalah sistem basis data sumber terbuka populer sebagai alternatif MySQL.([https://www.php.net/manual/en/pgsql.installation.php Bagaimana mengompilasikan PHP dengan dukungan PostgreSQL])",
-       "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] adalah sistem basis data yang ringan yang sangat baik dukungannya. ([http://www.php.net/manual/en/pdo.installation.php cara mengompilasi PHP dengan dukungan SQLite], menggunakan PDO)",
+       "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] adalah sistem basis data yang ringan yang sangat baik dukungannya. ([http://www.php.net/manual/en/pdo.installation.php Bagaimana mengompilasi PHP dengan dukungan SQLite], menggunakan PDO)",
        "config-header-mysql": "Pengaturan MariaDB/MySQL",
        "config-header-postgres": "Pengaturan PostgreSQL",
        "config-header-sqlite": "Pengaturan SQLite",
        "config-missing-db-host": "Anda harus memasukkan nilai untuk \"{{int:config-db-host}}\"",
        "config-invalid-db-name": "Nama basis data \"$1\" tidak sah.\nGunakan hanya huruf ASCII (a-z, A-Z), angka (0-9), garis bawah (_), dan tanda hubung (-).",
        "config-invalid-db-prefix": "Prefiks basis data \"$1\" tidak sah.\nGunakan hanya huruf ASCII (a-z, A-Z), angka (0-9), garis bawah (_), dan tanda hubung (-).",
-       "config-connection-error": "$1.\n\nPeriksa nama inang, pengguna, dan sandi di bawah ini dan coba lagi.",
+       "config-connection-error": "$1.\n\nPeriksa nama inang, pengguna, dan kata sandi dan coba lagi. Jika menggunakan \"localhost\" sebagai inang basis data, coba gunakan \"127.0.0.1\" (atau sebaliknya).",
        "config-invalid-schema": "Skema MediaWiki \"$1\" tidak sah.\nGunakan hanya huruf ASCII (a-z, A-Z), angka (0-9), dan garis bawah (_).",
        "config-postgres-old": "PostgreSQL $1 atau versi terbaru diperlukan, Anda menggunakan $2.",
        "config-sqlite-name-help": "Pilih nama yang mengidentifikasi wiki Anda.\nJangan gunakan spasi atau tanda hubung.\nNama ini akan digunakan untuk nama berkas data SQLite.",
        "config-sqlite-cant-create-db": "Tidak dapat membuat berkas basis data <code>$1</code>.",
        "config-sqlite-fts3-downgrade": "PHP tidak memiliki dukungan FTS3, tabel dituruntarafkan.",
        "config-can-upgrade": "Ada tabel MediaWiki di basis dataini.\nUntuk memperbaruinya ke MediaWiki $1, klik '''Lanjut'''.",
+       "config-upgrade-error": "Terjadi sebuah galat ketika memperbarui tabel MediaWiki dalam basis data Anda.\n\nUntuk informasi lebih lanjut, lihat catatan di atas, untuk mencoba kembali klik <strong>Lanjutkan</strong>.",
        "config-upgrade-done": "Pemutakhiran selesai.\n\nAnda sekarang dapat [$1 mulai menggunakan wiki Anda].\n\nJika Anda ingin membuat ulang berkas <code>LocalSettings.php</code>, klik tombol di bawah ini.\nTindakan ini '''tidak dianjurkan''' kecuali jika Anda mengalami masalah dengan wiki Anda.",
        "config-upgrade-done-no-regenerate": "Pemutakhiran selesai.\n\nAnda sekarang dapat [$1 mulai menggunakan wiki Anda].",
        "config-regenerate": "Regenerasi LocalSettings.php →",
        "config-db-web-create": "Buat akun jika belum ada",
        "config-db-web-no-create-privs": "Akun Anda berikan untuk instalasi tidak memiliki hak yang cukup untuk membuat akun.\nAkun yang Anda berikan harus sudah ada.",
        "config-mysql-engine": "Mesin penyimpanan:",
-       "config-mysql-innodb": "InnoDB",
+       "config-mysql-innodb": "InnoDB (disarankan)",
        "config-mysql-engine-help": "'''InnoDB''' hampir selalu merupakan pilihan terbaik karena memiliki dukungan konkurensi yang baik.\n\n'''MyISAM''' mungkin lebih cepat dalam instalasi pengguna-tunggal atau hanya-baca.\nBasis data MyISAM cenderung lebih sering rusak daripada basis data InnoDB.",
        "config-site-name": "Nama wiki:",
        "config-site-name-help": "Ini akan muncul di bilah judul peramban dan di berbagai tempat lainnya.",
        "config-install-subscribe-fail": "Tidak dapat berlangganan mediawiki-announce: $1",
        "config-install-subscribe-notpossible": "cURL tidak diinstal dan <code>allow_url_fopen</code> tidak tersedia.",
        "config-install-mainpage": "Membuat halaman utama dengan konten bawaan",
+       "config-install-mainpage-exists": "Halaman utama sudah ada, meloncati",
        "config-install-extension-tables": "Pembuatan tabel untuk ekstensi yang diaktifkan",
        "config-install-mainpage-failed": "Tidak dapat membuat halaman utama: $1",
        "config-install-done": "<strong>Selamat!</strong>\nAnda telah berhasil menginstal MediaWiki.\n\nPemasang telah membuat sebuah berkas <code>LocalSettings.php</code>.\nBerkas itu berisi semua setelan Anda.\n\nAnda perlu mengunduh berkas itu dan meletakkannya di direktori instalasi wiki (direktori yang sama dengan index.php). Pengunduhan akan dimulai secara otomatis.\n\nJika pengunduhan tidak terjadi, atau jika Anda membatalkannya, Anda dapat mengulangi pengunduhan dengan mengeklik tautan berikut:\n\n$3\n\n<strong>Catatan</strong>: Jika Anda tidak melakukannya sekarang, berkas konfigurasi yang dihasilkan ini tidak akan tersedia lagi setelah Anda keluar dari proses instalasi tanpa mengunduhnya.\n\nSetelah melakukannya, Anda dapat <strong>[$2 memasuki wiki Anda]</strong>.",
+       "config-install-success": "MediaWiki telah dipasang dengan sukses. Anda dapat mengunjungi <$1$2> untuk melihat wiki ini. Jika Anda memiliki pertanyaan, lihat daftar pertanyaan yang sering ditanyakan: <https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ> atau gunakan salah satu forum yang ada di halaman tersebut.",
+       "config-install-db-success": "Basis data telah sukses diatur",
        "config-download-localsettings": "Unduh <code>LocalSettings.php</code>",
        "config-help": "bantuan",
        "config-help-tooltip": "klik untuk memperluas",
        "config-skins-screenshots": "$1 (tangkapan layar: $2)",
        "config-extensions-requires": "$1 (memerlukan $2)",
        "config-screenshot": "tangkapan layar",
+       "config-extension-not-found": "Tidak dapat menemukan berkas registrasi untuk ekstensi \"$1\"",
        "mainpagetext": "<strong>MediaWiki telah terpasang dengan sukses.</strong>",
        "mainpagedocfooter": "Konsultasikan [https://www.mediawiki.org/wiki/Help:Contents Panduan Pengguna] untuk cara penggunaan perangkat lunak wiki ini.\n\n== Memulai ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Daftar pengaturan konfigurasi]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Pertanyaan yang sering diajukan mengenai MediaWiki]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Milis rilis MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Pelokalan MediaWiki untuk bahasa Anda]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Belajar bagaimana menghadapi spam di wiki lokal]"
 }
index a6e9ddb..3d2311e 100644 (file)
        "config-welcome": "=== Verificări ale mediului ===\nVerificări de bază vor fi efectuate pentru a vedea dacă este potrivit pentru instalarea MediaWiki.\nNu uitați să includeți aceste informații dacă doriți asistență pentru completarea instalării.",
        "config-welcome-section-copyright": "=== Drepturi de autor și termeni ===\n\n$1\n\nAcest program este un software liber; îl puteți redistribui și / sau modifica în conformitate cu termenii Licenței Publice Generale GNU, publicată de Fundația pentru Software Liber; fie versiunea 2 a Licenței, fie (la alegere) orice versiune ulterioară.\nAcest program este distribuit în speranța că va fi util, dar <strong>fără nicio garanție</strong>; fără nici măcar garanția implicită de <strong>vandabilitate</strong> sau <strong>fitness pentru un anumit scop</strong>.\nPentru mai multe detalii, consultați Licența publică generală GNU.\nAr fi trebuit să fi primit [$2 o copie a GNU General Public License] împreună cu acest program; dacă nu, scrieți la Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, SUA, sau [https://www.gnu.org/copyleft/gpl.html citiți-o online] .",
        "config-sidebar": "* [https://www.mediawiki.org Acasă MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents User's Guide]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Administrator's Guide]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ FAQ]\n----\n* <doclink href=Readme>Read me</doclink>\n* <doclink href=ReleaseNotes>Release notes</doclink>\n* <doclink href=Copying>Copying</doclink>\n* <doclink href=UpgradeDoc>Upgrading</doclink>",
+       "config-sidebar-readme": "Read me",
+       "config-sidebar-relnotes": "Note de lansare",
+       "config-sidebar-license": "Copiere",
+       "config-sidebar-upgrade": "Actualizare",
        "config-env-good": "Verificarea mediului a fost efectuată cu succes.\nPuteți instala MediaWiki.",
        "config-env-bad": "Verificarea mediului a fost efectuată.\nNu puteți instala MediaWiki.",
        "config-env-php": "PHP $1 este instalat.",
        "config-env-hhvm": "HHVM $1 este instalat.",
-       "config-unicode-using-intl": "Utilizarea extensiei [https://pecl.php.net/intl intl PECL] pentru normalizarea Unicode.",
-       "config-unicode-pure-php-warning": "<strong>Atenție:</strong> Extensia [https://pecl.php.net/intl intl PECL] nu este disponibilă pentru a face față normalizării Unicode, revenind la o implementare lentă pur PHP.\nDacă rulați un site cu trafic ridicat, ar trebui să citiți puțin în [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Normalizarea Unicode].",
+       "config-unicode-using-intl": "Se folosește extensia [https://php.net/manual/en/book.intl.php PHP intl] pentru normalizarea Unicode.",
+       "config-unicode-pure-php-warning": "<strong>Atenție:</strong> Extensia [https://php.net/manual/en/book.intl.php PHP intl] nu este disponibilă pentru a procesa normalizarea Unicode, se folosește o implementare lentă pur PHP.\nDacă rulați un site cu trafic ridicat, ar trebui să citiți despre [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Normalizarea Unicode].",
        "config-unicode-update-warning": "<strong>Avertisment:</strong> Versiunea instalată a pachetului de normalizare Unicode utilizează o versiune mai veche a bibliotecii [http://site.icu-project.org/ proiectul ICU].\nAr trebui să faceți upgrade [https://www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations] dacă sunteți preocupat de utilizarea Unicode.",
        "config-no-db": "Nu am găsit un driver de bază de date potrivit! Trebuie să instalați un driver de bază de date pentru PHP.\nUrmătoarea bază de date {{PLURAL:$2|tip este|tipuri sunt}} este acceptată: $1.\nDacă ați compilat singuri PHP, reconfigurați-l cu un client de bază de date activat, de exemplu, utilizând <code>./ configure --with-mysqli</code>.\nDacă ați instalat PHP dintr-un pachet Debian sau Ubuntu, atunci trebuie să instalați, de exemplu, pachetul <code>php-mysql</code>",
-       "config-outdated-sqlite": "<strong>Atenție:</strong> ai SQLite $1, care este mai mic decât minimul necesar pentru versiunea $2. SQLite va fi nedisponibil.",
+       "config-outdated-sqlite": "<strong>Atenție:</strong> aveții SQLite $2, care este mai mic decât versiunea minimă $1. SQLite nu va fi disponibil.",
        "config-no-fts3": "<strong>Atenție:</strong> SQLite este compus fără [//sqlite.org/fts3.html modulu FTS3], caută caracteristici care nu vor fi disponibile la finalul acesta.",
        "config-pcre-old": "<strong>Fatal:</> PCRE $1 sau mai târziu este necesar este necesar. \nPHP tău este binar este legat de PCRE $2. \n[https://www.mediawiki.org/wiki/Manual:Errors_and_symptoms/PCRE Mai multe informații].",
        "config-pcre-no-utf8": "<strong>Fatal:</strong> Modul PCRE al PHP pare să fie compilat fără suport PCRE_UTF8.\nMediaWiki necesită ca suportul UTF-8 să funcționeze corect.",
        "config-admin-name": "Numele dumneavoastră de utilizator:",
        "config-admin-password": "Parolă:",
        "config-admin-password-confirm": "Parola, din nou:",
+       "config-admin-name-blank": "Introduceți numele de utilizator al administratorului.",
        "config-admin-password-blank": "Introduceți o parolă pentru contul de administrator.",
        "config-admin-password-mismatch": "Cele două parole introduse nu corespund.",
        "config-admin-email": "Adresa de e-mail:",
        "config-license-pd": "Domeniu public",
        "config-license-cc-choose": "Alegeți o licență Creative Commons personalizată",
        "config-email-settings": "Setări pentru e-mail",
+       "config-enable-email": "Permiteți trimiterea de e-mail",
+       "config-email-user": "Permiteți e-mailurile între utilizatori",
        "config-email-usertalk": "Activați notificările pentru pagina de discuții a utilizatorului",
        "config-upload-settings": "Încărcare de imagini și fișiere",
        "config-upload-deleted": "Director pentru fișierele șterse:",
index 9bab12f..3ea2df6 100644 (file)
        "config-memcached-servers": "Memcached-serveri:",
        "config-memcached-help": "Lista IP adresa za uporabu u Memcached.\nTreba da se navede jednu u svaki red, kao i port što će se koristiti. Na primer:\n 127.0.0.1:11211\n 192.168.1.25:1234",
        "config-memcache-needservers": "Odabrali ste Memcached kao vaš tip međuspremnika (keša), ali niste naveli nijedan server.",
-       "mainpagetext": "<strong>MediaWiki je uspješno instaliran.</strong>",
+       "config-install-step-done": "gotovo",
+       "config-install-step-failed": "nije uspjelo",
+       "config-install-extensions": "Uključujem dodatke",
+       "config-install-database": "Postavljam bazu podataka",
+       "config-install-schema": "Pravim šemu",
+       "config-install-pg-schema-not-exist": "PostgreSQL-šema ne postoji.",
+       "config-install-pg-schema-failed": "Pravljenje natabela nije uspelo.\nUvjerite se da korisnik „$1” može da zapisuje u šemi „$2”.",
+       "config-install-pg-commit": "Usproveđivanje promjena",
+       "config-install-user": "Pravim korisnika baze podataka",
+       "config-install-user-alreadyexists": "Korisnik \"$1\" već postoji",
+       "config-install-user-create-failed": "Pravljenje korisnika \"$1\" nije uspjelo: $2",
+       "config-install-user-grant-failed": "Dodjeljivanje dozvola korisniku \"$1\" nije uspjelo: $2",
+       "config-install-user-missing": "Navedeni korisnik \"$1\" ne postoji.",
+       "config-install-user-missing-create": "Navedeni korisnik \"$1\" ne postoji.\nAko želite da ga otvorite, štiklirajte mogućnost „napravi račun”.",
+       "config-install-tables": "Pravim tabele",
+       "config-install-tables-exist": "<strong>Upozorenje:</strong> Izgleda da MediaWiki tabele već postoje.\nPreskočim pravljenje.",
+       "config-install-tables-failed": "<strong>Greška:</strong> Pravljenje tabele nije uspjelo zbog sljedeće greške: $1",
+       "config-install-interwiki": "Popunjavam predodređene međuprojektne tabele",
+       "config-install-interwiki-list": "Nisam mogao pronaći datoteku <code>interwiki.list</code>.",
+       "config-install-interwiki-exists": "<strong>Upozorenje:</strong> Tabela međuwikija već ima unose.\nPreskočim podrazumevano-zadanu listu.",
+       "config-install-stats": "Pokrećem statistiku",
+       "config-install-keys": "Generisanje tajnih ključeva",
+       "config-install-updates": "Spriječi vršenje nepotrebnih podnova",
+       "config-install-updates-failed": "<strong>Greška:</strong> Umetanje podnovnih klučeva u tabele nije uspjelo, zbog sljedeće greške: $1",
+       "config-install-sysop": "Otvaranje korisničkog računa administratora",
+       "config-install-subscribe-fail": "Nije moguće Vas pretplatiti se na izvješćenje mediawiki-announce: $1",
+       "config-install-subscribe-notpossible": "cURL nije instaliran, a <code>allow_url_fopen</code> nije dostupno.",
+       "config-install-mainpage": "Pravim početnu stranicu sa standardnim sadržajem",
+       "config-install-mainpage-exists": "Početna strana već postoji. Prelazim na sljedeće.",
+       "config-install-extension-tables": "Izrada tabela za omogućene dodatke",
+       "config-install-mainpage-failed": "Nisam mogao umetnuti početnu stranu: $1",
+       "config-download-localsettings": "Preuzmi <code>LocalSettings.php</code>",
+       "config-help": "pomoć",
+       "config-help-tooltip": "kliknite da rasklopite",
+       "config-nofile": "Datoteka \"$1\" nije pronađena. Da nije obrisana?",
+       "config-skins-screenshots": "$1 (ekr. snimci: $2)",
+       "config-extensions-requires": "$1 (zahtjeva $2)",
+       "config-screenshot": "ekranski snimak",
+       "config-extension-not-found": "Nisam mogao naći datoteku registracije za dodatak „$1”",
+       "config-extension-dependency": "Naišao na grešku sa zavisnošću pri instaliranju dodatka „$1”: $2",
+       "mainpagetext": "<strong>MediaWiki je instaliran.</strong>",
        "mainpagedocfooter": "Za informacije o korištenju wiki softvera konzultirajte [https://meta.wikimedia.org/wiki/Help:Contents Vodič za korisnike].\n\n== Uvod u rad ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Lista konfiguracije postavki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Lista primatelja izdanja MediaWikija]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Lokalizirajte MediaWiki za svoj jezik]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Saznajte kako se boriti protiv spama na svojem wikiju]"
 }
index d449e8a..4be41a2 100644 (file)
@@ -424,7 +424,7 @@ class JobQueueDB extends JobQueue {
                        if ( $dbw->getType() === 'mysql' ) {
                                // Per https://bugs.mysql.com/bug.php?id=6980, we can't use subqueries on the
                                // same table being changed in an UPDATE query in MySQL (gives Error: 1093).
-                               // Oracle and Postgre have no such limitation. However, MySQL offers an
+                               // Postgres has no such limitation. However, MySQL offers an
                                // alternative here by supporting ORDER BY + LIMIT for UPDATE queries.
                                $dbw->query( "UPDATE {$dbw->tableName( 'job' )} " .
                                        "SET " .
index 21d8c7e..adb4221 100644 (file)
@@ -279,16 +279,26 @@ class JobRunner implements LoggerAwareInterface {
                ] );
                $this->debugCallback( $msg );
 
+               // Clear out title cache data from prior snapshots
+               // (e.g. from before JobRunner was invoked in this process)
+               MediaWikiServices::getInstance()->getLinkCache()->clear();
+
                // Run the job...
                $rssStart = $this->getMaxRssKb();
                $jobStartTime = microtime( true );
                try {
                        $fnameTrxOwner = get_class( $job ) . '::run'; // give run() outer scope
-                       if ( !$job->hasExecutionFlag( $job::JOB_NO_EXPLICIT_TRX_ROUND ) ) {
-                               $lbFactory->beginMasterChanges( $fnameTrxOwner );
+                       // Flush any pending changes left over from an implicit transaction round
+                       if ( $job->hasExecutionFlag( $job::JOB_NO_EXPLICIT_TRX_ROUND ) ) {
+                               $lbFactory->commitMasterChanges( $fnameTrxOwner ); // new implicit round
+                       } else {
+                               $lbFactory->beginMasterChanges( $fnameTrxOwner ); // new explicit round
                        }
+                       // Clear any stale REPEATABLE-READ snapshots from replica DB connections
+                       $lbFactory->flushReplicaSnapshots( $fnameTrxOwner );
                        $status = $job->run();
                        $error = $job->getLastError();
+                       // Commit all pending changes from this job
                        $this->commitMasterChanges( $lbFactory, $job, $fnameTrxOwner );
                        // Run any deferred update tasks; doUpdates() manages transactions itself
                        DeferredUpdates::doUpdates();
@@ -304,12 +314,6 @@ class JobRunner implements LoggerAwareInterface {
                        MWExceptionHandler::logException( $e );
                }
 
-               // Commit all outstanding connections that are in a transaction
-               // to get a fresh repeatable read snapshot on every connection.
-               // Note that jobs are still responsible for handling replica DB lag.
-               $lbFactory->flushReplicaSnapshots( __METHOD__ );
-               // Clear out title cache data from prior snapshots
-               MediaWikiServices::getInstance()->getLinkCache()->clear();
                $timeMs = intval( ( microtime( true ) - $jobStartTime ) * 1000 );
                $rssEnd = $this->getMaxRssKb();
 
diff --git a/includes/language/ConverterRule.php b/includes/language/ConverterRule.php
new file mode 100644 (file)
index 0000000..4a330ad
--- /dev/null
@@ -0,0 +1,498 @@
+<?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 Language
+ */
+
+/**
+ * Parser for rules of language conversion, parse rules in -{ }- tag.
+ * @ingroup Language
+ * @author fdcn <fdcn64@gmail.com>, PhiLiP <philip.npc@gmail.com>
+ */
+class ConverterRule {
+       public $mText; // original text in -{text}-
+       public $mConverter; // LanguageConverter object
+       public $mRuleDisplay = '';
+       public $mRuleTitle = false;
+       public $mRules = ''; // string : the text of the rules
+       public $mRulesAction = 'none';
+       public $mFlags = [];
+       public $mVariantFlags = [];
+       public $mConvTable = [];
+       public $mBidtable = []; // array of the translation in each variant
+       public $mUnidtable = []; // array of the translation in each variant
+
+       /**
+        * @param string $text The text between -{ and }-
+        * @param LanguageConverter $converter
+        */
+       public function __construct( $text, $converter ) {
+               $this->mText = $text;
+               $this->mConverter = $converter;
+       }
+
+       /**
+        * Check if variants array in convert array.
+        *
+        * @param array|string $variants Variant language code
+        * @return string Translated text
+        */
+       public function getTextInBidtable( $variants ) {
+               $variants = (array)$variants;
+               if ( !$variants ) {
+                       return false;
+               }
+               foreach ( $variants as $variant ) {
+                       if ( isset( $this->mBidtable[$variant] ) ) {
+                               return $this->mBidtable[$variant];
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * Parse flags with syntax -{FLAG| ... }-
+        * @private
+        */
+       function parseFlags() {
+               $text = $this->mText;
+               $flags = [];
+               $variantFlags = [];
+
+               $sepPos = strpos( $text, '|' );
+               if ( $sepPos !== false ) {
+                       $validFlags = $this->mConverter->mFlags;
+                       $f = StringUtils::explode( ';', substr( $text, 0, $sepPos ) );
+                       foreach ( $f as $ff ) {
+                               $ff = trim( $ff );
+                               if ( isset( $validFlags[$ff] ) ) {
+                                       $flags[$validFlags[$ff]] = true;
+                               }
+                       }
+                       $text = strval( substr( $text, $sepPos + 1 ) );
+               }
+
+               if ( !$flags ) {
+                       $flags['S'] = true;
+               } elseif ( isset( $flags['R'] ) ) {
+                       $flags = [ 'R' => true ];// remove other flags
+               } elseif ( isset( $flags['N'] ) ) {
+                       $flags = [ 'N' => true ];// remove other flags
+               } elseif ( isset( $flags['-'] ) ) {
+                       $flags = [ '-' => true ];// remove other flags
+               } elseif ( count( $flags ) == 1 && isset( $flags['T'] ) ) {
+                       $flags['H'] = true;
+               } elseif ( isset( $flags['H'] ) ) {
+                       // replace A flag, and remove other flags except T
+                       $temp = [ '+' => true, 'H' => true ];
+                       if ( isset( $flags['T'] ) ) {
+                               $temp['T'] = true;
+                       }
+                       if ( isset( $flags['D'] ) ) {
+                               $temp['D'] = true;
+                       }
+                       $flags = $temp;
+               } else {
+                       if ( isset( $flags['A'] ) ) {
+                               $flags['+'] = true;
+                               $flags['S'] = true;
+                       }
+                       if ( isset( $flags['D'] ) ) {
+                               unset( $flags['S'] );
+                       }
+                       // try to find flags like "zh-hans", "zh-hant"
+                       // allow syntaxes like "-{zh-hans;zh-hant|XXXX}-"
+                       $variantFlags = array_intersect( array_keys( $flags ), $this->mConverter->mVariants );
+                       if ( $variantFlags ) {
+                               $variantFlags = array_flip( $variantFlags );
+                               $flags = [];
+                       }
+               }
+               $this->mVariantFlags = $variantFlags;
+               $this->mRules = $text;
+               $this->mFlags = $flags;
+       }
+
+       /**
+        * Generate conversion table.
+        * @private
+        */
+       function parseRules() {
+               $rules = $this->mRules;
+               $bidtable = [];
+               $unidtable = [];
+               $variants = $this->mConverter->mVariants;
+               $varsep_pattern = $this->mConverter->getVarSeparatorPattern();
+
+               // Split according to $varsep_pattern, but ignore semicolons from HTML entities
+               $rules = preg_replace( '/(&[#a-zA-Z0-9]+);/', "$1\x01", $rules );
+               $choice = preg_split( $varsep_pattern, $rules );
+               $choice = str_replace( "\x01", ';', $choice );
+
+               foreach ( $choice as $c ) {
+                       $v = explode( ':', $c, 2 );
+                       if ( count( $v ) != 2 ) {
+                               // syntax error, skip
+                               continue;
+                       }
+                       $to = trim( $v[1] );
+                       $v = trim( $v[0] );
+                       $u = explode( '=>', $v, 2 );
+                       $vv = $this->mConverter->validateVariant( $v );
+                       // if $to is empty (which is also used as $from in bidtable),
+                       // strtr() could return a wrong result.
+                       if ( count( $u ) == 1 && $to !== '' && $vv ) {
+                               $bidtable[$vv] = $to;
+                       } elseif ( count( $u ) == 2 ) {
+                               $from = trim( $u[0] );
+                               $v = trim( $u[1] );
+                               $vv = $this->mConverter->validateVariant( $v );
+                               // if $from is empty, strtr() could return a wrong result.
+                               if ( array_key_exists( $vv, $unidtable )
+                                       && !is_array( $unidtable[$vv] )
+                                       && $from !== ''
+                                       && $vv ) {
+                                       $unidtable[$vv] = [ $from => $to ];
+                               } elseif ( $from !== '' && $vv ) {
+                                       $unidtable[$vv][$from] = $to;
+                               }
+                       }
+                       // syntax error, pass
+                       if ( !isset( $this->mConverter->mVariantNames[$vv] ) ) {
+                               $bidtable = [];
+                               $unidtable = [];
+                               break;
+                       }
+               }
+               $this->mBidtable = $bidtable;
+               $this->mUnidtable = $unidtable;
+       }
+
+       /**
+        * @private
+        *
+        * @return string
+        */
+       function getRulesDesc() {
+               $codesep = $this->mConverter->mDescCodeSep;
+               $varsep = $this->mConverter->mDescVarSep;
+               $text = '';
+               foreach ( $this->mBidtable as $k => $v ) {
+                       $text .= $this->mConverter->mVariantNames[$k] . "$codesep$v$varsep";
+               }
+               foreach ( $this->mUnidtable as $k => $a ) {
+                       foreach ( $a as $from => $to ) {
+                               $text .= $from . '⇒' . $this->mConverter->mVariantNames[$k] .
+                                       "$codesep$to$varsep";
+                       }
+               }
+               return $text;
+       }
+
+       /**
+        * Parse rules conversion.
+        * @private
+        *
+        * @param string $variant
+        *
+        * @return string
+        */
+       function getRuleConvertedStr( $variant ) {
+               $bidtable = $this->mBidtable;
+               $unidtable = $this->mUnidtable;
+
+               if ( count( $bidtable ) + count( $unidtable ) == 0 ) {
+                       return $this->mRules;
+               } else {
+                       // display current variant in bidirectional array
+                       $disp = $this->getTextInBidtable( $variant );
+                       // or display current variant in fallbacks
+                       if ( $disp === false ) {
+                               $disp = $this->getTextInBidtable(
+                                       $this->mConverter->getVariantFallbacks( $variant ) );
+                       }
+                       // or display current variant in unidirectional array
+                       if ( $disp === false && array_key_exists( $variant, $unidtable ) ) {
+                               $disp = array_values( $unidtable[$variant] )[0];
+                       }
+                       // or display first text under disable manual convert
+                       if ( $disp === false && $this->mConverter->mManualLevel[$variant] == 'disable' ) {
+                               if ( count( $bidtable ) > 0 ) {
+                                       $disp = array_values( $bidtable )[0];
+                               } else {
+                                       $disp = array_values( array_values( $unidtable )[0] )[0];
+                               }
+                       }
+                       return $disp;
+               }
+       }
+
+       /**
+        * Similar to getRuleConvertedStr(), but this prefers to use original
+        * page title if $variant === $this->mConverter->mMainLanguageCode
+        * and may return false in this case (so this title conversion rule
+        * will be ignored and the original title is shown).
+        *
+        * @since 1.22
+        * @param string $variant The variant code to display page title in
+        * @return string|bool The converted title or false if just page name
+        */
+       function getRuleConvertedTitle( $variant ) {
+               if ( $variant === $this->mConverter->mMainLanguageCode ) {
+                       // If a string targeting exactly this variant is set,
+                       // use it. Otherwise, just return false, so the real
+                       // page name can be shown (and because variant === main,
+                       // there'll be no further automatic conversion).
+                       $disp = $this->getTextInBidtable( $variant );
+                       if ( $disp ) {
+                               return $disp;
+                       }
+                       if ( array_key_exists( $variant, $this->mUnidtable ) ) {
+                               $disp = array_values( $this->mUnidtable[$variant] )[0];
+                       }
+                       // Assigned above or still false.
+                       return $disp;
+               } else {
+                       return $this->getRuleConvertedStr( $variant );
+               }
+       }
+
+       /**
+        * Generate conversion table for all text.
+        * @private
+        */
+       function generateConvTable() {
+               // Special case optimisation
+               if ( !$this->mBidtable && !$this->mUnidtable ) {
+                       $this->mConvTable = [];
+                       return;
+               }
+
+               $bidtable = $this->mBidtable;
+               $unidtable = $this->mUnidtable;
+               $manLevel = $this->mConverter->mManualLevel;
+
+               $vmarked = [];
+               foreach ( $this->mConverter->mVariants as $v ) {
+                       /* for bidirectional array
+                               fill in the missing variants, if any,
+                               with fallbacks */
+                       if ( !isset( $bidtable[$v] ) ) {
+                               $variantFallbacks =
+                                       $this->mConverter->getVariantFallbacks( $v );
+                               $vf = $this->getTextInBidtable( $variantFallbacks );
+                               if ( $vf ) {
+                                       $bidtable[$v] = $vf;
+                               }
+                       }
+
+                       if ( isset( $bidtable[$v] ) ) {
+                               foreach ( $vmarked as $vo ) {
+                                       // use syntax: -{A|zh:WordZh;zh-tw:WordTw}-
+                                       // or -{H|zh:WordZh;zh-tw:WordTw}-
+                                       // or -{-|zh:WordZh;zh-tw:WordTw}-
+                                       // to introduce a custom mapping between
+                                       // words WordZh and WordTw in the whole text
+                                       if ( $manLevel[$v] == 'bidirectional' ) {
+                                               $this->mConvTable[$v][$bidtable[$vo]] = $bidtable[$v];
+                                       }
+                                       if ( $manLevel[$vo] == 'bidirectional' ) {
+                                               $this->mConvTable[$vo][$bidtable[$v]] = $bidtable[$vo];
+                                       }
+                               }
+                               $vmarked[] = $v;
+                       }
+                       /* for unidirectional array fill to convert tables */
+                       if ( ( $manLevel[$v] == 'bidirectional' || $manLevel[$v] == 'unidirectional' )
+                               && isset( $unidtable[$v] )
+                       ) {
+                               if ( isset( $this->mConvTable[$v] ) ) {
+                                       $this->mConvTable[$v] = $unidtable[$v] + $this->mConvTable[$v];
+                               } else {
+                                       $this->mConvTable[$v] = $unidtable[$v];
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Parse rules and flags.
+        * @param string|null $variant Variant language code
+        */
+       public function parse( $variant = null ) {
+               if ( !$variant ) {
+                       $variant = $this->mConverter->getPreferredVariant();
+               }
+
+               $this->parseFlags();
+               $flags = $this->mFlags;
+
+               // convert to specified variant
+               // syntax: -{zh-hans;zh-hant[;...]|<text to convert>}-
+               if ( $this->mVariantFlags ) {
+                       // check if current variant in flags
+                       if ( isset( $this->mVariantFlags[$variant] ) ) {
+                               // then convert <text to convert> to current language
+                               $this->mRules = $this->mConverter->autoConvert( $this->mRules,
+                                       $variant );
+                       } else {
+                               // if current variant no in flags,
+                               // then we check its fallback variants.
+                               $variantFallbacks =
+                                       $this->mConverter->getVariantFallbacks( $variant );
+                               if ( is_array( $variantFallbacks ) ) {
+                                       foreach ( $variantFallbacks as $variantFallback ) {
+                                               // if current variant's fallback exist in flags
+                                               if ( isset( $this->mVariantFlags[$variantFallback] ) ) {
+                                                       // then convert <text to convert> to fallback language
+                                                       $this->mRules =
+                                                               $this->mConverter->autoConvert( $this->mRules,
+                                                                       $variantFallback );
+                                                       break;
+                                               }
+                                       }
+                               }
+                       }
+                       $this->mFlags = $flags = [ 'R' => true ];
+               }
+
+               if ( !isset( $flags['R'] ) && !isset( $flags['N'] ) ) {
+                       // decode => HTML entities modified by Sanitizer::removeHTMLtags
+                       $this->mRules = str_replace( '=&gt;', '=>', $this->mRules );
+                       $this->parseRules();
+               }
+               $rules = $this->mRules;
+
+               if ( !$this->mBidtable && !$this->mUnidtable ) {
+                       if ( isset( $flags['+'] ) || isset( $flags['-'] ) ) {
+                               // fill all variants if text in -{A/H/-|text}- is non-empty but without rules
+                               if ( $rules !== '' ) {
+                                       foreach ( $this->mConverter->mVariants as $v ) {
+                                               $this->mBidtable[$v] = $rules;
+                                       }
+                               }
+                       } elseif ( !isset( $flags['N'] ) && !isset( $flags['T'] ) ) {
+                               $this->mFlags = $flags = [ 'R' => true ];
+                       }
+               }
+
+               $this->mRuleDisplay = false;
+               foreach ( $flags as $flag => $unused ) {
+                       switch ( $flag ) {
+                               case 'R':
+                                       // if we don't do content convert, still strip the -{}- tags
+                                       $this->mRuleDisplay = $rules;
+                                       break;
+                               case 'N':
+                                       // process N flag: output current variant name
+                                       $ruleVar = trim( $rules );
+                                       $this->mRuleDisplay = $this->mConverter->mVariantNames[$ruleVar] ?? '';
+                                       break;
+                               case 'D':
+                                       // process D flag: output rules description
+                                       $this->mRuleDisplay = $this->getRulesDesc();
+                                       break;
+                               case 'H':
+                                       // process H,- flag or T only: output nothing
+                                       $this->mRuleDisplay = '';
+                                       break;
+                               case '-':
+                                       $this->mRulesAction = 'remove';
+                                       $this->mRuleDisplay = '';
+                                       break;
+                               case '+':
+                                       $this->mRulesAction = 'add';
+                                       $this->mRuleDisplay = '';
+                                       break;
+                               case 'S':
+                                       $this->mRuleDisplay = $this->getRuleConvertedStr( $variant );
+                                       break;
+                               case 'T':
+                                       $this->mRuleTitle = $this->getRuleConvertedTitle( $variant );
+                                       $this->mRuleDisplay = '';
+                                       break;
+                               default:
+                                       // ignore unknown flags (but see error case below)
+                       }
+               }
+               if ( $this->mRuleDisplay === false ) {
+                       $this->mRuleDisplay = '<span class="error">'
+                               . wfMessage( 'converter-manual-rule-error' )->inContentLanguage()->escaped()
+                               . '</span>';
+               }
+
+               $this->generateConvTable();
+       }
+
+       /**
+        * Checks if there are conversion rules.
+        * @return bool
+        */
+       public function hasRules() {
+               return $this->mRules !== '';
+       }
+
+       /**
+        * Get display text on markup -{...}-
+        * @return string
+        */
+       public function getDisplay() {
+               return $this->mRuleDisplay;
+       }
+
+       /**
+        * Get converted title.
+        * @return string
+        */
+       public function getTitle() {
+               return $this->mRuleTitle;
+       }
+
+       /**
+        * Return how deal with conversion rules.
+        * @return string
+        */
+       public function getRulesAction() {
+               return $this->mRulesAction;
+       }
+
+       /**
+        * Get conversion table. (bidirectional and unidirectional
+        * conversion table)
+        * @return array
+        */
+       public function getConvTable() {
+               return $this->mConvTable;
+       }
+
+       /**
+        * Get conversion rules string.
+        * @return string
+        */
+       public function getRules() {
+               return $this->mRules;
+       }
+
+       /**
+        * Get conversion flags.
+        * @return array
+        */
+       public function getFlags() {
+               return $this->mFlags;
+       }
+}
index 1d2f0b4..7d954d3 100644 (file)
@@ -21,6 +21,7 @@
 
 /**
  * Methods for dealing with language codes.
+ * @todo Move some of the code-related static methods out of Language into this class
  *
  * @since 1.29
  * @ingroup Language
diff --git a/includes/language/LanguageNameUtils.php b/includes/language/LanguageNameUtils.php
deleted file mode 100644 (file)
index 08d9ab3..0000000
+++ /dev/null
@@ -1,319 +0,0 @@
-<?php
-/**
- * Internationalisation code.
- * See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more information.
- *
- * 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
- */
-
-/**
- * @defgroup Language Language
- */
-
-namespace MediaWiki\Languages;
-
-use HashBagOStuff;
-use Hooks;
-use MediaWiki\Config\ServiceOptions;
-use MediaWikiTitleCodec;
-use MWException;
-use Wikimedia\Assert\Assert;
-
-/**
- * @ingroup Language
- *
- * A service that provides utilities to do with language names and codes.
- *
- * @since 1.34
- */
-class LanguageNameUtils {
-       /**
-        * Return autonyms in getLanguageName(s).
-        */
-       const AUTONYMS = null;
-
-       /**
-        * Return all known languages in getLanguageName(s).
-        */
-       const ALL = 'all';
-
-       /**
-        * Return in getLanguageName(s) only the languages that are defined by MediaWiki.
-        */
-       const DEFINED = 'mw';
-
-       /**
-        * Return in getLanguageName(s) only the languages for which we have at least some localisation.
-        */
-       const SUPPORTED = 'mwfile';
-
-       /** @var ServiceOptions */
-       private $options;
-
-       /**
-        * Cache for language names
-        * @var HashBagOStuff|null
-        */
-       private $languageNameCache;
-
-       /**
-        * Cache for validity of language codes
-        * @var array
-        */
-       private $validCodeCache = [];
-
-       public static $constructorOptions = [
-               'ExtraLanguageNames',
-               'UsePigLatinVariant',
-       ];
-
-       /**
-        * @param ServiceOptions $options
-        */
-       public function __construct( ServiceOptions $options ) {
-               $options->assertRequiredOptions( self::$constructorOptions );
-               $this->options = $options;
-       }
-
-       /**
-        * Checks whether any localisation is available for that language tag in MediaWiki
-        * (MessagesXx.php or xx.json exists).
-        *
-        * @param string $code Language tag (in lower case)
-        * @return bool Whether language is supported
-        */
-       public function isSupportedLanguage( $code ) {
-               if ( !$this->isValidBuiltInCode( $code ) ) {
-                       return false;
-               }
-
-               if ( $code === 'qqq' ) {
-                       // Special code for internal use, not supported even though there is a qqq.json
-                       return false;
-               }
-
-               return is_readable( $this->getMessagesFileName( $code ) ) ||
-                       is_readable( $this->getJsonMessagesFileName( $code ) );
-       }
-
-       /**
-        * Returns true if a language code string is of a valid form, whether or not it exists. This
-        * includes codes which are used solely for customisation via the MediaWiki namespace.
-        *
-        * @param string $code
-        *
-        * @return bool
-        */
-       public function isValidCode( $code ) {
-               Assert::parameterType( 'string', $code, '$code' );
-               if ( !isset( $this->validCodeCache[$code] ) ) {
-                       // People think language codes are HTML-safe, so enforce it.  Ideally we should only
-                       // allow a-zA-Z0-9- but .+ and other chars are often used for {{int:}} hacks.  See bugs
-                       // T39564, T39587, T38938.
-                       $this->validCodeCache[$code] =
-                               // Protect against path traversal
-                               strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code ) &&
-                               !preg_match( MediaWikiTitleCodec::getTitleInvalidRegex(), $code );
-               }
-               return $this->validCodeCache[$code];
-       }
-
-       /**
-        * Returns true if a language code is of a valid form for the purposes of internal customisation
-        * of MediaWiki, via Messages*.php or *.json.
-        *
-        * @param string $code
-        * @return bool
-        */
-       public function isValidBuiltInCode( $code ) {
-               Assert::parameterType( 'string', $code, '$code' );
-
-               return (bool)preg_match( '/^[a-z0-9-]{2,}$/', $code );
-       }
-
-       /**
-        * Returns true if a language code is an IETF tag known to MediaWiki.
-        *
-        * @param string $tag
-        *
-        * @return bool
-        */
-       public function isKnownLanguageTag( $tag ) {
-               // Quick escape for invalid input to avoid exceptions down the line when code tries to
-               // process tags which are not valid at all.
-               if ( !$this->isValidBuiltInCode( $tag ) ) {
-                       return false;
-               }
-
-               if ( isset( Data\Names::$names[$tag] ) || $this->getLanguageName( $tag, $tag ) !== '' ) {
-                       return true;
-               }
-
-               return false;
-       }
-
-       /**
-        * Get an array of language names, indexed by code.
-        * @param null|string $inLanguage Code of language in which to return the names
-        *   Use self::AUTONYMS for autonyms (native names)
-        * @param string $include One of:
-        *   self::ALL all available languages
-        *   self::DEFINED only if the language is defined in MediaWiki or wgExtraLanguageNames
-        *     (default)
-        *   self::SUPPORTED only if the language is in self::DEFINED *and* has a message file
-        * @return array Language code => language name (sorted by key)
-        */
-       public function getLanguageNames( $inLanguage = self::AUTONYMS, $include = self::DEFINED ) {
-               $cacheKey = $inLanguage === self::AUTONYMS ? 'null' : $inLanguage;
-               $cacheKey .= ":$include";
-               if ( !$this->languageNameCache ) {
-                       $this->languageNameCache = new HashBagOStuff( [ 'maxKeys' => 20 ] );
-               }
-
-               $ret = $this->languageNameCache->get( $cacheKey );
-               if ( !$ret ) {
-                       $ret = $this->getLanguageNamesUncached( $inLanguage, $include );
-                       $this->languageNameCache->set( $cacheKey, $ret );
-               }
-               return $ret;
-       }
-
-       /**
-        * Uncached helper for getLanguageNames
-        * @param null|string $inLanguage As getLanguageNames
-        * @param string $include As getLanguageNames
-        * @return array Language code => language name (sorted by key)
-        */
-       private function getLanguageNamesUncached( $inLanguage, $include ) {
-               // If passed an invalid language code to use, fallback to en
-               if ( $inLanguage !== self::AUTONYMS && !$this->isValidCode( $inLanguage ) ) {
-                       $inLanguage = 'en';
-               }
-
-               $names = [];
-
-               if ( $inLanguage !== self::AUTONYMS ) {
-                       # TODO: also include for self::AUTONYMS, when this code is more efficient
-                       Hooks::run( 'LanguageGetTranslatedLanguageNames', [ &$names, $inLanguage ] );
-               }
-
-               $mwNames = $this->options->get( 'ExtraLanguageNames' ) + Data\Names::$names;
-               if ( $this->options->get( 'UsePigLatinVariant' ) ) {
-                       // Pig Latin (for variant development)
-                       $mwNames['en-x-piglatin'] = 'Igpay Atinlay';
-               }
-
-               foreach ( $mwNames as $mwCode => $mwName ) {
-                       # - Prefer own MediaWiki native name when not using the hook
-                       # - For other names just add if not added through the hook
-                       if ( $mwCode === $inLanguage || !isset( $names[$mwCode] ) ) {
-                               $names[$mwCode] = $mwName;
-                       }
-               }
-
-               if ( $include === self::ALL ) {
-                       ksort( $names );
-                       return $names;
-               }
-
-               $returnMw = [];
-               $coreCodes = array_keys( $mwNames );
-               foreach ( $coreCodes as $coreCode ) {
-                       $returnMw[$coreCode] = $names[$coreCode];
-               }
-
-               if ( $include === self::SUPPORTED ) {
-                       $namesMwFile = [];
-                       # We do this using a foreach over the codes instead of a directory loop so that messages
-                       # files in extensions will work correctly.
-                       foreach ( $returnMw as $code => $value ) {
-                               if ( is_readable( $this->getMessagesFileName( $code ) ) ||
-                                       is_readable( $this->getJsonMessagesFileName( $code ) )
-                               ) {
-                                       $namesMwFile[$code] = $names[$code];
-                               }
-                       }
-
-                       ksort( $namesMwFile );
-                       return $namesMwFile;
-               }
-
-               ksort( $returnMw );
-               # self::DEFINED option; default if it's not one of the other two options
-               # (self::ALL/self::SUPPORTED)
-               return $returnMw;
-       }
-
-       /**
-        * @param string $code The code of the language for which to get the name
-        * @param null|string $inLanguage Code of language in which to return the name (self::AUTONYMS
-        *   for autonyms)
-        * @param string $include See getLanguageNames(), except this defaults to self::ALL instead of
-        *   self::DEFINED
-        * @return string Language name or empty
-        * @since 1.20
-        */
-       public function getLanguageName( $code, $inLanguage = self::AUTONYMS, $include = self::ALL ) {
-               $code = strtolower( $code );
-               $array = $this->getLanguageNames( $inLanguage, $include );
-               return $array[$code] ?? '';
-       }
-
-       /**
-        * Get the name of a file for a certain language code
-        * @param string $prefix Prepend this to the filename
-        * @param string $code Language code
-        * @param string $suffix Append this to the filename
-        * @throws MWException
-        * @return string $prefix . $mangledCode . $suffix
-        */
-       public function getFileName( $prefix, $code, $suffix = '.php' ) {
-               if ( !$this->isValidBuiltInCode( $code ) ) {
-                       throw new MWException( "Invalid language code \"$code\"" );
-               }
-
-               return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
-       }
-
-       /**
-        * @param string $code
-        * @return string
-        */
-       public function getMessagesFileName( $code ) {
-               global $IP;
-               $file = $this->getFileName( "$IP/languages/messages/Messages", $code, '.php' );
-               Hooks::run( 'Language::getMessagesFileName', [ $code, &$file ] );
-               return $file;
-       }
-
-       /**
-        * @param string $code
-        * @return string
-        * @throws MWException
-        */
-       public function getJsonMessagesFileName( $code ) {
-               global $IP;
-
-               if ( !$this->isValidBuiltInCode( $code ) ) {
-                       throw new MWException( "Invalid language code \"$code\"" );
-               }
-
-               return "$IP/languages/i18n/$code.json";
-       }
-}
index a1ddfd0..575fbe7 100644 (file)
@@ -53,6 +53,12 @@ class Xhprof {
                if ( self::isEnabled() ) {
                        throw new Exception( 'Profiling is already enabled.' );
                }
+
+               $args = [ $flags ];
+               if ( $options ) {
+                       $args[] = $options;
+               }
+
                self::$enabled = true;
                self::callAny(
                        [
@@ -60,7 +66,7 @@ class Xhprof {
                                'tideways_enable',
                                'tideways_xhprof_enable'
                        ],
-                       [ $flags, $options ]
+                       $args
                );
        }
 
index e6a49c7..c05dc28 100644 (file)
@@ -795,8 +795,16 @@ class FSFileBackend extends FileBackendStore {
         * Listen for E_WARNING errors and track whether any happen
         */
        protected function trapWarnings() {
-               $this->hadWarningErrors[] = false; // push to stack
-               set_error_handler( [ $this, 'handleWarning' ], E_WARNING );
+               // push to stack
+               $this->hadWarningErrors[] = false;
+               set_error_handler( function ( $errno, $errstr ) {
+                       // more detailed error logging
+                       $this->logger->error( $errstr );
+                       $this->hadWarningErrors[count( $this->hadWarningErrors ) - 1] = true;
+
+                       // suppress from PHP handler
+                       return true;
+               }, E_WARNING );
        }
 
        /**
@@ -805,20 +813,9 @@ class FSFileBackend extends FileBackendStore {
         * @return bool
         */
        protected function untrapWarnings() {
-               restore_error_handler(); // restore previous handler
-               return array_pop( $this->hadWarningErrors ); // pop from stack
-       }
-
-       /**
-        * @param int $errno
-        * @param string $errstr
-        * @return bool
-        * @private
-        */
-       public function handleWarning( $errno, $errstr ) {
-               $this->logger->error( $errstr ); // more detailed error logging
-               $this->hadWarningErrors[count( $this->hadWarningErrors ) - 1] = true;
-
-               return true; // suppress from PHP handler
+               // restore previous handler
+               restore_error_handler();
+               // pop from stack
+               return array_pop( $this->hadWarningErrors );
        }
 }
index b187fe5..4cacb7a 100644 (file)
@@ -420,7 +420,7 @@ abstract class FileBackend implements LoggerAwareInterface {
         *
         * The StatusValue will be "OK" unless:
         *   - a) unexpected operation errors occurred (network partitions, disk full...)
-        *   - b) significant operation errors occurred and 'force' was not set
+        *   - b) predicted operation errors occurred and 'force' was not set
         *
         * @param array $ops List of operations to execute in order
         * @param array $opts Batch operation options
index f92d5fa..27ad870 100644 (file)
@@ -88,7 +88,7 @@ class FileBackendMultiWrite extends FileBackend {
         *                      any checks from "syncChecks" are still synchronous.
         *
         * @param array $config
-        * @throws FileBackendError
+        * @throws LogicException
         */
        public function __construct( array $config ) {
                parent::__construct( $config );
@@ -178,9 +178,8 @@ class FileBackendMultiWrite extends FileBackend {
                $masterStatus = $mbe->doOperations( $realOps, $opts );
                $status->merge( $masterStatus );
                // Propagate the operations to the clone backends if there were no unexpected errors
-               // and if there were either no expected errors or if the 'force' option was used.
-               // However, if nothing succeeded at all, then don't replicate any of the operations.
-               // If $ops only had one operation, this might avoid backend sync inconsistencies.
+               // and everything didn't fail due to predicted errors. If $ops only had one operation,
+               // this might avoid backend sync inconsistencies.
                if ( $masterStatus->isOK() && $masterStatus->successCount > 0 ) {
                        foreach ( $this->backends as $index => $backend ) {
                                if ( $index === $this->masterIndex ) {
@@ -271,7 +270,10 @@ class FileBackendMultiWrite extends FileBackend {
                                                        continue;
                                                }
                                        }
-                                       if ( ( $this->syncChecks & self::CHECK_SHA1 ) && $cBackend->getFileSha1Base36( $cParams ) !== $mSha1 ) { // wrong SHA1
+                                       if (
+                                               ( $this->syncChecks & self::CHECK_SHA1 ) &&
+                                               $cBackend->getFileSha1Base36( $cParams ) !== $mSha1
+                                       ) { // wrong SHA1
                                                $status->fatal( 'backend-fail-synced', $path );
                                                continue;
                                        }
index e2a25fc..aa95ee4 100644 (file)
@@ -20,6 +20,7 @@
  * @file
  * @ingroup FileBackend
  */
+use Wikimedia\AtEase\AtEase;
 use Wikimedia\Timestamp\ConvertibleTimestamp;
 
 /**
@@ -376,9 +377,9 @@ abstract class FileBackendStore extends FileBackend {
                unset( $params['latest'] ); // sanity
 
                // Check that the specified temp file is valid...
-               Wikimedia\suppressWarnings();
+               AtEase::suppressWarnings();
                $ok = ( is_file( $tmpPath ) && filesize( $tmpPath ) == 0 );
-               Wikimedia\restoreWarnings();
+               AtEase::restoreWarnings();
                if ( !$ok ) { // not present or not empty
                        $status->fatal( 'backend-fail-opentemp', $tmpPath );
 
@@ -714,9 +715,9 @@ abstract class FileBackendStore extends FileBackend {
        protected function doGetFileContentsMulti( array $params ) {
                $contents = [];
                foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
-                       Wikimedia\suppressWarnings();
+                       AtEase::suppressWarnings();
                        $contents[$path] = $fsFile ? file_get_contents( $fsFile->getPath() ) : false;
-                       Wikimedia\restoreWarnings();
+                       AtEase::restoreWarnings();
                }
 
                return $contents;
index 540961e..bbcda08 100644 (file)
@@ -46,7 +46,7 @@ class FileOpBatch {
         *
         * The resulting StatusValue will be "OK" unless:
         *   - a) unexpected operation errors occurred (network partitions, disk full...)
-        *   - b) significant operation errors occurred and 'force' was not set
+        *   - b) predicted operation errors occurred and 'force' was not set
         *
         * @param FileOp[] $performOps List of FileOp operations
         * @param array $opts Batch operation options
index 7a11aeb..653a102 100644 (file)
@@ -19,6 +19,8 @@
  *
  * @file
  */
+
+use Wikimedia\AtEase\AtEase;
 use Wikimedia\Timestamp\ConvertibleTimestamp;
 
 /**
@@ -100,9 +102,9 @@ class HTTPFileStreamer {
                                is_int( $header ) ? HttpStatus::header( $header ) : header( $header );
                        };
 
-               Wikimedia\suppressWarnings();
+               AtEase::suppressWarnings();
                $info = stat( $this->path );
-               Wikimedia\restoreWarnings();
+               AtEase::restoreWarnings();
 
                if ( !is_array( $info ) ) {
                        if ( $sendErrors ) {
index 3932f34..f0cbf3b 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup FileBackend
  */
 
+use Wikimedia\AtEase\AtEase;
+
 /**
  * Simulation of a backend storage in memory.
  *
@@ -70,9 +72,9 @@ class MemoryFileBackend extends FileBackendStore {
                        return $status;
                }
 
-               Wikimedia\suppressWarnings();
+               AtEase::suppressWarnings();
                $data = file_get_contents( $params['src'] );
-               Wikimedia\restoreWarnings();
+               AtEase::restoreWarnings();
                if ( $data === false ) { // source doesn't exist?
                        $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
 
index d6a0c6c..afd1688 100644 (file)
@@ -22,6 +22,8 @@
  * @author Russ Nelson
  */
 
+use Wikimedia\AtEase\AtEase;
+
 /**
  * @brief Class for an OpenStack Swift (or Ceph RGW) based file backend.
  *
@@ -326,9 +328,9 @@ class SwiftFileBackend extends FileBackendStore {
                        return $status;
                }
 
-               Wikimedia\suppressWarnings();
+               AtEase::suppressWarnings();
                $sha1Hash = sha1_file( $params['src'] );
-               Wikimedia\restoreWarnings();
+               AtEase::restoreWarnings();
                if ( $sha1Hash === false ) { // source doesn't exist?
                        $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
 
index 206048b..961fdb9 100644 (file)
@@ -77,7 +77,7 @@ abstract class FileOp {
         * @param FileBackendStore $backend
         * @param array $params
         * @param LoggerInterface $logger PSR logger instance
-        * @throws FileBackendError
+        * @throws InvalidArgumentException
         */
        final public function __construct(
                FileBackendStore $backend, array $params, LoggerInterface $logger
index 69ae47f..5783a82 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup FileBackend
  */
 
+use Wikimedia\AtEase\AtEase;
+
 /**
  * Store a file into the backend from a file on the file system.
  * Parameters for this operation are outlined in FileBackend::doOperations().
@@ -77,9 +79,9 @@ class StoreFileOp extends FileOp {
        }
 
        protected function getSourceSha1Base36() {
-               Wikimedia\suppressWarnings();
+               AtEase::suppressWarnings();
                $hash = sha1_file( $this->params['src'] );
-               Wikimedia\restoreWarnings();
+               AtEase::restoreWarnings();
                if ( $hash !== false ) {
                        $hash = Wikimedia\base_convert( $hash, 16, 36, 31 );
                }
index 553c9aa..1937e37 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup FileBackend
  */
 
+use Wikimedia\AtEase\AtEase;
+
 /**
  * Class representing a non-directory file on the file system
  *
@@ -75,9 +77,9 @@ class FSFile {
         * @return string|bool TS_MW timestamp or false on failure
         */
        public function getTimestamp() {
-               Wikimedia\suppressWarnings();
+               AtEase::suppressWarnings();
                $timestamp = filemtime( $this->path );
-               Wikimedia\restoreWarnings();
+               AtEase::restoreWarnings();
                if ( $timestamp !== false ) {
                        $timestamp = wfTimestamp( TS_MW, $timestamp );
                }
@@ -168,9 +170,9 @@ class FSFile {
                        return $this->sha1Base36;
                }
 
-               Wikimedia\suppressWarnings();
+               AtEase::suppressWarnings();
                $this->sha1Base36 = sha1_file( $this->path );
-               Wikimedia\restoreWarnings();
+               AtEase::restoreWarnings();
 
                if ( $this->sha1Base36 !== false ) {
                        $this->sha1Base36 = Wikimedia\base_convert( $this->sha1Base36, 16, 36, 31 );
index 378dbe7..46fa5e1 100644 (file)
@@ -24,6 +24,8 @@ use MediaWiki\FileBackend\FSFile\TempFSFileFactory;
  * @ingroup FileBackend
  */
 
+use Wikimedia\AtEase\AtEase;
+
 /**
  * This class is used to hold the location and do limited manipulation
  * of files stored temporarily (this will be whatever wfTempDir() returns)
@@ -38,7 +40,9 @@ class TempFSFile extends FSFile {
        protected static $pathsCollect = null;
 
        /**
-        * Do not call directly. Use TempFSFileFactory.
+        * Do not call directly. Use TempFSFileFactory
+        *
+        * @param string $path
         */
        public function __construct( $path ) {
                parent::__construct( $path );
@@ -110,9 +114,9 @@ class TempFSFile extends FSFile {
         */
        public function purge() {
                $this->canDelete = false; // done
-               Wikimedia\suppressWarnings();
+               AtEase::suppressWarnings();
                $ok = unlink( $this->path );
-               Wikimedia\restoreWarnings();
+               AtEase::restoreWarnings();
 
                unset( self::$pathsCollect[$this->path] );
 
@@ -172,9 +176,9 @@ class TempFSFile extends FSFile {
         */
        public static function purgeAllOnShutdown() {
                foreach ( self::$pathsCollect as $path => $unused ) {
-                       Wikimedia\suppressWarnings();
+                       AtEase::suppressWarnings();
                        unlink( $path );
-                       Wikimedia\restoreWarnings();
+                       AtEase::restoreWarnings();
                }
        }
 
index 65cc54b..f25287f 100644 (file)
@@ -422,7 +422,7 @@ class XmlTypeCheck {
         *  * Only contains entity definitions (e.g. No <!ATLIST )
         *  * Entity definitions are not "system" entities
         *  * Entity definitions are not "parameter" (i.e. %) entities
-        *  * Entity definitions do not reference other entites except &amp;
+        *  * Entity definitions do not reference other entities except &amp;
         *    and quotes. Entity aliases (where the entity contains only
         *    another entity are allowed)
         *  * Entity references aren't overly long (>255 bytes).
index aa83b1f..e9bd7be 100644 (file)
@@ -87,11 +87,11 @@ class APCBagOStuff extends MediumSpecificBagOStuff {
                return true;
        }
 
-       public function incr( $key, $value = 1 ) {
+       public function incr( $key, $value = 1, $flags = 0 ) {
                return apc_inc( $key . self::KEY_SUFFIX, $value );
        }
 
-       public function decr( $key, $value = 1 ) {
+       public function decr( $key, $value = 1, $flags = 0 ) {
                return apc_dec( $key . self::KEY_SUFFIX, $value );
        }
 }
index 80383d1..2b26500 100644 (file)
@@ -85,7 +85,7 @@ class APCUBagOStuff extends MediumSpecificBagOStuff {
                return true;
        }
 
-       public function incr( $key, $value = 1 ) {
+       public function incr( $key, $value = 1, $flags = 0 ) {
                // https://github.com/krakjoe/apcu/issues/166
                if ( apcu_exists( $key . self::KEY_SUFFIX ) ) {
                        return apcu_inc( $key . self::KEY_SUFFIX, $value );
@@ -94,7 +94,7 @@ class APCUBagOStuff extends MediumSpecificBagOStuff {
                }
        }
 
-       public function decr( $key, $value = 1 ) {
+       public function decr( $key, $value = 1, $flags = 0 ) {
                // https://github.com/krakjoe/apcu/issues/166
                if ( apcu_exists( $key . self::KEY_SUFFIX ) ) {
                        return apcu_dec( $key . self::KEY_SUFFIX, $value );
index da60c01..42da5f0 100644 (file)
@@ -362,31 +362,36 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
         * Increase stored value of $key by $value while preserving its TTL
         * @param string $key Key to increase
         * @param int $value Value to add to $key (default: 1) [optional]
+        * @param int $flags Bit field of class WRITE_* constants [optional]
         * @return int|bool New value or false on failure
         */
-       abstract public function incr( $key, $value = 1 );
+       abstract public function incr( $key, $value = 1, $flags = 0 );
 
        /**
         * Decrease stored value of $key by $value while preserving its TTL
         * @param string $key
         * @param int $value Value to subtract from $key (default: 1) [optional]
+        * @param int $flags Bit field of class WRITE_* constants [optional]
         * @return int|bool New value or false on failure
         */
-       abstract public function decr( $key, $value = 1 );
+       abstract public function decr( $key, $value = 1, $flags = 0 );
 
        /**
-        * Increase stored value of $key by $value while preserving its TTL
+        * Increase the value of the given key (no TTL change) if it exists or create it otherwise
         *
-        * This will create the key with value $init and TTL $ttl instead if not present
+        * This will create the key with the value $init and TTL $ttl instead if not present.
+        * Callers should make sure that both ($init - $value) and $ttl are invariants for all
+        * operations to any given key. The value of $init should be at least that of $value.
         *
-        * @param string $key
-        * @param int $ttl
-        * @param int $value
-        * @param int $init
+        * @param string $key Key built via makeKey() or makeGlobalKey()
+        * @param int $exptime Time-to-live (in seconds) or a UNIX timestamp expiration
+        * @param int $value Amount to increase the key value by [default: 1]
+        * @param int|null $init Value to initialize the key to if it does not exist [default: $value]
+        * @param int $flags Bit field of class WRITE_* constants [optional]
         * @return int|bool New value or false on failure
         * @since 1.24
         */
-       abstract public function incrWithInit( $key, $ttl, $value = 1, $init = 1 );
+       abstract public function incrWithInit( $key, $exptime, $value = 1, $init = null, $flags = 0 );
 
        /**
         * Get the "last error" registered; clearLastError() should be called manually
@@ -478,6 +483,16 @@ abstract class BagOStuff implements IExpiringStore, IStoreKeyEncoder, LoggerAwar
                return INF;
        }
 
+       /**
+        * @param int $field
+        * @param int $flags
+        * @return bool
+        * @since 1.34
+        */
+       final protected function fieldHasFlags( $field, $flags ) {
+               return ( ( $field & $flags ) === $flags );
+       }
+
        /**
         * Merge the flag maps of one or more BagOStuff objects into a "lowest common denominator" map
         *
index 9fa9a89..8b4c9c6 100644 (file)
@@ -87,7 +87,8 @@ class CachedBagOStuff extends BagOStuff {
 
        public function set( $key, $value, $exptime = 0, $flags = 0 ) {
                $this->procCache->set( $key, $value, $exptime, $flags );
-               if ( ( $flags & self::WRITE_CACHE_ONLY ) != self::WRITE_CACHE_ONLY ) {
+
+               if ( !$this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) {
                        $this->backend->set( $key, $value, $exptime, $flags );
                }
 
@@ -96,7 +97,8 @@ class CachedBagOStuff extends BagOStuff {
 
        public function delete( $key, $flags = 0 ) {
                $this->procCache->delete( $key, $flags );
-               if ( ( $flags & self::WRITE_CACHE_ONLY ) != self::WRITE_CACHE_ONLY ) {
+
+               if ( !$this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) {
                        $this->backend->delete( $key, $flags );
                }
 
@@ -166,7 +168,8 @@ class CachedBagOStuff extends BagOStuff {
 
        public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
                $this->procCache->setMulti( $data, $exptime, $flags );
-               if ( ( $flags & self::WRITE_CACHE_ONLY ) != self::WRITE_CACHE_ONLY ) {
+
+               if ( !$this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) {
                        return $this->backend->setMulti( $data, $exptime, $flags );
                }
 
@@ -175,7 +178,8 @@ class CachedBagOStuff extends BagOStuff {
 
        public function deleteMulti( array $keys, $flags = 0 ) {
                $this->procCache->deleteMulti( $keys, $flags );
-               if ( ( $flags & self::WRITE_CACHE_ONLY ) != self::WRITE_CACHE_ONLY ) {
+
+               if ( !$this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) {
                        return $this->backend->deleteMulti( $keys, $flags );
                }
 
@@ -184,29 +188,30 @@ class CachedBagOStuff extends BagOStuff {
 
        public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
                $this->procCache->changeTTLMulti( $keys, $exptime, $flags );
-               if ( ( $flags & self::WRITE_CACHE_ONLY ) != self::WRITE_CACHE_ONLY ) {
+
+               if ( !$this->fieldHasFlags( $flags, self::WRITE_CACHE_ONLY ) ) {
                        return $this->backend->changeTTLMulti( $keys, $exptime, $flags );
                }
 
                return true;
        }
 
-       public function incr( $key, $value = 1 ) {
+       public function incr( $key, $value = 1, $flags = 0 ) {
                $this->procCache->delete( $key );
 
-               return $this->backend->incr( $key, $value );
+               return $this->backend->incr( $key, $value, $flags );
        }
 
-       public function decr( $key, $value = 1 ) {
+       public function decr( $key, $value = 1, $flags = 0 ) {
                $this->procCache->delete( $key );
 
-               return $this->backend->decr( $key, $value );
+               return $this->backend->decr( $key, $value, $flags );
        }
 
-       public function incrWithInit( $key, $ttl, $value = 1, $init = 1 ) {
+       public function incrWithInit( $key, $exptime, $value = 1, $init = null, $flags = 0 ) {
                $this->procCache->delete( $key );
 
-               return $this->backend->incrWithInit( $key, $ttl, $value, $init );
+               return $this->backend->incrWithInit( $key, $exptime, $value, $init, $flags );
        }
 
        public function addBusyCallback( callable $workCallback ) {
index b2613b2..9723cad 100644 (file)
@@ -45,11 +45,15 @@ class EmptyBagOStuff extends MediumSpecificBagOStuff {
                return true;
        }
 
-       public function incr( $key, $value = 1 ) {
+       public function incr( $key, $value = 1, $flags = 0 ) {
                return false;
        }
 
-       public function incrWithInit( $key, $ttl, $value = 1, $init = 1 ) {
+       public function decr( $key, $value = 1, $flags = 0 ) {
+               return false;
+       }
+
+       public function incrWithInit( $key, $exptime, $value = 1, $init = null, $flags = 0 ) {
                return false; // faster
        }
 
index b4087be..6d0940b 100644 (file)
@@ -108,10 +108,10 @@ class HashBagOStuff extends MediumSpecificBagOStuff {
                return true;
        }
 
-       public function incr( $key, $value = 1 ) {
+       public function incr( $key, $value = 1, $flags = 0 ) {
                $n = $this->get( $key );
                if ( $this->isInteger( $n ) ) {
-                       $n = max( $n + intval( $value ), 0 );
+                       $n = max( $n + (int)$value, 0 );
                        $this->bag[$key][self::KEY_VAL] = $n;
 
                        return $n;
@@ -120,6 +120,10 @@ class HashBagOStuff extends MediumSpecificBagOStuff {
                return false;
        }
 
+       public function decr( $key, $value = 1, $flags = 0 ) {
+               return $this->incr( $key, -$value, $flags );
+       }
+
        /**
         * Clear all values in cache
         */
index 329e600..9d36187 100644 (file)
@@ -188,7 +188,7 @@ abstract class MediumSpecificBagOStuff extends BagOStuff {
         * @return bool True if the item was deleted or not found, false on failure
         */
        public function delete( $key, $flags = 0 ) {
-               if ( ( $flags & self::WRITE_PRUNE_SEGMENTS ) != self::WRITE_PRUNE_SEGMENTS ) {
+               if ( !$this->fieldHasFlags( $flags, self::WRITE_PRUNE_SEGMENTS ) ) {
                        return $this->doDelete( $key, $flags );
                }
 
@@ -208,7 +208,7 @@ abstract class MediumSpecificBagOStuff extends BagOStuff {
                        $mainValue->{SerializedValueContainer::SEGMENTED_HASHES}
                );
 
-               return $this->deleteMulti( $orderedKeys, $flags );
+               return $this->deleteMulti( $orderedKeys, $flags & ~self::WRITE_PRUNE_SEGMENTS );
        }
 
        /**
@@ -269,12 +269,12 @@ abstract class MediumSpecificBagOStuff extends BagOStuff {
        final protected function mergeViaCas( $key, callable $callback, $exptime, $attempts, $flags ) {
                $attemptsLeft = $attempts;
                do {
-                       $casToken = null; // passed by reference
+                       $token = null; // passed by reference
                        // Get the old value and CAS token from cache
                        $this->clearLastError();
                        $currentValue = $this->resolveSegments(
                                $key,
-                               $this->doGet( $key, self::READ_LATEST, $casToken )
+                               $this->doGet( $key, $flags, $token )
                        );
                        if ( $this->getLastError() ) {
                                // Don't spam slow retries due to network problems (retry only on races)
@@ -293,7 +293,7 @@ abstract class MediumSpecificBagOStuff extends BagOStuff {
                        unset( $currentValue ); // free RAM in case the value is large
 
                        $this->clearLastError();
-                       if ( $value === false ) {
+                       if ( $value === false || $exptime < 0 ) {
                                $success = true; // do nothing
                        } elseif ( $valueMatchesOldValue && $attemptsLeft !== $attempts ) {
                                $success = true; // recently set by another thread to the same value
@@ -302,7 +302,7 @@ abstract class MediumSpecificBagOStuff extends BagOStuff {
                                $success = $this->add( $key, $value, $exptime, $flags );
                        } else {
                                // Try to update the key, failing if it gets changed in the meantime
-                               $success = $this->cas( $casToken, $key, $value, $exptime, $flags );
+                               $success = $this->cas( $token, $key, $value, $exptime, $flags );
                        }
                        if ( $this->getLastError() ) {
                                // Don't spam slow retries due to network problems (retry only on races)
@@ -598,9 +598,10 @@ abstract class MediumSpecificBagOStuff extends BagOStuff {
         * @since 1.24
         */
        public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
-               if ( ( $flags & self::WRITE_ALLOW_SEGMENTS ) === self::WRITE_ALLOW_SEGMENTS ) {
+               if ( $this->fieldHasFlags( $flags, self::WRITE_ALLOW_SEGMENTS ) ) {
                        throw new InvalidArgumentException( __METHOD__ . ' got WRITE_ALLOW_SEGMENTS' );
                }
+
                return $this->doSetMulti( $data, $exptime, $flags );
        }
 
@@ -615,6 +616,7 @@ abstract class MediumSpecificBagOStuff extends BagOStuff {
                foreach ( $data as $key => $value ) {
                        $res = $this->doSet( $key, $value, $exptime, $flags ) && $res;
                }
+
                return $res;
        }
 
@@ -629,9 +631,10 @@ abstract class MediumSpecificBagOStuff extends BagOStuff {
         * @since 1.33
         */
        public function deleteMulti( array $keys, $flags = 0 ) {
-               if ( ( $flags & self::WRITE_ALLOW_SEGMENTS ) === self::WRITE_ALLOW_SEGMENTS ) {
-                       throw new InvalidArgumentException( __METHOD__ . ' got WRITE_ALLOW_SEGMENTS' );
+               if ( $this->fieldHasFlags( $flags, self::WRITE_PRUNE_SEGMENTS ) ) {
+                       throw new InvalidArgumentException( __METHOD__ . ' got WRITE_PRUNE_SEGMENTS' );
                }
+
                return $this->doDeleteMulti( $keys, $flags );
        }
 
@@ -668,37 +671,16 @@ abstract class MediumSpecificBagOStuff extends BagOStuff {
                return $res;
        }
 
-       /**
-        * Decrease stored value of $key by $value while preserving its TTL
-        * @param string $key
-        * @param int $value Value to subtract from $key (default: 1) [optional]
-        * @return int|bool New value or false on failure
-        */
-       public function decr( $key, $value = 1 ) {
-               return $this->incr( $key, -$value );
-       }
-
-       /**
-        * Increase stored value of $key by $value while preserving its TTL
-        *
-        * This will create the key with value $init and TTL $ttl instead if not present
-        *
-        * @param string $key
-        * @param int $ttl
-        * @param int $value
-        * @param int $init
-        * @return int|bool New value or false on failure
-        * @since 1.24
-        */
-       public function incrWithInit( $key, $ttl, $value = 1, $init = 1 ) {
+       public function incrWithInit( $key, $exptime, $value = 1, $init = null, $flags = 0 ) {
+               $init = is_int( $init ) ? $init : $value;
                $this->clearLastError();
-               $newValue = $this->incr( $key, $value );
+               $newValue = $this->incr( $key, $value, $flags );
                if ( $newValue === false && !$this->getLastError() ) {
                        // No key set; initialize
-                       $newValue = $this->add( $key, (int)$init, $ttl ) ? $init : false;
+                       $newValue = $this->add( $key, (int)$init, $exptime, $flags ) ? $init : false;
                        if ( $newValue === false && !$this->getLastError() ) {
                                // Raced out initializing; increment
-                               $newValue = $this->incr( $key, $value );
+                               $newValue = $this->incr( $key, $value, $flags );
                        }
                }
 
@@ -768,26 +750,6 @@ abstract class MediumSpecificBagOStuff extends BagOStuff {
                $this->lastError = $err;
        }
 
-       /**
-        * Let a callback be run to avoid wasting time on special blocking calls
-        *
-        * The callbacks may or may not be called ever, in any particular order.
-        * They are likely to be invoked when something WRITE_SYNC is used used.
-        * They should follow a caching pattern as shown below, so that any code
-        * using the work will get it's result no matter what happens.
-        * @code
-        *     $result = null;
-        *     $workCallback = function () use ( &$result ) {
-        *         if ( !$result ) {
-        *             $result = ....
-        *         }
-        *         return $result;
-        *     }
-        * @endcode
-        *
-        * @param callable $workCallback
-        * @since 1.28
-        */
        final public function addBusyCallback( callable $workCallback ) {
                $this->busyCallbacks[] = $workCallback;
        }
@@ -807,7 +769,7 @@ abstract class MediumSpecificBagOStuff extends BagOStuff {
                $usable = true;
 
                if (
-                       ( $flags & self::WRITE_ALLOW_SEGMENTS ) === self::WRITE_ALLOW_SEGMENTS &&
+                       $this->fieldHasFlags( $flags, self::WRITE_ALLOW_SEGMENTS ) &&
                        !is_int( $value ) && // avoid breaking incr()/decr()
                        is_finite( $this->segmentationSize )
                ) {
@@ -919,14 +881,6 @@ abstract class MediumSpecificBagOStuff extends BagOStuff {
                return ( $value === (string)$integer );
        }
 
-       /**
-        * Construct a cache key.
-        *
-        * @param string $keyspace
-        * @param array $args
-        * @return string Colon-delimited list of $keyspace followed by escaped components of $args
-        * @since 1.27
-        */
        public function makeKeyInternal( $keyspace, $args ) {
                $key = $keyspace;
                foreach ( $args as $arg ) {
@@ -968,18 +922,10 @@ abstract class MediumSpecificBagOStuff extends BagOStuff {
                return $this->attrMap[$flag] ?? self::QOS_UNKNOWN;
        }
 
-       /**
-        * @return int|float The chunk size, in bytes, of segmented objects (INF for no limit)
-        * @since 1.34
-        */
        public function getSegmentationSize() {
                return $this->segmentationSize;
        }
 
-       /**
-        * @return int|float Maximum total segmented object size in bytes (INF for no limit)
-        * @since 1.34
-        */
        public function getSegmentedValueMaxSize() {
                return $this->segmentedValueMaxSize;
        }
diff --git a/includes/libs/objectcache/MemcachedClient.php b/includes/libs/objectcache/MemcachedClient.php
deleted file mode 100644 (file)
index 2c40854..0000000
+++ /dev/null
@@ -1,1311 +0,0 @@
-<?php
-// phpcs:ignoreFile -- It's an external lib and it isn't. Let's not bother.
-/**
- * Memcached client for PHP.
- *
- * +---------------------------------------------------------------------------+
- * | memcached client, PHP                                                     |
- * +---------------------------------------------------------------------------+
- * | Copyright (c) 2003 Ryan T. Dean <rtdean@cytherianage.net>                 |
- * | All rights reserved.                                                      |
- * |                                                                           |
- * | Redistribution and use in source and binary forms, with or without        |
- * | modification, are permitted provided that the following conditions        |
- * | are met:                                                                  |
- * |                                                                           |
- * | 1. Redistributions of source code must retain the above copyright         |
- * |    notice, this list of conditions and the following disclaimer.          |
- * | 2. Redistributions in binary form must reproduce the above copyright      |
- * |    notice, this list of conditions and the following disclaimer in the    |
- * |    documentation and/or other materials provided with the distribution.   |
- * |                                                                           |
- * | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR      |
- * | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
- * | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.   |
- * | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,          |
- * | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT  |
- * | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
- * | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY     |
- * | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT       |
- * | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF  |
- * | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.         |
- * +---------------------------------------------------------------------------+
- * | Author: Ryan T. Dean <rtdean@cytherianage.net>                            |
- * | Heavily influenced by the Perl memcached client by Brad Fitzpatrick.      |
- * |   Permission granted by Brad Fitzpatrick for relicense of ported Perl     |
- * |   client logic under 2-clause BSD license.                                |
- * +---------------------------------------------------------------------------+
- *
- * @file
- * $TCAnet$
- */
-
-/**
- * This is a PHP client for memcached - a distributed memory cache daemon.
- *
- * More information is available at http://www.danga.com/memcached/
- *
- * Usage example:
- *
- *     $mc = new MemcachedClient(array(
- *         'servers' => array(
- *             '127.0.0.1:10000',
- *             array( '192.0.0.1:10010', 2 ),
- *             '127.0.0.1:10020'
- *         ),
- *         'debug'   => false,
- *         'compress_threshold' => 10240,
- *         'persistent' => true
- *     ));
- *
- *     $mc->add( 'key', array( 'some', 'array' ) );
- *     $mc->replace( 'key', 'some random string' );
- *     $val = $mc->get( 'key' );
- *
- * @author Ryan T. Dean <rtdean@cytherianage.net>
- * @version 0.1.2
- */
-
-use Psr\Log\LoggerInterface;
-use Psr\Log\NullLogger;
-
-// {{{ class MemcachedClient
-/**
- * memcached client class implemented using (p)fsockopen()
- *
- * @author  Ryan T. Dean <rtdean@cytherianage.net>
- * @ingroup Cache
- */
-class MemcachedClient {
-       // {{{ properties
-       // {{{ public
-
-       // {{{ constants
-       // {{{ flags
-
-       /**
-        * Flag: indicates data is serialized
-        */
-       const SERIALIZED = 1;
-
-       /**
-        * Flag: indicates data is compressed
-        */
-       const COMPRESSED = 2;
-
-       /**
-        * Flag: indicates data is an integer
-        */
-       const INTVAL = 4;
-
-       // }}}
-
-       /**
-        * Minimum savings to store data compressed
-        */
-       const COMPRESSION_SAVINGS = 0.20;
-
-       // }}}
-
-       /**
-        * Command statistics
-        *
-        * @var array
-        * @access public
-        */
-       public $stats;
-
-       // }}}
-       // {{{ private
-
-       /**
-        * Cached Sockets that are connected
-        *
-        * @var array
-        * @access private
-        */
-       public $_cache_sock;
-
-       /**
-        * Current debug status; 0 - none to 9 - profiling
-        *
-        * @var bool
-        * @access private
-        */
-       public $_debug;
-
-       /**
-        * Dead hosts, assoc array, 'host'=>'unixtime when ok to check again'
-        *
-        * @var array
-        * @access private
-        */
-       public $_host_dead;
-
-       /**
-        * Is compression available?
-        *
-        * @var bool
-        * @access private
-        */
-       public $_have_zlib;
-
-       /**
-        * Do we want to use compression?
-        *
-        * @var bool
-        * @access private
-        */
-       public $_compress_enable;
-
-       /**
-        * At how many bytes should we compress?
-        *
-        * @var int
-        * @access private
-        */
-       public $_compress_threshold;
-
-       /**
-        * Are we using persistent links?
-        *
-        * @var bool
-        * @access private
-        */
-       public $_persistent;
-
-       /**
-        * If only using one server; contains ip:port to connect to
-        *
-        * @var string
-        * @access private
-        */
-       public $_single_sock;
-
-       /**
-        * Array containing ip:port or array(ip:port, weight)
-        *
-        * @var array
-        * @access private
-        */
-       public $_servers;
-
-       /**
-        * Our bit buckets
-        *
-        * @var array
-        * @access private
-        */
-       public $_buckets;
-
-       /**
-        * Total # of bit buckets we have
-        *
-        * @var int
-        * @access private
-        */
-       public $_bucketcount;
-
-       /**
-        * # of total servers we have
-        *
-        * @var int
-        * @access private
-        */
-       public $_active;
-
-       /**
-        * Stream timeout in seconds. Applies for example to fread()
-        *
-        * @var int
-        * @access private
-        */
-       public $_timeout_seconds;
-
-       /**
-        * Stream timeout in microseconds
-        *
-        * @var int
-        * @access private
-        */
-       public $_timeout_microseconds;
-
-       /**
-        * Connect timeout in seconds
-        */
-       public $_connect_timeout;
-
-       /**
-        * Number of connection attempts for each server
-        */
-       public $_connect_attempts;
-
-       /**
-        * @var LoggerInterface
-        */
-       private $_logger;
-
-       // }}}
-       // }}}
-       // {{{ methods
-       // {{{ public functions
-       // {{{ memcached()
-
-       /**
-        * Memcache initializer
-        *
-        * @param array $args Associative array of settings
-        */
-       public function __construct( $args ) {
-               $this->set_servers( $args['servers'] ?? array() );
-               $this->_debug = $args['debug'] ?? false;
-               $this->stats = array();
-               $this->_compress_threshold = $args['compress_threshold'] ?? 0;
-               $this->_persistent = $args['persistent'] ?? false;
-               $this->_compress_enable = true;
-               $this->_have_zlib = function_exists( 'gzcompress' );
-
-               $this->_cache_sock = array();
-               $this->_host_dead = array();
-
-               $this->_timeout_seconds = 0;
-               $this->_timeout_microseconds = $args['timeout'] ?? 500000;
-
-               $this->_connect_timeout = $args['connect_timeout'] ?? 0.1;
-               $this->_connect_attempts = 2;
-
-               $this->_logger = $args['logger'] ?? new NullLogger();
-       }
-
-       // }}}
-
-       /**
-        * @param mixed $value
-        * @return string|integer
-        */
-       public function serialize( $value ) {
-               return serialize( $value );
-       }
-
-       /**
-        * @param string $value
-        * @return mixed
-        */
-       public function unserialize( $value ) {
-               return unserialize( $value );
-       }
-
-       // {{{ add()
-
-       /**
-        * Adds a key/value to the memcache server if one isn't already set with
-        * that key
-        *
-        * @param string $key Key to set with data
-        * @param mixed $val Value to store
-        * @param int $exp (optional) Expiration time. This can be a number of seconds
-        * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
-        * longer must be the timestamp of the time at which the mapping should expire. It
-        * is safe to use timestamps in all cases, regardless of expiration
-        * eg: strtotime("+3 hour")
-        *
-        * @return bool
-        */
-       public function add( $key, $val, $exp = 0 ) {
-               return $this->_set( 'add', $key, $val, $exp );
-       }
-
-       // }}}
-       // {{{ decr()
-
-       /**
-        * Decrease a value stored on the memcache server
-        *
-        * @param string $key Key to decrease
-        * @param int $amt (optional) amount to decrease
-        *
-        * @return mixed False on failure, value on success
-        */
-       public function decr( $key, $amt = 1 ) {
-               return $this->_incrdecr( 'decr', $key, $amt );
-       }
-
-       // }}}
-       // {{{ delete()
-
-       /**
-        * Deletes a key from the server, optionally after $time
-        *
-        * @param string $key Key to delete
-        * @param int $time (optional) how long to wait before deleting
-        *
-        * @return bool True on success, false on failure
-        */
-       public function delete( $key, $time = 0 ) {
-               if ( !$this->_active ) {
-                       return false;
-               }
-
-               $sock = $this->get_sock( $key );
-               if ( !is_resource( $sock ) ) {
-                       return false;
-               }
-
-               $key = is_array( $key ) ? $key[1] : $key;
-
-               if ( isset( $this->stats['delete'] ) ) {
-                       $this->stats['delete']++;
-               } else {
-                       $this->stats['delete'] = 1;
-               }
-               $cmd = "delete $key $time\r\n";
-               if ( !$this->_fwrite( $sock, $cmd ) ) {
-                       return false;
-               }
-               $res = $this->_fgets( $sock );
-
-               if ( $this->_debug ) {
-                       $this->_debugprint( sprintf( "MemCache: delete %s (%s)", $key, $res ) );
-               }
-
-               if ( $res == "DELETED" || $res == "NOT_FOUND" ) {
-                       return true;
-               }
-
-               return false;
-       }
-
-       /**
-        * Changes the TTL on a key from the server to $time
-        *
-        * @param string $key
-        * @param int $time TTL in seconds
-        *
-        * @return bool True on success, false on failure
-        */
-       public function touch( $key, $time = 0 ) {
-               if ( !$this->_active ) {
-                       return false;
-               }
-
-               $sock = $this->get_sock( $key );
-               if ( !is_resource( $sock ) ) {
-                       return false;
-               }
-
-               $key = is_array( $key ) ? $key[1] : $key;
-
-               if ( isset( $this->stats['touch'] ) ) {
-                       $this->stats['touch']++;
-               } else {
-                       $this->stats['touch'] = 1;
-               }
-               $cmd = "touch $key $time\r\n";
-               if ( !$this->_fwrite( $sock, $cmd ) ) {
-                       return false;
-               }
-               $res = $this->_fgets( $sock );
-
-               if ( $this->_debug ) {
-                       $this->_debugprint( sprintf( "MemCache: touch %s (%s)", $key, $res ) );
-               }
-
-               if ( $res == "TOUCHED" ) {
-                       return true;
-               }
-
-               return false;
-       }
-
-       // }}}
-       // {{{ disconnect_all()
-
-       /**
-        * Disconnects all connected sockets
-        */
-       public function disconnect_all() {
-               foreach ( $this->_cache_sock as $sock ) {
-                       fclose( $sock );
-               }
-
-               $this->_cache_sock = array();
-       }
-
-       // }}}
-       // {{{ enable_compress()
-
-       /**
-        * Enable / Disable compression
-        *
-        * @param bool $enable True to enable, false to disable
-        */
-       public function enable_compress( $enable ) {
-               $this->_compress_enable = $enable;
-       }
-
-       // }}}
-       // {{{ forget_dead_hosts()
-
-       /**
-        * Forget about all of the dead hosts
-        */
-       public function forget_dead_hosts() {
-               $this->_host_dead = array();
-       }
-
-       // }}}
-       // {{{ get()
-
-       /**
-        * Retrieves the value associated with the key from the memcache server
-        *
-        * @param array|string $key key to retrieve
-        * @param float $casToken [optional]
-        *
-        * @return mixed
-        */
-       public function get( $key, &$casToken = null ) {
-               if ( $this->_debug ) {
-                       $this->_debugprint( "get($key)" );
-               }
-
-               if ( !is_array( $key ) && strval( $key ) === '' ) {
-                       $this->_debugprint( "Skipping key which equals to an empty string" );
-                       return false;
-               }
-
-               if ( !$this->_active ) {
-                       return false;
-               }
-
-               $sock = $this->get_sock( $key );
-
-               if ( !is_resource( $sock ) ) {
-                       return false;
-               }
-
-               $key = is_array( $key ) ? $key[1] : $key;
-               if ( isset( $this->stats['get'] ) ) {
-                       $this->stats['get']++;
-               } else {
-                       $this->stats['get'] = 1;
-               }
-
-               $cmd = "gets $key\r\n";
-               if ( !$this->_fwrite( $sock, $cmd ) ) {
-                       return false;
-               }
-
-               $val = array();
-               $this->_load_items( $sock, $val, $casToken );
-
-               if ( $this->_debug ) {
-                       foreach ( $val as $k => $v ) {
-                               $this->_debugprint(
-                                       sprintf( "MemCache: sock %s got %s", $this->serialize( $sock ), $k ) );
-                       }
-               }
-
-               $value = false;
-               if ( isset( $val[$key] ) ) {
-                       $value = $val[$key];
-               }
-               return $value;
-       }
-
-       // }}}
-       // {{{ get_multi()
-
-       /**
-        * Get multiple keys from the server(s)
-        *
-        * @param array $keys Keys to retrieve
-        *
-        * @return array
-        */
-       public function get_multi( $keys ) {
-               if ( !$this->_active ) {
-                       return array();
-               }
-
-               if ( isset( $this->stats['get_multi'] ) ) {
-                       $this->stats['get_multi']++;
-               } else {
-                       $this->stats['get_multi'] = 1;
-               }
-               $sock_keys = array();
-               $socks = array();
-               foreach ( $keys as $key ) {
-                       $sock = $this->get_sock( $key );
-                       if ( !is_resource( $sock ) ) {
-                               continue;
-                       }
-                       $key = is_array( $key ) ? $key[1] : $key;
-                       if ( !isset( $sock_keys[$sock] ) ) {
-                               $sock_keys[intval( $sock )] = array();
-                               $socks[] = $sock;
-                       }
-                       $sock_keys[intval( $sock )][] = $key;
-               }
-
-               $gather = array();
-               // Send out the requests
-               foreach ( $socks as $sock ) {
-                       $cmd = 'gets';
-                       foreach ( $sock_keys[intval( $sock )] as $key ) {
-                               $cmd .= ' ' . $key;
-                       }
-                       $cmd .= "\r\n";
-
-                       if ( $this->_fwrite( $sock, $cmd ) ) {
-                               $gather[] = $sock;
-                       }
-               }
-
-               // Parse responses
-               $val = array();
-               foreach ( $gather as $sock ) {
-                       $this->_load_items( $sock, $val, $casToken );
-               }
-
-               if ( $this->_debug ) {
-                       foreach ( $val as $k => $v ) {
-                               $this->_debugprint( sprintf( "MemCache: got %s", $k ) );
-                       }
-               }
-
-               return $val;
-       }
-
-       // }}}
-       // {{{ incr()
-
-       /**
-        * Increments $key (optionally) by $amt
-        *
-        * @param string $key Key to increment
-        * @param int $amt (optional) amount to increment
-        *
-        * @return int|null Null if the key does not exist yet (this does NOT
-        * create new mappings if the key does not exist). If the key does
-        * exist, this returns the new value for that key.
-        */
-       public function incr( $key, $amt = 1 ) {
-               return $this->_incrdecr( 'incr', $key, $amt );
-       }
-
-       // }}}
-       // {{{ replace()
-
-       /**
-        * Overwrites an existing value for key; only works if key is already set
-        *
-        * @param string $key Key to set value as
-        * @param mixed $value Value to store
-        * @param int $exp (optional) Expiration time. This can be a number of seconds
-        * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
-        * longer must be the timestamp of the time at which the mapping should expire. It
-        * is safe to use timestamps in all cases, regardless of exipration
-        * eg: strtotime("+3 hour")
-        *
-        * @return bool
-        */
-       public function replace( $key, $value, $exp = 0 ) {
-               return $this->_set( 'replace', $key, $value, $exp );
-       }
-
-       // }}}
-       // {{{ run_command()
-
-       /**
-        * Passes through $cmd to the memcache server connected by $sock; returns
-        * output as an array (null array if no output)
-        *
-        * @param Resource $sock Socket to send command on
-        * @param string $cmd Command to run
-        *
-        * @return array Output array
-        */
-       public function run_command( $sock, $cmd ) {
-               if ( !is_resource( $sock ) ) {
-                       return array();
-               }
-
-               if ( !$this->_fwrite( $sock, $cmd ) ) {
-                       return array();
-               }
-
-               $ret = array();
-               while ( true ) {
-                       $res = $this->_fgets( $sock );
-                       $ret[] = $res;
-                       if ( preg_match( '/^END/', $res ) ) {
-                               break;
-                       }
-                       if ( strlen( $res ) == 0 ) {
-                               break;
-                       }
-               }
-               return $ret;
-       }
-
-       // }}}
-       // {{{ set()
-
-       /**
-        * Unconditionally sets a key to a given value in the memcache.  Returns true
-        * if set successfully.
-        *
-        * @param string $key Key to set value as
-        * @param mixed $value Value to set
-        * @param int $exp (optional) Expiration time. This can be a number of seconds
-        * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
-        * longer must be the timestamp of the time at which the mapping should expire. It
-        * is safe to use timestamps in all cases, regardless of exipration
-        * eg: strtotime("+3 hour")
-        *
-        * @return bool True on success
-        */
-       public function set( $key, $value, $exp = 0 ) {
-               return $this->_set( 'set', $key, $value, $exp );
-       }
-
-       // }}}
-       // {{{ cas()
-
-       /**
-        * Sets a key to a given value in the memcache if the current value still corresponds
-        * to a known, given value.  Returns true if set successfully.
-        *
-        * @param float $casToken Current known value
-        * @param string $key Key to set value as
-        * @param mixed $value Value to set
-        * @param int $exp (optional) Expiration time. This can be a number of seconds
-        * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
-        * longer must be the timestamp of the time at which the mapping should expire. It
-        * is safe to use timestamps in all cases, regardless of exipration
-        * eg: strtotime("+3 hour")
-        *
-        * @return bool True on success
-        */
-       public function cas( $casToken, $key, $value, $exp = 0 ) {
-               return $this->_set( 'cas', $key, $value, $exp, $casToken );
-       }
-
-       // }}}
-       // {{{ set_compress_threshold()
-
-       /**
-        * Set the compression threshold
-        *
-        * @param int $thresh Threshold to compress if larger than
-        */
-       public function set_compress_threshold( $thresh ) {
-               $this->_compress_threshold = $thresh;
-       }
-
-       // }}}
-       // {{{ set_debug()
-
-       /**
-        * Set the debug flag
-        *
-        * @see __construct()
-        * @param bool $dbg True for debugging, false otherwise
-        */
-       public function set_debug( $dbg ) {
-               $this->_debug = $dbg;
-       }
-
-       // }}}
-       // {{{ set_servers()
-
-       /**
-        * Set the server list to distribute key gets and puts between
-        *
-        * @see __construct()
-        * @param array $list Array of servers to connect to
-        */
-       public function set_servers( $list ) {
-               $this->_servers = $list;
-               $this->_active = count( $list );
-               $this->_buckets = null;
-               $this->_bucketcount = 0;
-
-               $this->_single_sock = null;
-               if ( $this->_active == 1 ) {
-                       $this->_single_sock = $this->_servers[0];
-               }
-       }
-
-       /**
-        * Sets the timeout for new connections
-        *
-        * @param int $seconds Number of seconds
-        * @param int $microseconds Number of microseconds
-        */
-       public function set_timeout( $seconds, $microseconds ) {
-               $this->_timeout_seconds = $seconds;
-               $this->_timeout_microseconds = $microseconds;
-       }
-
-       // }}}
-       // }}}
-       // {{{ private methods
-       // {{{ _close_sock()
-
-       /**
-        * Close the specified socket
-        *
-        * @param string $sock Socket to close
-        *
-        * @access private
-        */
-       function _close_sock( $sock ) {
-               $host = array_search( $sock, $this->_cache_sock );
-               fclose( $this->_cache_sock[$host] );
-               unset( $this->_cache_sock[$host] );
-       }
-
-       // }}}
-       // {{{ _connect_sock()
-
-       /**
-        * Connects $sock to $host, timing out after $timeout
-        *
-        * @param int $sock Socket to connect
-        * @param string $host Host:IP to connect to
-        *
-        * @return bool
-        * @access private
-        */
-       function _connect_sock( &$sock, $host ) {
-               list( $ip, $port ) = preg_split( '/:(?=\d)/', $host );
-               $sock = false;
-               $timeout = $this->_connect_timeout;
-               $errno = $errstr = null;
-               for ( $i = 0; !$sock && $i < $this->_connect_attempts; $i++ ) {
-                       Wikimedia\suppressWarnings();
-                       if ( $this->_persistent == 1 ) {
-                               $sock = pfsockopen( $ip, $port, $errno, $errstr, $timeout );
-                       } else {
-                               $sock = fsockopen( $ip, $port, $errno, $errstr, $timeout );
-                       }
-                       Wikimedia\restoreWarnings();
-               }
-               if ( !$sock ) {
-                       $this->_error_log( "Error connecting to $host: $errstr" );
-                       $this->_dead_host( $host );
-                       return false;
-               }
-
-               // Initialise timeout
-               stream_set_timeout( $sock, $this->_timeout_seconds, $this->_timeout_microseconds );
-
-               // If the connection was persistent, flush the read buffer in case there
-               // was a previous incomplete request on this connection
-               if ( $this->_persistent ) {
-                       $this->_flush_read_buffer( $sock );
-               }
-               return true;
-       }
-
-       // }}}
-       // {{{ _dead_sock()
-
-       /**
-        * Marks a host as dead until 30-40 seconds in the future
-        *
-        * @param string $sock Socket to mark as dead
-        *
-        * @access private
-        */
-       function _dead_sock( $sock ) {
-               $host = array_search( $sock, $this->_cache_sock );
-               $this->_dead_host( $host );
-       }
-
-       /**
-        * @param string $host
-        */
-       function _dead_host( $host ) {
-               $ip = explode( ':', $host )[0];
-               $this->_host_dead[$ip] = time() + 30 + intval( rand( 0, 10 ) );
-               $this->_host_dead[$host] = $this->_host_dead[$ip];
-               unset( $this->_cache_sock[$host] );
-       }
-
-       // }}}
-       // {{{ get_sock()
-
-       /**
-        * get_sock
-        *
-        * @param string $key Key to retrieve value for;
-        *
-        * @return Resource|bool Resource on success, false on failure
-        * @access private
-        */
-       function get_sock( $key ) {
-               if ( !$this->_active ) {
-                       return false;
-               }
-
-               if ( $this->_single_sock !== null ) {
-                       return $this->sock_to_host( $this->_single_sock );
-               }
-
-               $hv = is_array( $key ) ? intval( $key[0] ) : $this->_hashfunc( $key );
-               if ( $this->_buckets === null ) {
-                       $bu = array();
-                       foreach ( $this->_servers as $v ) {
-                               if ( is_array( $v ) ) {
-                                       for ( $i = 0; $i < $v[1]; $i++ ) {
-                                               $bu[] = $v[0];
-                                       }
-                               } else {
-                                       $bu[] = $v;
-                               }
-                       }
-                       $this->_buckets = $bu;
-                       $this->_bucketcount = count( $bu );
-               }
-
-               $realkey = is_array( $key ) ? $key[1] : $key;
-               for ( $tries = 0; $tries < 20; $tries++ ) {
-                       $host = $this->_buckets[$hv % $this->_bucketcount];
-                       $sock = $this->sock_to_host( $host );
-                       if ( is_resource( $sock ) ) {
-                               return $sock;
-                       }
-                       $hv = $this->_hashfunc( $hv . $realkey );
-               }
-
-               return false;
-       }
-
-       // }}}
-       // {{{ _hashfunc()
-
-       /**
-        * Creates a hash integer based on the $key
-        *
-        * @param string $key Key to hash
-        *
-        * @return int Hash value
-        * @access private
-        */
-       function _hashfunc( $key ) {
-               # Hash function must be in [0,0x7ffffff]
-               # We take the first 31 bits of the MD5 hash, which unlike the hash
-               # function used in a previous version of this client, works
-               return hexdec( substr( md5( $key ), 0, 8 ) ) & 0x7fffffff;
-       }
-
-       // }}}
-       // {{{ _incrdecr()
-
-       /**
-        * Perform increment/decriment on $key
-        *
-        * @param string $cmd Command to perform
-        * @param string|array $key Key to perform it on
-        * @param int $amt Amount to adjust
-        *
-        * @return int New value of $key
-        * @access private
-        */
-       function _incrdecr( $cmd, $key, $amt = 1 ) {
-               if ( !$this->_active ) {
-                       return null;
-               }
-
-               $sock = $this->get_sock( $key );
-               if ( !is_resource( $sock ) ) {
-                       return null;
-               }
-
-               $key = is_array( $key ) ? $key[1] : $key;
-               if ( isset( $this->stats[$cmd] ) ) {
-                       $this->stats[$cmd]++;
-               } else {
-                       $this->stats[$cmd] = 1;
-               }
-               if ( !$this->_fwrite( $sock, "$cmd $key $amt\r\n" ) ) {
-                       return null;
-               }
-
-               $line = $this->_fgets( $sock );
-               $match = array();
-               if ( !preg_match( '/^(\d+)/', $line, $match ) ) {
-                       return null;
-               }
-               return $match[1];
-       }
-
-       // }}}
-       // {{{ _load_items()
-
-       /**
-        * Load items into $ret from $sock
-        *
-        * @param Resource $sock Socket to read from
-        * @param array $ret returned values
-        * @param float $casToken [optional]
-        * @return bool True for success, false for failure
-        *
-        * @access private
-        */
-       function _load_items( $sock, &$ret, &$casToken = null ) {
-               $results = array();
-
-               while ( 1 ) {
-                       $decl = $this->_fgets( $sock );
-
-                       if ( $decl === false ) {
-                               /*
-                                * If nothing can be read, something is wrong because we know exactly when
-                                * to stop reading (right after "END") and we return right after that.
-                                */
-                               return false;
-                       } elseif ( preg_match( '/^VALUE (\S+) (\d+) (\d+) (\d+)$/', $decl, $match ) ) {
-                               /*
-                                * Read all data returned. This can be either one or multiple values.
-                                * Save all that data (in an array) to be processed later: we'll first
-                                * want to continue reading until "END" before doing anything else,
-                                * to make sure that we don't leave our client in a state where it's
-                                * output is not yet fully read.
-                                */
-                               $results[] = array(
-                                       $match[1], // rkey
-                                       $match[2], // flags
-                                       $match[3], // len
-                                       $match[4], // casToken
-                                       $this->_fread( $sock, $match[3] + 2 ), // data
-                               );
-                       } elseif ( $decl == "END" ) {
-                               if ( count( $results ) == 0 ) {
-                                       return false;
-                               }
-
-                               /**
-                                * All data has been read, time to process the data and build
-                                * meaningful return values.
-                                */
-                               foreach ( $results as $vars ) {
-                                       list( $rkey, $flags, $len, $casToken, $data ) = $vars;
-
-                                       if ( $data === false || substr( $data, -2 ) !== "\r\n" ) {
-                                               $this->_handle_error( $sock,
-                                                       'line ending missing from data block from $1' );
-                                               return false;
-                                       }
-                                       $data = substr( $data, 0, -2 );
-                                       $ret[$rkey] = $data;
-
-                                       if ( $this->_have_zlib && $flags & self::COMPRESSED ) {
-                                               $ret[$rkey] = gzuncompress( $ret[$rkey] );
-                                       }
-
-                                       /*
-                                        * This unserialize is the exact reason that we only want to
-                                        * process data after having read until "END" (instead of doing
-                                        * this right away): "unserialize" can trigger outside code:
-                                        * in the event that $ret[$rkey] is a serialized object,
-                                        * unserializing it will trigger __wakeup() if present. If that
-                                        * function attempted to read from memcached (while we did not
-                                        * yet read "END"), these 2 calls would collide.
-                                        */
-                                       if ( $flags & self::SERIALIZED ) {
-                                               $ret[$rkey] = $this->unserialize( $ret[$rkey] );
-                                       } elseif ( $flags & self::INTVAL ) {
-                                               $ret[$rkey] = intval( $ret[$rkey] );
-                                       }
-                               }
-
-                               return true;
-                       } else {
-                               $this->_handle_error( $sock, 'Error parsing response from $1' );
-                               return false;
-                       }
-               }
-       }
-
-       // }}}
-       // {{{ _set()
-
-       /**
-        * Performs the requested storage operation to the memcache server
-        *
-        * @param string $cmd Command to perform
-        * @param string $key Key to act on
-        * @param mixed $val What we need to store
-        * @param int $exp (optional) Expiration time. This can be a number of seconds
-        * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
-        * longer must be the timestamp of the time at which the mapping should expire. It
-        * is safe to use timestamps in all cases, regardless of exipration
-        * eg: strtotime("+3 hour")
-        * @param float $casToken [optional]
-        *
-        * @return bool
-        * @access private
-        */
-       function _set( $cmd, $key, $val, $exp, $casToken = null ) {
-               if ( !$this->_active ) {
-                       return false;
-               }
-
-               $sock = $this->get_sock( $key );
-               if ( !is_resource( $sock ) ) {
-                       return false;
-               }
-
-               if ( isset( $this->stats[$cmd] ) ) {
-                       $this->stats[$cmd]++;
-               } else {
-                       $this->stats[$cmd] = 1;
-               }
-
-               $flags = 0;
-
-               if ( is_int( $val ) ) {
-                       $flags |= self::INTVAL;
-               } elseif ( !is_scalar( $val ) ) {
-                       $val = $this->serialize( $val );
-                       $flags |= self::SERIALIZED;
-                       if ( $this->_debug ) {
-                               $this->_debugprint( sprintf( "client: serializing data as it is not scalar" ) );
-                       }
-               }
-
-               $len = strlen( $val );
-
-               if ( $this->_have_zlib && $this->_compress_enable
-                       && $this->_compress_threshold && $len >= $this->_compress_threshold
-               ) {
-                       $c_val = gzcompress( $val, 9 );
-                       $c_len = strlen( $c_val );
-
-                       if ( $c_len < $len * ( 1 - self::COMPRESSION_SAVINGS ) ) {
-                               if ( $this->_debug ) {
-                                       $this->_debugprint( sprintf( "client: compressing data; was %d bytes is now %d bytes", $len, $c_len ) );
-                               }
-                               $val = $c_val;
-                               $len = $c_len;
-                               $flags |= self::COMPRESSED;
-                       }
-               }
-
-               $command = "$cmd $key $flags $exp $len";
-               if ( $casToken ) {
-                       $command .= " $casToken";
-               }
-
-               if ( !$this->_fwrite( $sock, "$command\r\n$val\r\n" ) ) {
-                       return false;
-               }
-
-               $line = $this->_fgets( $sock );
-
-               if ( $this->_debug ) {
-                       $this->_debugprint( sprintf( "%s %s (%s)", $cmd, $key, $line ) );
-               }
-               if ( $line === "STORED" ) {
-                       return true;
-               } elseif ( $line === "NOT_STORED" && $cmd === "set" ) {
-                       // "Not stored" is always used as the mcrouter response with AllAsyncRoute
-                       return true;
-               }
-
-               return false;
-       }
-
-       // }}}
-       // {{{ sock_to_host()
-
-       /**
-        * Returns the socket for the host
-        *
-        * @param string $host Host:IP to get socket for
-        *
-        * @return Resource|bool IO Stream or false
-        * @access private
-        */
-       function sock_to_host( $host ) {
-               if ( isset( $this->_cache_sock[$host] ) ) {
-                       return $this->_cache_sock[$host];
-               }
-
-               $sock = null;
-               $now = time();
-               list( $ip, /* $port */) = explode( ':', $host );
-               if ( isset( $this->_host_dead[$host] ) && $this->_host_dead[$host] > $now ||
-                       isset( $this->_host_dead[$ip] ) && $this->_host_dead[$ip] > $now
-               ) {
-                       return null;
-               }
-
-               if ( !$this->_connect_sock( $sock, $host ) ) {
-                       return null;
-               }
-
-               // Do not buffer writes
-               stream_set_write_buffer( $sock, 0 );
-
-               $this->_cache_sock[$host] = $sock;
-
-               return $this->_cache_sock[$host];
-       }
-
-       /**
-        * @param string $text
-        */
-       function _debugprint( $text ) {
-               $this->_logger->debug( $text );
-       }
-
-       /**
-        * @param string $text
-        */
-       function _error_log( $text ) {
-               $this->_logger->error( "Memcached error: $text" );
-       }
-
-       /**
-        * Write to a stream. If there is an error, mark the socket dead.
-        *
-        * @param Resource $sock The socket
-        * @param string $buf The string to write
-        * @return bool True on success, false on failure
-        */
-       function _fwrite( $sock, $buf ) {
-               $bytesWritten = 0;
-               $bufSize = strlen( $buf );
-               while ( $bytesWritten < $bufSize ) {
-                       $result = fwrite( $sock, $buf );
-                       $data = stream_get_meta_data( $sock );
-                       if ( $data['timed_out'] ) {
-                               $this->_handle_error( $sock, 'timeout writing to $1' );
-                               return false;
-                       }
-                       // Contrary to the documentation, fwrite() returns zero on error in PHP 5.3.
-                       if ( $result === false || $result === 0 ) {
-                               $this->_handle_error( $sock, 'error writing to $1' );
-                               return false;
-                       }
-                       $bytesWritten += $result;
-               }
-
-               return true;
-       }
-
-       /**
-        * Handle an I/O error. Mark the socket dead and log an error.
-        *
-        * @param Resource $sock
-        * @param string $msg
-        */
-       function _handle_error( $sock, $msg ) {
-               $peer = stream_socket_get_name( $sock, true /** remote **/ );
-               if ( strval( $peer ) === '' ) {
-                       $peer = array_search( $sock, $this->_cache_sock );
-                       if ( $peer === false ) {
-                               $peer = '[unknown host]';
-                       }
-               }
-               $msg = str_replace( '$1', $peer, $msg );
-               $this->_error_log( "$msg" );
-               $this->_dead_sock( $sock );
-       }
-
-       /**
-        * Read the specified number of bytes from a stream. If there is an error,
-        * mark the socket dead.
-        *
-        * @param Resource $sock The socket
-        * @param int $len The number of bytes to read
-        * @return string|bool The string on success, false on failure.
-        */
-       function _fread( $sock, $len ) {
-               $buf = '';
-               while ( $len > 0 ) {
-                       $result = fread( $sock, $len );
-                       $data = stream_get_meta_data( $sock );
-                       if ( $data['timed_out'] ) {
-                               $this->_handle_error( $sock, 'timeout reading from $1' );
-                               return false;
-                       }
-                       if ( $result === false ) {
-                               $this->_handle_error( $sock, 'error reading buffer from $1' );
-                               return false;
-                       }
-                       if ( $result === '' ) {
-                               // This will happen if the remote end of the socket is shut down
-                               $this->_handle_error( $sock, 'unexpected end of file reading from $1' );
-                               return false;
-                       }
-                       $len -= strlen( $result );
-                       $buf .= $result;
-               }
-               return $buf;
-       }
-
-       /**
-        * Read a line from a stream. If there is an error, mark the socket dead.
-        * The \r\n line ending is stripped from the response.
-        *
-        * @param Resource $sock The socket
-        * @return string|bool The string on success, false on failure
-        */
-       function _fgets( $sock ) {
-               $result = fgets( $sock );
-               // fgets() may return a partial line if there is a select timeout after
-               // a successful recv(), so we have to check for a timeout even if we
-               // got a string response.
-               $data = stream_get_meta_data( $sock );
-               if ( $data['timed_out'] ) {
-                       $this->_handle_error( $sock, 'timeout reading line from $1' );
-                       return false;
-               }
-               if ( $result === false ) {
-                       $this->_handle_error( $sock, 'error reading line from $1' );
-                       return false;
-               }
-               if ( substr( $result, -2 ) === "\r\n" ) {
-                       $result = substr( $result, 0, -2 );
-               } elseif ( substr( $result, -1 ) === "\n" ) {
-                       $result = substr( $result, 0, -1 );
-               } else {
-                       $this->_handle_error( $sock, 'line ending missing in response from $1' );
-                       return false;
-               }
-               return $result;
-       }
-
-       /**
-        * Flush the read buffer of a stream
-        * @param Resource $f
-        */
-       function _flush_read_buffer( $f ) {
-               if ( !is_resource( $f ) ) {
-                       return;
-               }
-               $r = array( $f );
-               $w = null;
-               $e = null;
-               $n = stream_select( $r, $w, $e, 0, 0 );
-               while ( $n == 1 && !feof( $f ) ) {
-                       fread( $f, 1024 );
-                       $r = array( $f );
-                       $w = null;
-                       $e = null;
-                       $n = stream_select( $r, $w, $e, 0, 0 );
-               }
-       }
-
-       // }}}
-       // }}}
-       // }}}
-}
-
-// }}}
index 3df483d..9bf3f42 100644 (file)
@@ -250,7 +250,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
                return $this->checkResult( $key, $result );
        }
 
-       public function incr( $key, $value = 1 ) {
+       public function incr( $key, $value = 1, $flags = 0 ) {
                $this->debug( "incr($key)" );
 
                $result = $this->acquireSyncClient()->increment( $key, $value );
@@ -258,7 +258,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
                return $this->checkResult( $key, $result );
        }
 
-       public function decr( $key, $value = 1 ) {
+       public function decr( $key, $value = 1, $flags = 0 ) {
                $this->debug( "decr($key)" );
 
                $result = $this->acquireSyncClient()->decrement( $key, $value );
@@ -332,7 +332,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
 
                // The PECL implementation is a naïve for-loop so use async I/O to pipeline;
                // https://github.com/php-memcached-dev/php-memcached/blob/master/php_memcached.c#L1852
-               if ( ( $flags & self::WRITE_BACKGROUND ) == self::WRITE_BACKGROUND ) {
+               if ( $this->fieldHasFlags( $flags, self::WRITE_BACKGROUND ) ) {
                        $client = $this->acquireAsyncClient();
                        $result = $client->setMulti( $data, $exptime );
                        $this->releaseAsyncClient( $client );
@@ -352,7 +352,7 @@ class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
 
                // The PECL implementation is a naïve for-loop so use async I/O to pipeline;
                // https://github.com/php-memcached-dev/php-memcached/blob/7443d16d02fb73cdba2e90ae282446f80969229c/php_memcached.c#L1852
-               if ( ( $flags & self::WRITE_BACKGROUND ) == self::WRITE_BACKGROUND ) {
+               if ( $this->fieldHasFlags( $flags, self::WRITE_BACKGROUND ) ) {
                        $client = $this->acquireAsyncClient();
                        $resultArray = $client->deleteMulti( $keys ) ?: [];
                        $this->releaseAsyncClient( $client );
index 8144231..fc6deef 100644 (file)
@@ -93,13 +93,13 @@ class MemcachedPhpBagOStuff extends MemcachedBagOStuff {
                );
        }
 
-       public function incr( $key, $value = 1 ) {
+       public function incr( $key, $value = 1, $flags = 0 ) {
                $n = $this->client->incr( $this->validateKeyEncoding( $key ), $value );
 
                return ( $n !== false && $n !== null ) ? $n : false;
        }
 
-       public function decr( $key, $value = 1 ) {
+       public function decr( $key, $value = 1, $flags = 0 ) {
                $n = $this->client->decr( $this->validateKeyEncoding( $key ), $value );
 
                return ( $n !== false && $n !== null ) ? $n : false;
index d150880..d0aa380 100644 (file)
@@ -106,7 +106,7 @@ class MultiWriteBagOStuff extends BagOStuff {
        }
 
        public function get( $key, $flags = 0 ) {
-               if ( ( $flags & self::READ_LATEST ) == self::READ_LATEST ) {
+               if ( $this->fieldHasFlags( $flags, self::READ_LATEST ) ) {
                        // If the latest write was a delete(), we do NOT want to fallback
                        // to the other tiers and possibly see the old value. Also, this
                        // is used by merge(), which only needs to hit the primary.
@@ -123,9 +123,10 @@ class MultiWriteBagOStuff extends BagOStuff {
                        $missIndexes[] = $i;
                }
 
-               if ( $value !== false
-                       && $missIndexes
-                       && ( $flags & self::READ_VERIFIED ) == self::READ_VERIFIED
+               if (
+                       $value !== false &&
+                       $this->fieldHasFlags( $flags, self::READ_VERIFIED ) &&
+                       $missIndexes
                ) {
                        // Backfill the value to the higher (and often faster/smaller) cache tiers
                        $this->doWrite(
@@ -265,7 +266,7 @@ class MultiWriteBagOStuff extends BagOStuff {
                );
        }
 
-       public function incr( $key, $value = 1 ) {
+       public function incr( $key, $value = 1, $flags = 0 ) {
                return $this->doWrite(
                        $this->cacheIndexes,
                        $this->asyncWrites,
@@ -274,7 +275,7 @@ class MultiWriteBagOStuff extends BagOStuff {
                );
        }
 
-       public function decr( $key, $value = 1 ) {
+       public function decr( $key, $value = 1, $flags = 0 ) {
                return $this->doWrite(
                        $this->cacheIndexes,
                        $this->asyncWrites,
@@ -283,7 +284,7 @@ class MultiWriteBagOStuff extends BagOStuff {
                );
        }
 
-       public function incrWithInit( $key, $ttl, $value = 1, $init = 1 ) {
+       public function incrWithInit( $key, $exptime, $value = 1, $init = null, $flags = 0 ) {
                return $this->doWrite(
                        $this->cacheIndexes,
                        $this->asyncWrites,
@@ -346,7 +347,7 @@ class MultiWriteBagOStuff extends BagOStuff {
         * @return bool
         */
        protected function usesAsyncWritesGivenFlags( $flags ) {
-               return ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC ) ? false : $this->asyncWrites;
+               return $this->fieldHasFlags( $flags, self::WRITE_SYNC ) ? false : $this->asyncWrites;
        }
 
        public function makeKeyInternal( $keyspace, $args ) {
index b8ce38b..82b5ac0 100644 (file)
@@ -188,11 +188,11 @@ class RESTBagOStuff extends MediumSpecificBagOStuff {
                return $this->handleError( "Failed to delete $key", $rcode, $rerr, $rhdrs, $rbody );
        }
 
-       public function incr( $key, $value = 1 ) {
+       public function incr( $key, $value = 1, $flags = 0 ) {
                // @TODO: make this atomic
                $n = $this->get( $key, self::READ_LATEST );
                if ( $this->isInteger( $n ) ) { // key exists?
-                       $n = max( $n + intval( $value ), 0 );
+                       $n = max( $n + (int)$value, 0 );
                        // @TODO: respect $exptime
                        return $this->set( $key, $n ) ? $n : false;
                }
@@ -200,6 +200,10 @@ class RESTBagOStuff extends MediumSpecificBagOStuff {
                return false;
        }
 
+       public function decr( $key, $value = 1, $flags = 0 ) {
+               return $this->incr( $key, -$value, $flags );
+       }
+
        /**
         * Processes the response body.
         *
index 252b1fa..57a2507 100644 (file)
@@ -364,7 +364,7 @@ class RedisBagOStuff extends MediumSpecificBagOStuff {
                return $result;
        }
 
-       public function incr( $key, $value = 1 ) {
+       public function incr( $key, $value = 1, $flags = 0 ) {
                $conn = $this->getConnection( $key );
                if ( !$conn ) {
                        return false;
@@ -386,6 +386,28 @@ class RedisBagOStuff extends MediumSpecificBagOStuff {
                return $result;
        }
 
+       public function decr( $key, $value = 1, $flags = 0 ) {
+               $conn = $this->getConnection( $key );
+               if ( !$conn ) {
+                       return false;
+               }
+
+               try {
+                       if ( !$conn->exists( $key ) ) {
+                               return false;
+                       }
+                       // @FIXME: on races, the key may have a 0 TTL
+                       $result = $conn->decrBy( $key, $value );
+               } catch ( RedisException $e ) {
+                       $result = false;
+                       $this->handleException( $conn, $e );
+               }
+
+               $this->logRequest( 'decr', $key, $conn->getServer(), $result );
+
+               return $result;
+       }
+
        protected function doChangeTTL( $key, $exptime, $flags ) {
                $conn = $this->getConnection( $key );
                if ( !$conn ) {
index 504d515..0b5ac46 100644 (file)
@@ -76,7 +76,7 @@ class ReplicatedBagOStuff extends BagOStuff {
        }
 
        public function get( $key, $flags = 0 ) {
-               return ( ( $flags & self::READ_LATEST ) == self::READ_LATEST )
+               return $this->fieldHasFlags( $flags, self::READ_LATEST )
                        ? $this->writeStore->get( $key, $flags )
                        : $this->readStore->get( $key, $flags );
        }
@@ -118,7 +118,7 @@ class ReplicatedBagOStuff extends BagOStuff {
        }
 
        public function getMulti( array $keys, $flags = 0 ) {
-               return ( ( $flags & self::READ_LATEST ) == self::READ_LATEST )
+               return $this->fieldHasFlags( $flags, self::READ_LATEST )
                        ? $this->writeStore->getMulti( $keys, $flags )
                        : $this->readStore->getMulti( $keys, $flags );
        }
@@ -135,16 +135,16 @@ class ReplicatedBagOStuff extends BagOStuff {
                return $this->writeStore->changeTTLMulti( $keys, $exptime, $flags );
        }
 
-       public function incr( $key, $value = 1 ) {
-               return $this->writeStore->incr( $key, $value );
+       public function incr( $key, $value = 1, $flags = 0 ) {
+               return $this->writeStore->incr( $key, $value, $flags );
        }
 
-       public function decr( $key, $value = 1 ) {
-               return $this->writeStore->decr( $key, $value );
+       public function decr( $key, $value = 1, $flags = 0 ) {
+               return $this->writeStore->decr( $key, $value, $flags );
        }
 
-       public function incrWithInit( $key, $ttl, $value = 1, $init = 1 ) {
-               return $this->writeStore->incrWithInit( $key, $ttl, $value, $init );
+       public function incrWithInit( $key, $exptime, $value = 1, $init = null, $flags = 0 ) {
+               return $this->writeStore->incrWithInit( $key, $exptime, $value, $init, $flags );
        }
 
        public function getLastError() {
index 3c4efbb..5b38628 100644 (file)
@@ -100,14 +100,6 @@ class WinCacheBagOStuff extends MediumSpecificBagOStuff {
                return true;
        }
 
-       /**
-        * Construct a cache key.
-        *
-        * @since 1.27
-        * @param string $keyspace
-        * @param array $args
-        * @return string
-        */
        public function makeKeyInternal( $keyspace, $args ) {
                // WinCache keys have a maximum length of 150 characters. From that,
                // subtract the number of characters we need for the keyspace and for
@@ -136,13 +128,7 @@ class WinCacheBagOStuff extends MediumSpecificBagOStuff {
                return $keyspace . ':' . implode( ':', $args );
        }
 
-       /**
-        * Increase stored value of $key by $value while preserving its original TTL
-        * @param string $key Key to increase
-        * @param int $value Value to add to $key (Default 1)
-        * @return int|bool New value or false on failure
-        */
-       public function incr( $key, $value = 1 ) {
+       public function incr( $key, $value = 1, $flags = 0 ) {
                if ( !wincache_lock( $key ) ) { // optimize with FIFO lock
                        return false;
                }
@@ -160,4 +146,8 @@ class WinCacheBagOStuff extends MediumSpecificBagOStuff {
 
                return $n;
        }
+
+       public function decr( $key, $value = 1, $flags = 0 ) {
+               return $this->incr( $key, -$value, $flags );
+       }
 }
diff --git a/includes/libs/objectcache/utils/MemcachedClient.php b/includes/libs/objectcache/utils/MemcachedClient.php
new file mode 100644 (file)
index 0000000..2c40854
--- /dev/null
@@ -0,0 +1,1311 @@
+<?php
+// phpcs:ignoreFile -- It's an external lib and it isn't. Let's not bother.
+/**
+ * Memcached client for PHP.
+ *
+ * +---------------------------------------------------------------------------+
+ * | memcached client, PHP                                                     |
+ * +---------------------------------------------------------------------------+
+ * | Copyright (c) 2003 Ryan T. Dean <rtdean@cytherianage.net>                 |
+ * | All rights reserved.                                                      |
+ * |                                                                           |
+ * | Redistribution and use in source and binary forms, with or without        |
+ * | modification, are permitted provided that the following conditions        |
+ * | are met:                                                                  |
+ * |                                                                           |
+ * | 1. Redistributions of source code must retain the above copyright         |
+ * |    notice, this list of conditions and the following disclaimer.          |
+ * | 2. Redistributions in binary form must reproduce the above copyright      |
+ * |    notice, this list of conditions and the following disclaimer in the    |
+ * |    documentation and/or other materials provided with the distribution.   |
+ * |                                                                           |
+ * | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR      |
+ * | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
+ * | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.   |
+ * | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,          |
+ * | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT  |
+ * | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
+ * | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY     |
+ * | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT       |
+ * | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF  |
+ * | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.         |
+ * +---------------------------------------------------------------------------+
+ * | Author: Ryan T. Dean <rtdean@cytherianage.net>                            |
+ * | Heavily influenced by the Perl memcached client by Brad Fitzpatrick.      |
+ * |   Permission granted by Brad Fitzpatrick for relicense of ported Perl     |
+ * |   client logic under 2-clause BSD license.                                |
+ * +---------------------------------------------------------------------------+
+ *
+ * @file
+ * $TCAnet$
+ */
+
+/**
+ * This is a PHP client for memcached - a distributed memory cache daemon.
+ *
+ * More information is available at http://www.danga.com/memcached/
+ *
+ * Usage example:
+ *
+ *     $mc = new MemcachedClient(array(
+ *         'servers' => array(
+ *             '127.0.0.1:10000',
+ *             array( '192.0.0.1:10010', 2 ),
+ *             '127.0.0.1:10020'
+ *         ),
+ *         'debug'   => false,
+ *         'compress_threshold' => 10240,
+ *         'persistent' => true
+ *     ));
+ *
+ *     $mc->add( 'key', array( 'some', 'array' ) );
+ *     $mc->replace( 'key', 'some random string' );
+ *     $val = $mc->get( 'key' );
+ *
+ * @author Ryan T. Dean <rtdean@cytherianage.net>
+ * @version 0.1.2
+ */
+
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+
+// {{{ class MemcachedClient
+/**
+ * memcached client class implemented using (p)fsockopen()
+ *
+ * @author  Ryan T. Dean <rtdean@cytherianage.net>
+ * @ingroup Cache
+ */
+class MemcachedClient {
+       // {{{ properties
+       // {{{ public
+
+       // {{{ constants
+       // {{{ flags
+
+       /**
+        * Flag: indicates data is serialized
+        */
+       const SERIALIZED = 1;
+
+       /**
+        * Flag: indicates data is compressed
+        */
+       const COMPRESSED = 2;
+
+       /**
+        * Flag: indicates data is an integer
+        */
+       const INTVAL = 4;
+
+       // }}}
+
+       /**
+        * Minimum savings to store data compressed
+        */
+       const COMPRESSION_SAVINGS = 0.20;
+
+       // }}}
+
+       /**
+        * Command statistics
+        *
+        * @var array
+        * @access public
+        */
+       public $stats;
+
+       // }}}
+       // {{{ private
+
+       /**
+        * Cached Sockets that are connected
+        *
+        * @var array
+        * @access private
+        */
+       public $_cache_sock;
+
+       /**
+        * Current debug status; 0 - none to 9 - profiling
+        *
+        * @var bool
+        * @access private
+        */
+       public $_debug;
+
+       /**
+        * Dead hosts, assoc array, 'host'=>'unixtime when ok to check again'
+        *
+        * @var array
+        * @access private
+        */
+       public $_host_dead;
+
+       /**
+        * Is compression available?
+        *
+        * @var bool
+        * @access private
+        */
+       public $_have_zlib;
+
+       /**
+        * Do we want to use compression?
+        *
+        * @var bool
+        * @access private
+        */
+       public $_compress_enable;
+
+       /**
+        * At how many bytes should we compress?
+        *
+        * @var int
+        * @access private
+        */
+       public $_compress_threshold;
+
+       /**
+        * Are we using persistent links?
+        *
+        * @var bool
+        * @access private
+        */
+       public $_persistent;
+
+       /**
+        * If only using one server; contains ip:port to connect to
+        *
+        * @var string
+        * @access private
+        */
+       public $_single_sock;
+
+       /**
+        * Array containing ip:port or array(ip:port, weight)
+        *
+        * @var array
+        * @access private
+        */
+       public $_servers;
+
+       /**
+        * Our bit buckets
+        *
+        * @var array
+        * @access private
+        */
+       public $_buckets;
+
+       /**
+        * Total # of bit buckets we have
+        *
+        * @var int
+        * @access private
+        */
+       public $_bucketcount;
+
+       /**
+        * # of total servers we have
+        *
+        * @var int
+        * @access private
+        */
+       public $_active;
+
+       /**
+        * Stream timeout in seconds. Applies for example to fread()
+        *
+        * @var int
+        * @access private
+        */
+       public $_timeout_seconds;
+
+       /**
+        * Stream timeout in microseconds
+        *
+        * @var int
+        * @access private
+        */
+       public $_timeout_microseconds;
+
+       /**
+        * Connect timeout in seconds
+        */
+       public $_connect_timeout;
+
+       /**
+        * Number of connection attempts for each server
+        */
+       public $_connect_attempts;
+
+       /**
+        * @var LoggerInterface
+        */
+       private $_logger;
+
+       // }}}
+       // }}}
+       // {{{ methods
+       // {{{ public functions
+       // {{{ memcached()
+
+       /**
+        * Memcache initializer
+        *
+        * @param array $args Associative array of settings
+        */
+       public function __construct( $args ) {
+               $this->set_servers( $args['servers'] ?? array() );
+               $this->_debug = $args['debug'] ?? false;
+               $this->stats = array();
+               $this->_compress_threshold = $args['compress_threshold'] ?? 0;
+               $this->_persistent = $args['persistent'] ?? false;
+               $this->_compress_enable = true;
+               $this->_have_zlib = function_exists( 'gzcompress' );
+
+               $this->_cache_sock = array();
+               $this->_host_dead = array();
+
+               $this->_timeout_seconds = 0;
+               $this->_timeout_microseconds = $args['timeout'] ?? 500000;
+
+               $this->_connect_timeout = $args['connect_timeout'] ?? 0.1;
+               $this->_connect_attempts = 2;
+
+               $this->_logger = $args['logger'] ?? new NullLogger();
+       }
+
+       // }}}
+
+       /**
+        * @param mixed $value
+        * @return string|integer
+        */
+       public function serialize( $value ) {
+               return serialize( $value );
+       }
+
+       /**
+        * @param string $value
+        * @return mixed
+        */
+       public function unserialize( $value ) {
+               return unserialize( $value );
+       }
+
+       // {{{ add()
+
+       /**
+        * Adds a key/value to the memcache server if one isn't already set with
+        * that key
+        *
+        * @param string $key Key to set with data
+        * @param mixed $val Value to store
+        * @param int $exp (optional) Expiration time. This can be a number of seconds
+        * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
+        * longer must be the timestamp of the time at which the mapping should expire. It
+        * is safe to use timestamps in all cases, regardless of expiration
+        * eg: strtotime("+3 hour")
+        *
+        * @return bool
+        */
+       public function add( $key, $val, $exp = 0 ) {
+               return $this->_set( 'add', $key, $val, $exp );
+       }
+
+       // }}}
+       // {{{ decr()
+
+       /**
+        * Decrease a value stored on the memcache server
+        *
+        * @param string $key Key to decrease
+        * @param int $amt (optional) amount to decrease
+        *
+        * @return mixed False on failure, value on success
+        */
+       public function decr( $key, $amt = 1 ) {
+               return $this->_incrdecr( 'decr', $key, $amt );
+       }
+
+       // }}}
+       // {{{ delete()
+
+       /**
+        * Deletes a key from the server, optionally after $time
+        *
+        * @param string $key Key to delete
+        * @param int $time (optional) how long to wait before deleting
+        *
+        * @return bool True on success, false on failure
+        */
+       public function delete( $key, $time = 0 ) {
+               if ( !$this->_active ) {
+                       return false;
+               }
+
+               $sock = $this->get_sock( $key );
+               if ( !is_resource( $sock ) ) {
+                       return false;
+               }
+
+               $key = is_array( $key ) ? $key[1] : $key;
+
+               if ( isset( $this->stats['delete'] ) ) {
+                       $this->stats['delete']++;
+               } else {
+                       $this->stats['delete'] = 1;
+               }
+               $cmd = "delete $key $time\r\n";
+               if ( !$this->_fwrite( $sock, $cmd ) ) {
+                       return false;
+               }
+               $res = $this->_fgets( $sock );
+
+               if ( $this->_debug ) {
+                       $this->_debugprint( sprintf( "MemCache: delete %s (%s)", $key, $res ) );
+               }
+
+               if ( $res == "DELETED" || $res == "NOT_FOUND" ) {
+                       return true;
+               }
+
+               return false;
+       }
+
+       /**
+        * Changes the TTL on a key from the server to $time
+        *
+        * @param string $key
+        * @param int $time TTL in seconds
+        *
+        * @return bool True on success, false on failure
+        */
+       public function touch( $key, $time = 0 ) {
+               if ( !$this->_active ) {
+                       return false;
+               }
+
+               $sock = $this->get_sock( $key );
+               if ( !is_resource( $sock ) ) {
+                       return false;
+               }
+
+               $key = is_array( $key ) ? $key[1] : $key;
+
+               if ( isset( $this->stats['touch'] ) ) {
+                       $this->stats['touch']++;
+               } else {
+                       $this->stats['touch'] = 1;
+               }
+               $cmd = "touch $key $time\r\n";
+               if ( !$this->_fwrite( $sock, $cmd ) ) {
+                       return false;
+               }
+               $res = $this->_fgets( $sock );
+
+               if ( $this->_debug ) {
+                       $this->_debugprint( sprintf( "MemCache: touch %s (%s)", $key, $res ) );
+               }
+
+               if ( $res == "TOUCHED" ) {
+                       return true;
+               }
+
+               return false;
+       }
+
+       // }}}
+       // {{{ disconnect_all()
+
+       /**
+        * Disconnects all connected sockets
+        */
+       public function disconnect_all() {
+               foreach ( $this->_cache_sock as $sock ) {
+                       fclose( $sock );
+               }
+
+               $this->_cache_sock = array();
+       }
+
+       // }}}
+       // {{{ enable_compress()
+
+       /**
+        * Enable / Disable compression
+        *
+        * @param bool $enable True to enable, false to disable
+        */
+       public function enable_compress( $enable ) {
+               $this->_compress_enable = $enable;
+       }
+
+       // }}}
+       // {{{ forget_dead_hosts()
+
+       /**
+        * Forget about all of the dead hosts
+        */
+       public function forget_dead_hosts() {
+               $this->_host_dead = array();
+       }
+
+       // }}}
+       // {{{ get()
+
+       /**
+        * Retrieves the value associated with the key from the memcache server
+        *
+        * @param array|string $key key to retrieve
+        * @param float $casToken [optional]
+        *
+        * @return mixed
+        */
+       public function get( $key, &$casToken = null ) {
+               if ( $this->_debug ) {
+                       $this->_debugprint( "get($key)" );
+               }
+
+               if ( !is_array( $key ) && strval( $key ) === '' ) {
+                       $this->_debugprint( "Skipping key which equals to an empty string" );
+                       return false;
+               }
+
+               if ( !$this->_active ) {
+                       return false;
+               }
+
+               $sock = $this->get_sock( $key );
+
+               if ( !is_resource( $sock ) ) {
+                       return false;
+               }
+
+               $key = is_array( $key ) ? $key[1] : $key;
+               if ( isset( $this->stats['get'] ) ) {
+                       $this->stats['get']++;
+               } else {
+                       $this->stats['get'] = 1;
+               }
+
+               $cmd = "gets $key\r\n";
+               if ( !$this->_fwrite( $sock, $cmd ) ) {
+                       return false;
+               }
+
+               $val = array();
+               $this->_load_items( $sock, $val, $casToken );
+
+               if ( $this->_debug ) {
+                       foreach ( $val as $k => $v ) {
+                               $this->_debugprint(
+                                       sprintf( "MemCache: sock %s got %s", $this->serialize( $sock ), $k ) );
+                       }
+               }
+
+               $value = false;
+               if ( isset( $val[$key] ) ) {
+                       $value = $val[$key];
+               }
+               return $value;
+       }
+
+       // }}}
+       // {{{ get_multi()
+
+       /**
+        * Get multiple keys from the server(s)
+        *
+        * @param array $keys Keys to retrieve
+        *
+        * @return array
+        */
+       public function get_multi( $keys ) {
+               if ( !$this->_active ) {
+                       return array();
+               }
+
+               if ( isset( $this->stats['get_multi'] ) ) {
+                       $this->stats['get_multi']++;
+               } else {
+                       $this->stats['get_multi'] = 1;
+               }
+               $sock_keys = array();
+               $socks = array();
+               foreach ( $keys as $key ) {
+                       $sock = $this->get_sock( $key );
+                       if ( !is_resource( $sock ) ) {
+                               continue;
+                       }
+                       $key = is_array( $key ) ? $key[1] : $key;
+                       if ( !isset( $sock_keys[$sock] ) ) {
+                               $sock_keys[intval( $sock )] = array();
+                               $socks[] = $sock;
+                       }
+                       $sock_keys[intval( $sock )][] = $key;
+               }
+
+               $gather = array();
+               // Send out the requests
+               foreach ( $socks as $sock ) {
+                       $cmd = 'gets';
+                       foreach ( $sock_keys[intval( $sock )] as $key ) {
+                               $cmd .= ' ' . $key;
+                       }
+                       $cmd .= "\r\n";
+
+                       if ( $this->_fwrite( $sock, $cmd ) ) {
+                               $gather[] = $sock;
+                       }
+               }
+
+               // Parse responses
+               $val = array();
+               foreach ( $gather as $sock ) {
+                       $this->_load_items( $sock, $val, $casToken );
+               }
+
+               if ( $this->_debug ) {
+                       foreach ( $val as $k => $v ) {
+                               $this->_debugprint( sprintf( "MemCache: got %s", $k ) );
+                       }
+               }
+
+               return $val;
+       }
+
+       // }}}
+       // {{{ incr()
+
+       /**
+        * Increments $key (optionally) by $amt
+        *
+        * @param string $key Key to increment
+        * @param int $amt (optional) amount to increment
+        *
+        * @return int|null Null if the key does not exist yet (this does NOT
+        * create new mappings if the key does not exist). If the key does
+        * exist, this returns the new value for that key.
+        */
+       public function incr( $key, $amt = 1 ) {
+               return $this->_incrdecr( 'incr', $key, $amt );
+       }
+
+       // }}}
+       // {{{ replace()
+
+       /**
+        * Overwrites an existing value for key; only works if key is already set
+        *
+        * @param string $key Key to set value as
+        * @param mixed $value Value to store
+        * @param int $exp (optional) Expiration time. This can be a number of seconds
+        * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
+        * longer must be the timestamp of the time at which the mapping should expire. It
+        * is safe to use timestamps in all cases, regardless of exipration
+        * eg: strtotime("+3 hour")
+        *
+        * @return bool
+        */
+       public function replace( $key, $value, $exp = 0 ) {
+               return $this->_set( 'replace', $key, $value, $exp );
+       }
+
+       // }}}
+       // {{{ run_command()
+
+       /**
+        * Passes through $cmd to the memcache server connected by $sock; returns
+        * output as an array (null array if no output)
+        *
+        * @param Resource $sock Socket to send command on
+        * @param string $cmd Command to run
+        *
+        * @return array Output array
+        */
+       public function run_command( $sock, $cmd ) {
+               if ( !is_resource( $sock ) ) {
+                       return array();
+               }
+
+               if ( !$this->_fwrite( $sock, $cmd ) ) {
+                       return array();
+               }
+
+               $ret = array();
+               while ( true ) {
+                       $res = $this->_fgets( $sock );
+                       $ret[] = $res;
+                       if ( preg_match( '/^END/', $res ) ) {
+                               break;
+                       }
+                       if ( strlen( $res ) == 0 ) {
+                               break;
+                       }
+               }
+               return $ret;
+       }
+
+       // }}}
+       // {{{ set()
+
+       /**
+        * Unconditionally sets a key to a given value in the memcache.  Returns true
+        * if set successfully.
+        *
+        * @param string $key Key to set value as
+        * @param mixed $value Value to set
+        * @param int $exp (optional) Expiration time. This can be a number of seconds
+        * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
+        * longer must be the timestamp of the time at which the mapping should expire. It
+        * is safe to use timestamps in all cases, regardless of exipration
+        * eg: strtotime("+3 hour")
+        *
+        * @return bool True on success
+        */
+       public function set( $key, $value, $exp = 0 ) {
+               return $this->_set( 'set', $key, $value, $exp );
+       }
+
+       // }}}
+       // {{{ cas()
+
+       /**
+        * Sets a key to a given value in the memcache if the current value still corresponds
+        * to a known, given value.  Returns true if set successfully.
+        *
+        * @param float $casToken Current known value
+        * @param string $key Key to set value as
+        * @param mixed $value Value to set
+        * @param int $exp (optional) Expiration time. This can be a number of seconds
+        * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
+        * longer must be the timestamp of the time at which the mapping should expire. It
+        * is safe to use timestamps in all cases, regardless of exipration
+        * eg: strtotime("+3 hour")
+        *
+        * @return bool True on success
+        */
+       public function cas( $casToken, $key, $value, $exp = 0 ) {
+               return $this->_set( 'cas', $key, $value, $exp, $casToken );
+       }
+
+       // }}}
+       // {{{ set_compress_threshold()
+
+       /**
+        * Set the compression threshold
+        *
+        * @param int $thresh Threshold to compress if larger than
+        */
+       public function set_compress_threshold( $thresh ) {
+               $this->_compress_threshold = $thresh;
+       }
+
+       // }}}
+       // {{{ set_debug()
+
+       /**
+        * Set the debug flag
+        *
+        * @see __construct()
+        * @param bool $dbg True for debugging, false otherwise
+        */
+       public function set_debug( $dbg ) {
+               $this->_debug = $dbg;
+       }
+
+       // }}}
+       // {{{ set_servers()
+
+       /**
+        * Set the server list to distribute key gets and puts between
+        *
+        * @see __construct()
+        * @param array $list Array of servers to connect to
+        */
+       public function set_servers( $list ) {
+               $this->_servers = $list;
+               $this->_active = count( $list );
+               $this->_buckets = null;
+               $this->_bucketcount = 0;
+
+               $this->_single_sock = null;
+               if ( $this->_active == 1 ) {
+                       $this->_single_sock = $this->_servers[0];
+               }
+       }
+
+       /**
+        * Sets the timeout for new connections
+        *
+        * @param int $seconds Number of seconds
+        * @param int $microseconds Number of microseconds
+        */
+       public function set_timeout( $seconds, $microseconds ) {
+               $this->_timeout_seconds = $seconds;
+               $this->_timeout_microseconds = $microseconds;
+       }
+
+       // }}}
+       // }}}
+       // {{{ private methods
+       // {{{ _close_sock()
+
+       /**
+        * Close the specified socket
+        *
+        * @param string $sock Socket to close
+        *
+        * @access private
+        */
+       function _close_sock( $sock ) {
+               $host = array_search( $sock, $this->_cache_sock );
+               fclose( $this->_cache_sock[$host] );
+               unset( $this->_cache_sock[$host] );
+       }
+
+       // }}}
+       // {{{ _connect_sock()
+
+       /**
+        * Connects $sock to $host, timing out after $timeout
+        *
+        * @param int $sock Socket to connect
+        * @param string $host Host:IP to connect to
+        *
+        * @return bool
+        * @access private
+        */
+       function _connect_sock( &$sock, $host ) {
+               list( $ip, $port ) = preg_split( '/:(?=\d)/', $host );
+               $sock = false;
+               $timeout = $this->_connect_timeout;
+               $errno = $errstr = null;
+               for ( $i = 0; !$sock && $i < $this->_connect_attempts; $i++ ) {
+                       Wikimedia\suppressWarnings();
+                       if ( $this->_persistent == 1 ) {
+                               $sock = pfsockopen( $ip, $port, $errno, $errstr, $timeout );
+                       } else {
+                               $sock = fsockopen( $ip, $port, $errno, $errstr, $timeout );
+                       }
+                       Wikimedia\restoreWarnings();
+               }
+               if ( !$sock ) {
+                       $this->_error_log( "Error connecting to $host: $errstr" );
+                       $this->_dead_host( $host );
+                       return false;
+               }
+
+               // Initialise timeout
+               stream_set_timeout( $sock, $this->_timeout_seconds, $this->_timeout_microseconds );
+
+               // If the connection was persistent, flush the read buffer in case there
+               // was a previous incomplete request on this connection
+               if ( $this->_persistent ) {
+                       $this->_flush_read_buffer( $sock );
+               }
+               return true;
+       }
+
+       // }}}
+       // {{{ _dead_sock()
+
+       /**
+        * Marks a host as dead until 30-40 seconds in the future
+        *
+        * @param string $sock Socket to mark as dead
+        *
+        * @access private
+        */
+       function _dead_sock( $sock ) {
+               $host = array_search( $sock, $this->_cache_sock );
+               $this->_dead_host( $host );
+       }
+
+       /**
+        * @param string $host
+        */
+       function _dead_host( $host ) {
+               $ip = explode( ':', $host )[0];
+               $this->_host_dead[$ip] = time() + 30 + intval( rand( 0, 10 ) );
+               $this->_host_dead[$host] = $this->_host_dead[$ip];
+               unset( $this->_cache_sock[$host] );
+       }
+
+       // }}}
+       // {{{ get_sock()
+
+       /**
+        * get_sock
+        *
+        * @param string $key Key to retrieve value for;
+        *
+        * @return Resource|bool Resource on success, false on failure
+        * @access private
+        */
+       function get_sock( $key ) {
+               if ( !$this->_active ) {
+                       return false;
+               }
+
+               if ( $this->_single_sock !== null ) {
+                       return $this->sock_to_host( $this->_single_sock );
+               }
+
+               $hv = is_array( $key ) ? intval( $key[0] ) : $this->_hashfunc( $key );
+               if ( $this->_buckets === null ) {
+                       $bu = array();
+                       foreach ( $this->_servers as $v ) {
+                               if ( is_array( $v ) ) {
+                                       for ( $i = 0; $i < $v[1]; $i++ ) {
+                                               $bu[] = $v[0];
+                                       }
+                               } else {
+                                       $bu[] = $v;
+                               }
+                       }
+                       $this->_buckets = $bu;
+                       $this->_bucketcount = count( $bu );
+               }
+
+               $realkey = is_array( $key ) ? $key[1] : $key;
+               for ( $tries = 0; $tries < 20; $tries++ ) {
+                       $host = $this->_buckets[$hv % $this->_bucketcount];
+                       $sock = $this->sock_to_host( $host );
+                       if ( is_resource( $sock ) ) {
+                               return $sock;
+                       }
+                       $hv = $this->_hashfunc( $hv . $realkey );
+               }
+
+               return false;
+       }
+
+       // }}}
+       // {{{ _hashfunc()
+
+       /**
+        * Creates a hash integer based on the $key
+        *
+        * @param string $key Key to hash
+        *
+        * @return int Hash value
+        * @access private
+        */
+       function _hashfunc( $key ) {
+               # Hash function must be in [0,0x7ffffff]
+               # We take the first 31 bits of the MD5 hash, which unlike the hash
+               # function used in a previous version of this client, works
+               return hexdec( substr( md5( $key ), 0, 8 ) ) & 0x7fffffff;
+       }
+
+       // }}}
+       // {{{ _incrdecr()
+
+       /**
+        * Perform increment/decriment on $key
+        *
+        * @param string $cmd Command to perform
+        * @param string|array $key Key to perform it on
+        * @param int $amt Amount to adjust
+        *
+        * @return int New value of $key
+        * @access private
+        */
+       function _incrdecr( $cmd, $key, $amt = 1 ) {
+               if ( !$this->_active ) {
+                       return null;
+               }
+
+               $sock = $this->get_sock( $key );
+               if ( !is_resource( $sock ) ) {
+                       return null;
+               }
+
+               $key = is_array( $key ) ? $key[1] : $key;
+               if ( isset( $this->stats[$cmd] ) ) {
+                       $this->stats[$cmd]++;
+               } else {
+                       $this->stats[$cmd] = 1;
+               }
+               if ( !$this->_fwrite( $sock, "$cmd $key $amt\r\n" ) ) {
+                       return null;
+               }
+
+               $line = $this->_fgets( $sock );
+               $match = array();
+               if ( !preg_match( '/^(\d+)/', $line, $match ) ) {
+                       return null;
+               }
+               return $match[1];
+       }
+
+       // }}}
+       // {{{ _load_items()
+
+       /**
+        * Load items into $ret from $sock
+        *
+        * @param Resource $sock Socket to read from
+        * @param array $ret returned values
+        * @param float $casToken [optional]
+        * @return bool True for success, false for failure
+        *
+        * @access private
+        */
+       function _load_items( $sock, &$ret, &$casToken = null ) {
+               $results = array();
+
+               while ( 1 ) {
+                       $decl = $this->_fgets( $sock );
+
+                       if ( $decl === false ) {
+                               /*
+                                * If nothing can be read, something is wrong because we know exactly when
+                                * to stop reading (right after "END") and we return right after that.
+                                */
+                               return false;
+                       } elseif ( preg_match( '/^VALUE (\S+) (\d+) (\d+) (\d+)$/', $decl, $match ) ) {
+                               /*
+                                * Read all data returned. This can be either one or multiple values.
+                                * Save all that data (in an array) to be processed later: we'll first
+                                * want to continue reading until "END" before doing anything else,
+                                * to make sure that we don't leave our client in a state where it's
+                                * output is not yet fully read.
+                                */
+                               $results[] = array(
+                                       $match[1], // rkey
+                                       $match[2], // flags
+                                       $match[3], // len
+                                       $match[4], // casToken
+                                       $this->_fread( $sock, $match[3] + 2 ), // data
+                               );
+                       } elseif ( $decl == "END" ) {
+                               if ( count( $results ) == 0 ) {
+                                       return false;
+                               }
+
+                               /**
+                                * All data has been read, time to process the data and build
+                                * meaningful return values.
+                                */
+                               foreach ( $results as $vars ) {
+                                       list( $rkey, $flags, $len, $casToken, $data ) = $vars;
+
+                                       if ( $data === false || substr( $data, -2 ) !== "\r\n" ) {
+                                               $this->_handle_error( $sock,
+                                                       'line ending missing from data block from $1' );
+                                               return false;
+                                       }
+                                       $data = substr( $data, 0, -2 );
+                                       $ret[$rkey] = $data;
+
+                                       if ( $this->_have_zlib && $flags & self::COMPRESSED ) {
+                                               $ret[$rkey] = gzuncompress( $ret[$rkey] );
+                                       }
+
+                                       /*
+                                        * This unserialize is the exact reason that we only want to
+                                        * process data after having read until "END" (instead of doing
+                                        * this right away): "unserialize" can trigger outside code:
+                                        * in the event that $ret[$rkey] is a serialized object,
+                                        * unserializing it will trigger __wakeup() if present. If that
+                                        * function attempted to read from memcached (while we did not
+                                        * yet read "END"), these 2 calls would collide.
+                                        */
+                                       if ( $flags & self::SERIALIZED ) {
+                                               $ret[$rkey] = $this->unserialize( $ret[$rkey] );
+                                       } elseif ( $flags & self::INTVAL ) {
+                                               $ret[$rkey] = intval( $ret[$rkey] );
+                                       }
+                               }
+
+                               return true;
+                       } else {
+                               $this->_handle_error( $sock, 'Error parsing response from $1' );
+                               return false;
+                       }
+               }
+       }
+
+       // }}}
+       // {{{ _set()
+
+       /**
+        * Performs the requested storage operation to the memcache server
+        *
+        * @param string $cmd Command to perform
+        * @param string $key Key to act on
+        * @param mixed $val What we need to store
+        * @param int $exp (optional) Expiration time. This can be a number of seconds
+        * to cache for (up to 30 days inclusive).  Any timespans of 30 days + 1 second or
+        * longer must be the timestamp of the time at which the mapping should expire. It
+        * is safe to use timestamps in all cases, regardless of exipration
+        * eg: strtotime("+3 hour")
+        * @param float $casToken [optional]
+        *
+        * @return bool
+        * @access private
+        */
+       function _set( $cmd, $key, $val, $exp, $casToken = null ) {
+               if ( !$this->_active ) {
+                       return false;
+               }
+
+               $sock = $this->get_sock( $key );
+               if ( !is_resource( $sock ) ) {
+                       return false;
+               }
+
+               if ( isset( $this->stats[$cmd] ) ) {
+                       $this->stats[$cmd]++;
+               } else {
+                       $this->stats[$cmd] = 1;
+               }
+
+               $flags = 0;
+
+               if ( is_int( $val ) ) {
+                       $flags |= self::INTVAL;
+               } elseif ( !is_scalar( $val ) ) {
+                       $val = $this->serialize( $val );
+                       $flags |= self::SERIALIZED;
+                       if ( $this->_debug ) {
+                               $this->_debugprint( sprintf( "client: serializing data as it is not scalar" ) );
+                       }
+               }
+
+               $len = strlen( $val );
+
+               if ( $this->_have_zlib && $this->_compress_enable
+                       && $this->_compress_threshold && $len >= $this->_compress_threshold
+               ) {
+                       $c_val = gzcompress( $val, 9 );
+                       $c_len = strlen( $c_val );
+
+                       if ( $c_len < $len * ( 1 - self::COMPRESSION_SAVINGS ) ) {
+                               if ( $this->_debug ) {
+                                       $this->_debugprint( sprintf( "client: compressing data; was %d bytes is now %d bytes", $len, $c_len ) );
+                               }
+                               $val = $c_val;
+                               $len = $c_len;
+                               $flags |= self::COMPRESSED;
+                       }
+               }
+
+               $command = "$cmd $key $flags $exp $len";
+               if ( $casToken ) {
+                       $command .= " $casToken";
+               }
+
+               if ( !$this->_fwrite( $sock, "$command\r\n$val\r\n" ) ) {
+                       return false;
+               }
+
+               $line = $this->_fgets( $sock );
+
+               if ( $this->_debug ) {
+                       $this->_debugprint( sprintf( "%s %s (%s)", $cmd, $key, $line ) );
+               }
+               if ( $line === "STORED" ) {
+                       return true;
+               } elseif ( $line === "NOT_STORED" && $cmd === "set" ) {
+                       // "Not stored" is always used as the mcrouter response with AllAsyncRoute
+                       return true;
+               }
+
+               return false;
+       }
+
+       // }}}
+       // {{{ sock_to_host()
+
+       /**
+        * Returns the socket for the host
+        *
+        * @param string $host Host:IP to get socket for
+        *
+        * @return Resource|bool IO Stream or false
+        * @access private
+        */
+       function sock_to_host( $host ) {
+               if ( isset( $this->_cache_sock[$host] ) ) {
+                       return $this->_cache_sock[$host];
+               }
+
+               $sock = null;
+               $now = time();
+               list( $ip, /* $port */) = explode( ':', $host );
+               if ( isset( $this->_host_dead[$host] ) && $this->_host_dead[$host] > $now ||
+                       isset( $this->_host_dead[$ip] ) && $this->_host_dead[$ip] > $now
+               ) {
+                       return null;
+               }
+
+               if ( !$this->_connect_sock( $sock, $host ) ) {
+                       return null;
+               }
+
+               // Do not buffer writes
+               stream_set_write_buffer( $sock, 0 );
+
+               $this->_cache_sock[$host] = $sock;
+
+               return $this->_cache_sock[$host];
+       }
+
+       /**
+        * @param string $text
+        */
+       function _debugprint( $text ) {
+               $this->_logger->debug( $text );
+       }
+
+       /**
+        * @param string $text
+        */
+       function _error_log( $text ) {
+               $this->_logger->error( "Memcached error: $text" );
+       }
+
+       /**
+        * Write to a stream. If there is an error, mark the socket dead.
+        *
+        * @param Resource $sock The socket
+        * @param string $buf The string to write
+        * @return bool True on success, false on failure
+        */
+       function _fwrite( $sock, $buf ) {
+               $bytesWritten = 0;
+               $bufSize = strlen( $buf );
+               while ( $bytesWritten < $bufSize ) {
+                       $result = fwrite( $sock, $buf );
+                       $data = stream_get_meta_data( $sock );
+                       if ( $data['timed_out'] ) {
+                               $this->_handle_error( $sock, 'timeout writing to $1' );
+                               return false;
+                       }
+                       // Contrary to the documentation, fwrite() returns zero on error in PHP 5.3.
+                       if ( $result === false || $result === 0 ) {
+                               $this->_handle_error( $sock, 'error writing to $1' );
+                               return false;
+                       }
+                       $bytesWritten += $result;
+               }
+
+               return true;
+       }
+
+       /**
+        * Handle an I/O error. Mark the socket dead and log an error.
+        *
+        * @param Resource $sock
+        * @param string $msg
+        */
+       function _handle_error( $sock, $msg ) {
+               $peer = stream_socket_get_name( $sock, true /** remote **/ );
+               if ( strval( $peer ) === '' ) {
+                       $peer = array_search( $sock, $this->_cache_sock );
+                       if ( $peer === false ) {
+                               $peer = '[unknown host]';
+                       }
+               }
+               $msg = str_replace( '$1', $peer, $msg );
+               $this->_error_log( "$msg" );
+               $this->_dead_sock( $sock );
+       }
+
+       /**
+        * Read the specified number of bytes from a stream. If there is an error,
+        * mark the socket dead.
+        *
+        * @param Resource $sock The socket
+        * @param int $len The number of bytes to read
+        * @return string|bool The string on success, false on failure.
+        */
+       function _fread( $sock, $len ) {
+               $buf = '';
+               while ( $len > 0 ) {
+                       $result = fread( $sock, $len );
+                       $data = stream_get_meta_data( $sock );
+                       if ( $data['timed_out'] ) {
+                               $this->_handle_error( $sock, 'timeout reading from $1' );
+                               return false;
+                       }
+                       if ( $result === false ) {
+                               $this->_handle_error( $sock, 'error reading buffer from $1' );
+                               return false;
+                       }
+                       if ( $result === '' ) {
+                               // This will happen if the remote end of the socket is shut down
+                               $this->_handle_error( $sock, 'unexpected end of file reading from $1' );
+                               return false;
+                       }
+                       $len -= strlen( $result );
+                       $buf .= $result;
+               }
+               return $buf;
+       }
+
+       /**
+        * Read a line from a stream. If there is an error, mark the socket dead.
+        * The \r\n line ending is stripped from the response.
+        *
+        * @param Resource $sock The socket
+        * @return string|bool The string on success, false on failure
+        */
+       function _fgets( $sock ) {
+               $result = fgets( $sock );
+               // fgets() may return a partial line if there is a select timeout after
+               // a successful recv(), so we have to check for a timeout even if we
+               // got a string response.
+               $data = stream_get_meta_data( $sock );
+               if ( $data['timed_out'] ) {
+                       $this->_handle_error( $sock, 'timeout reading line from $1' );
+                       return false;
+               }
+               if ( $result === false ) {
+                       $this->_handle_error( $sock, 'error reading line from $1' );
+                       return false;
+               }
+               if ( substr( $result, -2 ) === "\r\n" ) {
+                       $result = substr( $result, 0, -2 );
+               } elseif ( substr( $result, -1 ) === "\n" ) {
+                       $result = substr( $result, 0, -1 );
+               } else {
+                       $this->_handle_error( $sock, 'line ending missing in response from $1' );
+                       return false;
+               }
+               return $result;
+       }
+
+       /**
+        * Flush the read buffer of a stream
+        * @param Resource $f
+        */
+       function _flush_read_buffer( $f ) {
+               if ( !is_resource( $f ) ) {
+                       return;
+               }
+               $r = array( $f );
+               $w = null;
+               $e = null;
+               $n = stream_select( $r, $w, $e, 0, 0 );
+               while ( $n == 1 && !feof( $f ) ) {
+                       fread( $f, 1024 );
+                       $r = array( $f );
+                       $w = null;
+                       $e = null;
+                       $n = stream_select( $r, $w, $e, 0, 0 );
+               }
+       }
+
+       // }}}
+       // }}}
+       // }}}
+}
+
+// }}}
index 1852685..b88b496 100644 (file)
@@ -506,6 +506,7 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                        }
                        $purgeValues[] = $purge;
                }
+
                return $purgeValues;
        }
 
@@ -2207,14 +2208,14 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
                        // Wildcards select all matching routes, e.g. the WAN cluster on all DCs
                        $ok = $this->cache->set(
                                "/*/{$this->cluster}/{$key}",
-                               $this->makePurgeValue( $this->getCurrentTime(), self::HOLDOFF_TTL_NONE ),
+                               $this->makePurgeValue( $this->getCurrentTime(), $holdoff ),
                                $ttl
                        );
                } else {
-                       // This handles the mcrouter and the single-DC case
+                       // Some other proxy handles broadcasting or there is only one datacenter
                        $ok = $this->cache->set(
                                $key,
-                               $this->makePurgeValue( $this->getCurrentTime(), self::HOLDOFF_TTL_NONE ),
+                               $this->makePurgeValue( $this->getCurrentTime(), $holdoff ),
                                $ttl
                        );
                }
index 8615cfc..e1398b8 100644 (file)
@@ -224,10 +224,9 @@ class ChronologyProtector implements LoggerAwareInterface {
                        implode( ', ', array_keys( $this->shutdownPositions ) ) . "\n"
                );
 
-               // CP-protected writes should overwhelmingly go to the master datacenter, so use a
-               // DC-local lock to merge the values. Use a DC-local get() and a synchronous all-DC
-               // set(). This makes it possible for the BagOStuff class to write in parallel to all
-               // DCs with one RTT. The use of WRITE_SYNC avoids needing READ_LATEST for the get().
+               // CP-protected writes should overwhelmingly go to the master datacenter, so merge the
+               // positions with a DC-local lock, a DC-local get(), and an all-DC set() with WRITE_SYNC.
+               // If set() returns success, then any get() should be able to see the new positions.
                if ( $store->lock( $this->key, 3 ) ) {
                        if ( $workCallback ) {
                                // Let the store run the work before blocking on a replication sync barrier.
index 0e08044..084500a 100644 (file)
@@ -47,37 +47,6 @@ use Throwable;
  * @since 1.28
  */
 abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAwareInterface {
-       /** @var string Server that this instance is currently connected to */
-       protected $server;
-       /** @var string User that this instance is currently connected under the name of */
-       protected $user;
-       /** @var string Password used to establish the current connection */
-       protected $password;
-       /** @var array[] Map of (table => (dbname, schema, prefix) map) */
-       protected $tableAliases = [];
-       /** @var string[] Map of (index alias => index) */
-       protected $indexAliases = [];
-       /** @var bool Whether this PHP instance is for a CLI script */
-       protected $cliMode;
-       /** @var string Agent name for query profiling */
-       protected $agent;
-       /** @var int Bit field of class DBO_* constants */
-       protected $flags;
-       /** @var array LoadBalancer tracking information */
-       protected $lbInfo = [];
-       /** @var array|bool Variables use for schema element placeholders */
-       protected $schemaVars = false;
-       /** @var array Parameters used by initConnection() to establish a connection */
-       protected $connectionParams = [];
-       /** @var array SQL variables values to use for all new connections */
-       protected $connectionVariables = [];
-       /** @var string Current SQL query delimiter */
-       protected $delimiter = ';';
-       /** @var string|bool|null Stashed value of html_errors INI setting */
-       protected $htmlErrors;
-       /** @var int Row batch size to use for emulated INSERT SELECT queries */
-       protected $nonNativeInsertSelectBatchSize = 10000;
-
        /** @var BagOStuff APC cache */
        protected $srvCache;
        /** @var LoggerInterface */
@@ -92,25 +61,62 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        protected $profiler;
        /** @var TransactionProfiler */
        protected $trxProfiler;
+
        /** @var DatabaseDomain */
        protected $currentDomain;
+
        /** @var object|resource|null Database connection */
        protected $conn;
 
        /** @var IDatabase|null Lazy handle to the master DB this server replicates from */
        private $lazyMasterHandle;
 
+       /** @var string Server that this instance is currently connected to */
+       protected $server;
+       /** @var string User that this instance is currently connected under the name of */
+       protected $user;
+       /** @var string Password used to establish the current connection */
+       protected $password;
+       /** @var bool Whether this PHP instance is for a CLI script */
+       protected $cliMode;
+       /** @var string Agent name for query profiling */
+       protected $agent;
+       /** @var array Parameters used by initConnection() to establish a connection */
+       protected $connectionParams;
+       /** @var string[]|int[]|float[] SQL variables values to use for all new connections */
+       protected $connectionVariables;
+       /** @var int Row batch size to use for emulated INSERT SELECT queries */
+       protected $nonNativeInsertSelectBatchSize;
+
+       /** @var int Current bit field of class DBO_* constants */
+       protected $flags;
+       /** @var array Current LoadBalancer tracking information */
+       protected $lbInfo = [];
+       /** @var string Current SQL query delimiter */
+       protected $delimiter = ';';
+       /** @var array[] Current map of (table => (dbname, schema, prefix) map) */
+       protected $tableAliases = [];
+       /** @var string[] Current map of (index alias => index) */
+       protected $indexAliases = [];
+       /** @var array|null Current variables use for schema element placeholders */
+       protected $schemaVars;
+
+       /** @var string|bool|null Stashed value of html_errors INI setting */
+       private $htmlErrors;
+       /** @var int[] Prior flags member variable values */
+       private $priorFlags = [];
+
        /** @var array Map of (name => 1) for locks obtained via lock() */
        protected $sessionNamedLocks = [];
        /** @var array Map of (table name => 1) for TEMPORARY tables */
        protected $sessionTempTables = [];
 
        /** @var string ID of the active transaction or the empty string otherwise */
-       protected $trxShortId = '';
+       private $trxShortId = '';
        /** @var int Transaction status */
-       protected $trxStatus = self::STATUS_TRX_NONE;
+       private $trxStatus = self::STATUS_TRX_NONE;
        /** @var Exception|null The last error that caused the status to become STATUS_TRX_ERROR */
-       protected $trxStatusCause;
+       private $trxStatusCause;
        /** @var array|null Error details of the last statement-only rollback */
        private $trxStatusIgnoredCause;
        /** @var float|null UNIX timestamp at the time of BEGIN for the last transaction */
@@ -154,9 +160,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        /** @var bool Whether to suppress triggering of transaction end callbacks */
        private $trxEndCallbacksSuppressed = false;
 
-       /** @var int[] Prior flags member variable values */
-       private $priorFlags = [];
-
        /** @var integer|null Rows affected by the last query to query() or its CRUD wrappers */
        protected $affectedRowCount;
 
@@ -233,15 +236,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * @note exceptions for missing libraries/drivers should be thrown in initConnection()
         * @param array $params Parameters passed from Database::factory()
         */
-       protected function __construct( array $params ) {
+       public function __construct( array $params ) {
+               $this->connectionParams = [];
                foreach ( [ 'host', 'user', 'password', 'dbname', 'schema', 'tablePrefix' ] as $name ) {
                        $this->connectionParams[$name] = $params[$name];
                }
-
+               $this->connectionVariables = $params['variables'] ?? [];
                $this->cliMode = $params['cliMode'];
-               // Agent name is added to SQL queries in a comment, so make sure it can't break out
-               $this->agent = str_replace( '/', '-', $params['agent'] );
-
+               $this->agent = $params['agent'];
                $this->flags = $params['flags'];
                if ( $this->flags & self::DBO_DEFAULT ) {
                        if ( $this->cliMode ) {
@@ -250,11 +252,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                $this->flags |= self::DBO_TRX;
                        }
                }
-
-               $this->connectionVariables = $params['variables'];
+               $this->nonNativeInsertSelectBatchSize = $params['nonNativeInsertSelectBatchSize'] ?? 10000;
 
                $this->srvCache = $params['srvCache'] ?? new HashBagOStuff();
-
                $this->profiler = is_callable( $params['profiler'] ) ? $params['profiler'] : null;
                $this->trxProfiler = $params['trxProfiler'];
                $this->connLogger = $params['connLogger'];
@@ -262,10 +262,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                $this->errorLogger = $params['errorLogger'];
                $this->deprecationLogger = $params['deprecationLogger'];
 
-               if ( isset( $params['nonNativeInsertSelectBatchSize'] ) ) {
-                       $this->nonNativeInsertSelectBatchSize = $params['nonNativeInsertSelectBatchSize'];
-               }
-
                // Set initial dummy domain until open() sets the final DB/prefix
                $this->currentDomain = new DatabaseDomain(
                        $params['dbname'] != '' ? $params['dbname'] : null,
@@ -395,6 +391,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                'cliMode' => (bool)$params['cliMode'],
                                'agent' => (string)$params['agent'],
                                // Objects and callbacks
+                               'srvCache' => $params['srvCache'] ?? new HashBagOStuff(),
                                'profiler' => $params['profiler'] ?? null,
                                'trxProfiler' => $params['trxProfiler'] ?? new TransactionProfiler(),
                                'connLogger' => $params['connLogger'] ?? new NullLogger(),
@@ -491,7 +488,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        /**
-        * @return array Map of (Database::ATTR_* constant => value
+        * @return array Map of (Database::ATTR_* constant => value)
         * @since 1.31
         */
        protected static function getAttributes() {
@@ -965,7 +962,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * @throws DBReadOnlyError
         */
        protected function assertIsWritableMaster() {
-               if ( $this->getLBInfo( 'replica' ) === true ) {
+               if ( $this->getLBInfo( 'replica' ) ) {
                        throw new DBReadOnlyRoleError(
                                $this,
                                'Write operations are not allowed on replica database connections'
@@ -1148,7 +1145,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                // Send the query to the server and fetch any corresponding errors
                list( $ret, $err, $errno, $unignorable ) = $this->executeQuery( $sql, $fname, $flags );
                if ( $ret === false ) {
-                       $ignoreErrors = $this->hasFlags( $flags, self::QUERY_SILENCE_ERRORS );
+                       $ignoreErrors = $this->fieldHasBit( $flags, self::QUERY_SILENCE_ERRORS );
                        // Throw an error unless both the ignore flag was set and a rollback is not needed
                        $this->reportQueryError( $err, $errno, $sql, $fname, $ignoreErrors && !$unignorable );
                }
@@ -1188,11 +1185,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        // Do not treat temporary table writes as "meaningful writes" since they are only
                        // visible to one session and are not permanent. Profile them as reads. Integration
                        // tests can override this behavior via $flags.
-                       $pseudoPermanent = $this->hasFlags( $flags, self::QUERY_PSEUDO_PERMANENT );
+                       $pseudoPermanent = $this->fieldHasBit( $flags, self::QUERY_PSEUDO_PERMANENT );
                        list( $tmpType, $tmpNew, $tmpDel ) = $this->getTempWrites( $sql, $pseudoPermanent );
                        $isPermWrite = ( $tmpType !== self::$TEMP_NORMAL );
                        // DBConnRef uses QUERY_REPLICA_ROLE to enforce the replica role for raw SQL queries
-                       if ( $isPermWrite && $this->hasFlags( $flags, self::QUERY_REPLICA_ROLE ) ) {
+                       if ( $isPermWrite && $this->fieldHasBit( $flags, self::QUERY_REPLICA_ROLE ) ) {
                                throw new DBReadOnlyRoleError( $this, "Cannot write; target role is DB_REPLICA" );
                        }
                } else {
@@ -1203,8 +1200,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                }
 
                // Add trace comment to the begin of the sql string, right after the operator.
-               // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598)
-               $commentedSql = preg_replace( '/\s|$/', " /* $fname {$this->agent} */ ", $sql, 1 );
+               // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598).
+               $encAgent = str_replace( '/', '-', $this->agent );
+               $commentedSql = preg_replace( '/\s|$/', " /* $fname $encAgent */ ", $sql, 1 );
 
                // Send the query to the server and fetch any corresponding errors.
                // This also doubles as a "ping" to see if the connection was dropped.
@@ -1212,7 +1210,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $this->executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags );
 
                // Check if the query failed due to a recoverable connection loss
-               $allowRetry = !$this->hasFlags( $flags, self::QUERY_NO_RETRY );
+               $allowRetry = !$this->fieldHasBit( $flags, self::QUERY_NO_RETRY );
                if ( $ret === false && $recoverableCL && $reconnected && $allowRetry ) {
                        // Silently resend the query to the server since it is safe and possible
                        list( $ret, $err, $errno, $recoverableSR, $recoverableCL ) =
@@ -1283,7 +1281,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        }
                }
 
-               $prefix = !is_null( $this->getLBInfo( 'master' ) ) ? 'query-m: ' : 'query: ';
+               $prefix = $this->getLBInfo( 'master' ) ? 'query-m: ' : 'query: ';
                $generalizedSql = new GeneralizedSql( $sql, $this->trxShortId, $prefix );
 
                $startTime = microtime( true );
@@ -1820,7 +1818,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $this->selectOptionsIncludeLocking( $options ) &&
                        $this->selectFieldsOrOptionsAggregate( $vars, $options )
                ) {
-                       // Some DB types (postgres/oracle) disallow FOR UPDATE with aggregate
+                       // Some DB types (e.g. postgres) disallow FOR UPDATE with aggregate
                        // functions. Discourage use of such queries to encourage compatibility.
                        call_user_func(
                                $this->deprecationLogger,
@@ -4470,7 +4468,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        public function setSchemaVars( $vars ) {
-               $this->schemaVars = $vars;
+               $this->schemaVars = is_array( $vars ) ? $vars : null;
        }
 
        public function sourceStream(
@@ -4622,11 +4620,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * @return array
         */
        protected function getSchemaVars() {
-               if ( $this->schemaVars ) {
-                       return $this->schemaVars;
-               } else {
-                       return $this->getDefaultSchemaVars();
-               }
+               return $this->schemaVars ?? $this->getDefaultSchemaVars();
        }
 
        /**
@@ -4816,8 +4810,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * @param int $field
         * @param int $flags
         * @return bool
+        * @since 1.34
         */
-       protected function hasFlags( $field, $flags ) {
+       final protected function fieldHasBit( $field, $flags ) {
                return ( ( $field & $flags ) === $flags );
        }
 
index ac8c7c3..851a178 100644 (file)
@@ -1056,11 +1056,12 @@ abstract class DatabaseMysqlBase extends Database {
        }
 
        public function serverIsReadOnly() {
-               $flags = self::QUERY_IGNORE_DBO_TRX;
-               $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'read_only'", __METHOD__, $flags );
+               // Avoid SHOW to avoid internal temporary tables
+               $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_SILENCE_ERRORS;
+               $res = $this->query( "SELECT @@GLOBAL.read_only AS Value", __METHOD__, $flags );
                $row = $this->fetchObject( $res );
 
-               return $row ? ( strtolower( $row->Value ) === 'on' ) : false;
+               return $row ? (bool)$row->Value : false;
        }
 
        /**
index b1521dc..2977291 100644 (file)
@@ -57,9 +57,6 @@ class DatabaseSqlite extends Database {
        /** @var array List of shared database already attached to this connection */
        private $alreadyAttached = [];
 
-       /** @var bool Whether full text is enabled */
-       private static $fulltextEnabled = null;
-
        /** @var string[] See https://www.sqlite.org/lang_transaction.html */
        private static $VALID_TRX_MODES = [ '', 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ];
 
@@ -291,8 +288,9 @@ class DatabaseSqlite extends Database {
        }
 
        /**
-        * Attaches external database to our connection, see https://sqlite.org/lang_attach.html
-        * for details.
+        * Attaches external database to the connection handle
+        *
+        * @see https://sqlite.org/lang_attach.html
         *
         * @param string $name Database name to be used in queries like
         *   SELECT foo FROM dbname.table
index 8768213..b4eb89a 100644 (file)
@@ -21,7 +21,7 @@ namespace Wikimedia\Rdbms;
 
 use InvalidArgumentException;
 use Wikimedia\ScopedCallback;
-use RuntimeException;
+use Exception;
 use stdClass;
 
 /**
@@ -99,9 +99,9 @@ interface IDatabase {
        const DBO_DEFAULT = 16;
        /** @var int Use DB persistent connections if possible */
        const DBO_PERSISTENT = 32;
-       /** @var int DBA session mode; mostly for Oracle */
+       /** @var int DBA session mode; was used by Oracle */
        const DBO_SYSDBA = 64;
-       /** @var int Schema file mode; mostly for Oracle */
+       /** @var int Schema file mode; was used by Oracle */
        const DBO_DDLMODE = 128;
        /** @var int Enable SSL/TLS in connection protocol */
        const DBO_SSL = 256;
@@ -130,8 +130,8 @@ interface IDatabase {
        const UNION_DISTINCT = false;
 
        /**
-        * A string describing the current software version, and possibly
-        * other details in a user-friendly way. Will be listed on Special:Version, etc.
+        * Get a human-readable string describing the current software version
+        *
         * Use getServerVersion() to get machine-friendly information.
         *
         * @return string Version information from the database server
@@ -168,34 +168,33 @@ interface IDatabase {
        public function explicitTrxActive();
 
        /**
-        * Assert that all explicit transactions or atomic sections have been closed.
+        * Assert that all explicit transactions or atomic sections have been closed
+        *
         * @throws DBTransactionError
         * @since 1.32
         */
        public function assertNoOpenTransactions();
 
        /**
-        * Get/set the table prefix.
-        * @param string|null $prefix The table prefix to set, or omitted to leave it unchanged.
+        * Get/set the table prefix
+        *
+        * @param string|null $prefix The table prefix to set, or omitted to leave it unchanged
         * @return string The previous table prefix
-        * @throws DBUnexpectedError
         */
        public function tablePrefix( $prefix = null );
 
        /**
-        * Get/set the db schema.
-        * @param string|null $schema The database schema to set, or omitted to leave it unchanged.
+        * Get/set the db schema
+        *
+        * @param string|null $schema The database schema to set, or omitted to leave it unchanged
         * @return string The previous db schema
         */
        public function dbSchema( $schema = null );
 
        /**
-        * Get properties passed down from the server info array of the load
-        * balancer.
-        *
-        * @param string|null $name The entry of the info array to get, or null to get the
-        *   whole array
+        * Get properties passed down from the server info array of the load balancer
         *
+        * @param string|null $name The entry of the info array to get, or null to get the whole array
         * @return array|mixed|null
         */
        public function getLBInfo( $name = null );
@@ -225,14 +224,14 @@ interface IDatabase {
        public function implicitOrderby();
 
        /**
-        * Return the last query that sent on account of IDatabase::query()
+        * Get the last query that sent on account of IDatabase::query()
+        *
         * @return string SQL text or empty string if there was no such query
         */
        public function lastQuery();
 
        /**
-        * Returns the last time the connection may have been used for write queries.
-        * Should return a timestamp if unsure.
+        * Get the last time the connection may have been used for a write query
         *
         * @return int|float UNIX timestamp or false
         * @since 1.24
@@ -264,7 +263,7 @@ interface IDatabase {
        /**
         * Get the time spend running write queries for this transaction
         *
-        * High times could be due to scanning, updates, locking, and such
+        * High values could be due to scanning, updates, locking, and such.
         *
         * @param string $type IDatabase::ESTIMATE_* constant [default: ESTIMATE_ALL]
         * @return float|bool Returns false if not transaction is active
@@ -289,8 +288,7 @@ interface IDatabase {
        public function pendingWriteRowsAffected();
 
        /**
-        * Is a connection to the database open?
-        * @return bool
+        * @return bool Whether a connection to the database open
         */
        public function isOpen();
 
@@ -336,38 +334,38 @@ interface IDatabase {
        public function getDomainID();
 
        /**
-        * Get the type of the DBMS, as it appears in $wgDBtype.
+        * Get the type of the DBMS (e.g. "mysql", "sqlite")
         *
         * @return string
         */
        public function getType();
 
        /**
-        * Fetch the next row from the given result object, in object form.
+        * Fetch the next row from the given result object, in object form
+        *
         * Fields can be retrieved with $row->fieldname, with fields acting like
-        * member variables.
-        * If no more rows are available, false is returned.
+        * member variables. If no more rows are available, false is returned.
         *
         * @param IResultWrapper|stdClass $res Object as returned from IDatabase::query(), etc.
         * @return stdClass|bool
-        * @throws DBUnexpectedError Thrown if the database returns an error
         */
        public function fetchObject( $res );
 
        /**
-        * Fetch the next row from the given result object, in associative array
-        * form. Fields are retrieved with $row['fieldname'].
+        * Fetch the next row from the given result object, in associative array form
+        *
+        * Fields are retrieved with $row['fieldname'].
         * If no more rows are available, false is returned.
         *
         * @param IResultWrapper $res Result object as returned from IDatabase::query(), etc.
         * @return array|bool
-        * @throws DBUnexpectedError Thrown if the database returns an error
         */
        public function fetchRow( $res );
 
        /**
-        * Get the number of rows in a query result. If the query did not return
-        * any rows (for example, if it was a write query), this returns zero.
+        * Get the number of rows in a query result
+        *
+        * Returns zero if the query did not return any rows or was a write query.
         *
         * @param mixed $res A SQL result
         * @return int
@@ -440,18 +438,16 @@ interface IDatabase {
        public function affectedRows();
 
        /**
-        * Returns a wikitext link to the DB's website, e.g.,
-        *   return "[https://www.mysql.com/ MySQL]";
-        * Should at least contain plain text, if for some reason
-        * your database has no website.
+        * Returns a wikitext style link to the DB's website (e.g. "[https://www.mysql.com/ MySQL]")
+        *
+        * Should at least contain plain text, if for some reason your database has no website.
         *
         * @return string Wikitext of a link to the server software's web site
         */
        public function getSoftwareLink();
 
        /**
-        * A string describing the current software version, like from
-        * mysql_get_server_info().
+        * A string describing the current software version, like from mysql_get_server_info()
         *
         * @return string Version information from the database server.
         */
@@ -464,14 +460,13 @@ interface IDatabase {
         * aside from read-only automatic transactions (assuming no callbacks are registered).
         * If a transaction is still open anyway, it will be rolled back.
         *
+        * @return bool Success
         * @throws DBError
-        * @return bool Operation success. true if already closed.
         */
        public function close();
 
        /**
-        * Run an SQL query and return the result. Normally throws a DBQueryError
-        * on failure. If errors are ignored, returns false instead.
+        * Run an SQL query and return the result
         *
         * If a connection loss is detected, then an attempt to reconnect will be made.
         * For queries that involve no larger transactions or locks, they will be re-issued
@@ -493,24 +488,24 @@ interface IDatabase {
         *     of errors is best handled by try/catch rather than using one of these flags.
         * @return bool|IResultWrapper True for a successful write query, IResultWrapper object
         *     for a successful read query, or false on failure if QUERY_SILENCE_ERRORS is set.
-        * @throws DBError
+        * @throws DBQueryError If the query is issued, fails, and QUERY_SILENCE_ERRORS is not set.
+        * @throws DBExpectedError If the query is not, and cannot, be issued yet (non-DBQueryError)
+        * @throws DBError If the query is inherently not allowed (non-DBExpectedError)
         */
        public function query( $sql, $fname = __METHOD__, $flags = 0 );
 
        /**
-        * Free a result object returned by query() or select(). It's usually not
-        * necessary to call this, just use unset() or let the variable holding
-        * the result object go out of scope.
+        * Free a result object returned by query() or select()
+        *
+        * It's usually not necessary to call this, just use unset() or let the variable
+        * holding the result object go out of scope.
         *
         * @param mixed $res A SQL result
         */
        public function freeResult( $res );
 
        /**
-        * A SELECT wrapper which returns a single field from a single result row.
-        *
-        * Usually throws a DBQueryError on failure. If errors are explicitly
-        * ignored, returns false on failure.
+        * A SELECT wrapper which returns a single field from a single result row
         *
         * If no result rows are returned from the query, false is returned.
         *
@@ -521,19 +516,15 @@ interface IDatabase {
         * @param string $fname The function name of the caller.
         * @param string|array $options The query options. See IDatabase::select() for details.
         * @param string|array $join_conds The query join conditions. See IDatabase::select() for details.
-        *
         * @return mixed The value from the field
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         */
        public function selectField(
                $table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
        );
 
        /**
-        * A SELECT wrapper which returns a list of single field values from result rows.
-        *
-        * Usually throws a DBQueryError on failure. If errors are explicitly
-        * ignored, returns false on failure.
+        * A SELECT wrapper which returns a list of single field values from result rows
         *
         * If no result rows are returned from the query, false is returned.
         *
@@ -546,7 +537,7 @@ interface IDatabase {
         * @param string|array $join_conds The query join conditions. See IDatabase::select() for details.
         *
         * @return array The values from the field in the order they were returned from the DB
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         * @since 1.25
         */
        public function selectFieldValues(
@@ -554,8 +545,7 @@ interface IDatabase {
        );
 
        /**
-        * Execute a SELECT query constructed using the various parameters provided.
-        * See below for full details of the parameters.
+        * Execute a SELECT query constructed using the various parameters provided
         *
         * @param string|array $table Table name(s)
         *
@@ -712,18 +702,23 @@ interface IDatabase {
         *    [ 'page' => [ 'LEFT JOIN', 'page_latest=rev_id' ] ]
         *
         * @return IResultWrapper Resulting rows
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         */
        public function select(
-               $table, $vars, $conds = '', $fname = __METHOD__,
-               $options = [], $join_conds = []
+               $table,
+               $vars,
+               $conds = '',
+               $fname = __METHOD__,
+               $options = [],
+               $join_conds = []
        );
 
        /**
-        * The equivalent of IDatabase::select() except that the constructed SQL
-        * is returned, instead of being immediately executed. This can be useful for
-        * doing UNION queries, where the SQL text of each query is needed. In general,
-        * however, callers outside of Database classes should just use select().
+        * Take the same arguments as IDatabase::select() and return the SQL it would use
+        *
+        * This can be useful for making UNION queries, where the SQL text of each query
+        * is needed. In general, however, callers outside of Database classes should just
+        * use select().
         *
         * @see IDatabase::select()
         *
@@ -736,14 +731,20 @@ interface IDatabase {
         * @return string SQL query string
         */
        public function selectSQLText(
-               $table, $vars, $conds = '', $fname = __METHOD__,
-               $options = [], $join_conds = []
+               $table,
+               $vars,
+               $conds = '',
+               $fname = __METHOD__,
+               $options = [],
+               $join_conds = []
        );
 
        /**
-        * Single row SELECT wrapper. Equivalent to IDatabase::select(), except
-        * that a single row object is returned. If the query returns no rows,
-        * false is returned.
+        * Wrapper to IDatabase::select() that only fetches one row (via LIMIT)
+        *
+        * If the query returns no rows, false is returned.
+        *
+        * This method is convenient for fetching a row based on a unique key condition.
         *
         * @param string|array $table Table name
         * @param string|array $vars Field names
@@ -751,12 +752,16 @@ interface IDatabase {
         * @param string $fname Caller function name
         * @param string|array $options Query options
         * @param array|string $join_conds Join conditions
-        *
         * @return stdClass|bool
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         */
-       public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
-               $options = [], $join_conds = []
+       public function selectRow(
+               $table,
+               $vars,
+               $conds,
+               $fname = __METHOD__,
+               $options = [],
+               $join_conds = []
        );
 
        /**
@@ -779,7 +784,7 @@ interface IDatabase {
         * @param array $options Options for select
         * @param array|string $join_conds Join conditions
         * @return int Row count
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         */
        public function estimateRowCount(
                $table, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
@@ -801,7 +806,7 @@ interface IDatabase {
         * @param array $options Options for select
         * @param array $join_conds Join conditions (since 1.27)
         * @return int Row count
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         */
        public function selectRowCount(
                $tables, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
@@ -816,6 +821,7 @@ interface IDatabase {
         * @param array $options Options for select ("FOR UPDATE" is added automatically)
         * @param array $join_conds Join conditions
         * @return int Number of matching rows found (and locked)
+        * @throws DBError If an error occurs, see IDatabase::query()
         * @since 1.32
         */
        public function lockForUpdate(
@@ -829,20 +835,18 @@ interface IDatabase {
         * @param string $field Filed to check on that table
         * @param string $fname Calling function name (optional)
         * @return bool Whether $table has filed $field
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         */
        public function fieldExists( $table, $field, $fname = __METHOD__ );
 
        /**
         * Determines whether an index exists
-        * Usually throws a DBQueryError on failure
-        * If errors are explicitly ignored, returns NULL on failure
         *
         * @param string $table
         * @param string $index
         * @param string $fname
         * @return bool|null
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         */
        public function indexExists( $table, $index, $fname = __METHOD__ );
 
@@ -852,12 +856,12 @@ interface IDatabase {
         * @param string $table
         * @param string $fname
         * @return bool
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         */
        public function tableExists( $table, $fname = __METHOD__ );
 
        /**
-        * INSERT wrapper, inserts an array into a table.
+        * INSERT wrapper, inserts an array into a table
         *
         * $a may be either:
         *
@@ -869,9 +873,6 @@ interface IDatabase {
         *     This causes a multi-row INSERT on DBMSs that support it. The keys in
         *     each subarray must be identical to each other, and in the same order.
         *
-        * Usually throws a DBQueryError on failure. If errors are explicitly ignored,
-        * returns success.
-        *
         * $options is an array of options, with boolean options encoded as values
         * with numeric keys, in the same style as $options in
         * IDatabase::select(). Supported options are:
@@ -887,7 +888,7 @@ interface IDatabase {
         * @param string $fname Calling function name (use __METHOD__) for logs/profiling
         * @param array $options Array of options
         * @return bool Return true if no exception was thrown (deprecated since 1.33)
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         */
        public function insert( $table, $a, $fname = __METHOD__, $options = [] );
 
@@ -909,7 +910,7 @@ interface IDatabase {
         * @param array $options An array of UPDATE options, can be:
         *   - IGNORE: Ignore unique key conflicts
         * @return bool Return true if no exception was thrown (deprecated since 1.33)
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         */
        public function update( $table, $values, $conds, $fname = __METHOD__, $options = [] );
 
@@ -935,7 +936,7 @@ interface IDatabase {
         *    - IDatabase::LIST_OR:    ORed WHERE clause (without the WHERE)
         *    - IDatabase::LIST_SET:   Comma separated with field names, like a SET clause
         *    - IDatabase::LIST_NAMES: Comma separated field names
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         * @return string
         */
        public function makeList( $a, $mode = self::LIST_COMMA );
@@ -985,8 +986,7 @@ interface IDatabase {
 
        /**
         * Build a concatenation list to feed into a SQL query
-        * @param array $stringList List of raw SQL expressions; caller is
-        *   responsible for any quoting
+        * @param string[] $stringList Raw SQL expression list; caller is responsible for escaping
         * @return string
         */
        public function buildConcat( $stringList );
@@ -1012,7 +1012,7 @@ interface IDatabase {
        );
 
        /**
-        * Build a SUBSTRING function.
+        * Build a SUBSTRING function
         *
         * Behavior for non-ASCII values is undefined.
         *
@@ -1054,13 +1054,18 @@ interface IDatabase {
         * @since 1.31
         */
        public function buildSelectSubquery(
-               $table, $vars, $conds = '', $fname = __METHOD__,
-               $options = [], $join_conds = []
+               $table,
+               $vars,
+               $conds = '',
+               $fname = __METHOD__,
+               $options = [],
+               $join_conds = []
        );
 
        /**
-        * Construct a LIMIT query with optional offset. This is used for query
-        * pages. The SQL should be adjusted so that only the first $limit rows
+        * Construct a LIMIT query with optional offset
+        *
+        * The SQL should be adjusted so that only the first $limit rows
         * are returned. If $offset is provided as well, then the first $offset
         * rows should be discarded, and the next $limit rows should be returned.
         * If the result of the query is not ordered, then the rows to be returned
@@ -1071,7 +1076,6 @@ interface IDatabase {
         * @param string $sql SQL query we will append the limit too
         * @param int $limit The SQL limit
         * @param int|bool $offset The SQL offset (default false)
-        * @throws DBUnexpectedError
         * @return string
         * @since 1.34
         */
@@ -1130,7 +1134,7 @@ interface IDatabase {
        public function getServer();
 
        /**
-        * Adds quotes and backslashes.
+        * Escape and quote a raw value string for use in a SQL query
         *
         * @param string|int|null|bool|Blob $s
         * @return string|int
@@ -1138,7 +1142,7 @@ interface IDatabase {
        public function addQuotes( $s );
 
        /**
-        * Quotes an identifier, in order to make user controlled input safe
+        * Escape a SQL identifier (e.g. table, column, database) for use in a SQL query
         *
         * Depending on the database this will either be `backticks` or "double quotes"
         *
@@ -1149,11 +1153,12 @@ interface IDatabase {
        public function addIdentifierQuotes( $s );
 
        /**
-        * LIKE statement wrapper, receives a variable-length argument list with
-        * parts of pattern to match containing either string literals that will be
-        * escaped or tokens returned by anyChar() or anyString(). Alternatively,
-        * the function could be provided with an array of aforementioned
-        * parameters.
+        * LIKE statement wrapper
+        *
+        * This takes a variable-length argument list with parts of pattern to match
+        * containing either string literals that will be escaped or tokens returned by
+        * anyChar() or anyString(). Alternatively, the function could be provided with
+        * an array of aforementioned parameters.
         *
         * Example: $dbr->buildLike( 'My_page_title/', $dbr->anyString() ) returns
         * a LIKE clause that searches for subpages of 'My page title'.
@@ -1184,12 +1189,12 @@ interface IDatabase {
        public function anyString();
 
        /**
-        * Deprecated method, calls should be removed.
+        * Deprecated method, calls should be removed
         *
-        * This was formerly used for PostgreSQL and Oracle to handle
+        * This was formerly used for PostgreSQL to handle
         * self::insertId() auto-incrementing fields. It is no longer necessary
         * since DatabasePostgres::insertId() has been reimplemented using
-        * `lastval()` and Oracle has been reimplemented using triggers.
+        * `lastval()`
         *
         * Implementations should return null if inserting `NULL` into an
         * auto-incrementing field works, otherwise it should return an instance of
@@ -1202,7 +1207,7 @@ interface IDatabase {
        public function nextSequenceValue( $seqName );
 
        /**
-        * REPLACE query wrapper.
+        * REPLACE query wrapper
         *
         * REPLACE is a very handy MySQL extension, which functions like an INSERT
         * except that when there is a duplicate key error, the old row is deleted
@@ -1224,7 +1229,7 @@ interface IDatabase {
         * @param array $rows Can be either a single row to insert, or multiple rows,
         *   in the same format as for IDatabase::insert()
         * @param string $fname Calling function name (use __METHOD__) for logs/profiling
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         */
        public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ );
 
@@ -1247,11 +1252,6 @@ interface IDatabase {
         * to collide. However if you do this, you run the risk of encountering
         * errors which wouldn't have occurred in MySQL.
         *
-        * Usually throws a DBQueryError on failure. If errors are explicitly ignored,
-        * returns success.
-        *
-        * @since 1.22
-        *
         * @param string $table Table name. This will be passed through Database::tableName().
         * @param array $rows A single row or list of rows to insert
         * @param array[]|string[]|string $uniqueIndexes All unique indexes. One of the following:
@@ -1264,8 +1264,9 @@ interface IDatabase {
         *   Values with integer keys form unquoted SET statements, which can be used for
         *   things like "field = field + 1" or similar computed values.
         * @param string $fname Calling function name (use __METHOD__) for logs/profiling
-        * @throws DBError
         * @return bool Return true if no exception was thrown (deprecated since 1.33)
+        * @throws DBError If an error occurs, see IDatabase::query()
+        * @since 1.22
         */
        public function upsert(
                $table, array $rows, $uniqueIndexes, array $set, $fname = __METHOD__
@@ -1289,28 +1290,31 @@ interface IDatabase {
         * @param array $conds Condition array of field names mapped to variables,
         *   ANDed together in the WHERE clause
         * @param string $fname Calling function name (use __METHOD__) for logs/profiling
-        * @throws DBError
-        */
-       public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
+        * @throws DBError If an error occurs, see IDatabase::query()
+        */
+       public function deleteJoin(
+               $delTable,
+               $joinTable,
+               $delVar,
+               $joinVar,
+               $conds,
                $fname = __METHOD__
        );
 
        /**
-        * DELETE query wrapper.
+        * DELETE query wrapper
         *
         * @param string $table Table name
         * @param string|array $conds Array of conditions. See $conds in IDatabase::select()
         *   for the format. Use $conds == "*" to delete all rows
         * @param string $fname Name of the calling function
-        * @throws DBUnexpectedError
         * @return bool Return true if no exception was thrown (deprecated since 1.33)
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         */
        public function delete( $table, $conds, $fname = __METHOD__ );
 
        /**
-        * INSERT SELECT wrapper. Takes data from a SELECT query and inserts it
-        * into another table.
+        * INSERT SELECT wrapper
         *
         * @warning If the insert will use an auto-increment or sequence to
         *  determine the value of a column, this may break replication on
@@ -1320,18 +1324,14 @@ interface IDatabase {
         * @param string $destTable The table name to insert into
         * @param string|array $srcTable May be either a table name, or an array of table names
         *    to include in a join.
-        *
         * @param array $varMap Must be an associative array of the form
         *    [ 'dest1' => 'source1', ... ]. Source items may be literals
         *    rather than field names, but strings should be quoted with
         *    IDatabase::addQuotes()
-        *
         * @param array $conds Condition array. See $conds in IDatabase::select() for
         *    the details of the format of condition arrays. May be "*" to copy the
         *    whole table.
-        *
         * @param string $fname The function name of the caller, from __METHOD__
-        *
         * @param array $insertOptions Options for the INSERT part of the query, see
         *    IDatabase::insert() for details. Also, one additional option is
         *    available: pass 'NO_AUTO_COLUMNS' to hint that the query does not use
@@ -1340,24 +1340,30 @@ interface IDatabase {
         *    IDatabase::select() for details.
         * @param array $selectJoinConds Join conditions for the SELECT part of the query, see
         *    IDatabase::select() for details.
-        *
         * @return bool Return true if no exception was thrown (deprecated since 1.33)
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         */
-       public function insertSelect( $destTable, $srcTable, $varMap, $conds,
+       public function insertSelect(
+               $destTable,
+               $srcTable,
+               $varMap,
+               $conds,
                $fname = __METHOD__,
-               $insertOptions = [], $selectOptions = [], $selectJoinConds = []
+               $insertOptions = [],
+               $selectOptions = [],
+               $selectJoinConds = []
        );
 
        /**
-        * Returns true if current database backend supports ORDER BY or LIMIT for separate subqueries
-        * within the UNION construct.
+        * Determine if the RDBMS supports ORDER BY and LIMIT for separate subqueries within UNION
+        *
         * @return bool
         */
        public function unionSupportsOrderAndLimit();
 
        /**
         * Construct a UNION query
+        *
         * This is used for providing overload point for other DB abstractions
         * not compatible with the MySQL syntax.
         * @param array $sqls SQL statements to combine
@@ -1375,7 +1381,6 @@ interface IDatabase {
         * conditions and unions them all together.
         *
         * @see IDatabase::select()
-        * @since 1.30
         * @param string|array $table Table name
         * @param string|array $vars Field names
         * @param array $permute_conds Conditions for the Cartesian product. Keys
@@ -1391,15 +1396,22 @@ interface IDatabase {
         *     instead of ORDER BY.
         * @param string|array $join_conds Join conditions
         * @return string SQL query string.
+        * @since 1.30
         */
        public function unionConditionPermutations(
-               $table, $vars, array $permute_conds, $extra_conds = '', $fname = __METHOD__,
-               $options = [], $join_conds = []
+               $table,
+               $vars,
+               array $permute_conds,
+               $extra_conds = '',
+               $fname = __METHOD__,
+               $options = [],
+               $join_conds = []
        );
 
        /**
-        * Returns an SQL expression for a simple conditional. This doesn't need
-        * to be overridden unless CASE isn't supported in your DBMS.
+        * Returns an SQL expression for a simple conditional
+        *
+        * This doesn't need to be overridden unless CASE isn't supported in the RDBMS.
         *
         * @param string|array $cond SQL expression which will result in a boolean value
         * @param string $trueVal SQL expression to return if true
@@ -1409,13 +1421,11 @@ interface IDatabase {
        public function conditional( $cond, $trueVal, $falseVal );
 
        /**
-        * Returns a command for str_replace function in SQL query.
-        * Uses REPLACE() in MySQL
+        * Returns a SQL expression for simple string replacement (e.g. REPLACE() in mysql)
         *
         * @param string $orig Column to modify
         * @param string $old Column to seek
         * @param string $new Column to replace with
-        *
         * @return string
         */
        public function strreplace( $orig, $old, $new );
@@ -1457,7 +1467,7 @@ interface IDatabase {
        public function wasConnectionLoss();
 
        /**
-        * Determines if the last failure was due to the database being read-only.
+        * Determines if the last failure was due to the database being read-only
         *
         * @return bool
         */
@@ -1484,7 +1494,7 @@ interface IDatabase {
         * @return int|null Zero if the replica DB was past that position already,
         *   greater than zero if we waited for some period of time, less than
         *   zero if it timed out, and null on error
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         */
        public function masterPosWait( DBMasterPos $pos, $timeout );
 
@@ -1492,7 +1502,7 @@ interface IDatabase {
         * Get the replication position of this replica DB
         *
         * @return DBMasterPos|bool False if this is not a replica DB
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         */
        public function getReplicaPos();
 
@@ -1500,7 +1510,7 @@ interface IDatabase {
         * Get the position of this master
         *
         * @return DBMasterPos|bool False if this is not a master
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         */
        public function getMasterPos();
 
@@ -1511,7 +1521,8 @@ interface IDatabase {
        public function serverIsReadOnly();
 
        /**
-        * Run a callback as soon as the current transaction commits or rolls back.
+        * Run a callback as soon as the current transaction commits or rolls back
+        *
         * An error is thrown if no transaction is pending. Queries in the function will run in
         * AUTOCOMMIT mode unless there are begin() calls. Callbacks must commit any transactions
         * that they begin.
@@ -1529,12 +1540,15 @@ interface IDatabase {
         *
         * @param callable $callback
         * @param string $fname Caller name
+        * @throws DBError If an error occurs, see IDatabase::query()
+        * @throws Exception If the callback runs immediately and an error occurs in it
         * @since 1.28
         */
        public function onTransactionResolution( callable $callback, $fname = __METHOD__ );
 
        /**
-        * Run a callback as soon as there is no transaction pending.
+        * Run a callback as soon as there is no transaction pending
+        *
         * If there is a transaction and it is rolled back, then the callback is cancelled.
         *
         * When transaction round mode (DBO_TRX) is set, the callback will run at the end
@@ -1563,6 +1577,8 @@ interface IDatabase {
         *
         * @param callable $callback
         * @param string $fname Caller name
+        * @throws DBError If an error occurs, see IDatabase::query()
+        * @throws Exception If the callback runs immediately and an error occurs in it
         * @since 1.32
         */
        public function onTransactionCommitOrIdle( callable $callback, $fname = __METHOD__ );
@@ -1578,7 +1594,8 @@ interface IDatabase {
        public function onTransactionIdle( callable $callback, $fname = __METHOD__ );
 
        /**
-        * Run a callback before the current transaction commits or now if there is none.
+        * Run a callback before the current transaction commits or now if there is none
+        *
         * If there is a transaction and it is rolled back, then the callback is cancelled.
         *
         * When transaction round mode (DBO_TRX) is set, the callback will run at the end
@@ -1598,12 +1615,14 @@ interface IDatabase {
         *
         * @param callable $callback
         * @param string $fname Caller name
+        * @throws DBError If an error occurs, see IDatabase::query()
+        * @throws Exception If the callback runs immediately and an error occurs in it
         * @since 1.22
         */
        public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ );
 
        /**
-        * Run a callback when the atomic section is cancelled.
+        * Run a callback when the atomic section is cancelled
         *
         * The callback is run just after the current atomic section, any outer
         * atomic section, or the whole transaction is rolled back.
@@ -1718,7 +1737,7 @@ interface IDatabase {
         * @param string $cancelable Pass self::ATOMIC_CANCELABLE to use a
         *  savepoint and enable self::cancelAtomic() for this section.
         * @return AtomicSectionIdentifier section ID token
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         */
        public function startAtomic( $fname = __METHOD__, $cancelable = self::ATOMIC_NOT_CANCELABLE );
 
@@ -1731,7 +1750,7 @@ interface IDatabase {
         * @since 1.23
         * @see IDatabase::startAtomic
         * @param string $fname
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         */
        public function endAtomic( $fname = __METHOD__ );
 
@@ -1758,7 +1777,7 @@ interface IDatabase {
         * @param string $fname
         * @param AtomicSectionIdentifier|null $sectionId Section ID from startAtomic();
         *   passing this enables cancellation of unclosed nested sections [optional]
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         */
        public function cancelAtomic( $fname = __METHOD__, AtomicSectionIdentifier $sectionId = null );
 
@@ -1828,8 +1847,8 @@ interface IDatabase {
         * @param string $cancelable Pass self::ATOMIC_CANCELABLE to use a
         *  savepoint and enable self::cancelAtomic() for this section.
         * @return mixed $res Result of the callback (since 1.28)
-        * @throws DBError
-        * @throws RuntimeException
+        * @throws DBError If an error occurs, see IDatabase::query()
+        * @throws Exception If an error occurs in the callback
         * @since 1.27; prior to 1.31 this did a rollback() instead of
         *  cancelAtomic(), and assumed no callers up the stack would ever try to
         *  catch the exception.
@@ -1839,8 +1858,7 @@ interface IDatabase {
        );
 
        /**
-        * Begin a transaction. If a transaction is already in progress,
-        * that transaction will be committed before the new transaction is started.
+        * Begin a transaction
         *
         * Only call this from code with outer transcation scope.
         * See https://www.mediawiki.org/wiki/Database_transactions for details.
@@ -1856,12 +1874,13 @@ interface IDatabase {
         *
         * @param string $fname Calling function name
         * @param string $mode A situationally valid IDatabase::TRANSACTION_* constant [optional]
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         */
        public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT );
 
        /**
-        * Commits a transaction previously started using begin().
+        * Commits a transaction previously started using begin()
+        *
         * If no transaction is in progress, a warning is issued.
         *
         * Only call this from code with outer transcation scope.
@@ -1872,19 +1891,15 @@ interface IDatabase {
         * @param string $flush Flush flag, set to situationally valid IDatabase::FLUSHING_*
         *   constant to disable warnings about explicitly committing implicit transactions,
         *   or calling commit when no transaction is in progress.
-        *
         *   This will trigger an exception if there is an ongoing explicit transaction.
-        *
         *   Only set the flush flag if you are sure that these warnings are not applicable,
         *   and no explicit transactions are open.
-        *
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         */
        public function commit( $fname = __METHOD__, $flush = self::FLUSHING_ONE );
 
        /**
-        * Rollback a transaction previously started using begin().
-        * If no transaction is in progress, a warning is issued.
+        * Rollback a transaction previously started using begin()
         *
         * Only call this from code with outer transcation scope.
         * See https://www.mediawiki.org/wiki/Database_transactions for details.
@@ -1899,7 +1914,7 @@ interface IDatabase {
         *   constant to disable warnings about calling rollback when no transaction is in
         *   progress. This will silently break any ongoing explicit transaction. Only set the
         *   flush flag if you are sure that it is safe to ignore these warnings in your context.
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         * @since 1.23 Added $flush parameter
         */
        public function rollback( $fname = __METHOD__, $flush = self::FLUSHING_ONE );
@@ -1916,21 +1931,18 @@ interface IDatabase {
         * @param string $flush Flush flag, set to situationally valid IDatabase::FLUSHING_*
         *   constant to disable warnings about explicitly committing implicit transactions,
         *   or calling commit when no transaction is in progress.
-        *
         *   This will trigger an exception if there is an ongoing explicit transaction.
-        *
         *   Only set the flush flag if you are sure that these warnings are not applicable,
         *   and no explicit transactions are open.
-        *
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         * @since 1.28
         * @since 1.34 Added $flush parameter
         */
        public function flushSnapshot( $fname = __METHOD__, $flush = self::FLUSHING_ONE );
 
        /**
-        * Convert a timestamp in one of the formats accepted by wfTimestamp()
-        * to the format used for inserting into timestamp fields in this DBMS.
+        * Convert a timestamp in one of the formats accepted by ConvertibleTimestamp
+        * to the format used for inserting into timestamp fields in this DBMS
         *
         * The result is unquoted, and needs to be passed through addQuotes()
         * before it can be included in raw SQL.
@@ -1942,9 +1954,10 @@ interface IDatabase {
        public function timestamp( $ts = 0 );
 
        /**
-        * Convert a timestamp in one of the formats accepted by wfTimestamp()
-        * to the format used for inserting into timestamp fields in this DBMS. If
-        * NULL is input, it is passed through, allowing NULL values to be inserted
+        * Convert a timestamp in one of the formats accepted by ConvertibleTimestamp
+        * to the format used for inserting into timestamp fields in this DBMS
+        *
+        * If NULL is input, it is passed through, allowing NULL values to be inserted
         * into timestamp fields.
         *
         * The result is unquoted, and needs to be passed through addQuotes()
@@ -1970,7 +1983,7 @@ interface IDatabase {
         * Callers should avoid using this method while a transaction is active
         *
         * @return int|bool Database replication lag in seconds or false on error
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         */
        public function getLag();
 
@@ -1985,13 +1998,13 @@ interface IDatabase {
         * indication of the staleness of subsequent reads.
         *
         * @return array ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN)
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         * @since 1.27
         */
        public function getSessionLagStatus();
 
        /**
-        * Return the maximum number of items allowed in a list, or 0 for unlimited.
+        * Return the maximum number of items allowed in a list, or 0 for unlimited
         *
         * @return int
         */
@@ -2005,6 +2018,7 @@ interface IDatabase {
         *
         * @param string $b
         * @return string|Blob
+        * @throws DBError
         */
        public function encodeBlob( $b );
 
@@ -2015,6 +2029,7 @@ interface IDatabase {
         *
         * @param string|Blob $b
         * @return string
+        * @throws DBError
         */
        public function decodeBlob( $b );
 
@@ -2027,7 +2042,7 @@ interface IDatabase {
         *
         * @param array $options
         * @return void
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         */
        public function setSessionOptions( array $options );
 
@@ -2046,7 +2061,7 @@ interface IDatabase {
         * @param string $lockName Name of lock to poll
         * @param string $method Name of method calling us
         * @return bool
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         * @since 1.20
         */
        public function lockIsFree( $lockName, $method );
@@ -2059,8 +2074,8 @@ interface IDatabase {
         * @param string $lockName Name of lock to aquire
         * @param string $method Name of the calling method
         * @param int $timeout Acquisition timeout in seconds (0 means non-blocking)
-        * @return bool
-        * @throws DBError
+        * @return bool Success
+        * @throws DBError If an error occurs, see IDatabase::query()
         */
        public function lock( $lockName, $method, $timeout = 5 );
 
@@ -2071,12 +2086,8 @@ interface IDatabase {
         *
         * @param string $lockName Name of lock to release
         * @param string $method Name of the calling method
-        *
-        * @return int Returns 1 if the lock was released, 0 if the lock was not established
-        * by this thread (in which case the lock is not released), and NULL if the named lock
-        * did not exist
-        *
-        * @throws DBError
+        * @return bool Success
+        * @throws DBError If an error occurs, see IDatabase::query()
         */
        public function unlock( $lockName, $method );
 
@@ -2098,7 +2109,7 @@ interface IDatabase {
         * @param string $fname Name of the calling method
         * @param int $timeout Acquisition timeout in seconds
         * @return ScopedCallback|null
-        * @throws DBError
+        * @throws DBError If an error occurs, see IDatabase::query()
         * @since 1.27
         */
        public function getScopedLockAndFlush( $lockKey, $fname, $timeout );
diff --git a/includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php b/includes/libs/rdbms/database/resultwrapper/MssqlResultWrapper.php
deleted file mode 100644 (file)
index ba79be1..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-<?php
-
-namespace Wikimedia\Rdbms;
-
-use stdClass;
-
-class MssqlResultWrapper extends ResultWrapper {
-       /** @var int|null */
-       private $seekTo = null;
-
-       /**
-        * @return stdClass|bool
-        */
-       public function fetchObject() {
-               $res = $this->result;
-
-               if ( $this->seekTo !== null ) {
-                       $result = sqlsrv_fetch_object( $res, stdClass::class, [],
-                               SQLSRV_SCROLL_ABSOLUTE, $this->seekTo );
-                       $this->seekTo = null;
-               } else {
-                       $result = sqlsrv_fetch_object( $res );
-               }
-
-               // Return boolean false when there are no more rows instead of null
-               if ( $result === null ) {
-                       return false;
-               }
-
-               return $result;
-       }
-
-       /**
-        * @return array|bool
-        */
-       public function fetchRow() {
-               $res = $this->result;
-
-               if ( $this->seekTo !== null ) {
-                       $result = sqlsrv_fetch_array( $res, SQLSRV_FETCH_BOTH,
-                               SQLSRV_SCROLL_ABSOLUTE, $this->seekTo );
-                       $this->seekTo = null;
-               } else {
-                       $result = sqlsrv_fetch_array( $res );
-               }
-
-               // Return boolean false when there are no more rows instead of null
-               if ( $result === null ) {
-                       return false;
-               }
-
-               return $result;
-       }
-
-       /**
-        * @param int $row
-        * @return bool
-        */
-       public function seek( $row ) {
-               $res = $this->result;
-
-               // check bounds
-               $numRows = $this->db->numRows( $res );
-               $row = intval( $row );
-
-               if ( $numRows === 0 ) {
-                       return false;
-               } elseif ( $row < 0 || $row > $numRows - 1 ) {
-                       return false;
-               }
-
-               // Unlike MySQL, the seek actually happens on the next access
-               $this->seekTo = $row;
-               return true;
-       }
-}
diff --git a/includes/libs/rdbms/encasing/MssqlBlob.php b/includes/libs/rdbms/encasing/MssqlBlob.php
deleted file mode 100644 (file)
index 1819a9a..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-<?php
-
-namespace Wikimedia\Rdbms;
-
-class MssqlBlob extends Blob {
-       /** @noinspection PhpMissingParentConstructorInspection */
-
-       /**
-        * @param Blob|string $data
-        */
-       public function __construct( $data ) {
-               if ( $data instanceof MssqlBlob ) {
-                       $this->data = $data->data;
-               } elseif ( $data instanceof Blob ) {
-                       $this->data = $data->fetch();
-               } else {
-                       $this->data = $data;
-               }
-       }
-
-       /**
-        * Returns an unquoted hex representation of a binary string
-        * for insertion into varbinary-type fields
-        * @return string
-        */
-       public function fetch() {
-               if ( $this->data === null ) {
-                       return 'null';
-               }
-
-               $ret = '0x';
-               $dataLength = strlen( $this->data );
-               for ( $i = 0; $i < $dataLength; $i++ ) {
-                       $ret .= bin2hex( pack( 'C', ord( $this->data[$i] ) ) );
-               }
-
-               return $ret;
-       }
-}
diff --git a/includes/libs/rdbms/field/MssqlField.php b/includes/libs/rdbms/field/MssqlField.php
deleted file mode 100644 (file)
index 98cc2b1..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-<?php
-
-namespace Wikimedia\Rdbms;
-
-class MssqlField implements Field {
-       private $name, $tableName, $default, $max_length, $nullable, $type;
-
-       function __construct( $info ) {
-               $this->name = $info['COLUMN_NAME'];
-               $this->tableName = $info['TABLE_NAME'];
-               $this->default = $info['COLUMN_DEFAULT'];
-               $this->max_length = $info['CHARACTER_MAXIMUM_LENGTH'];
-               $this->nullable = !( strtolower( $info['IS_NULLABLE'] ) == 'no' );
-               $this->type = $info['DATA_TYPE'];
-       }
-
-       function name() {
-               return $this->name;
-       }
-
-       function tableName() {
-               return $this->tableName;
-       }
-
-       function defaultValue() {
-               return $this->default;
-       }
-
-       function maxLength() {
-               return $this->max_length;
-       }
-
-       function isNullable() {
-               return $this->nullable;
-       }
-
-       function type() {
-               return $this->type;
-       }
-}
index 4426654..77467f0 100644 (file)
@@ -160,7 +160,6 @@ abstract class LBFactory implements ILBFactory {
        }
 
        public function destroy() {
-               $this->shutdown( self::SHUTDOWN_NO_CHRONPROT );
                $this->forEachLBCallMethod( 'disable' );
        }
 
index 1125572..160d501 100644 (file)
@@ -95,6 +95,8 @@ interface ILoadBalancer {
        const CONN_SILENCE_ERRORS = 2;
        /** @var int Caller is requesting the master DB server for possibly writes */
        const CONN_INTENT_WRITABLE = 4;
+       /** @var int Bypass and update any server-side read-only mode state cache */
+       const CONN_REFRESH_READ_ONLY = 8;
 
        /** @var string Manager of ILoadBalancer instances is running post-commit callbacks */
        const STAGE_POSTCOMMIT_CALLBACKS = 'stage-postcommit-callbacks';
@@ -155,6 +157,18 @@ interface ILoadBalancer {
         */
        public function redefineLocalDomain( $domain );
 
+       /**
+        * Indicate whether the tables on this domain are only temporary tables for testing
+        *
+        * In "temporary tables mode", the ILoadBalancer::CONN_TRX_AUTOCOMMIT flag is ignored
+        *
+        * @param bool $value
+        * @param string $domain
+        * @return bool Whether "temporary tables mode" was active
+        * @since 1.34
+        */
+       public function setTempTablesOnlyMode( $value, $domain );
+
        /**
         * Get the server index of the reader connection for a given group
         *
@@ -649,10 +663,9 @@ interface ILoadBalancer {
        /**
         * @note This method may trigger a DB connection if not yet done
         * @param string|bool $domain DB domain ID or false for the local domain
-        * @param IDatabase|null $conn DB master connection; used to avoid loops [optional]
         * @return string|bool Reason the master is read-only or false if it is not
         */
-       public function getReadOnlyReason( $domain = false, IDatabase $conn = null );
+       public function getReadOnlyReason( $domain = false );
 
        /**
         * Disables/enables lag checks
index d088aa9..066d4b4 100644 (file)
@@ -101,6 +101,8 @@ class LoadBalancer implements ILoadBalancer {
        private $indexAliases = [];
        /** @var array[] Map of (name => callable) */
        private $trxRecurringCallbacks = [];
+       /** @var bool[] Map of (domain => whether to use "temp tables only" mode) */
+       private $tempTablesOnlyMode = [];
 
        /** @var Database Connection handle that caused a problem */
        private $errorConnection;
@@ -297,9 +299,10 @@ class LoadBalancer implements ILoadBalancer {
        /**
         * @param int $flags Bitfield of class CONN_* constants
         * @param int $i Specific server index or DB_MASTER/DB_REPLICA
+        * @param string $domain Database domain
         * @return int Sanitized bitfield
         */
-       private function sanitizeConnectionFlags( $flags, $i ) {
+       private function sanitizeConnectionFlags( $flags, $i, $domain ) {
                // Whether an outside caller is explicitly requesting the master database server
                if ( $i === self::DB_MASTER || $i === $this->getWriterIndex() ) {
                        $flags |= self::CONN_INTENT_WRITABLE;
@@ -320,6 +323,12 @@ class LoadBalancer implements ILoadBalancer {
                                $flags &= ~self::CONN_TRX_AUTOCOMMIT;
                                $type = $this->getServerType( $this->getWriterIndex() );
                                $this->connLogger->info( __METHOD__ . ": CONN_TRX_AUTOCOMMIT disallowed ($type)" );
+                       } elseif ( isset( $this->tempTablesOnlyMode[$domain] ) ) {
+                               // T202116: integration tests are active and queries should be all be using
+                               // temporary clone tables (via prefix). Such tables are not visible accross
+                               // different connections nor can there be REPEATABLE-READ snapshot staleness,
+                               // so use the same connection for everything.
+                               $flags &= ~self::CONN_TRX_AUTOCOMMIT;
                        }
                }
 
@@ -874,7 +883,7 @@ class LoadBalancer implements ILoadBalancer {
        public function getConnection( $i, $groups = [], $domain = false, $flags = 0 ) {
                $domain = $this->resolveDomainID( $domain );
                $groups = $this->resolveGroups( $groups, $i );
-               $flags = $this->sanitizeConnectionFlags( $flags, $i );
+               $flags = $this->sanitizeConnectionFlags( $flags, $i, $domain );
                // If given DB_MASTER/DB_REPLICA, resolve it to a specific server index. Resolving
                // DB_REPLICA might trigger getServerConnection() calls due to the getReaderIndex()
                // connectivity checks or LoadMonitor::scaleLoads() server state cache regeneration.
@@ -883,7 +892,11 @@ class LoadBalancer implements ILoadBalancer {
                // Get an open connection to that server (might trigger a new connection)
                $conn = $this->getServerConnection( $serverIndex, $domain, $flags );
                // Set master DB handles as read-only if there is high replication lag
-               if ( $serverIndex === $this->getWriterIndex() && $this->getLaggedReplicaMode( $domain ) ) {
+               if (
+                       $serverIndex === $this->getWriterIndex() &&
+                       $this->getLaggedReplicaMode( $domain ) &&
+                       !is_string( $conn->getLBInfo( 'readOnlyReason' ) )
+               ) {
                        $reason = ( $this->getExistingReaderIndex( self::GROUP_GENERIC ) >= 0 )
                                ? 'The database is read-only until replication lag decreases.'
                                : 'The database is read-only until replica database servers becomes reachable.';
@@ -939,15 +952,15 @@ class LoadBalancer implements ILoadBalancer {
                // or the master database server is running in server-side read-only mode. Note that
                // replica DB handles are always read-only via Database::assertIsWritableMaster().
                // Read-only mode due to replication lag is *avoided* here to avoid recursion.
-               if ( $conn->getLBInfo( 'serverIndex' ) === $this->getWriterIndex() ) {
+               if ( $i === $this->getWriterIndex() ) {
                        if ( $this->readOnlyReason !== false ) {
-                               $conn->setLBInfo( 'readOnlyReason', $this->readOnlyReason );
-                       } elseif ( $this->masterRunningReadOnly( $domain, $conn ) ) {
-                               $conn->setLBInfo(
-                                       'readOnlyReason',
-                                       'The master database server is running in read-only mode.'
-                               );
+                               $readOnlyReason = $this->readOnlyReason;
+                       } elseif ( $this->isMasterConnectionReadOnly( $conn, $flags ) ) {
+                               $readOnlyReason = 'The master database server is running in read-only mode.';
+                       } else {
+                               $readOnlyReason = false;
                        }
+                       $conn->setLBInfo( 'readOnlyReason', $readOnlyReason );
                }
 
                return $conn;
@@ -957,17 +970,7 @@ class LoadBalancer implements ILoadBalancer {
                $serverIndex = $conn->getLBInfo( 'serverIndex' );
                $refCount = $conn->getLBInfo( 'foreignPoolRefCount' );
                if ( $serverIndex === null || $refCount === null ) {
-                       /**
-                        * This can happen in code like:
-                        *   foreach ( $dbs as $db ) {
-                        *     $conn = $lb->getConnection( $lb::DB_REPLICA, [], $db );
-                        *     ...
-                        *     $lb->reuseConnection( $conn );
-                        *   }
-                        * When a connection to the local DB is opened in this way, reuseConnection()
-                        * should be ignored
-                        */
-                       return;
+                       return; // non-foreign connection; no domain-use tracking to update
                } elseif ( $conn instanceof DBConnRef ) {
                        // DBConnRef already handles calling reuseConnection() and only passes the live
                        // Database instance to this method. Any caller passing in a DBConnRef is broken.
@@ -1299,7 +1302,7 @@ class LoadBalancer implements ILoadBalancer {
 
                // Create a live connection object
                try {
-                       $db = Database::factory( $server['type'], $server );
+                       $conn = Database::factory( $server['type'], $server );
                        // Log when many connection are made on requests
                        ++$this->connectionCounter;
                        $currentConnCount = $this->getCurrentConnectionCount();
@@ -1312,28 +1315,28 @@ class LoadBalancer implements ILoadBalancer {
                } catch ( DBConnectionError $e ) {
                        // FIXME: This is probably the ugliest thing I have ever done to
                        // PHP. I'm half-expecting it to segfault, just out of disgust. -- TS
-                       $db = $e->db;
+                       $conn = $e->db;
                }
 
-               $db->setLBInfo( $server );
-               $db->setLazyMasterHandle(
-                       $this->getLazyConnectionRef( self::DB_MASTER, [], $db->getDomainID() )
+               $conn->setLBInfo( $server );
+               $conn->setLazyMasterHandle(
+                       $this->getLazyConnectionRef( self::DB_MASTER, [], $conn->getDomainID() )
                );
-               $db->setTableAliases( $this->tableAliases );
-               $db->setIndexAliases( $this->indexAliases );
+               $conn->setTableAliases( $this->tableAliases );
+               $conn->setIndexAliases( $this->indexAliases );
 
                if ( $server['serverIndex'] === $this->getWriterIndex() ) {
                        if ( $this->trxRoundId !== false ) {
-                               $this->applyTransactionRoundFlags( $db );
+                               $this->applyTransactionRoundFlags( $conn );
                        }
                        foreach ( $this->trxRecurringCallbacks as $name => $callback ) {
-                               $db->setTransactionListener( $name, $callback );
+                               $conn->setTransactionListener( $name, $callback );
                        }
                }
 
                $this->lazyLoadReplicationPositions(); // session consistency
 
-               return $db;
+               return $conn;
        }
 
        /**
@@ -1929,10 +1932,12 @@ class LoadBalancer implements ILoadBalancer {
                return $this->laggedReplicaMode;
        }
 
-       public function getReadOnlyReason( $domain = false, IDatabase $conn = null ) {
+       public function getReadOnlyReason( $domain = false ) {
+               $domainInstance = DatabaseDomain::newFromId( $this->resolveDomainID( $domain ) );
+
                if ( $this->readOnlyReason !== false ) {
                        return $this->readOnlyReason;
-               } elseif ( $this->masterRunningReadOnly( $domain, $conn ) ) {
+               } elseif ( $this->isMasterRunningReadOnly( $domainInstance ) ) {
                        return 'The master database server is running in read-only mode.';
                } elseif ( $this->getLaggedReplicaMode( $domain ) ) {
                        return ( $this->getExistingReaderIndex( self::GROUP_GENERIC ) >= 0 )
@@ -1944,26 +1949,68 @@ class LoadBalancer implements ILoadBalancer {
        }
 
        /**
-        * @param string $domain Domain ID, or false for the current domain
-        * @param IDatabase|null $conn DB master connectionl used to avoid loops [optional]
-        * @return bool
+        * @param IDatabase $conn Master connection
+        * @param int $flags Bitfield of class CONN_* constants
+        * @return bool Whether the entire server or currently selected DB/schema is read-only
         */
-       private function masterRunningReadOnly( $domain, IDatabase $conn = null ) {
-               $cache = $this->wanCache;
-               $masterServer = $this->getServerName( $this->getWriterIndex() );
+       private function isMasterConnectionReadOnly( IDatabase $conn, $flags = 0 ) {
+               // Note that table prefixes are not related to server-side read-only mode
+               $key = $this->srvCache->makeGlobalKey(
+                       'rdbms-server-readonly',
+                       $conn->getServer(),
+                       $conn->getDBname(),
+                       $conn->dbSchema()
+               );
 
-               return (bool)$cache->getWithSetCallback(
-                       $cache->makeGlobalKey( __CLASS__, 'server-read-only', $masterServer ),
+               if ( ( $flags & self::CONN_REFRESH_READ_ONLY ) == self::CONN_REFRESH_READ_ONLY ) {
+                       try {
+                               $readOnly = (int)$conn->serverIsReadOnly();
+                       } catch ( DBError $e ) {
+                               $readOnly = 0;
+                       }
+                       $this->srvCache->set( $key, $readOnly, BagOStuff::TTL_PROC_SHORT );
+               } else {
+                       $readOnly = $this->srvCache->getWithSetCallback(
+                               $key,
+                               BagOStuff::TTL_PROC_SHORT,
+                               function () use ( $conn ) {
+                                       try {
+                                               return (int)$conn->serverIsReadOnly();
+                                       } catch ( DBError $e ) {
+                                               return 0;
+                                       }
+                               }
+                       );
+               }
+
+               return (bool)$readOnly;
+       }
+
+       /**
+        * @param DatabaseDomain $domain
+        * @return bool Whether the entire master server or the local domain DB is read-only
+        */
+       private function isMasterRunningReadOnly( DatabaseDomain $domain ) {
+               // Context will often be HTTP GET/HEAD; heavily cache the results
+               return (bool)$this->wanCache->getWithSetCallback(
+                       // Note that table prefixes are not related to server-side read-only mode
+                       $this->wanCache->makeGlobalKey(
+                               'rdbms-server-readonly',
+                               $this->getMasterServerName(),
+                               $domain->getDatabase(),
+                               $domain->getSchema()
+                       ),
                        self::TTL_CACHE_READONLY,
-                       function () use ( $domain, $conn ) {
+                       function () use ( $domain ) {
                                $old = $this->trxProfiler->setSilenced( true );
                                try {
                                        $index = $this->getWriterIndex();
-                                       $dbw = $conn ?: $this->getServerConnection( $index, $domain );
-                                       $readOnly = (int)$dbw->serverIsReadOnly();
-                                       if ( !$conn ) {
-                                               $this->reuseConnection( $dbw );
-                                       }
+                                       // Reset the cache for isMasterConnectionReadOnly()
+                                       $flags = self::CONN_REFRESH_READ_ONLY;
+                                       $conn = $this->getServerConnection( $index, $domain->getId(), $flags );
+                                       // Reuse the process cache set above
+                                       $readOnly = (int)$this->isMasterConnectionReadOnly( $conn );
+                                       $this->reuseConnection( $conn );
                                } catch ( DBError $e ) {
                                        $readOnly = 0;
                                }
@@ -1971,7 +2018,7 @@ class LoadBalancer implements ILoadBalancer {
 
                                return $readOnly;
                        },
-                       [ 'pcTTL' => $cache::TTL_PROC_LONG, 'busyValue' => 0 ]
+                       [ 'pcTTL' => WANObjectCache::TTL_PROC_LONG, 'lockTSE' => 10, 'busyValue' => 0 ]
                );
        }
 
@@ -2226,9 +2273,9 @@ class LoadBalancer implements ILoadBalancer {
                ) );
 
                // Update the prefix for all local connections...
-               $this->forEachOpenConnection( function ( IDatabase $db ) use ( $prefix ) {
-                       if ( !$db->getLBInfo( 'foreign' ) ) {
-                               $db->tablePrefix( $prefix );
+               $this->forEachOpenConnection( function ( IDatabase $conn ) use ( $prefix ) {
+                       if ( !$conn->getLBInfo( 'foreign' ) ) {
+                               $conn->tablePrefix( $prefix );
                        }
                } );
        }
@@ -2239,6 +2286,17 @@ class LoadBalancer implements ILoadBalancer {
                $this->setLocalDomain( DatabaseDomain::newFromId( $domain ) );
        }
 
+       public function setTempTablesOnlyMode( $value, $domain ) {
+               $old = $this->tempTablesOnlyMode[$domain] ?? false;
+               if ( $value ) {
+                       $this->tempTablesOnlyMode[$domain] = true;
+               } else {
+                       unset( $this->tempTablesOnlyMode[$domain] );
+               }
+
+               return $old;
+       }
+
        /**
         * @param DatabaseDomain $domain
         */
@@ -2276,6 +2334,13 @@ class LoadBalancer implements ILoadBalancer {
                return $this->servers[$i];
        }
 
+       /**
+        * @return string
+        */
+       private function getMasterServerName() {
+               return $this->getServerName( $this->getWriterIndex() );
+       }
+
        function __destruct() {
                // Avoid connection leaks for sanity
                $this->disable();
index 7361032..cba68ef 100644 (file)
@@ -173,13 +173,23 @@ class EmailNotification {
         * @param string $pageStatus
         * @throws MWException
         */
-       public function actuallyNotifyOnPageChange( $editor, $title, $timestamp, $summary, $minorEdit,
-               $oldid, $watchers, $pageStatus = 'changed' ) {
+       public function actuallyNotifyOnPageChange(
+               $editor,
+               $title,
+               $timestamp,
+               $summary,
+               $minorEdit,
+               $oldid,
+               $watchers,
+               $pageStatus = 'changed'
+       ) {
                # we use $wgPasswordSender as sender's address
                global $wgUsersNotifiedOnAllChanges;
                global $wgEnotifWatchlist, $wgBlockDisablesLogin;
                global $wgEnotifMinorEdits, $wgEnotifUserTalk;
 
+               $messageCache = MediaWikiServices::getInstance()->getMessageCache();
+
                # The following code is only run, if several conditions are met:
                # 1. EmailNotification for pages (other than user_talk pages) must be enabled
                # 2. minor edits (changes) are only regarded if the global flag indicates so
@@ -210,7 +220,7 @@ class EmailNotification {
                                && $this->canSendUserTalkEmail( $editor, $title, $minorEdit )
                        ) {
                                $targetUser = User::newFromName( $title->getText() );
-                               $this->compose( $targetUser, self::USER_TALK );
+                               $this->compose( $targetUser, self::USER_TALK, $messageCache );
                                $userTalkId = $targetUser->getId();
                        }
 
@@ -229,7 +239,7 @@ class EmailNotification {
                                                && !( $wgBlockDisablesLogin && $watchingUser->getBlock() )
                                                && Hooks::run( 'SendWatchlistEmailNotification', [ $watchingUser, $title, $this ] )
                                        ) {
-                                               $this->compose( $watchingUser, self::WATCHLIST );
+                                               $this->compose( $watchingUser, self::WATCHLIST, $messageCache );
                                        }
                                }
                        }
@@ -241,7 +251,7 @@ class EmailNotification {
                                continue;
                        }
                        $user = User::newFromName( $name );
-                       $this->compose( $user, self::ALL_CHANGES );
+                       $this->compose( $user, self::ALL_CHANGES, $messageCache );
                }
 
                $this->sendMails();
@@ -288,8 +298,9 @@ class EmailNotification {
 
        /**
         * Generate the generic "this page has been changed" e-mail text.
+        * @param MessageCache $messageCache
         */
-       private function composeCommonMailtext() {
+       private function composeCommonMailtext( MessageCache $messageCache ) {
                global $wgPasswordSender, $wgNoReplyAddress;
                global $wgEnotifFromEditor, $wgEnotifRevealEditorAddress;
                global $wgEnotifImpersonal, $wgEnotifUseRealName;
@@ -374,7 +385,7 @@ class EmailNotification {
 
                $body = wfMessage( 'enotif_body' )->inContentLanguage()->plain();
                $body = strtr( $body, $keys );
-               $body = MessageCache::singleton()->transform( $body, false, null, $this->title );
+               $body = $messageCache->transform( $body, false, null, $this->title );
                $this->body = wordwrap( strtr( $body, $postTransformKeys ), 72 );
 
                # Reveal the page editor's address as REPLY-TO address only if
@@ -406,12 +417,13 @@ class EmailNotification {
         * Call sendMails() to send any mails that were queued.
         * @param User $user
         * @param string $source
+        * @param MessageCache $messageCache
         */
-       private function compose( $user, $source ) {
+       private function compose( $user, $source, MessageCache $messageCache ) {
                global $wgEnotifImpersonal;
 
                if ( !$this->composed_common ) {
-                       $this->composeCommonMailtext();
+                       $this->composeCommonMailtext( $messageCache );
                }
 
                if ( $wgEnotifImpersonal ) {
index 333c610..f328760 100644 (file)
@@ -490,8 +490,16 @@ class FormatMetadata extends ContextSource {
 
                                        case 'CustomRendered':
                                                switch ( $val ) {
-                                                       case 0:
-                                                       case 1:
+                                                       case 0: /* normal */
+                                                       case 1: /* custom */
+                                                               /* The following are unofficial Apple additions */
+                                                       case 2: /* HDR (no original saved) */
+                                                       case 3: /* HDR (original saved) */
+                                                       case 4: /* Original (for HDR) */
+                                                               /* Yes 5 is not present ;) */
+                                                       case 6: /* Panorama */
+                                                       case 7: /* Portrait HDR */
+                                                       case 8: /* Portrait */
                                                                $val = $this->exifMsg( $tag, $val );
                                                                break;
                                                        default:
index d9fe319..e634edc 100644 (file)
@@ -22,6 +22,7 @@
  */
 
 use MediaWiki\MediaWikiServices;
+use Wikimedia\AtEase\AtEase;
 use Wikimedia\Rdbms\Database;
 use Wikimedia\Rdbms\IDatabase;
 use Wikimedia\Rdbms\DBError;
@@ -30,6 +31,7 @@ use Wikimedia\Rdbms\DBConnectionError;
 use Wikimedia\Rdbms\IMaintainableDatabase;
 use Wikimedia\Rdbms\LoadBalancer;
 use Wikimedia\ScopedCallback;
+use Wikimedia\Timestamp\ConvertibleTimestamp;
 use Wikimedia\WaitConditionLoop;
 
 /**
@@ -43,7 +45,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
        /** @var string[] (server index => tag/host name) */
        protected $serverTags;
        /** @var int */
-       protected $numServers;
+       protected $numServerShards;
        /** @var int UNIX timestamp */
        protected $lastGarbageCollect = 0;
        /** @var int */
@@ -51,7 +53,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
        /** @var int */
        protected $purgeLimit = 100;
        /** @var int */
-       protected $shards = 1;
+       protected $numTableShards = 1;
        /** @var string */
        protected $tableName = 'objectcache';
        /** @var bool */
@@ -126,7 +128,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                if ( isset( $params['servers'] ) ) {
                        $this->serverInfos = [];
                        $this->serverTags = [];
-                       $this->numServers = count( $params['servers'] );
+                       $this->numServerShards = count( $params['servers'] );
                        $index = 0;
                        foreach ( $params['servers'] as $tag => $info ) {
                                $this->serverInfos[$index] = $info;
@@ -139,11 +141,11 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                        }
                } elseif ( isset( $params['server'] ) ) {
                        $this->serverInfos = [ $params['server'] ];
-                       $this->numServers = count( $this->serverInfos );
+                       $this->numServerShards = count( $this->serverInfos );
                } else {
                        // Default to using the main wiki's database servers
                        $this->serverInfos = false;
-                       $this->numServers = 1;
+                       $this->numServerShards = 1;
                        $this->attrMap[self::ATTR_SYNCWRITES] = self::QOS_SYNCWRITES_BE;
                }
                if ( isset( $params['purgePeriod'] ) ) {
@@ -156,7 +158,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                        $this->tableName = $params['tableName'];
                }
                if ( isset( $params['shards'] ) ) {
-                       $this->shards = intval( $params['shards'] );
+                       $this->numTableShards = intval( $params['shards'] );
                }
                // Backwards-compatibility for < 1.34
                $this->replicaOnly = $params['replicaOnly'] ?? ( $params['slaveOnly'] ?? false );
@@ -165,52 +167,54 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
        /**
         * Get a connection to the specified database
         *
-        * @param int $serverIndex
+        * @param int $shardIndex
         * @return IMaintainableDatabase
         * @throws MWException
         */
-       protected function getDB( $serverIndex ) {
-               if ( $serverIndex >= $this->numServers ) {
-                       throw new MWException( __METHOD__ . ": Invalid server index \"$serverIndex\"" );
+       private function getConnection( $shardIndex ) {
+               if ( $shardIndex >= $this->numServerShards ) {
+                       throw new MWException( __METHOD__ . ": Invalid server index \"$shardIndex\"" );
                }
 
                # Don't keep timing out trying to connect for each call if the DB is down
                if (
-                       isset( $this->connFailureErrors[$serverIndex] ) &&
-                       ( $this->getCurrentTime() - $this->connFailureTimes[$serverIndex] ) < 60
+                       isset( $this->connFailureErrors[$shardIndex] ) &&
+                       ( $this->getCurrentTime() - $this->connFailureTimes[$shardIndex] ) < 60
                ) {
-                       throw $this->connFailureErrors[$serverIndex];
+                       throw $this->connFailureErrors[$shardIndex];
                }
 
                if ( $this->serverInfos ) {
-                       if ( !isset( $this->conns[$serverIndex] ) ) {
+                       if ( !isset( $this->conns[$shardIndex] ) ) {
                                // Use custom database defined by server connection info
-                               $info = $this->serverInfos[$serverIndex];
+                               $info = $this->serverInfos[$shardIndex];
                                $type = $info['type'] ?? 'mysql';
                                $host = $info['host'] ?? '[unknown]';
                                $this->logger->debug( __CLASS__ . ": connecting to $host" );
-                               $db = Database::factory( $type, $info );
-                               $db->clearFlag( DBO_TRX ); // auto-commit mode
-                               $this->conns[$serverIndex] = $db;
+                               $conn = Database::factory( $type, $info );
+                               $conn->clearFlag( DBO_TRX ); // auto-commit mode
+                               $this->conns[$shardIndex] = $conn;
                        }
-                       $db = $this->conns[$serverIndex];
+                       $conn = $this->conns[$shardIndex];
                } else {
                        // Use the main LB database
                        $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
                        $index = $this->replicaOnly ? DB_REPLICA : DB_MASTER;
-                       if ( $lb->getServerType( $lb->getWriterIndex() ) !== 'sqlite' ) {
-                               // Keep a separate connection to avoid contention and deadlocks
-                               $db = $lb->getConnectionRef( $index, [], false, $lb::CONN_TRX_AUTOCOMMIT );
-                       } else {
-                               // However, SQLite has the opposite behavior due to DB-level locking.
-                               // Stock sqlite MediaWiki installs use a separate sqlite cache DB instead.
-                               $db = $lb->getConnectionRef( $index );
+                       // If the RDBMS has row-level locking, use the autocommit connection to avoid
+                       // contention and deadlocks. Do not do this if it only has DB-level locking since
+                       // that would just cause deadlocks.
+                       $attribs = $lb->getServerAttributes( $lb->getWriterIndex() );
+                       $flags = $attribs[Database::ATTR_DB_LEVEL_LOCKING] ? 0 : $lb::CONN_TRX_AUTOCOMMIT;
+                       $conn = $lb->getMaintenanceConnectionRef( $index, [], false, $flags );
+                       // Automatically create the objectcache table for sqlite as needed
+                       if ( $conn->getType() === 'sqlite' ) {
+                               $this->initSqliteDatabase( $conn );
                        }
                }
 
-               $this->logger->debug( sprintf( "Connection %s will be used for SqlBagOStuff", $db ) );
+               $this->logger->debug( sprintf( "Connection %s will be used for SqlBagOStuff", $conn ) );
 
-               return $db;
+               return $conn;
        }
 
        /**
@@ -218,22 +222,22 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
         * @param string $key
         * @return array Server index and table name
         */
-       protected function getTableByKey( $key ) {
-               if ( $this->shards > 1 ) {
+       private function getTableByKey( $key ) {
+               if ( $this->numTableShards > 1 ) {
                        $hash = hexdec( substr( md5( $key ), 0, 8 ) ) & 0x7fffffff;
-                       $tableIndex = $hash % $this->shards;
+                       $tableIndex = $hash % $this->numTableShards;
                } else {
                        $tableIndex = 0;
                }
-               if ( $this->numServers > 1 ) {
+               if ( $this->numServerShards > 1 ) {
                        $sortedServers = $this->serverTags;
                        ArrayUtils::consistentHashSort( $sortedServers, $key );
                        reset( $sortedServers );
-                       $serverIndex = key( $sortedServers );
+                       $shardIndex = key( $sortedServers );
                } else {
-                       $serverIndex = 0;
+                       $shardIndex = 0;
                }
-               return [ $serverIndex, $this->getTableNameByShard( $tableIndex ) ];
+               return [ $shardIndex, $this->getTableNameByShard( $tableIndex ) ];
        }
 
        /**
@@ -241,9 +245,9 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
         * @param int $index
         * @return string
         */
-       protected function getTableNameByShard( $index ) {
-               if ( $this->shards > 1 ) {
-                       $decimals = strlen( $this->shards - 1 );
+       private function getTableNameByShard( $index ) {
+               if ( $this->numTableShards > 1 ) {
+                       $decimals = strlen( $this->numTableShards - 1 );
                        return $this->tableName .
                                sprintf( "%0{$decimals}d", $index );
                } else {
@@ -278,19 +282,19 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                return $values;
        }
 
-       protected function fetchBlobMulti( array $keys, $flags = 0 ) {
+       private function fetchBlobMulti( array $keys, $flags = 0 ) {
                $values = []; // array of (key => value)
 
                $keysByTable = [];
                foreach ( $keys as $key ) {
-                       list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
-                       $keysByTable[$serverIndex][$tableName][] = $key;
+                       list( $shardIndex, $tableName ) = $this->getTableByKey( $key );
+                       $keysByTable[$shardIndex][$tableName][] = $key;
                }
 
                $dataRows = [];
-               foreach ( $keysByTable as $serverIndex => $serverKeys ) {
+               foreach ( $keysByTable as $shardIndex => $serverKeys ) {
                        try {
-                               $db = $this->getDB( $serverIndex );
+                               $db = $this->getConnection( $shardIndex );
                                foreach ( $serverKeys as $tableName => $tableKeys ) {
                                        $res = $db->select( $tableName,
                                                [ 'keyname', 'value', 'exptime' ],
@@ -305,13 +309,13 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                                                continue;
                                        }
                                        foreach ( $res as $row ) {
-                                               $row->serverIndex = $serverIndex;
+                                               $row->shardIndex = $shardIndex;
                                                $row->tableName = $tableName;
                                                $dataRows[$row->keyname] = $row;
                                        }
                                }
                        } catch ( DBError $e ) {
-                               $this->handleReadError( $e, $serverIndex );
+                               $this->handleReadError( $e, $shardIndex );
                        }
                }
 
@@ -321,14 +325,14 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                                $this->debug( "get: retrieved data; expiry time is " . $row->exptime );
                                $db = null; // in case of connection failure
                                try {
-                                       $db = $this->getDB( $row->serverIndex );
+                                       $db = $this->getConnection( $row->shardIndex );
                                        if ( $this->isExpired( $db, $row->exptime ) ) { // MISS
                                                $this->debug( "get: key has expired" );
                                        } else { // HIT
                                                $values[$key] = $db->decodeBlob( $row->value );
                                        }
                                } catch ( DBQueryError $e ) {
-                                       $this->handleWriteError( $e, $db, $row->serverIndex );
+                                       $this->handleWriteError( $e, $db, $row->shardIndex );
                                }
                        } else { // MISS
                                $this->debug( 'get: no matching rows' );
@@ -352,8 +356,8 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
        private function modifyMulti( array $data, $exptime, $flags, $op ) {
                $keysByTable = [];
                foreach ( $data as $key => $value ) {
-                       list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
-                       $keysByTable[$serverIndex][$tableName][] = $key;
+                       list( $shardIndex, $tableName ) = $this->getTableByKey( $key );
+                       $keysByTable[$shardIndex][$tableName][] = $key;
                }
 
                $exptime = $this->getExpirationAsTimestamp( $exptime );
@@ -361,14 +365,14 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                $result = true;
                /** @noinspection PhpUnusedLocalVariableInspection */
                $silenceScope = $this->silenceTransactionProfiler();
-               foreach ( $keysByTable as $serverIndex => $serverKeys ) {
+               foreach ( $keysByTable as $shardIndex => $serverKeys ) {
                        $db = null; // in case of connection failure
                        try {
-                               $db = $this->getDB( $serverIndex );
+                               $db = $this->getConnection( $shardIndex );
                                $this->occasionallyGarbageCollect( $db ); // expire old entries if any
                                $dbExpiry = $exptime ? $db->timestamp( $exptime ) : $this->getMaxDateTime( $db );
                        } catch ( DBError $e ) {
-                               $this->handleWriteError( $e, $db, $serverIndex );
+                               $this->handleWriteError( $e, $db, $shardIndex );
                                $result = false;
                                continue;
                        }
@@ -384,14 +388,14 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                                                $dbExpiry
                                        ) && $result;
                                } catch ( DBError $e ) {
-                                       $this->handleWriteError( $e, $db, $serverIndex );
+                                       $this->handleWriteError( $e, $db, $shardIndex );
                                        $result = false;
                                }
 
                        }
                }
 
-               if ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC ) {
+               if ( $this->fieldHasFlags( $flags, self::WRITE_SYNC ) ) {
                        $result = $this->waitForReplication() && $result;
                }
 
@@ -472,14 +476,14 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
        }
 
        protected function doCas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) {
-               list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
+               list( $shardIndex, $tableName ) = $this->getTableByKey( $key );
                $exptime = $this->getExpirationAsTimestamp( $exptime );
 
                /** @noinspection PhpUnusedLocalVariableInspection */
                $silenceScope = $this->silenceTransactionProfiler();
                $db = null; // in case of connection failure
                try {
-                       $db = $this->getDB( $serverIndex );
+                       $db = $this->getConnection( $shardIndex );
                        // (T26425) use a replace if the db supports it instead of
                        // delete/insert to avoid clashes with conflicting keynames
                        $db->update(
@@ -499,12 +503,17 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                                __METHOD__
                        );
                } catch ( DBQueryError $e ) {
-                       $this->handleWriteError( $e, $db, $serverIndex );
+                       $this->handleWriteError( $e, $db, $shardIndex );
 
                        return false;
                }
 
-               return (bool)$db->affectedRows();
+               $success = (bool)$db->affectedRows();
+               if ( $this->fieldHasFlags( $flags, self::WRITE_SYNC ) ) {
+                       $success = $this->waitForReplication() && $success;
+               }
+
+               return $success;
        }
 
        protected function doDeleteMulti( array $keys, $flags = 0 ) {
@@ -520,15 +529,15 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                return $this->modifyMulti( [ $key => null ], 0, $flags, self::$OP_DELETE );
        }
 
-       public function incr( $key, $step = 1 ) {
-               list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
+       public function incr( $key, $step = 1, $flags = 0 ) {
+               list( $shardIndex, $tableName ) = $this->getTableByKey( $key );
 
                $newCount = false;
                /** @noinspection PhpUnusedLocalVariableInspection */
                $silenceScope = $this->silenceTransactionProfiler();
                $db = null; // in case of connection failure
                try {
-                       $db = $this->getDB( $serverIndex );
+                       $db = $this->getConnection( $shardIndex );
                        $encTimestamp = $db->addQuotes( $db->timestamp() );
                        $db->update(
                                $tableName,
@@ -548,19 +557,14 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                                }
                        }
                } catch ( DBError $e ) {
-                       $this->handleWriteError( $e, $db, $serverIndex );
+                       $this->handleWriteError( $e, $db, $shardIndex );
                }
 
                return $newCount;
        }
 
-       public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
-               $ok = $this->mergeViaCas( $key, $callback, $exptime, $attempts, $flags );
-               if ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC ) {
-                       $ok = $this->waitForReplication() && $ok;
-               }
-
-               return $ok;
+       public function decr( $key, $value = 1, $flags = 0 ) {
+               return $this->incr( $key, -$value, $flags );
        }
 
        public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
@@ -581,10 +585,10 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
         * @param string $exptime
         * @return bool
         */
-       protected function isExpired( $db, $exptime ) {
+       private function isExpired( IDatabase $db, $exptime ) {
                return (
                        $exptime != $this->getMaxDateTime( $db ) &&
-                       wfTimestamp( TS_UNIX, $exptime ) < $this->getCurrentTime()
+                       ConvertibleTimestamp::convert( TS_UNIX, $exptime ) < $this->getCurrentTime()
                );
        }
 
@@ -592,7 +596,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
         * @param IDatabase $db
         * @return string
         */
-       protected function getMaxDateTime( $db ) {
+       private function getMaxDateTime( $db ) {
                if ( (int)$this->getCurrentTime() > 0x7fffffff ) {
                        return $db->timestamp( 1 << 62 );
                } else {
@@ -604,7 +608,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
         * @param IDatabase $db
         * @throws DBError
         */
-       protected function occasionallyGarbageCollect( IDatabase $db ) {
+       private function occasionallyGarbageCollect( IDatabase $db ) {
                if (
                        // Random purging is enabled
                        $this->purgePeriod &&
@@ -642,16 +646,16 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $silenceScope = $this->silenceTransactionProfiler();
 
-               $serverIndexes = range( 0, $this->numServers - 1 );
-               shuffle( $serverIndexes );
+               $shardIndexes = range( 0, $this->numServerShards - 1 );
+               shuffle( $shardIndexes );
 
                $ok = true;
 
                $keysDeletedCount = 0;
-               foreach ( $serverIndexes as $numServersDone => $serverIndex ) {
+               foreach ( $shardIndexes as $numServersDone => $shardIndex ) {
                        $db = null; // in case of connection failure
                        try {
-                               $db = $this->getDB( $serverIndex );
+                               $db = $this->getConnection( $shardIndex );
                                $this->deleteServerObjectsExpiringBefore(
                                        $db,
                                        $timestamp,
@@ -661,7 +665,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                                        $keysDeletedCount
                                );
                        } catch ( DBError $e ) {
-                               $this->handleWriteError( $e, $db, $serverIndex );
+                               $this->handleWriteError( $e, $db, $shardIndex );
                                $ok = false;
                        }
                }
@@ -686,8 +690,8 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                $serversDoneCount = 0,
                &$keysDeletedCount = 0
        ) {
-               $cutoffUnix = wfTimestamp( TS_UNIX, $timestamp );
-               $shardIndexes = range( 0, $this->shards - 1 );
+               $cutoffUnix = ConvertibleTimestamp::convert( TS_UNIX, $timestamp );
+               $shardIndexes = range( 0, $this->numTableShards - 1 );
                shuffle( $shardIndexes );
 
                foreach ( $shardIndexes as $numShardsDone => $shardIndex ) {
@@ -708,7 +712,8 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                                if ( $res->numRows() ) {
                                        $row = $res->current();
                                        if ( $lag === null ) {
-                                               $lag = max( $cutoffUnix - wfTimestamp( TS_UNIX, $row->exptime ), 1 );
+                                               $rowExpUnix = ConvertibleTimestamp::convert( TS_UNIX, $row->exptime );
+                                               $lag = max( $cutoffUnix - $rowExpUnix, 1 );
                                        }
 
                                        $keys = [];
@@ -730,15 +735,16 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
 
                                if ( is_callable( $progressCallback ) ) {
                                        if ( $lag ) {
-                                               $remainingLag = $cutoffUnix - wfTimestamp( TS_UNIX, $continue );
+                                               $continueUnix = ConvertibleTimestamp::convert( TS_UNIX, $continue );
+                                               $remainingLag = $cutoffUnix - $continueUnix;
                                                $processedLag = max( $lag - $remainingLag, 0 );
-                                               $doneRatio = ( $numShardsDone + $processedLag / $lag ) / $this->shards;
+                                               $doneRatio = ( $numShardsDone + $processedLag / $lag ) / $this->numTableShards;
                                        } else {
                                                $doneRatio = 1;
                                        }
 
-                                       $overallRatio = ( $doneRatio / $this->numServers )
-                                               + ( $serversDoneCount / $this->numServers );
+                                       $overallRatio = ( $doneRatio / $this->numServerShards )
+                                               + ( $serversDoneCount / $this->numServerShards );
                                        call_user_func( $progressCallback, $overallRatio * 100 );
                                }
                        } while ( $res->numRows() && $keysDeletedCount < $limit );
@@ -753,15 +759,15 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
        public function deleteAll() {
                /** @noinspection PhpUnusedLocalVariableInspection */
                $silenceScope = $this->silenceTransactionProfiler();
-               for ( $serverIndex = 0; $serverIndex < $this->numServers; $serverIndex++ ) {
+               for ( $shardIndex = 0; $shardIndex < $this->numServerShards; $shardIndex++ ) {
                        $db = null; // in case of connection failure
                        try {
-                               $db = $this->getDB( $serverIndex );
-                               for ( $i = 0; $i < $this->shards; $i++ ) {
+                               $db = $this->getConnection( $shardIndex );
+                               for ( $i = 0; $i < $this->numTableShards; $i++ ) {
                                        $db->delete( $this->getTableNameByShard( $i ), '*', __METHOD__ );
                                }
                        } catch ( DBError $e ) {
-                               $this->handleWriteError( $e, $db, $serverIndex );
+                               $this->handleWriteError( $e, $db, $shardIndex );
                                return false;
                        }
                }
@@ -779,11 +785,11 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                        }
                }
 
-               list( $serverIndex ) = $this->getTableByKey( $key );
+               list( $shardIndex ) = $this->getTableByKey( $key );
 
                $db = null; // in case of connection failure
                try {
-                       $db = $this->getDB( $serverIndex );
+                       $db = $this->getConnection( $shardIndex );
                        $ok = $db->lock( $key, __METHOD__, $timeout );
                        if ( $ok ) {
                                $this->locks[$key] = [ 'class' => $rclass, 'depth' => 1 ];
@@ -796,7 +802,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
 
                        return $ok;
                } catch ( DBError $e ) {
-                       $this->handleWriteError( $e, $db, $serverIndex );
+                       $this->handleWriteError( $e, $db, $shardIndex );
                        $ok = false;
                }
 
@@ -811,11 +817,11 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                if ( --$this->locks[$key]['depth'] <= 0 ) {
                        unset( $this->locks[$key] );
 
-                       list( $serverIndex ) = $this->getTableByKey( $key );
+                       list( $shardIndex ) = $this->getTableByKey( $key );
 
                        $db = null; // in case of connection failure
                        try {
-                               $db = $this->getDB( $serverIndex );
+                               $db = $this->getConnection( $shardIndex );
                                $ok = $db->unlock( $key, __METHOD__ );
                                if ( !$ok ) {
                                        $this->logger->warning(
@@ -824,7 +830,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                                        );
                                }
                        } catch ( DBError $e ) {
-                               $this->handleWriteError( $e, $db, $serverIndex );
+                               $this->handleWriteError( $e, $db, $shardIndex );
                                $ok = false;
                        }
 
@@ -866,9 +872,9 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                }
 
                if ( function_exists( 'gzinflate' ) ) {
-                       Wikimedia\suppressWarnings();
+                       AtEase::suppressWarnings();
                        $decomp = gzinflate( $serial );
-                       Wikimedia\restoreWarnings();
+                       AtEase::restoreWarnings();
 
                        if ( $decomp !== false ) {
                                $serial = $decomp;
@@ -882,11 +888,11 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
         * Handle a DBError which occurred during a read operation.
         *
         * @param DBError $exception
-        * @param int $serverIndex
+        * @param int $shardIndex
         */
-       protected function handleReadError( DBError $exception, $serverIndex ) {
+       private function handleReadError( DBError $exception, $shardIndex ) {
                if ( $exception instanceof DBConnectionError ) {
-                       $this->markServerDown( $exception, $serverIndex );
+                       $this->markServerDown( $exception, $shardIndex );
                }
 
                $this->setAndLogDBError( $exception );
@@ -897,12 +903,12 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
         *
         * @param DBError $exception
         * @param IDatabase|null $db DB handle or null if connection failed
-        * @param int $serverIndex
+        * @param int $shardIndex
         * @throws Exception
         */
-       protected function handleWriteError( DBError $exception, $db, $serverIndex ) {
+       private function handleWriteError( DBError $exception, $db, $shardIndex ) {
                if ( !( $db instanceof IDatabase ) ) {
-                       $this->markServerDown( $exception, $serverIndex );
+                       $this->markServerDown( $exception, $shardIndex );
                }
 
                $this->setAndLogDBError( $exception );
@@ -926,41 +932,68 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
         * Mark a server down due to a DBConnectionError exception
         *
         * @param DBError $exception
-        * @param int $serverIndex
+        * @param int $shardIndex
         */
-       protected function markServerDown( DBError $exception, $serverIndex ) {
-               unset( $this->conns[$serverIndex] ); // bug T103435
+       private function markServerDown( DBError $exception, $shardIndex ) {
+               unset( $this->conns[$shardIndex] ); // bug T103435
 
                $now = $this->getCurrentTime();
-               if ( isset( $this->connFailureTimes[$serverIndex] ) ) {
-                       if ( $now - $this->connFailureTimes[$serverIndex] >= 60 ) {
-                               unset( $this->connFailureTimes[$serverIndex] );
-                               unset( $this->connFailureErrors[$serverIndex] );
+               if ( isset( $this->connFailureTimes[$shardIndex] ) ) {
+                       if ( $now - $this->connFailureTimes[$shardIndex] >= 60 ) {
+                               unset( $this->connFailureTimes[$shardIndex] );
+                               unset( $this->connFailureErrors[$shardIndex] );
                        } else {
-                               $this->logger->debug( __METHOD__ . ": Server #$serverIndex already down" );
+                               $this->logger->debug( __METHOD__ . ": Server #$shardIndex already down" );
                                return;
                        }
                }
-               $this->logger->info( __METHOD__ . ": Server #$serverIndex down until " . ( $now + 60 ) );
-               $this->connFailureTimes[$serverIndex] = $now;
-               $this->connFailureErrors[$serverIndex] = $exception;
+               $this->logger->info( __METHOD__ . ": Server #$shardIndex down until " . ( $now + 60 ) );
+               $this->connFailureTimes[$shardIndex] = $now;
+               $this->connFailureErrors[$shardIndex] = $exception;
        }
 
        /**
-        * Create shard tables. For use from eval.php.
+        * @param IMaintainableDatabase $db
+        * @throws DBError
         */
-       public function createTables() {
-               for ( $serverIndex = 0; $serverIndex < $this->numServers; $serverIndex++ ) {
-                       $db = $this->getDB( $serverIndex );
-                       if ( $db->getType() !== 'mysql' ) {
-                               throw new MWException( __METHOD__ . ' is not supported on this DB server' );
-                       }
+       private function initSqliteDatabase( IMaintainableDatabase $db ) {
+               if ( $db->tableExists( 'objectcache' ) ) {
+                       return;
+               }
+               // Use one table for SQLite; sharding does not seem to have much benefit
+               $db->query( "PRAGMA journal_mode=WAL" ); // this is permanent
+               $db->startAtomic( __METHOD__ ); // atomic DDL
+               try {
+                       $encTable = $db->tableName( 'objectcache' );
+                       $encExptimeIndex = $db->addIdentifierQuotes( $db->tablePrefix() . 'exptime' );
+                       $db->query(
+                               "CREATE TABLE $encTable (\n" .
+                               "       keyname BLOB NOT NULL default '' PRIMARY KEY,\n" .
+                               "       value BLOB,\n" .
+                               "       exptime TEXT\n" .
+                               ")",
+                               __METHOD__
+                       );
+                       $db->query( "CREATE INDEX $encExptimeIndex ON $encTable (exptime)" );
+                       $db->endAtomic( __METHOD__ );
+               } catch ( DBError $e ) {
+                       $db->rollback( __METHOD__ );
+                       throw $e;
+               }
+       }
 
-                       for ( $i = 0; $i < $this->shards; $i++ ) {
-                               $db->query(
-                                       'CREATE TABLE ' . $db->tableName( $this->getTableNameByShard( $i ) ) .
-                                       ' LIKE ' . $db->tableName( 'objectcache' ),
-                                       __METHOD__ );
+       /**
+        * Create the shard tables on all databases (e.g. via eval.php/shell.php)
+        */
+       public function createTables() {
+               for ( $shardIndex = 0; $shardIndex < $this->numServerShards; $shardIndex++ ) {
+                       $db = $this->getConnection( $shardIndex );
+                       if ( in_array( $db->getType(), [ 'mysql', 'postgres' ], true ) ) {
+                               for ( $i = 0; $i < $this->numTableShards; $i++ ) {
+                                       $encBaseTable = $db->tableName( 'objectcache' );
+                                       $encShardTable = $db->tableName( $this->getTableNameByShard( $i ) );
+                                       $db->query( "CREATE TABLE $encShardTable LIKE $encBaseTable" );
+                               }
                        }
                }
        }
@@ -968,11 +1001,11 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
        /**
         * @return bool Whether the main DB is used, e.g. wfGetDB( DB_MASTER )
         */
-       protected function usesMainDB() {
+       private function usesMainDB() {
                return !$this->serverInfos;
        }
 
-       protected function waitForReplication() {
+       private function waitForReplication() {
                if ( !$this->usesMainDB() ) {
                        // Custom DB server list; probably doesn't use replication
                        return true;
@@ -1007,12 +1040,17 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
        }
 
        /**
-        * Returns a ScopedCallback which resets the silence flag in the transaction profiler when it is
-        * destroyed on the end of a scope, for example on return or throw
-        * @return ScopedCallback
-        * @since 1.32
+        * Silence the transaction profiler until the return value falls out of scope
+        *
+        * @return ScopedCallback|null
         */
-       protected function silenceTransactionProfiler() {
+       private function silenceTransactionProfiler() {
+               if ( !$this->usesMainDB() ) {
+                       // Custom DB is configured which either has no TransactionProfiler injected,
+                       // or has one specific for cache use, which we shouldn't silence
+                       return null;
+               }
+
                $trxProfiler = Profiler::instance()->getTransactionProfiler();
                $oldSilenced = $trxProfiler->setSilenced( true );
                return new ScopedCallback( function () use ( $trxProfiler, $oldSilenced ) {
index 04021cc..472bcdd 100644 (file)
  * @ingroup Pager
  */
 
-use Wikimedia\Rdbms\IResultWrapper;
-use Wikimedia\Rdbms\IDatabase;
+use MediaWiki\Linker\LinkRenderer;
 use MediaWiki\Linker\LinkTarget;
+use MediaWiki\MediaWikiServices;
 use MediaWiki\Navigation\PrevNextNavigationRenderer;
+use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Rdbms\IResultWrapper;
 
 /**
  * IndexPager is an efficient pager which uses a (roughly unique) index in the
@@ -157,7 +159,10 @@ abstract class IndexPager extends ContextSource implements Pager {
         */
        public $mResult;
 
-       public function __construct( IContextSource $context = null ) {
+       /** @var LinkRenderer */
+       private $linkRenderer;
+
+       public function __construct( IContextSource $context = null, LinkRenderer $linkRenderer = null ) {
                if ( $context ) {
                        $this->setContext( $context );
                }
@@ -209,6 +214,7 @@ abstract class IndexPager extends ContextSource implements Pager {
                                ? $dir[$this->mOrderType]
                                : $dir;
                }
+               $this->linkRenderer = $linkRenderer;
        }
 
        /**
@@ -526,9 +532,9 @@ abstract class IndexPager extends ContextSource implements Pager {
                        $attrs['class'] = "mw-{$type}link";
                }
 
-               return Linker::linkKnown(
+               return $this->getLinkRenderer()->makeKnownLink(
                        $this->getTitle(),
-                       $text,
+                       new HtmlArmor( $text ),
                        $attrs,
                        $query + $this->getDefaultQuery()
                );
@@ -804,4 +810,11 @@ abstract class IndexPager extends ContextSource implements Pager {
 
                return $prevNext->buildPrevNextNavigation( $title, $offset, $limit, $query,  $atend );
        }
+
+       protected function getLinkRenderer() {
+               if ( $this->linkRenderer === null ) {
+                        $this->linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
+               }
+               return $this->linkRenderer;
+       }
 }
index d94104b..f611699 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup Pager
  */
 
+use MediaWiki\Linker\LinkRenderer;
+
 /**
  * Table-based display with a user-selectable sort order
  * @ingroup Pager
@@ -32,7 +34,7 @@ abstract class TablePager extends IndexPager {
        /** @var stdClass */
        protected $mCurrentRow;
 
-       public function __construct( IContextSource $context = null ) {
+       public function __construct( IContextSource $context = null, LinkRenderer $linkRenderer = null ) {
                if ( $context ) {
                        $this->setContext( $context );
                }
@@ -49,7 +51,8 @@ abstract class TablePager extends IndexPager {
                        $this->mDefaultDirection = IndexPager::DIR_DESCENDING;
                } /* Else leave it at whatever the class default is */
 
-               parent::__construct();
+               // Parent constructor needs mSort set, so we call it last
+               parent::__construct( null, $linkRenderer );
        }
 
        /**
index a7916c5..e1f4f38 100644 (file)
@@ -434,7 +434,7 @@ class CoreParserFunctions {
                if ( !$wgRestrictDisplayTitle ||
                        ( $title instanceof Title
                        && !$title->hasFragment()
-                       && $title->equals( $parser->mTitle ) )
+                       && $title->equals( $parser->getTitle() ) )
                ) {
                        $old = $parser->mOutput->getProperty( 'displaytitle' );
                        if ( $old === false || $arg !== 'displaytitle_noreplace' ) {
@@ -845,10 +845,7 @@ class CoreParserFunctions {
         * @return string
         */
        public static function protectionlevel( $parser, $type = '', $title = '' ) {
-               $titleObject = Title::newFromText( $title );
-               if ( !( $titleObject instanceof Title ) ) {
-                       $titleObject = $parser->mTitle;
-               }
+               $titleObject = Title::newFromText( $title ) ?? $parser->getTitle();
                if ( $titleObject->areRestrictionsLoaded() || $parser->incrementExpensiveFunctionCount() ) {
                        $restrictions = $titleObject->getRestrictions( strtolower( $type ) );
                        # Title::getRestrictions returns an array, its possible it may have
@@ -871,10 +868,7 @@ class CoreParserFunctions {
         * @return string
         */
        public static function protectionexpiry( $parser, $type = '', $title = '' ) {
-               $titleObject = Title::newFromText( $title );
-               if ( !( $titleObject instanceof Title ) ) {
-                       $titleObject = $parser->mTitle;
-               }
+               $titleObject = Title::newFromText( $title ) ?? $parser->getTitle();
                if ( $titleObject->areRestrictionsLoaded() || $parser->incrementExpensiveFunctionCount() ) {
                        $expiry = $titleObject->getRestrictionExpiry( strtolower( $type ) );
                        // getRestrictionExpiry() returns false on invalid type; trying to
@@ -1377,10 +1371,7 @@ class CoreParserFunctions {
         * @since 1.23
         */
        public static function cascadingsources( $parser, $title = '' ) {
-               $titleObject = Title::newFromText( $title );
-               if ( !( $titleObject instanceof Title ) ) {
-                       $titleObject = $parser->mTitle;
-               }
+               $titleObject = Title::newFromText( $title ) ?? $parser->getTitle();
                if ( $titleObject->areCascadeProtectionSourcesLoaded()
                        || $parser->incrementExpensiveFunctionCount()
                ) {
index 452bab1..e3c12eb 100644 (file)
@@ -70,7 +70,7 @@ class PPFrame_DOM implements PPFrame {
        public function __construct( $preprocessor ) {
                $this->preprocessor = $preprocessor;
                $this->parser = $preprocessor->parser;
-               $this->title = $this->parser->mTitle;
+               $this->title = $this->parser->getTitle();
                $this->titleCache = [ $this->title ? $this->title->getPrefixedDBkey() : false ];
                $this->loopCheckHash = [];
                $this->depth = 0;
index 845ec73..f38cb06 100644 (file)
@@ -69,7 +69,7 @@ class PPFrame_Hash implements PPFrame {
        public function __construct( $preprocessor ) {
                $this->preprocessor = $preprocessor;
                $this->parser = $preprocessor->parser;
-               $this->title = $this->parser->mTitle;
+               $this->title = $this->parser->getTitle();
                $this->titleCache = [ $this->title ? $this->title->getPrefixedDBkey() : false ];
                $this->loopCheckHash = [];
                $this->depth = 0;
index 2585872..f871358 100644 (file)
@@ -49,7 +49,7 @@ class ParserCache {
        const USE_ANYTHING = 3;
 
        /** @var BagOStuff */
-       private $mMemc;
+       private $cache;
 
        /**
         * Anything cached prior to this is invalidated
@@ -79,7 +79,7 @@ class ParserCache {
         * @throws MWException
         */
        public function __construct( BagOStuff $cache, $cacheEpoch = '20030516000000' ) {
-               $this->mMemc = $cache;
+               $this->cache = $cache;
                $this->cacheEpoch = $cacheEpoch;
        }
 
@@ -95,7 +95,7 @@ class ParserCache {
                $pageid = $article->getId();
                $renderkey = (int)( $wgRequest->getVal( 'action' ) == 'render' );
 
-               $key = $this->mMemc->makeKey( 'pcache', 'idhash', "{$pageid}-{$renderkey}!{$hash}" );
+               $key = $this->cache->makeKey( 'pcache', 'idhash', "{$pageid}-{$renderkey}!{$hash}" );
                return $key;
        }
 
@@ -104,7 +104,7 @@ class ParserCache {
         * @return mixed|string
         */
        protected function getOptionsKey( $page ) {
-               return $this->mMemc->makeKey( 'pcache', 'idoptions', $page->getId() );
+               return $this->cache->makeKey( 'pcache', 'idoptions', $page->getId() );
        }
 
        /**
@@ -112,7 +112,7 @@ class ParserCache {
         * @since 1.28
         */
        public function deleteOptionsKey( $page ) {
-               $this->mMemc->delete( $this->getOptionsKey( $page ) );
+               $this->cache->delete( $this->getOptionsKey( $page ) );
        }
 
        /**
@@ -190,7 +190,7 @@ class ParserCache {
                }
 
                // Determine the options which affect this article
-               $optionsKey = $this->mMemc->get(
+               $optionsKey = $this->cache->get(
                        $this->getOptionsKey( $article ), BagOStuff::READ_VERIFIED );
                if ( $optionsKey instanceof CacheTime ) {
                        if ( $useOutdated < self::USE_EXPIRED && $optionsKey->expired( $article->getTouched() ) ) {
@@ -257,7 +257,7 @@ class ParserCache {
 
                $casToken = null;
                /** @var ParserOutput $value */
-               $value = $this->mMemc->get( $parserOutputKey, BagOStuff::READ_VERIFIED );
+               $value = $this->cache->get( $parserOutputKey, BagOStuff::READ_VERIFIED );
                if ( !$value ) {
                        wfDebug( "ParserOutput cache miss.\n" );
                        $this->incrementStats( $article, "miss.absent" );
@@ -319,7 +319,7 @@ class ParserCache {
                }
 
                $expire = $parserOutput->getCacheExpiry();
-               if ( $expire > 0 && !$this->mMemc instanceof EmptyBagOStuff ) {
+               if ( $expire > 0 && !$this->cache instanceof EmptyBagOStuff ) {
                        $cacheTime = $cacheTime ?: wfTimestampNow();
                        if ( !$revId ) {
                                $revision = $page->getRevision();
@@ -350,10 +350,15 @@ class ParserCache {
                        wfDebug( $msg );
 
                        // Save the parser output
-                       $this->mMemc->set( $parserOutputKey, $parserOutput, $expire );
+                       $this->cache->set(
+                               $parserOutputKey,
+                               $parserOutput,
+                               $expire,
+                               BagOStuff::WRITE_ALLOW_SEGMENTS
+                       );
 
                        // ...and its pointer
-                       $this->mMemc->set( $this->getOptionsKey( $page ), $optionsKey, $expire );
+                       $this->cache->set( $this->getOptionsKey( $page ), $optionsKey, $expire );
 
                        Hooks::run(
                                'ParserCacheSaveComplete',
@@ -372,6 +377,6 @@ class ParserCache {
         * @return BagOStuff
         */
        public function getCacheStorage() {
-               return $this->mMemc;
+               return $this->cache;
        }
 }
index c3af88f..8eecbcc 100644 (file)
@@ -151,8 +151,6 @@ class PasswordPolicyChecks {
                global $wgPopularPasswordFile, $wgSitename;
                $status = Status::newGood();
                if ( $policyVal > 0 ) {
-                       wfDeprecated( __METHOD__, '1.33' );
-
                        $langEn = Language::factory( 'en' );
                        $passwordKey = $langEn->lc( trim( $password ) );
 
index 001c975..00c2903 100644 (file)
@@ -232,7 +232,10 @@ class DefaultPreferencesFactory implements PreferencesFactory {
                        } elseif ( $field->validate( $globalDefault, $user->getOptions() ) === true ) {
                                $info['default'] = $globalDefault;
                        } else {
-                               throw new MWException( "Global default '$globalDefault' is invalid for field $name" );
+                               $globalDefault = json_encode( $globalDefault );
+                               throw new MWException(
+                                       "Default '$globalDefault' is invalid for preference $name of user $user"
+                               );
                        }
                }
 
index 554ca08..0fcc97d 100644 (file)
@@ -33,14 +33,15 @@ use Wikimedia\Rdbms\TransactionProfiler;
 abstract class Profiler {
        /** @var string|bool Profiler ID for bucketing data */
        protected $profileID = false;
-       /** @var bool Whether MediaWiki is in a SkinTemplate output context */
-       protected $templated = false;
        /** @var array All of the params passed from $wgProfiler */
        protected $params = [];
        /** @var IContextSource Current request context */
        protected $context = null;
        /** @var TransactionProfiler */
        protected $trxProfiler;
+       /** @var bool */
+       private $allowOutput = false;
+
        /** @var Profiler */
        private static $instance = null;
 
@@ -264,15 +265,20 @@ abstract class Profiler {
        }
 
        /**
-        * Get the content type sent out to the client.
-        * Used for profilers that output instead of store data.
-        * @return string
+        * Get the Content-Type for deciding how to format appended profile output.
+        *
+        * Disabled by default. Enable via setAllowOutput().
+        *
+        * @see ProfilerOutputText
         * @since 1.25
+        * @return string|null Returns null if disabled or no Content-Type found.
         */
        public function getContentType() {
-               foreach ( headers_list() as $header ) {
-                       if ( preg_match( '#^content-type: (\w+/\w+);?#i', $header, $m ) ) {
-                               return $m[1];
+               if ( $this->allowOutput ) {
+                       foreach ( headers_list() as $header ) {
+                               if ( preg_match( '#^content-type: (\w+/\w+);?#i', $header, $m ) ) {
+                                       return $m[1];
+                               }
                        }
                }
                return null;
@@ -281,19 +287,42 @@ abstract class Profiler {
        /**
         * Mark this call as templated or not
         *
+        * @deprecated since 1.34 Use setAllowOutput() instead.
         * @param bool $t
         */
        public function setTemplated( $t ) {
-               $this->templated = $t;
+               // wfDeprecated( __METHOD__, '1.34' );
+               $this->allowOutput = ( $t === true );
        }
 
        /**
         * Was this call as templated or not
         *
+        * @deprecated since 1.34 Use getAllowOutput() instead.
         * @return bool
         */
        public function getTemplated() {
-               return $this->templated;
+               // wfDeprecated( __METHOD__, '1.34' );
+               return $this->getAllowOutput();
+       }
+
+       /**
+        * Enable appending profiles to standard output.
+        *
+        * @since 1.34
+        */
+       public function setAllowOutput() {
+               $this->allowOutput = true;
+       }
+
+       /**
+        * Whether appending profiles is allowed.
+        *
+        * @since 1.34
+        * @return bool
+        */
+       public function getAllowOutput() {
+               return $this->allowOutput;
        }
 
        /**
index ff85c90..4bdaca5 100644 (file)
@@ -132,7 +132,7 @@ class IRCColourfulRCFeedFormatter implements RCFeedFormatter {
        }
 
        /**
-        * Remove newlines, carriage returns and decode html entites
+        * Remove newlines, carriage returns and decode html entities
         * @param string $text
         * @return string
         */
index 9892b15..0785225 100644 (file)
@@ -661,8 +661,9 @@ class ResourceLoader implements LoggerAwareInterface {
                                // Do not allow private modules to be loaded from the web.
                                // This is a security issue, see T36907.
                                if ( $module->getGroup() === 'private' ) {
+                                       // Not a serious error, just means something is trying to access it (T101806)
                                        $this->logger->debug( "Request for private module '$name' denied" );
-                                       $this->errors[] = "Cannot show private module \"$name\"";
+                                       $this->errors[] = "Cannot build private module \"$name\"";
                                        continue;
                                }
                                $modules[$name] = $module;
index 94e8a3e..c3948cb 100644 (file)
@@ -76,8 +76,8 @@ class ResourceLoaderContext implements MessageLocalizer {
                // Various parameters
                $this->user = $request->getRawVal( 'user' );
                $this->debug = $request->getRawVal( 'debug' ) === 'true';
-               $this->only = $request->getRawVal( 'only', null );
-               $this->version = $request->getRawVal( 'version', null );
+               $this->only = $request->getRawVal( 'only' );
+               $this->version = $request->getRawVal( 'version' );
                $this->raw = $request->getFuzzyBool( 'raw' );
 
                // Image requests
index 8f026dc..58c9ee5 100644 (file)
@@ -42,6 +42,15 @@ use MediaWiki\MediaWikiServices;
 class ResourceLoaderStartUpModule extends ResourceLoaderModule {
        protected $targets = [ 'desktop', 'mobile' ];
 
+       private $groupIds = [
+               // These reserved numbers MUST start at 0 and not skip any. These are preset
+               // for forward compatiblity so that they can be safely referenced by mediawiki.js,
+               // even when the code is cached and the order of registrations (and implicit
+               // group ids) changes between versions of the software.
+               'user' => 0,
+               'private' => 1,
+       ];
+
        /**
         * @param ResourceLoaderContext $context
         * @return array
@@ -304,7 +313,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
                        $registryData[$name] = [
                                'version' => $versionHash,
                                'dependencies' => $module->getDependencies( $context ),
-                               'group' => $module->getGroup(),
+                               'group' => $this->getGroupId( $module->getGroup() ),
                                'source' => $module->getSource(),
                                'skip' => $skipFunction,
                        ];
@@ -340,6 +349,18 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
                return $out;
        }
 
+       private function getGroupId( $groupName ) {
+               if ( $groupName === null ) {
+                       return null;
+               }
+
+               if ( !array_key_exists( $groupName, $this->groupIds ) ) {
+                       $this->groupIds[$groupName] = count( $this->groupIds );
+               }
+
+               return $this->groupIds[$groupName];
+       }
+
        /**
         * Base modules implicitly available to all modules.
         *
@@ -416,6 +437,8 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
                        ),
                        '$VARS.storeKey' => ResourceLoader::encodeJsonForScript( $this->getStoreKey() ),
                        '$VARS.storeVary' => ResourceLoader::encodeJsonForScript( $this->getStoreVary( $context ) ),
+                       '$VARS.groupUser' => ResourceLoader::encodeJsonForScript( $this->getGroupId( 'user' ) ),
+                       '$VARS.groupPrivate' => ResourceLoader::encodeJsonForScript( $this->getGroupId( 'private' ) ),
                ];
                $profilerStubs = [
                        '$CODE.profileExecuteStart();' => 'mw.loader.profiler.onExecuteStart( module );',
diff --git a/includes/search/RevisionSearchResult.php b/includes/search/RevisionSearchResult.php
new file mode 100644 (file)
index 0000000..f94ea2a
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * SearchResult class based on the Revision information.
+ * This class is suited for search engines that do not store a specialized version of the searched
+ * content.
+ */
+class RevisionSearchResult extends SearchResult {
+       use RevisionSearchResultTrait;
+
+       /**
+        * @param Title|null $title
+        */
+       public function __construct( $title ) {
+               $this->mTitle = $title;
+               $this->initFromTitle( $title );
+       }
+}
diff --git a/includes/search/RevisionSearchResultTrait.php b/includes/search/RevisionSearchResultTrait.php
new file mode 100644 (file)
index 0000000..24370c3
--- /dev/null
@@ -0,0 +1,200 @@
+<?php
+
+use MediaWiki\MediaWikiServices;
+
+/**
+ * Transitional trait used to share the methods between SearchResult and RevisionSearchResult.
+ * All the content of this trait can be moved to RevisionSearchResult once SearchResult is finally
+ * refactored into an abstract class.
+ * NOTE: This trait MUST NOT be used by something else than SearchResult and RevisionSearchResult.
+ * It will be removed without deprecation period once SearchResult
+ */
+trait RevisionSearchResultTrait {
+       /**
+        * @var Revision
+        */
+       protected $mRevision = null;
+
+       /**
+        * @var File
+        */
+       protected $mImage = null;
+
+       /**
+        * @var Title
+        */
+       protected $mTitle;
+
+       /**
+        * @var string
+        */
+       protected $mText;
+
+       /**
+        * Initialize from a Title and if possible initializes a corresponding
+        * Revision and File.
+        *
+        * @param Title $title
+        */
+       protected function initFromTitle( $title ) {
+               $this->mTitle = $title;
+               $services = MediaWikiServices::getInstance();
+               if ( !is_null( $this->mTitle ) ) {
+                       $id = false;
+                       Hooks::run( 'SearchResultInitFromTitle', [ $title, &$id ] );
+                       $this->mRevision = Revision::newFromTitle(
+                               $this->mTitle, $id, Revision::READ_NORMAL );
+                       if ( $this->mTitle->getNamespace() === NS_FILE ) {
+                               $this->mImage = $services->getRepoGroup()->findFile( $this->mTitle );
+                       }
+               }
+       }
+
+       /**
+        * Check if this is result points to an invalid title
+        *
+        * @return bool
+        */
+       public function isBrokenTitle() {
+               return is_null( $this->mTitle );
+       }
+
+       /**
+        * Check if target page is missing, happens when index is out of date
+        *
+        * @return bool
+        */
+       public function isMissingRevision() {
+               return !$this->mRevision && !$this->mImage;
+       }
+
+       /**
+        * @return Title
+        */
+       public function getTitle() {
+               return $this->mTitle;
+       }
+
+       /**
+        * Get the file for this page, if one exists
+        * @return File|null
+        */
+       public function getFile() {
+               return $this->mImage;
+       }
+
+       /**
+        * Lazy initialization of article text from DB
+        */
+       protected function initText() {
+               if ( !isset( $this->mText ) ) {
+                       if ( $this->mRevision != null ) {
+                               $content = $this->mRevision->getContent();
+                               $this->mText = $content !== null ? $content->getTextForSearchIndex() : '';
+                       } else { // TODO: can we fetch raw wikitext for commons images?
+                               $this->mText = '';
+                       }
+               }
+       }
+
+       /**
+        * @param string[] $terms Terms to highlight (this parameter is deprecated and ignored)
+        * @return string Highlighted text snippet, null (and not '') if not supported
+        */
+       public function getTextSnippet( $terms = [] ) {
+               return '';
+       }
+
+       /**
+        * @return string Highlighted title, '' if not supported
+        */
+       public function getTitleSnippet() {
+               return '';
+       }
+
+       /**
+        * @return string Highlighted redirect name (redirect to this page), '' if none or not supported
+        */
+       public function getRedirectSnippet() {
+               return '';
+       }
+
+       /**
+        * @return Title|null Title object for the redirect to this page, null if none or not supported
+        */
+       public function getRedirectTitle() {
+               return null;
+       }
+
+       /**
+        * @return string Highlighted relevant section name, null if none or not supported
+        */
+       public function getSectionSnippet() {
+               return '';
+       }
+
+       /**
+        * @return Title|null Title object (pagename+fragment) for the section,
+        *  null if none or not supported
+        */
+       public function getSectionTitle() {
+               return null;
+       }
+
+       /**
+        * @return string Highlighted relevant category name or '' if none or not supported
+        */
+       public function getCategorySnippet() {
+               return '';
+       }
+
+       /**
+        * @return string Timestamp
+        */
+       public function getTimestamp() {
+               if ( $this->mRevision ) {
+                       return $this->mRevision->getTimestamp();
+               } elseif ( $this->mImage ) {
+                       return $this->mImage->getTimestamp();
+               }
+               return '';
+       }
+
+       /**
+        * @return int Number of words
+        */
+       public function getWordCount() {
+               $this->initText();
+               return str_word_count( $this->mText );
+       }
+
+       /**
+        * @return int Size in bytes
+        */
+       public function getByteSize() {
+               $this->initText();
+               return strlen( $this->mText );
+       }
+
+       /**
+        * @return string Interwiki prefix of the title (return iw even if title is broken)
+        */
+       public function getInterwikiPrefix() {
+               return '';
+       }
+
+       /**
+        * @return string Interwiki namespace of the title (since we likely can't resolve it locally)
+        */
+       public function getInterwikiNamespaceText() {
+               return '';
+       }
+
+       /**
+        * Did this match file contents (eg: PDF/DJVU)?
+        * @return bool
+        */
+       public function isFileMatch() {
+               return false;
+       }
+}
index 1954e85..3d32de7 100644 (file)
  * @ingroup Search
  */
 
-use MediaWiki\MediaWikiServices;
-
 /**
- * @todo FIXME: This class is horribly factored. It would probably be better to
- * have a useful base class to which you pass some standard information, then
- * let the fancy self-highlighters extend that.
+ * NOTE: this class is being refactored into an abstract base class.
+ * If you extend this class directly, please implement all the methods declared
+ * in RevisionSearchResultTrait or extend RevisionSearchResult.
+ *
+ * Once the hard-deprecation period is over (1.36?):
+ * - all methods declared in RevisionSearchResultTrait should be declared
+ *   as abstract in this class
+ * - RevisionSearchResultTrait body should be moved to RevisionSearchResult and then removed without
+ *   deprecation
+ * - caveat: all classes extending this one may potentially break if they did not properly implement
+ *   all the methods.
  * @ingroup Search
  */
 class SearchResult {
+       use SearchResultTrait;
+       use RevisionSearchResultTrait;
 
-       /**
-        * @var Revision
-        */
-       protected $mRevision = null;
-
-       /**
-        * @var File
-        */
-       protected $mImage = null;
-
-       /**
-        * @var Title
-        */
-       protected $mTitle;
-
-       /**
-        * @var string
-        */
-       protected $mText;
-
-       /**
-        * A function returning a set of extension data.
-        * @var Closure|null
-        */
-       protected $extensionData;
+       public function __construct() {
+               if ( self::class === static::class ) {
+                       wfDeprecated( __METHOD__, '1.34' );
+               }
+       }
 
        /**
         * Return a new SearchResult and initializes it with a title.
@@ -65,216 +53,10 @@ class SearchResult {
         * @return SearchResult
         */
        public static function newFromTitle( $title, ISearchResultSet $parentSet = null ) {
-               $result = new static();
-               $result->initFromTitle( $title );
+               $result = new RevisionSearchResult( $title );
                if ( $parentSet ) {
                        $parentSet->augmentResult( $result );
                }
                return $result;
        }
-
-       /**
-        * Initialize from a Title and if possible initializes a corresponding
-        * Revision and File.
-        *
-        * @param Title $title
-        */
-       protected function initFromTitle( $title ) {
-               $this->mTitle = $title;
-               $services = MediaWikiServices::getInstance();
-               if ( !is_null( $this->mTitle ) ) {
-                       $id = false;
-                       Hooks::run( 'SearchResultInitFromTitle', [ $title, &$id ] );
-                       $this->mRevision = Revision::newFromTitle(
-                               $this->mTitle, $id, Revision::READ_NORMAL );
-                       if ( $this->mTitle->getNamespace() === NS_FILE ) {
-                               $this->mImage = $services->getRepoGroup()->findFile( $this->mTitle );
-                       }
-               }
-       }
-
-       /**
-        * Check if this is result points to an invalid title
-        *
-        * @return bool
-        */
-       public function isBrokenTitle() {
-               return is_null( $this->mTitle );
-       }
-
-       /**
-        * Check if target page is missing, happens when index is out of date
-        *
-        * @return bool
-        */
-       public function isMissingRevision() {
-               return !$this->mRevision && !$this->mImage;
-       }
-
-       /**
-        * @return Title
-        */
-       public function getTitle() {
-               return $this->mTitle;
-       }
-
-       /**
-        * Get the file for this page, if one exists
-        * @return File|null
-        */
-       public function getFile() {
-               return $this->mImage;
-       }
-
-       /**
-        * Lazy initialization of article text from DB
-        */
-       protected function initText() {
-               if ( !isset( $this->mText ) ) {
-                       if ( $this->mRevision != null ) {
-                               $content = $this->mRevision->getContent();
-                               $this->mText = $content !== null ? $content->getTextForSearchIndex() : '';
-                       } else { // TODO: can we fetch raw wikitext for commons images?
-                               $this->mText = '';
-                       }
-               }
-       }
-
-       /**
-        * @param string[] $terms Terms to highlight (this parameter is deprecated and ignored)
-        * @return string Highlighted text snippet, null (and not '') if not supported
-        */
-       public function getTextSnippet( $terms = [] ) {
-               return '';
-       }
-
-       /**
-        * @return string Highlighted title, '' if not supported
-        */
-       public function getTitleSnippet() {
-               return '';
-       }
-
-       /**
-        * @return string Highlighted redirect name (redirect to this page), '' if none or not supported
-        */
-       public function getRedirectSnippet() {
-               return '';
-       }
-
-       /**
-        * @return Title|null Title object for the redirect to this page, null if none or not supported
-        */
-       public function getRedirectTitle() {
-               return null;
-       }
-
-       /**
-        * @return string Highlighted relevant section name, null if none or not supported
-        */
-       public function getSectionSnippet() {
-               return '';
-       }
-
-       /**
-        * @return Title|null Title object (pagename+fragment) for the section,
-        *  null if none or not supported
-        */
-       public function getSectionTitle() {
-               return null;
-       }
-
-       /**
-        * @return string Highlighted relevant category name or '' if none or not supported
-        */
-       public function getCategorySnippet() {
-               return '';
-       }
-
-       /**
-        * @return string Timestamp
-        */
-       public function getTimestamp() {
-               if ( $this->mRevision ) {
-                       return $this->mRevision->getTimestamp();
-               } elseif ( $this->mImage ) {
-                       return $this->mImage->getTimestamp();
-               }
-               return '';
-       }
-
-       /**
-        * @return int Number of words
-        */
-       public function getWordCount() {
-               $this->initText();
-               return str_word_count( $this->mText );
-       }
-
-       /**
-        * @return int Size in bytes
-        */
-       public function getByteSize() {
-               $this->initText();
-               return strlen( $this->mText );
-       }
-
-       /**
-        * @return string Interwiki prefix of the title (return iw even if title is broken)
-        */
-       public function getInterwikiPrefix() {
-               return '';
-       }
-
-       /**
-        * @return string Interwiki namespace of the title (since we likely can't resolve it locally)
-        */
-       public function getInterwikiNamespaceText() {
-               return '';
-       }
-
-       /**
-        * Did this match file contents (eg: PDF/DJVU)?
-        * @return bool
-        */
-       public function isFileMatch() {
-               return false;
-       }
-
-       /**
-        * Get the extension data as:
-        * augmentor name => data
-        * @return array[]
-        */
-       public function getExtensionData() {
-               if ( $this->extensionData ) {
-                       return call_user_func( $this->extensionData );
-               } else {
-                       return [];
-               }
-       }
-
-       /**
-        * Set extension data for this result.
-        * The data is:
-        * augmentor name => data
-        * @param Closure|array $extensionData Takes no arguments, returns
-        *  either array of extension data or null.
-        */
-       public function setExtensionData( $extensionData ) {
-               if ( $extensionData instanceof Closure ) {
-                       $this->extensionData = $extensionData;
-               } elseif ( is_array( $extensionData ) ) {
-                       wfDeprecated( __METHOD__ . ' with array argument', '1.32' );
-                       $this->extensionData = function () use ( $extensionData ) {
-                               return $extensionData;
-                       };
-               } else {
-                       $type = is_object( $extensionData )
-                               ? get_class( $extensionData )
-                               : gettype( $extensionData );
-                       throw new \InvalidArgumentException(
-                               __METHOD__ . " must be called with Closure|array, but received $type" );
-               }
-       }
 }
diff --git a/includes/search/SearchResultTrait.php b/includes/search/SearchResultTrait.php
new file mode 100644 (file)
index 0000000..9a0df25
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * Trait for SearchResult subclasses to share non-obvious behaviors or methods
+ * that rarely specialized
+ */
+trait SearchResultTrait {
+       /**
+        * A function returning a set of extension data.
+        * @var Closure|null
+        */
+       protected $extensionData;
+
+       /**
+        * Get the extension data as:
+        * augmentor name => data
+        * @return array[]
+        */
+       public function getExtensionData() {
+               if ( $this->extensionData ) {
+                       return call_user_func( $this->extensionData );
+               } else {
+                       return [];
+               }
+       }
+
+       /**
+        * Set extension data for this result.
+        * The data is:
+        * augmentor name => data
+        * @param Closure|array $extensionData Takes no arguments, returns
+        *  either array of extension data or null.
+        */
+       public function setExtensionData( $extensionData ) {
+               if ( $extensionData instanceof Closure ) {
+                       $this->extensionData = $extensionData;
+               } elseif ( is_array( $extensionData ) ) {
+                       wfDeprecated( __METHOD__ . ' with array argument', '1.32' );
+                       $this->extensionData = function () use ( $extensionData ) {
+                               return $extensionData;
+                       };
+               } else {
+                       $type = is_object( $extensionData )
+                               ? get_class( $extensionData )
+                               : gettype( $extensionData );
+                       throw new \InvalidArgumentException(
+                               __METHOD__ . " must be called with Closure|array, but received $type" );
+               }
+       }
+}
index 9804e44..f470dbb 100644 (file)
@@ -22,7 +22,7 @@
  * @ingroup Search
  */
 
-class SqlSearchResult extends SearchResult {
+class SqlSearchResult extends RevisionSearchResult {
        /** @var string[] */
        private $terms;
 
@@ -32,7 +32,7 @@ class SqlSearchResult extends SearchResult {
         * @param string[] $terms list of parsed terms
         */
        public function __construct( Title $title, array $terms ) {
-               $this->initFromTitle( $title );
+               parent::__construct( $title );
                $this->terms = $terms;
        }
 
index e46f99d..eeed05e 100644 (file)
@@ -308,6 +308,7 @@ abstract class Skin extends ContextSource {
        /**
         * Get the current revision ID
         *
+        * @deprecated since 1.34, use OutputPage::getRevisionId instead
         * @return int
         */
        public function getRevisionId() {
@@ -317,11 +318,11 @@ abstract class Skin extends ContextSource {
        /**
         * Whether the revision displayed is the latest revision of the page
         *
+        * @deprecated since 1.34, use OutputPage::isRevisionCurrent instead
         * @return bool
         */
        public function isRevisionCurrent() {
-               $revID = $this->getRevisionId();
-               return $revID == 0 || $revID == $this->getTitle()->getLatestRevID();
+               return $this->getOutput()->isRevisionCurrent();
        }
 
        /**
@@ -701,7 +702,7 @@ abstract class Skin extends ContextSource {
         * @return string HTML text with an URL
         */
        function printSource() {
-               $oldid = $this->getRevisionId();
+               $oldid = $this->getOutput()->getRevisionId();
                if ( $oldid ) {
                        $canonicalUrl = $this->getTitle()->getCanonicalURL( 'oldid=' . $oldid );
                        $url = htmlspecialchars( wfExpandIRI( $canonicalUrl ) );
@@ -735,11 +736,24 @@ abstract class Skin extends ContextSource {
                                        $msg = 'viewdeleted';
                                }
 
-                               return $this->msg( $msg )->rawParams(
+                               $subtitle = $this->msg( $msg )->rawParams(
                                        $linkRenderer->makeKnownLink(
                                                SpecialPage::getTitleFor( 'Undelete', $this->getTitle()->getPrefixedDBkey() ),
                                                $this->msg( 'restorelink' )->numParams( $n )->text() )
                                        )->escaped();
+
+                               // Allow extensions to add more links
+                               $links = [];
+                               Hooks::run( 'UndeletePageToolLinks', [ $this->getContext(), $linkRenderer, &$links ] );
+
+                               if ( $links ) {
+                                       $subtitle .= ''
+                                               . $this->msg( 'word-separator' )->escaped()
+                                               . $this->msg( 'parentheses' )
+                                                       ->rawParams( $this->getLanguage()->pipeList( $links ) )
+                                                       ->escaped();
+                               }
+                               return Html::rawElement( 'div', [ 'class' => 'mw-undelete-subtitle' ], $subtitle );
                        }
                }
 
@@ -830,7 +844,7 @@ abstract class Skin extends ContextSource {
        function getCopyright( $type = 'detect' ) {
                $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
                if ( $type == 'detect' ) {
-                       if ( !$this->isRevisionCurrent()
+                       if ( !$this->getOutput()->isRevisionCurrent()
                                && !$this->msg( 'history_copyright' )->inContentLanguage()->isDisabled()
                        ) {
                                $type = 'history';
@@ -934,7 +948,8 @@ abstract class Skin extends ContextSource {
 
                # No cached timestamp, load it from the database
                if ( $timestamp === null ) {
-                       $timestamp = Revision::getTimestampFromId( $this->getTitle(), $this->getRevisionId() );
+                       $timestamp = Revision::getTimestampFromId( $this->getTitle(),
+                               $this->getOutput()->getRevisionId() );
                }
 
                if ( $timestamp ) {
@@ -1088,8 +1103,8 @@ abstract class Skin extends ContextSource {
        function editUrlOptions() {
                $options = [ 'action' => 'edit' ];
 
-               if ( !$this->isRevisionCurrent() ) {
-                       $options['oldid'] = intval( $this->getRevisionId() );
+               if ( !$this->getOutput()->isRevisionCurrent() ) {
+                       $options['oldid'] = intval( $this->getOutput()->getRevisionId() );
                }
 
                return $options;
index d1345b8..f348135 100644 (file)
@@ -371,7 +371,7 @@ class SkinTemplate extends Skin {
                $tpl->set( 'credits', false );
                $tpl->set( 'numberofwatchingusers', false );
                if ( $title->exists() ) {
-                       if ( $out->isArticle() && $this->isRevisionCurrent() ) {
+                       if ( $out->isArticle() && $out->isRevisionCurrent() ) {
                                if ( $wgMaxCredits != 0 ) {
                                        /** @var CreditsAction $action */
                                        $action = Action::factory(
@@ -975,7 +975,7 @@ class SkinTemplate extends Skin {
                                        // Whether to show the "Add a new section" tab
                                        // Checks if this is a current rev of talk page and is not forced to be hidden
                                        $showNewSection = !$out->forceHideNewSectionLink()
-                                               && ( ( $isTalk && $this->isRevisionCurrent() ) || $out->showNewSectionLink() );
+                                               && ( ( $isTalk && $out->isRevisionCurrent() ) || $out->showNewSectionLink() );
                                        $section = $request->getVal( 'section' );
 
                                        if ( $title->exists()
@@ -1295,7 +1295,7 @@ class SkinTemplate extends Skin {
 
                if ( $out->isArticle() ) {
                        // Also add a "permalink" while we're at it
-                       $revid = $this->getRevisionId();
+                       $revid = $this->getOutput()->getRevisionId();
                        if ( $revid ) {
                                $nav_urls['permalink'] = [
                                        'text' => $this->msg( 'permalink' )->text(),
index 2fa8fab..3893e92 100644 (file)
@@ -766,6 +766,33 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                }
        }
 
+       /**
+        * @see $wgRCLinkDays in DefaultSettings.php.
+        * @see $wgRCFilterByAge in DefaultSettings.php.
+        * @return int[]
+        */
+       protected function getLinkDays() {
+               $linkDays = $this->getConfig()->get( 'RCLinkDays' );
+               $filterByAge = $this->getConfig()->get( 'RCFilterByAge' );
+               $maxAge = $this->getConfig()->get( 'RCMaxAge' );
+               if ( $filterByAge ) {
+                       // Trim it to only links which are within $wgRCMaxAge.
+                       // Note that we allow one link higher than the max for things like
+                       // "age 56 days" being accessible through the "60 days" link.
+                       sort( $linkDays );
+
+                       $maxAgeDays = $maxAge / ( 3600 * 24 );
+                       foreach ( $linkDays as $i => $days ) {
+                               if ( $days >= $maxAgeDays ) {
+                                       array_splice( $linkDays, $i + 1 );
+                                       break;
+                               }
+                       }
+               }
+
+               return $linkDays;
+       }
+
        /**
         * Include the modules and configuration for the RCFilters app.
         * Conditional on the user having the feature enabled.
@@ -798,7 +825,7 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                                        'maxDays' => (int)$this->getConfig()->get( 'RCMaxAge' ) / ( 24 * 3600 ), // Translate to days
                                        'limitArray' => $this->getConfig()->get( 'RCLinkLimits' ),
                                        'limitDefault' => $this->getDefaultLimit(),
-                                       'daysArray' => $this->getConfig()->get( 'RCLinkDays' ),
+                                       'daysArray' => $this->getLinkDays(),
                                        'daysDefault' => $this->getDefaultDays(),
                                ]
                        );
index 0425a58..f1843ea 100644 (file)
@@ -55,13 +55,6 @@ class SpecialContributions extends IncludableSpecialPage {
 
                $target = $par ?? $request->getVal( 'target' );
 
-               if ( $request->getVal( 'contribs' ) == 'newbie' || $par === 'newbies' ) {
-                       $target = 'newbies';
-                       $this->opts['contribs'] = 'newbie';
-               } else {
-                       $this->opts['contribs'] = 'user';
-               }
-
                $this->opts['deletedOnly'] = $request->getBool( 'deletedOnly' );
 
                if ( !strlen( $target ) ) {
@@ -81,14 +74,7 @@ class SpecialContributions extends IncludableSpecialPage {
                $this->opts['hideMinor'] = $request->getBool( 'hideMinor' );
 
                $id = 0;
-               if ( $this->opts['contribs'] === 'newbie' ) {
-                       $userObj = User::newFromName( $target ); // hysterical raisins
-                       $out->addSubtitle( $this->msg( 'sp-contributions-newbies-sub' ) );
-                       $out->setHTMLTitle( $this->msg(
-                               'pagetitle',
-                               $this->msg( 'sp-contributions-newbies-title' )->plain()
-                       )->inContentLanguage() );
-               } elseif ( ExternalUserNames::isExternal( $target ) ) {
+               if ( ExternalUserNames::isExternal( $target ) ) {
                        $userObj = User::newFromName( $target, false );
                        if ( !$userObj ) {
                                $out->addHTML( $this->getForm() );
@@ -217,7 +203,8 @@ class SpecialContributions extends IncludableSpecialPage {
                        }
                        $pager = new ContribsPager( $this->getContext(), [
                                'target' => $target,
-                               'contribs' => $this->opts['contribs'],
+                               // Temporary, until newbie feature is fully removed from ContribsPager
+                               'contribs' => 'user',
                                'namespace' => $this->opts['namespace'],
                                'tagfilter' => $this->opts['tagfilter'],
                                'start' => $this->opts['start'],
@@ -256,9 +243,7 @@ class SpecialContributions extends IncludableSpecialPage {
                        $out->preventClickjacking( $pager->getPreventClickjacking() );
 
                        # Show the appropriate "footer" message - WHOIS tools, etc.
-                       if ( $this->opts['contribs'] == 'newbie' ) {
-                               $message = 'sp-contributions-footer-newbies';
-                       } elseif ( IP::isValidRange( $target ) ) {
+                       if ( IP::isValidRange( $target ) ) {
                                $message = 'sp-contributions-footer-anon-range';
                        } elseif ( IP::isIPAddress( $target ) ) {
                                $message = 'sp-contributions-footer-anon';
@@ -491,10 +476,6 @@ class SpecialContributions extends IncludableSpecialPage {
                        $this->opts['associated'] = false;
                }
 
-               if ( !isset( $this->opts['contribs'] ) ) {
-                       $this->opts['contribs'] = 'user';
-               }
-
                if ( !isset( $this->opts['start'] ) ) {
                        $this->opts['start'] = '';
                }
@@ -503,10 +484,6 @@ class SpecialContributions extends IncludableSpecialPage {
                        $this->opts['end'] = '';
                }
 
-               if ( $this->opts['contribs'] == 'newbie' ) {
-                       $this->opts['target'] = '';
-               }
-
                if ( !isset( $this->opts['tagfilter'] ) ) {
                        $this->opts['tagfilter'] = '';
                }
@@ -578,20 +555,12 @@ class SpecialContributions extends IncludableSpecialPage {
                        $filterSelection = Html::rawElement( 'div', [], '' );
                }
 
-               $labelNewbies = Xml::radioLabel(
-                       $this->msg( 'sp-contributions-newbies' )->text(),
-                       'contribs',
-                       'newbie',
-                       'newbie',
-                       $this->opts['contribs'] == 'newbie',
-                       [ 'class' => 'mw-input' ]
-               );
                $labelUsername = Xml::radioLabel(
                        $this->msg( 'sp-contributions-username' )->text(),
                        'contribs',
                        'user',
                        'user',
-                       $this->opts['contribs'] == 'user',
+                       true,
                        [ 'class' => 'mw-input' ]
                );
                $input = Html::input(
@@ -607,16 +576,15 @@ class SpecialContributions extends IncludableSpecialPage {
                                        'mw-autocomplete-user', // used by mediawiki.userSuggest
                                ],
                        ] + (
-                               // Only autofocus if target hasn't been specified or in non-newbies mode
-                               ( $this->opts['contribs'] === 'newbie' || $this->opts['target'] )
-                                       ? [] : [ 'autofocus' => true ]
-                               )
+                               // Only autofocus if target hasn't been specified
+                               $this->opts['target'] ? [] : [ 'autofocus' => true ]
+                       )
                );
 
                $targetSelection = Html::rawElement(
                        'div',
                        [],
-                       $labelNewbies . '<br>' . $labelUsername . ' ' . $input . ' '
+                       $labelUsername . ' ' . $input . ' '
                );
 
                $hidden = $this->opts['namespace'] === '' ? ' mw-input-hidden' : '';
index c124c14..6c328da 100644 (file)
@@ -24,7 +24,7 @@ class SpecialNewSection extends RedirectSpecialPage {
        public function __construct() {
                parent::__construct( 'NewSection' );
                $this->mAllowedRedirectParams = [ 'preloadtitle', 'nosummary', 'editintro',
-                       'preload', 'preloadparams[]', 'summary' ];
+                       'preload', 'preloadparams', 'summary' ];
        }
 
        /**
@@ -43,6 +43,7 @@ class SpecialNewSection extends RedirectSpecialPage {
        protected function showNoRedirectPage() {
                $this->setHeaders();
                $this->outputHeader();
+               $this->addHelpLink( 'Help:New section' );
                $this->showForm();
        }
 
index 06e1c77..e0834d5 100644 (file)
@@ -50,7 +50,6 @@ class SpecialNewFiles extends IncludableSpecialPage {
                $opts->add( 'like', '' );
                $opts->add( 'user', '' );
                $opts->add( 'showbots', false );
-               $opts->add( 'newbies', false );
                $opts->add( 'hidepatrolled', false );
                $opts->add( 'mediatype', $this->mediaTypes );
                $opts->add( 'limit', 50 );
@@ -137,12 +136,6 @@ class SpecialNewFiles extends IncludableSpecialPage {
                                'name' => 'user',
                        ],
 
-                       'newbies' => [
-                               'type' => 'check',
-                               'label-message' => 'newimages-newbies',
-                               'name' => 'newbies',
-                       ],
-
                        'showbots' => [
                                'type' => 'check',
                                'label-message' => 'newimages-showbots',
index 6949c61..30f4655 100644 (file)
@@ -847,7 +847,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
                sort( $linkLimits );
                $linkLimits = array_unique( $linkLimits );
 
-               $linkDays = $config->get( 'RCLinkDays' );
+               $linkDays = $this->getLinkDays();
                $linkDays[] = $options['days'];
                sort( $linkDays );
                $linkDays = array_unique( $linkDays );
index 76e2ab7..45d77ce 100644 (file)
@@ -235,7 +235,7 @@ class AllMessagesTablePager extends TablePager {
        }
 
        function formatValue( $field, $value ) {
-               $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
+               $linkRenderer = $this->getLinkRenderer();
                switch ( $field ) {
                        case 'am_title' :
                                $title = Title::makeTitle( NS_MEDIAWIKI, $value . $this->suffix );
@@ -256,8 +256,7 @@ class AllMessagesTablePager extends TablePager {
                                        $title = $linkRenderer->makeKnownLink( $title, $this->getLanguage()->lcfirst( $value ) );
                                } else {
                                        $title = $linkRenderer->makeBrokenLink(
-                                               $title,
-                                               $this->getLanguage()->lcfirst( $value )
+                                               $title, $this->getLanguage()->lcfirst( $value )
                                        );
                                }
                                if ( $this->mCurrentRow->am_talk_exists ) {
index 01aed22..77b7326 100644 (file)
@@ -45,9 +45,9 @@ class BlockListPager extends TablePager {
         * @param array $conds
         */
        public function __construct( $page, $conds ) {
+               parent::__construct( $page->getContext(), $page->getLinkRenderer() );
                $this->conds = $conds;
                $this->mDefaultDirection = IndexPager::DIR_DESCENDING;
-               parent::__construct( $page->getContext() );
        }
 
        function getFieldNames() {
@@ -97,7 +97,7 @@ class BlockListPager extends TablePager {
 
                $formatted = '';
 
-               $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
+               $linkRenderer = $this->getLinkRenderer();
 
                switch ( $name ) {
                        case 'ipb_timestamp':
@@ -250,7 +250,7 @@ class BlockListPager extends TablePager {
         */
        private function getRestrictionListHTML( stdClass $row ) {
                $items = [];
-               $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
+               $linkRenderer = $this->getLinkRenderer();
 
                foreach ( $this->restrictions as $restriction ) {
                        if ( $restriction->getBlockId() !== (int)$row->ipb_id ) {
index 7db90c1..db2ee38 100644 (file)
@@ -25,11 +25,6 @@ use MediaWiki\Linker\LinkRenderer;
  */
 class CategoryPager extends AlphabeticPager {
 
-       /**
-        * @var LinkRenderer
-        */
-       protected $linkRenderer;
-
        /**
         * @param IContextSource $context
         * @param string $from
@@ -37,15 +32,13 @@ class CategoryPager extends AlphabeticPager {
         */
        public function __construct( IContextSource $context, $from, LinkRenderer $linkRenderer
        ) {
-               parent::__construct( $context );
+               parent::__construct( $context, $linkRenderer );
                $from = str_replace( ' ', '_', $from );
                if ( $from !== '' ) {
                        $from = Title::capitalize( $from, NS_CATEGORY );
                        $this->setOffset( $from );
                        $this->setIncludeOffset( true );
                }
-
-               $this->linkRenderer = $linkRenderer;
        }
 
        function getQueryInfo() {
@@ -85,7 +78,7 @@ class CategoryPager extends AlphabeticPager {
        function formatRow( $result ) {
                $title = new TitleValue( NS_CATEGORY, $result->cat_title );
                $text = $title->getText();
-               $link = $this->linkRenderer->makeLink( $title, $text );
+               $link = $this->getLinkRenderer()->makeLink( $title, $text );
 
                $count = $this->msg( 'nmembers' )->numParams( $result->cat_pages )->escaped();
                return Html::rawElement( 'li', null, $this->getLanguage()->specialList( $link, $count ) ) . "\n";
index 9ac7df5..1b0c59a 100644 (file)
@@ -621,7 +621,7 @@ class ContribsPager extends RangeChronologicalPager {
                $classes = [];
                $attribs = [];
 
-               $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
+               $linkRenderer = $this->getLinkRenderer();
 
                $page = null;
                // Create a title for the revision if possible
index 2f40ace..7dbfae8 100644 (file)
@@ -289,7 +289,7 @@ class DeletedContribsPager extends IndexPager {
        function formatRevisionRow( $row ) {
                $page = Title::makeTitle( $row->ar_namespace, $row->ar_title );
 
-               $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
+               $linkRenderer = $this->getLinkRenderer();
 
                $rev = new Revision( [
                        'title' => $page,
index 2d3b6b2..81b7808 100644 (file)
@@ -54,6 +54,7 @@ class ImageListPager extends TablePager {
                $including = false, $showAll = false
        ) {
                $this->setContext( $context );
+
                $this->mIncluding = $including;
                $this->mShowAll = $showAll;
 
@@ -95,7 +96,7 @@ class ImageListPager extends TablePager {
                        $this->mDefaultDirection = IndexPager::DIR_DESCENDING;
                }
 
-               parent::__construct( $context );
+               parent::__construct();
        }
 
        /**
@@ -437,7 +438,7 @@ class ImageListPager extends TablePager {
         */
        function formatValue( $field, $value ) {
                $services = MediaWikiServices::getInstance();
-               $linkRenderer = $services->getLinkRenderer();
+               $linkRenderer = $this->getLinkRenderer();
                switch ( $field ) {
                        case 'thumb':
                                $opt = [ 'time' => wfTimestamp( TS_MW, $this->mCurrentRow->img_timestamp ) ];
@@ -478,7 +479,7 @@ class ImageListPager extends TablePager {
 
                                        // Add delete links if allowed
                                        // From https://github.com/Wikia/app/pull/3859
-                                       $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
+                                       $permissionManager = $services->getPermissionManager();
 
                                        if ( $permissionManager->userCan( 'delete', $this->getUser(), $filePage ) ) {
                                                $deleteMsg = $this->msg( 'listfiles-delete' )->text();
index ed86e54..57db8b3 100644 (file)
@@ -194,7 +194,7 @@ class NewFilesPager extends RangeChronologicalPager {
                $user = User::newFromId( $row->img_user );
 
                $title = Title::makeTitle( NS_FILE, $name );
-               $ul = MediaWikiServices::getInstance()->getLinkRenderer()->makeLink(
+               $ul = $this->getLinkRenderer()->makeLink(
                        $user->getUserPage(),
                        $user->getName()
                );
index 5583842..747dea2 100644 (file)
@@ -26,11 +26,6 @@ class ProtectedPagesPager extends TablePager {
        public $mConds;
        private $type, $level, $namespace, $sizetype, $size, $indefonly, $cascadeonly, $noredirect;
 
-       /**
-        * @var LinkRenderer
-        */
-       private $linkRenderer;
-
        /**
         * @param SpecialPage $form
         * @param array $conds
@@ -48,6 +43,7 @@ class ProtectedPagesPager extends TablePager {
                $sizetype, $size, $indefonly, $cascadeonly, $noredirect,
                LinkRenderer $linkRenderer
        ) {
+               parent::__construct( $form->getContext(), $linkRenderer );
                $this->mConds = $conds;
                $this->type = $type ?: 'edit';
                $this->level = $level;
@@ -57,8 +53,6 @@ class ProtectedPagesPager extends TablePager {
                $this->indefonly = (bool)$indefonly;
                $this->cascadeonly = (bool)$cascadeonly;
                $this->noredirect = (bool)$noredirect;
-               $this->linkRenderer = $linkRenderer;
-               parent::__construct( $form->getContext() );
        }
 
        function preprocessResults( $result ) {
@@ -119,6 +113,7 @@ class ProtectedPagesPager extends TablePager {
        function formatValue( $field, $value ) {
                /** @var object $row */
                $row = $this->mCurrentRow;
+               $linkRenderer = $this->getLinkRenderer();
 
                switch ( $field ) {
                        case 'log_timestamp':
@@ -148,7 +143,7 @@ class ProtectedPagesPager extends TablePager {
                                                )
                                        );
                                } else {
-                                       $formatted = $this->linkRenderer->makeLink( $title );
+                                       $formatted = $linkRenderer->makeLink( $title );
                                }
                                if ( !is_null( $row->page_len ) ) {
                                        $formatted .= $this->getLanguage()->getDirMark() .
@@ -165,7 +160,7 @@ class ProtectedPagesPager extends TablePager {
                                        $value, /* User preference timezone */true ) );
                                $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
                                if ( $this->getUser()->isAllowed( 'protect' ) && $title ) {
-                                       $changeProtection = $this->linkRenderer->makeKnownLink(
+                                       $changeProtection = $linkRenderer->makeKnownLink(
                                                $title,
                                                $this->msg( 'protect_change' )->text(),
                                                [],
index 4fcf98d..0553c92 100644 (file)
@@ -2042,14 +2042,10 @@ class User implements IDBAccessObject, UserIdentity {
                        $summary = "(limit $max in {$period}s)";
                        $count = $cache->get( $key );
                        // Already pinged?
-                       if ( $count ) {
-                               if ( $count >= $max ) {
-                                       wfDebugLog( 'ratelimit', "User '{$this->getName()}' " .
-                                               "(IP {$this->getRequest()->getIP()}) tripped $key at $count $summary" );
-                                       $triggered = true;
-                               } else {
-                                       wfDebug( __METHOD__ . ": ok. $key at $count $summary\n" );
-                               }
+                       if ( $count && $count >= $max ) {
+                               wfDebugLog( 'ratelimit', "User '{$this->getName()}' " .
+                                       "(IP {$this->getRequest()->getIP()}) tripped $key at $count $summary" );
+                               $triggered = true;
                        } else {
                                wfDebug( __METHOD__ . ": adding record for $key $summary\n" );
                                if ( $incrBy > 0 ) {
@@ -2057,7 +2053,7 @@ class User implements IDBAccessObject, UserIdentity {
                                }
                        }
                        if ( $incrBy > 0 ) {
-                               $cache->incr( $key, $incrBy );
+                               $cache->incrWithInit( $key, (int)$period, $incrBy, $incrBy );
                        }
                }
 
diff --git a/languages/ConverterRule.php b/languages/ConverterRule.php
deleted file mode 100644 (file)
index 4a330ad..0000000
+++ /dev/null
@@ -1,498 +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 Language
- */
-
-/**
- * Parser for rules of language conversion, parse rules in -{ }- tag.
- * @ingroup Language
- * @author fdcn <fdcn64@gmail.com>, PhiLiP <philip.npc@gmail.com>
- */
-class ConverterRule {
-       public $mText; // original text in -{text}-
-       public $mConverter; // LanguageConverter object
-       public $mRuleDisplay = '';
-       public $mRuleTitle = false;
-       public $mRules = ''; // string : the text of the rules
-       public $mRulesAction = 'none';
-       public $mFlags = [];
-       public $mVariantFlags = [];
-       public $mConvTable = [];
-       public $mBidtable = []; // array of the translation in each variant
-       public $mUnidtable = []; // array of the translation in each variant
-
-       /**
-        * @param string $text The text between -{ and }-
-        * @param LanguageConverter $converter
-        */
-       public function __construct( $text, $converter ) {
-               $this->mText = $text;
-               $this->mConverter = $converter;
-       }
-
-       /**
-        * Check if variants array in convert array.
-        *
-        * @param array|string $variants Variant language code
-        * @return string Translated text
-        */
-       public function getTextInBidtable( $variants ) {
-               $variants = (array)$variants;
-               if ( !$variants ) {
-                       return false;
-               }
-               foreach ( $variants as $variant ) {
-                       if ( isset( $this->mBidtable[$variant] ) ) {
-                               return $this->mBidtable[$variant];
-                       }
-               }
-               return false;
-       }
-
-       /**
-        * Parse flags with syntax -{FLAG| ... }-
-        * @private
-        */
-       function parseFlags() {
-               $text = $this->mText;
-               $flags = [];
-               $variantFlags = [];
-
-               $sepPos = strpos( $text, '|' );
-               if ( $sepPos !== false ) {
-                       $validFlags = $this->mConverter->mFlags;
-                       $f = StringUtils::explode( ';', substr( $text, 0, $sepPos ) );
-                       foreach ( $f as $ff ) {
-                               $ff = trim( $ff );
-                               if ( isset( $validFlags[$ff] ) ) {
-                                       $flags[$validFlags[$ff]] = true;
-                               }
-                       }
-                       $text = strval( substr( $text, $sepPos + 1 ) );
-               }
-
-               if ( !$flags ) {
-                       $flags['S'] = true;
-               } elseif ( isset( $flags['R'] ) ) {
-                       $flags = [ 'R' => true ];// remove other flags
-               } elseif ( isset( $flags['N'] ) ) {
-                       $flags = [ 'N' => true ];// remove other flags
-               } elseif ( isset( $flags['-'] ) ) {
-                       $flags = [ '-' => true ];// remove other flags
-               } elseif ( count( $flags ) == 1 && isset( $flags['T'] ) ) {
-                       $flags['H'] = true;
-               } elseif ( isset( $flags['H'] ) ) {
-                       // replace A flag, and remove other flags except T
-                       $temp = [ '+' => true, 'H' => true ];
-                       if ( isset( $flags['T'] ) ) {
-                               $temp['T'] = true;
-                       }
-                       if ( isset( $flags['D'] ) ) {
-                               $temp['D'] = true;
-                       }
-                       $flags = $temp;
-               } else {
-                       if ( isset( $flags['A'] ) ) {
-                               $flags['+'] = true;
-                               $flags['S'] = true;
-                       }
-                       if ( isset( $flags['D'] ) ) {
-                               unset( $flags['S'] );
-                       }
-                       // try to find flags like "zh-hans", "zh-hant"
-                       // allow syntaxes like "-{zh-hans;zh-hant|XXXX}-"
-                       $variantFlags = array_intersect( array_keys( $flags ), $this->mConverter->mVariants );
-                       if ( $variantFlags ) {
-                               $variantFlags = array_flip( $variantFlags );
-                               $flags = [];
-                       }
-               }
-               $this->mVariantFlags = $variantFlags;
-               $this->mRules = $text;
-               $this->mFlags = $flags;
-       }
-
-       /**
-        * Generate conversion table.
-        * @private
-        */
-       function parseRules() {
-               $rules = $this->mRules;
-               $bidtable = [];
-               $unidtable = [];
-               $variants = $this->mConverter->mVariants;
-               $varsep_pattern = $this->mConverter->getVarSeparatorPattern();
-
-               // Split according to $varsep_pattern, but ignore semicolons from HTML entities
-               $rules = preg_replace( '/(&[#a-zA-Z0-9]+);/', "$1\x01", $rules );
-               $choice = preg_split( $varsep_pattern, $rules );
-               $choice = str_replace( "\x01", ';', $choice );
-
-               foreach ( $choice as $c ) {
-                       $v = explode( ':', $c, 2 );
-                       if ( count( $v ) != 2 ) {
-                               // syntax error, skip
-                               continue;
-                       }
-                       $to = trim( $v[1] );
-                       $v = trim( $v[0] );
-                       $u = explode( '=>', $v, 2 );
-                       $vv = $this->mConverter->validateVariant( $v );
-                       // if $to is empty (which is also used as $from in bidtable),
-                       // strtr() could return a wrong result.
-                       if ( count( $u ) == 1 && $to !== '' && $vv ) {
-                               $bidtable[$vv] = $to;
-                       } elseif ( count( $u ) == 2 ) {
-                               $from = trim( $u[0] );
-                               $v = trim( $u[1] );
-                               $vv = $this->mConverter->validateVariant( $v );
-                               // if $from is empty, strtr() could return a wrong result.
-                               if ( array_key_exists( $vv, $unidtable )
-                                       && !is_array( $unidtable[$vv] )
-                                       && $from !== ''
-                                       && $vv ) {
-                                       $unidtable[$vv] = [ $from => $to ];
-                               } elseif ( $from !== '' && $vv ) {
-                                       $unidtable[$vv][$from] = $to;
-                               }
-                       }
-                       // syntax error, pass
-                       if ( !isset( $this->mConverter->mVariantNames[$vv] ) ) {
-                               $bidtable = [];
-                               $unidtable = [];
-                               break;
-                       }
-               }
-               $this->mBidtable = $bidtable;
-               $this->mUnidtable = $unidtable;
-       }
-
-       /**
-        * @private
-        *
-        * @return string
-        */
-       function getRulesDesc() {
-               $codesep = $this->mConverter->mDescCodeSep;
-               $varsep = $this->mConverter->mDescVarSep;
-               $text = '';
-               foreach ( $this->mBidtable as $k => $v ) {
-                       $text .= $this->mConverter->mVariantNames[$k] . "$codesep$v$varsep";
-               }
-               foreach ( $this->mUnidtable as $k => $a ) {
-                       foreach ( $a as $from => $to ) {
-                               $text .= $from . '⇒' . $this->mConverter->mVariantNames[$k] .
-                                       "$codesep$to$varsep";
-                       }
-               }
-               return $text;
-       }
-
-       /**
-        * Parse rules conversion.
-        * @private
-        *
-        * @param string $variant
-        *
-        * @return string
-        */
-       function getRuleConvertedStr( $variant ) {
-               $bidtable = $this->mBidtable;
-               $unidtable = $this->mUnidtable;
-
-               if ( count( $bidtable ) + count( $unidtable ) == 0 ) {
-                       return $this->mRules;
-               } else {
-                       // display current variant in bidirectional array
-                       $disp = $this->getTextInBidtable( $variant );
-                       // or display current variant in fallbacks
-                       if ( $disp === false ) {
-                               $disp = $this->getTextInBidtable(
-                                       $this->mConverter->getVariantFallbacks( $variant ) );
-                       }
-                       // or display current variant in unidirectional array
-                       if ( $disp === false && array_key_exists( $variant, $unidtable ) ) {
-                               $disp = array_values( $unidtable[$variant] )[0];
-                       }
-                       // or display first text under disable manual convert
-                       if ( $disp === false && $this->mConverter->mManualLevel[$variant] == 'disable' ) {
-                               if ( count( $bidtable ) > 0 ) {
-                                       $disp = array_values( $bidtable )[0];
-                               } else {
-                                       $disp = array_values( array_values( $unidtable )[0] )[0];
-                               }
-                       }
-                       return $disp;
-               }
-       }
-
-       /**
-        * Similar to getRuleConvertedStr(), but this prefers to use original
-        * page title if $variant === $this->mConverter->mMainLanguageCode
-        * and may return false in this case (so this title conversion rule
-        * will be ignored and the original title is shown).
-        *
-        * @since 1.22
-        * @param string $variant The variant code to display page title in
-        * @return string|bool The converted title or false if just page name
-        */
-       function getRuleConvertedTitle( $variant ) {
-               if ( $variant === $this->mConverter->mMainLanguageCode ) {
-                       // If a string targeting exactly this variant is set,
-                       // use it. Otherwise, just return false, so the real
-                       // page name can be shown (and because variant === main,
-                       // there'll be no further automatic conversion).
-                       $disp = $this->getTextInBidtable( $variant );
-                       if ( $disp ) {
-                               return $disp;
-                       }
-                       if ( array_key_exists( $variant, $this->mUnidtable ) ) {
-                               $disp = array_values( $this->mUnidtable[$variant] )[0];
-                       }
-                       // Assigned above or still false.
-                       return $disp;
-               } else {
-                       return $this->getRuleConvertedStr( $variant );
-               }
-       }
-
-       /**
-        * Generate conversion table for all text.
-        * @private
-        */
-       function generateConvTable() {
-               // Special case optimisation
-               if ( !$this->mBidtable && !$this->mUnidtable ) {
-                       $this->mConvTable = [];
-                       return;
-               }
-
-               $bidtable = $this->mBidtable;
-               $unidtable = $this->mUnidtable;
-               $manLevel = $this->mConverter->mManualLevel;
-
-               $vmarked = [];
-               foreach ( $this->mConverter->mVariants as $v ) {
-                       /* for bidirectional array
-                               fill in the missing variants, if any,
-                               with fallbacks */
-                       if ( !isset( $bidtable[$v] ) ) {
-                               $variantFallbacks =
-                                       $this->mConverter->getVariantFallbacks( $v );
-                               $vf = $this->getTextInBidtable( $variantFallbacks );
-                               if ( $vf ) {
-                                       $bidtable[$v] = $vf;
-                               }
-                       }
-
-                       if ( isset( $bidtable[$v] ) ) {
-                               foreach ( $vmarked as $vo ) {
-                                       // use syntax: -{A|zh:WordZh;zh-tw:WordTw}-
-                                       // or -{H|zh:WordZh;zh-tw:WordTw}-
-                                       // or -{-|zh:WordZh;zh-tw:WordTw}-
-                                       // to introduce a custom mapping between
-                                       // words WordZh and WordTw in the whole text
-                                       if ( $manLevel[$v] == 'bidirectional' ) {
-                                               $this->mConvTable[$v][$bidtable[$vo]] = $bidtable[$v];
-                                       }
-                                       if ( $manLevel[$vo] == 'bidirectional' ) {
-                                               $this->mConvTable[$vo][$bidtable[$v]] = $bidtable[$vo];
-                                       }
-                               }
-                               $vmarked[] = $v;
-                       }
-                       /* for unidirectional array fill to convert tables */
-                       if ( ( $manLevel[$v] == 'bidirectional' || $manLevel[$v] == 'unidirectional' )
-                               && isset( $unidtable[$v] )
-                       ) {
-                               if ( isset( $this->mConvTable[$v] ) ) {
-                                       $this->mConvTable[$v] = $unidtable[$v] + $this->mConvTable[$v];
-                               } else {
-                                       $this->mConvTable[$v] = $unidtable[$v];
-                               }
-                       }
-               }
-       }
-
-       /**
-        * Parse rules and flags.
-        * @param string|null $variant Variant language code
-        */
-       public function parse( $variant = null ) {
-               if ( !$variant ) {
-                       $variant = $this->mConverter->getPreferredVariant();
-               }
-
-               $this->parseFlags();
-               $flags = $this->mFlags;
-
-               // convert to specified variant
-               // syntax: -{zh-hans;zh-hant[;...]|<text to convert>}-
-               if ( $this->mVariantFlags ) {
-                       // check if current variant in flags
-                       if ( isset( $this->mVariantFlags[$variant] ) ) {
-                               // then convert <text to convert> to current language
-                               $this->mRules = $this->mConverter->autoConvert( $this->mRules,
-                                       $variant );
-                       } else {
-                               // if current variant no in flags,
-                               // then we check its fallback variants.
-                               $variantFallbacks =
-                                       $this->mConverter->getVariantFallbacks( $variant );
-                               if ( is_array( $variantFallbacks ) ) {
-                                       foreach ( $variantFallbacks as $variantFallback ) {
-                                               // if current variant's fallback exist in flags
-                                               if ( isset( $this->mVariantFlags[$variantFallback] ) ) {
-                                                       // then convert <text to convert> to fallback language
-                                                       $this->mRules =
-                                                               $this->mConverter->autoConvert( $this->mRules,
-                                                                       $variantFallback );
-                                                       break;
-                                               }
-                                       }
-                               }
-                       }
-                       $this->mFlags = $flags = [ 'R' => true ];
-               }
-
-               if ( !isset( $flags['R'] ) && !isset( $flags['N'] ) ) {
-                       // decode => HTML entities modified by Sanitizer::removeHTMLtags
-                       $this->mRules = str_replace( '=&gt;', '=>', $this->mRules );
-                       $this->parseRules();
-               }
-               $rules = $this->mRules;
-
-               if ( !$this->mBidtable && !$this->mUnidtable ) {
-                       if ( isset( $flags['+'] ) || isset( $flags['-'] ) ) {
-                               // fill all variants if text in -{A/H/-|text}- is non-empty but without rules
-                               if ( $rules !== '' ) {
-                                       foreach ( $this->mConverter->mVariants as $v ) {
-                                               $this->mBidtable[$v] = $rules;
-                                       }
-                               }
-                       } elseif ( !isset( $flags['N'] ) && !isset( $flags['T'] ) ) {
-                               $this->mFlags = $flags = [ 'R' => true ];
-                       }
-               }
-
-               $this->mRuleDisplay = false;
-               foreach ( $flags as $flag => $unused ) {
-                       switch ( $flag ) {
-                               case 'R':
-                                       // if we don't do content convert, still strip the -{}- tags
-                                       $this->mRuleDisplay = $rules;
-                                       break;
-                               case 'N':
-                                       // process N flag: output current variant name
-                                       $ruleVar = trim( $rules );
-                                       $this->mRuleDisplay = $this->mConverter->mVariantNames[$ruleVar] ?? '';
-                                       break;
-                               case 'D':
-                                       // process D flag: output rules description
-                                       $this->mRuleDisplay = $this->getRulesDesc();
-                                       break;
-                               case 'H':
-                                       // process H,- flag or T only: output nothing
-                                       $this->mRuleDisplay = '';
-                                       break;
-                               case '-':
-                                       $this->mRulesAction = 'remove';
-                                       $this->mRuleDisplay = '';
-                                       break;
-                               case '+':
-                                       $this->mRulesAction = 'add';
-                                       $this->mRuleDisplay = '';
-                                       break;
-                               case 'S':
-                                       $this->mRuleDisplay = $this->getRuleConvertedStr( $variant );
-                                       break;
-                               case 'T':
-                                       $this->mRuleTitle = $this->getRuleConvertedTitle( $variant );
-                                       $this->mRuleDisplay = '';
-                                       break;
-                               default:
-                                       // ignore unknown flags (but see error case below)
-                       }
-               }
-               if ( $this->mRuleDisplay === false ) {
-                       $this->mRuleDisplay = '<span class="error">'
-                               . wfMessage( 'converter-manual-rule-error' )->inContentLanguage()->escaped()
-                               . '</span>';
-               }
-
-               $this->generateConvTable();
-       }
-
-       /**
-        * Checks if there are conversion rules.
-        * @return bool
-        */
-       public function hasRules() {
-               return $this->mRules !== '';
-       }
-
-       /**
-        * Get display text on markup -{...}-
-        * @return string
-        */
-       public function getDisplay() {
-               return $this->mRuleDisplay;
-       }
-
-       /**
-        * Get converted title.
-        * @return string
-        */
-       public function getTitle() {
-               return $this->mRuleTitle;
-       }
-
-       /**
-        * Return how deal with conversion rules.
-        * @return string
-        */
-       public function getRulesAction() {
-               return $this->mRulesAction;
-       }
-
-       /**
-        * Get conversion table. (bidirectional and unidirectional
-        * conversion table)
-        * @return array
-        */
-       public function getConvTable() {
-               return $this->mConvTable;
-       }
-
-       /**
-        * Get conversion rules string.
-        * @return string
-        */
-       public function getRules() {
-               return $this->mRules;
-       }
-
-       /**
-        * Get conversion flags.
-        * @return array
-        */
-       public function getFlags() {
-               return $this->mFlags;
-       }
-}
index 872614c..bb256c9 100644 (file)
@@ -27,8 +27,8 @@
  */
 
 use CLDRPluralRuleParser\Evaluator;
-use MediaWiki\Languages\LanguageNameUtils;
 use MediaWiki\MediaWikiServices;
+use Wikimedia\Assert\Assert;
 
 /**
  * Internationalisation code
@@ -38,24 +38,21 @@ class Language {
        /**
         * Return autonyms in fetchLanguageName(s).
         * @since 1.32
-        * @deprecated since 1.34, LanguageNameUtils::AUTONYMS
         */
-       const AS_AUTONYMS = LanguageNameUtils::AUTONYMS;
+       const AS_AUTONYMS = null;
 
        /**
         * Return all known languages in fetchLanguageName(s).
         * @since 1.32
-        * @deprecated since 1.34, use LanguageNameUtils::ALL
         */
-       const ALL = LanguageNameUtils::ALL;
+       const ALL = 'all';
 
        /**
         * Return in fetchLanguageName(s) only the languages for which we have at
         * least some localisation.
         * @since 1.32
-        * @deprecated since 1.34, use LanguageNameUtils::SUPPORTED
         */
-       const SUPPORTED = LanguageNameUtils::SUPPORTED;
+       const SUPPORTED = 'mwfile';
 
        /**
         * @var LanguageConverter
@@ -78,11 +75,10 @@ class Language {
         */
        public $transformData = [];
 
-       /** @var LocalisationCache */
-       private $localisationCache;
-
-       /** @var LanguageNameUtils */
-       private $langNameUtils;
+       /**
+        * @var LocalisationCache
+        */
+       public static $dataCache;
 
        public static $mLangObjCache = [];
 
@@ -98,7 +94,6 @@ class Language {
         */
        const STRICT_FALLBACKS = 1;
 
-       // TODO Make these const once we drop HHVM support (T192166)
        public static $mWeekdayMsgs = [
                'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
                'friday', 'saturday'
@@ -183,6 +178,12 @@ class Language {
         */
        private static $grammarTransformations;
 
+       /**
+        * Cache for language names
+        * @var HashBagOStuff|null
+        */
+       private static $languageNameCache;
+
        /**
         * Unicode directional formatting characters, for embedBidi()
         */
@@ -238,12 +239,11 @@ class Language {
         * @return Language
         */
        protected static function newFromCode( $code, $fallback = false ) {
-               $langNameUtils = MediaWikiServices::getInstance()->getLanguageNameUtils();
-               if ( !$langNameUtils->isValidCode( $code ) ) {
+               if ( !self::isValidCode( $code ) ) {
                        throw new MWException( "Invalid language code \"$code\"" );
                }
 
-               if ( !$langNameUtils->isValidBuiltInCode( $code ) ) {
+               if ( !self::isValidBuiltInCode( $code ) ) {
                        // It's not possible to customise this code with class files, so
                        // just return a Language object. This is to support uselang= hacks.
                        $lang = new Language;
@@ -262,7 +262,7 @@ class Language {
                // Keep trying the fallback list until we find an existing class
                $fallbacks = self::getFallbacksFor( $code );
                foreach ( $fallbacks as $fallbackCode ) {
-                       if ( !$langNameUtils->isValidBuiltInCode( $fallbackCode ) ) {
+                       if ( !self::isValidBuiltInCode( $fallbackCode ) ) {
                                throw new MWException( "Invalid fallback '$fallbackCode' in fallback sequence for '$code'" );
                        }
 
@@ -283,30 +283,37 @@ class Language {
         * @since 1.32
         */
        public static function clearCaches() {
-               if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MEDIAWIKI_INSTALL' ) ) {
-                       throw new MWException( __METHOD__ . ' must not be used outside tests/installer' );
-               }
-               if ( !defined( 'MEDIAWIKI_INSTALL' ) ) {
-                       MediaWikiServices::getInstance()->resetServiceForTesting( 'LocalisationCache' );
-                       MediaWikiServices::getInstance()->resetServiceForTesting( 'LanguageNameUtils' );
+               if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
+                       throw new MWException( __METHOD__ . ' must not be used outside tests' );
                }
+               self::$dataCache = null;
+               // Reinitialize $dataCache, since it's expected to always be available
+               self::getLocalisationCache();
                self::$mLangObjCache = [];
                self::$fallbackLanguageCache = [];
                self::$grammarTransformations = null;
+               self::$languageNameCache = null;
        }
 
        /**
         * Checks whether any localisation is available for that language tag
         * in MediaWiki (MessagesXx.php exists).
         *
-        * @deprecated since 1.34, use LanguageNameUtils
         * @param string $code Language tag (in lower case)
         * @return bool Whether language is supported
         * @since 1.21
         */
        public static function isSupportedLanguage( $code ) {
-               return MediaWikiServices::getInstance()->getLanguageNameUtils()
-                       ->isSupportedLanguage( $code );
+               if ( !self::isValidBuiltInCode( $code ) ) {
+                       return false;
+               }
+
+               if ( $code === 'qqq' ) {
+                       return false;
+               }
+
+               return is_readable( self::getMessagesFileName( $code ) ) ||
+                       is_readable( self::getJsonMessagesFileName( $code ) );
        }
 
        /**
@@ -374,55 +381,77 @@ class Language {
         * not it exists. This includes codes which are used solely for
         * customisation via the MediaWiki namespace.
         *
-        * @deprecated since 1.34, use LanguageNameUtils
-        *
         * @param string $code
         *
         * @return bool
         */
        public static function isValidCode( $code ) {
-               return MediaWikiServices::getInstance()->getLanguageNameUtils()->isValidCode( $code );
+               static $cache = [];
+               Assert::parameterType( 'string', $code, '$code' );
+               if ( !isset( $cache[$code] ) ) {
+                       // People think language codes are html safe, so enforce it.
+                       // Ideally we should only allow a-zA-Z0-9-
+                       // but, .+ and other chars are often used for {{int:}} hacks
+                       // see bugs T39564, T39587, T38938
+                       $cache[$code] =
+                               // Protect against path traversal
+                               strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code )
+                               && !preg_match( MediaWikiTitleCodec::getTitleInvalidRegex(), $code );
+               }
+               return $cache[$code];
        }
 
        /**
         * Returns true if a language code is of a valid form for the purposes of
         * internal customisation of MediaWiki, via Messages*.php or *.json.
         *
-        * @deprecated since 1.34, use LanguageNameUtils
-        *
         * @param string $code
         *
         * @since 1.18
         * @return bool
         */
        public static function isValidBuiltInCode( $code ) {
-               return MediaWikiServices::getInstance()->getLanguageNameUtils()
-                       ->isValidBuiltInCode( $code );
+               Assert::parameterType( 'string', $code, '$code' );
+
+               return (bool)preg_match( '/^[a-z0-9-]{2,}$/', $code );
        }
 
        /**
         * Returns true if a language code is an IETF tag known to MediaWiki.
         *
-        * @deprecated since 1.34, use LanguageNameUtils
-        *
         * @param string $tag
         *
         * @since 1.21
         * @return bool
         */
        public static function isKnownLanguageTag( $tag ) {
-               return MediaWikiServices::getInstance()->getLanguageNameUtils()
-                       ->isKnownLanguageTag( $tag );
+               // Quick escape for invalid input to avoid exceptions down the line
+               // when code tries to process tags which are not valid at all.
+               if ( !self::isValidBuiltInCode( $tag ) ) {
+                       return false;
+               }
+
+               if ( isset( MediaWiki\Languages\Data\Names::$names[$tag] )
+                       || self::fetchLanguageName( $tag, $tag ) !== ''
+               ) {
+                       return true;
+               }
+
+               return false;
        }
 
        /**
         * Get the LocalisationCache instance
         *
-        * @deprecated since 1.34, use MediaWikiServices
         * @return LocalisationCache
         */
        public static function getLocalisationCache() {
-               return MediaWikiServices::getInstance()->getLocalisationCache();
+               if ( is_null( self::$dataCache ) ) {
+                       global $wgLocalisationCacheConf;
+                       $class = $wgLocalisationCacheConf['class'];
+                       self::$dataCache = new $class( $wgLocalisationCacheConf );
+               }
+               return self::$dataCache;
        }
 
        function __construct() {
@@ -433,9 +462,7 @@ class Language {
                } else {
                        $this->mCode = str_replace( '_', '-', strtolower( substr( static::class, 8 ) ) );
                }
-               $services = MediaWikiServices::getInstance();
-               $this->localisationCache = $services->getLocalisationCache();
-               $this->langNameUtils = $services->getLanguageNameUtils();
+               self::getLocalisationCache();
        }
 
        /**
@@ -467,7 +494,7 @@ class Language {
         * @return array
         */
        public function getBookstoreList() {
-               return $this->localisationCache->getItem( $this->mCode, 'bookstoreList' );
+               return self::$dataCache->getItem( $this->mCode, 'bookstoreList' );
        }
 
        /**
@@ -484,7 +511,7 @@ class Language {
                                getCanonicalNamespaces();
 
                        $this->namespaceNames = $wgExtraNamespaces +
-                               $this->localisationCache->getItem( $this->mCode, 'namespaceNames' );
+                               self::$dataCache->getItem( $this->mCode, 'namespaceNames' );
                        $this->namespaceNames += $validNamespaces;
 
                        $this->namespaceNames[NS_PROJECT] = $wgMetaNamespace;
@@ -591,7 +618,7 @@ class Language {
                global $wgExtraGenderNamespaces;
 
                $ns = $wgExtraGenderNamespaces +
-                       (array)$this->localisationCache->getItem( $this->mCode, 'namespaceGenderAliases' );
+                       (array)self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
 
                return $ns[$index][$gender] ?? $this->getNsText( $index );
        }
@@ -613,7 +640,7 @@ class Language {
                        return false;
                } else {
                        // Check what is in i18n files
-                       $aliases = $this->localisationCache->getItem( $this->mCode, 'namespaceGenderAliases' );
+                       $aliases = self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
                        return count( $aliases ) > 0;
                }
        }
@@ -637,7 +664,7 @@ class Language {
         */
        public function getNamespaceAliases() {
                if ( is_null( $this->namespaceAliases ) ) {
-                       $aliases = $this->localisationCache->getItem( $this->mCode, 'namespaceAliases' );
+                       $aliases = self::$dataCache->getItem( $this->mCode, 'namespaceAliases' );
                        if ( !$aliases ) {
                                $aliases = [];
                        } else {
@@ -651,8 +678,8 @@ class Language {
                        }
 
                        global $wgExtraGenderNamespaces;
-                       $genders = $wgExtraGenderNamespaces + (array)$this->localisationCache
-                               ->getItem( $this->mCode, 'namespaceGenderAliases' );
+                       $genders = $wgExtraGenderNamespaces +
+                               (array)self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
                        foreach ( $genders as $index => $forms ) {
                                foreach ( $forms as $alias ) {
                                        $aliases[$alias] = $index;
@@ -740,7 +767,7 @@ class Language {
                if ( $usemsg && wfMessage( $msg )->exists() ) {
                        return $this->getMessageFromDB( $msg );
                }
-               $name = $this->langNameUtils->getLanguageName( $code );
+               $name = self::fetchLanguageName( $code );
                if ( $name ) {
                        return $name; # if it's defined as a language name, show that
                } else {
@@ -753,21 +780,21 @@ class Language {
         * @return string[]|bool List of date format preference keys, or false if disabled.
         */
        public function getDatePreferences() {
-               return $this->localisationCache->getItem( $this->mCode, 'datePreferences' );
+               return self::$dataCache->getItem( $this->mCode, 'datePreferences' );
        }
 
        /**
         * @return array
         */
        function getDateFormats() {
-               return $this->localisationCache->getItem( $this->mCode, 'dateFormats' );
+               return self::$dataCache->getItem( $this->mCode, 'dateFormats' );
        }
 
        /**
         * @return array|string
         */
        public function getDefaultDateFormat() {
-               $df = $this->localisationCache->getItem( $this->mCode, 'defaultDateFormat' );
+               $df = self::$dataCache->getItem( $this->mCode, 'defaultDateFormat' );
                if ( $df === 'dmy or mdy' ) {
                        global $wgAmericanDates;
                        return $wgAmericanDates ? 'mdy' : 'dmy';
@@ -780,7 +807,7 @@ class Language {
         * @return array
         */
        public function getDatePreferenceMigrationMap() {
-               return $this->localisationCache->getItem( $this->mCode, 'datePreferenceMigrationMap' );
+               return self::$dataCache->getItem( $this->mCode, 'datePreferenceMigrationMap' );
        }
 
        /**
@@ -801,8 +828,6 @@ class Language {
 
        /**
         * Get an array of language names, indexed by code.
-        *
-        * @deprecated since 1.34, use LanguageNameUtils::getLanguageNames
         * @param null|string $inLanguage Code of language in which to return the names
         *              Use self::AS_AUTONYMS for autonyms (native names)
         * @param string $include One of:
@@ -813,12 +838,95 @@ class Language {
         * @since 1.20
         */
        public static function fetchLanguageNames( $inLanguage = self::AS_AUTONYMS, $include = 'mw' ) {
-               return MediaWikiServices::getInstance()->getLanguageNameUtils()
-                       ->getLanguageNames( $inLanguage, $include );
+               $cacheKey = $inLanguage === self::AS_AUTONYMS ? 'null' : $inLanguage;
+               $cacheKey .= ":$include";
+               if ( self::$languageNameCache === null ) {
+                       self::$languageNameCache = new HashBagOStuff( [ 'maxKeys' => 20 ] );
+               }
+
+               $ret = self::$languageNameCache->get( $cacheKey );
+               if ( !$ret ) {
+                       $ret = self::fetchLanguageNamesUncached( $inLanguage, $include );
+                       self::$languageNameCache->set( $cacheKey, $ret );
+               }
+               return $ret;
+       }
+
+       /**
+        * Uncached helper for fetchLanguageNames
+        * @param null|string $inLanguage Code of language in which to return the names
+        *              Use self::AS_AUTONYMS for autonyms (native names)
+        * @param string $include One of:
+        *              self::ALL all available languages
+        *              'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames (default)
+        *              self::SUPPORTED only if the language is in 'mw' *and* has a message file
+        * @return array Language code => language name (sorted by key)
+        */
+       private static function fetchLanguageNamesUncached(
+               $inLanguage = self::AS_AUTONYMS,
+               $include = 'mw'
+       ) {
+               global $wgExtraLanguageNames, $wgUsePigLatinVariant;
+
+               // If passed an invalid language code to use, fallback to en
+               if ( $inLanguage !== self::AS_AUTONYMS && !self::isValidCode( $inLanguage ) ) {
+                       $inLanguage = 'en';
+               }
+
+               $names = [];
+
+               if ( $inLanguage ) {
+                       # TODO: also include when $inLanguage is null, when this code is more efficient
+                       Hooks::run( 'LanguageGetTranslatedLanguageNames', [ &$names, $inLanguage ] );
+               }
+
+               $mwNames = $wgExtraLanguageNames + MediaWiki\Languages\Data\Names::$names;
+               if ( $wgUsePigLatinVariant ) {
+                       // Pig Latin (for variant development)
+                       $mwNames['en-x-piglatin'] = 'Igpay Atinlay';
+               }
+
+               foreach ( $mwNames as $mwCode => $mwName ) {
+                       # - Prefer own MediaWiki native name when not using the hook
+                       # - For other names just add if not added through the hook
+                       if ( $mwCode === $inLanguage || !isset( $names[$mwCode] ) ) {
+                               $names[$mwCode] = $mwName;
+                       }
+               }
+
+               if ( $include === self::ALL ) {
+                       ksort( $names );
+                       return $names;
+               }
+
+               $returnMw = [];
+               $coreCodes = array_keys( $mwNames );
+               foreach ( $coreCodes as $coreCode ) {
+                       $returnMw[$coreCode] = $names[$coreCode];
+               }
+
+               if ( $include === self::SUPPORTED ) {
+                       $namesMwFile = [];
+                       # We do this using a foreach over the codes instead of a directory
+                       # loop so that messages files in extensions will work correctly.
+                       foreach ( $returnMw as $code => $value ) {
+                               if ( is_readable( self::getMessagesFileName( $code ) )
+                                       || is_readable( self::getJsonMessagesFileName( $code ) )
+                               ) {
+                                       $namesMwFile[$code] = $names[$code];
+                               }
+                       }
+
+                       ksort( $namesMwFile );
+                       return $namesMwFile;
+               }
+
+               ksort( $returnMw );
+               # 'mw' option; default if it's not one of the other two options (all/mwfile)
+               return $returnMw;
        }
 
        /**
-        * @deprecated since 1.34, use LanguageNameUtils::getLanguageName
         * @param string $code The code of the language for which to get the name
         * @param null|string $inLanguage Code of language in which to return the name
         *   (SELF::AS_AUTONYMS for autonyms)
@@ -831,8 +939,9 @@ class Language {
                $inLanguage = self::AS_AUTONYMS,
                $include = self::ALL
        ) {
-               return MediaWikiServices::getInstance()->getLanguageNameUtils()
-                       ->getLanguageName( $code, $inLanguage, $include );
+               $code = strtolower( $code );
+               $array = self::fetchLanguageNames( $inLanguage, $include );
+               return !array_key_exists( $code, $array ) ? '' : $array[$code];
        }
 
        /**
@@ -2165,8 +2274,7 @@ class Language {
                }
 
                if ( !isset( $this->dateFormatStrings[$type][$pref] ) ) {
-                       $df =
-                               $this->localisationCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
+                       $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
 
                        if ( $type === 'pretty' && $df === null ) {
                                $df = $this->getDateFormatString( 'date', $pref );
@@ -2174,8 +2282,7 @@ class Language {
 
                        if ( !$wasDefault && $df === null ) {
                                $pref = $this->getDefaultDateFormat();
-                               $df = $this->getLocalisationCache()
-                                       ->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
+                               $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
                        }
 
                        $this->dateFormatStrings[$type][$pref] = $df;
@@ -2539,14 +2646,14 @@ class Language {
         * @return string|null
         */
        public function getMessage( $key ) {
-               return $this->localisationCache->getSubitem( $this->mCode, 'messages', $key );
+               return self::$dataCache->getSubitem( $this->mCode, 'messages', $key );
        }
 
        /**
         * @return array
         */
        function getAllMessages() {
-               return $this->localisationCache->getItem( $this->mCode, 'messages' );
+               return self::$dataCache->getItem( $this->mCode, 'messages' );
        }
 
        /**
@@ -2788,7 +2895,7 @@ class Language {
         * @return string
         */
        function fallback8bitEncoding() {
-               return $this->localisationCache->getItem( $this->mCode, 'fallback8bitEncoding' );
+               return self::$dataCache->getItem( $this->mCode, 'fallback8bitEncoding' );
        }
 
        /**
@@ -2978,7 +3085,7 @@ class Language {
         * @return bool
         */
        function isRTL() {
-               return $this->localisationCache->getItem( $this->mCode, 'rtl' );
+               return self::$dataCache->getItem( $this->mCode, 'rtl' );
        }
 
        /**
@@ -3054,7 +3161,7 @@ class Language {
         * @return array
         */
        function capitalizeAllNouns() {
-               return $this->localisationCache->getItem( $this->mCode, 'capitalizeAllNouns' );
+               return self::$dataCache->getItem( $this->mCode, 'capitalizeAllNouns' );
        }
 
        /**
@@ -3087,7 +3194,7 @@ class Language {
         * @return bool
         */
        function linkPrefixExtension() {
-               return $this->localisationCache->getItem( $this->mCode, 'linkPrefixExtension' );
+               return self::$dataCache->getItem( $this->mCode, 'linkPrefixExtension' );
        }
 
        /**
@@ -3095,7 +3202,7 @@ class Language {
         * @return array
         */
        function getMagicWords() {
-               return $this->localisationCache->getItem( $this->mCode, 'magicWords' );
+               return self::$dataCache->getItem( $this->mCode, 'magicWords' );
        }
 
        /**
@@ -3105,7 +3212,7 @@ class Language {
         */
        function getMagic( $mw ) {
                $rawEntry = $this->mMagicExtensions[$mw->mId] ??
-                       $this->localisationCache->getSubitem( $this->mCode, 'magicWords', $mw->mId );
+                       self::$dataCache->getSubitem( $this->mCode, 'magicWords', $mw->mId );
 
                if ( !is_array( $rawEntry ) ) {
                        wfWarn( "\"$rawEntry\" is not a valid magic word for \"$mw->mId\"" );
@@ -3140,7 +3247,7 @@ class Language {
                if ( is_null( $this->mExtendedSpecialPageAliases ) ) {
                        // Initialise array
                        $this->mExtendedSpecialPageAliases =
-                               $this->localisationCache->getItem( $this->mCode, 'specialPageAliases' );
+                               self::$dataCache->getItem( $this->mCode, 'specialPageAliases' );
                }
 
                return $this->mExtendedSpecialPageAliases;
@@ -3305,28 +3412,28 @@ class Language {
         * @return string
         */
        function digitGroupingPattern() {
-               return $this->localisationCache->getItem( $this->mCode, 'digitGroupingPattern' );
+               return self::$dataCache->getItem( $this->mCode, 'digitGroupingPattern' );
        }
 
        /**
         * @return array
         */
        function digitTransformTable() {
-               return $this->localisationCache->getItem( $this->mCode, 'digitTransformTable' );
+               return self::$dataCache->getItem( $this->mCode, 'digitTransformTable' );
        }
 
        /**
         * @return array
         */
        function separatorTransformTable() {
-               return $this->localisationCache->getItem( $this->mCode, 'separatorTransformTable' );
+               return self::$dataCache->getItem( $this->mCode, 'separatorTransformTable' );
        }
 
        /**
         * @return int|null
         */
        function minimumGroupingDigits() {
-               return $this->localisationCache->getItem( $this->mCode, 'minimumGroupingDigits' );
+               return self::$dataCache->getItem( $this->mCode, 'minimumGroupingDigits' );
        }
 
        /**
@@ -4226,7 +4333,7 @@ class Language {
         * @return string
         */
        public function linkTrail() {
-               return $this->localisationCache->getItem( $this->mCode, 'linkTrail' );
+               return self::$dataCache->getItem( $this->mCode, 'linkTrail' );
        }
 
        /**
@@ -4236,7 +4343,7 @@ class Language {
         * @return string
         */
        public function linkPrefixCharset() {
-               return $this->localisationCache->getItem( $this->mCode, 'linkPrefixCharset' );
+               return self::$dataCache->getItem( $this->mCode, 'linkPrefixCharset' );
        }
 
        /**
@@ -4338,8 +4445,6 @@ class Language {
 
        /**
         * Get the name of a file for a certain language code
-        *
-        * @deprecated since 1.34, use LanguageNameUtils
         * @param string $prefix Prepend this to the filename
         * @param string $code Language code
         * @param string $suffix Append this to the filename
@@ -4347,30 +4452,38 @@ class Language {
         * @return string $prefix . $mangledCode . $suffix
         */
        public static function getFileName( $prefix, $code, $suffix = '.php' ) {
-               return MediaWikiServices::getInstance()->getLanguageNameUtils()
-                       ->getFileName( $prefix, $code, $suffix );
+               if ( !self::isValidBuiltInCode( $code ) ) {
+                       throw new MWException( "Invalid language code \"$code\"" );
+               }
+
+               return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
        }
 
        /**
-        * @deprecated since 1.34, use LanguageNameUtils
         * @param string $code
         * @return string
         */
        public static function getMessagesFileName( $code ) {
-               return MediaWikiServices::getInstance()->getLanguageNameUtils()
-                       ->getMessagesFileName( $code );
+               global $IP;
+               $file = self::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
+               Hooks::run( 'Language::getMessagesFileName', [ $code, &$file ] );
+               return $file;
        }
 
        /**
-        * @deprecated since 1.34, use LanguageNameUtils
         * @param string $code
         * @return string
         * @throws MWException
         * @since 1.23
         */
        public static function getJsonMessagesFileName( $code ) {
-               return MediaWikiServices::getInstance()->getLanguageNameUtils()
-                       ->getJsonMessagesFileName( $code );
+               global $IP;
+
+               if ( !self::isValidBuiltInCode( $code ) ) {
+                       throw new MWException( "Invalid language code \"$code\"" );
+               }
+
+               return "$IP/languages/i18n/$code.json";
        }
 
        /**
@@ -4820,13 +4933,11 @@ class Language {
         * @return array Associative array with plural form, and plural rule as key-value pairs
         */
        public function getCompiledPluralRules() {
-               $pluralRules =
-                       $this->localisationCache->getItem( strtolower( $this->mCode ), 'compiledPluralRules' );
+               $pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'compiledPluralRules' );
                $fallbacks = self::getFallbacksFor( $this->mCode );
                if ( !$pluralRules ) {
                        foreach ( $fallbacks as $fallbackCode ) {
-                               $pluralRules = $this->localisationCache
-                                       ->getItem( strtolower( $fallbackCode ), 'compiledPluralRules' );
+                               $pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'compiledPluralRules' );
                                if ( $pluralRules ) {
                                        break;
                                }
@@ -4841,13 +4952,11 @@ class Language {
         * @return array Associative array with plural form number and plural rule as key-value pairs
         */
        public function getPluralRules() {
-               $pluralRules =
-                       $this->localisationCache->getItem( strtolower( $this->mCode ), 'pluralRules' );
+               $pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRules' );
                $fallbacks = self::getFallbacksFor( $this->mCode );
                if ( !$pluralRules ) {
                        foreach ( $fallbacks as $fallbackCode ) {
-                               $pluralRules = $this->localisationCache
-                                       ->getItem( strtolower( $fallbackCode ), 'pluralRules' );
+                               $pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRules' );
                                if ( $pluralRules ) {
                                        break;
                                }
@@ -4862,13 +4971,11 @@ class Language {
         * @return array Associative array with plural form number and plural rule type as key-value pairs
         */
        public function getPluralRuleTypes() {
-               $pluralRuleTypes =
-                       $this->localisationCache->getItem( strtolower( $this->mCode ), 'pluralRuleTypes' );
+               $pluralRuleTypes = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRuleTypes' );
                $fallbacks = self::getFallbacksFor( $this->mCode );
                if ( !$pluralRuleTypes ) {
                        foreach ( $fallbacks as $fallbackCode ) {
-                               $pluralRuleTypes = $this->localisationCache
-                                       ->getItem( strtolower( $fallbackCode ), 'pluralRuleTypes' );
+                               $pluralRuleTypes = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRuleTypes' );
                                if ( $pluralRuleTypes ) {
                                        break;
                                }
index 1d80f6b..00f35b2 100644 (file)
@@ -39,7 +39,7 @@ namespace MediaWiki\Languages\Data;
  * If you are adding support for such a language, add it also to
  * the relevant section in shared.css.
  *
- * Do not use this class directly. Use LanguageNameUtils::getLanguageNames(), which
+ * Do not use this class directly. Use Language::fetchLanguageNames(), which
  * includes support for the CLDR extension.
  *
  * @ingroup Language
index 863ee6e..83cd424 100644 (file)
        "pagehist": "Babad kaca",
        "deletedhist": "Babad sané kausapin",
        "mergehistory-from": "Kaca wit:",
+       "mergelog": "Gabung log",
        "revertmerge": "tansida nyarengin",
        "history-title": "Babad uahan saking \"$1\"",
        "difference-title": "$1: sane malianan ring revisi",
        "tooltip-summary": "Dagingin ringkesan",
        "simpleantispam-label": "Pamariksa anti-spam.\nPuniki <strong>wenten</strong> kaisi!",
        "pageinfo-title": "Pidarta indik \"$1\"",
+       "pageinfo-header-basic": "Pidarta kaca",
        "pageinfo-header-edits": "Babad uahan",
        "pageinfo-header-restrictions": "Saiban kaca",
+       "pageinfo-header-properties": "Properti suratan",
        "pageinfo-display-title": "Edengang judul",
        "pageinfo-namespace": "Genah wastan",
        "pageinfo-article-id": "ID kaca",
index bfaac9d..595ba7e 100644 (file)
@@ -47,7 +47,7 @@
        "tog-fancysig": "امضاءَ په داب ویکی متنی بزان(بی اتوماتیکی لینک)",
        "tog-uselivepreview": "پیش‌نمایش بدون نیاز به بروزرسانی صفحه",
        "tog-forceeditsummary": "من آ هال دی وهدی وارد کتن یک هالیکین خلاصه ی اصلاح",
-       "tog-watchlisthideown": "منی اصلاحات آ چه لیست چارگ پناه کن",
+       "tog-watchlisthideown": "منی ٹگلان چہ چارگء لیست‌ئا پناہ کن",
        "tog-watchlisthidebots": "اصلاحات بوت چه لیست چارگ پناه کن",
        "tog-watchlisthideminor": "هوردین اصلاحات چه لیست چارگ پناه کن",
        "tog-watchlisthideliu": "اصلاحات چه وارد بوتگین کاربران چه لیست چارگان پناه کن",
        "mimetype": "نوع مایم:",
        "download": "آیرگیزگ",
        "unwatchedpages": "نه چارتگین صفحات",
-       "listredirects": "Ù\84Û\8cست ØºÛ\8cر Ù\85ستÙ\82Û\8cÙ\85ان",
+       "listredirects": "Ù\86اتÙ\90Ú\86Ú©Ý\94Úº Ù\84Û\8cستان",
        "listduplicatedfiles": "فهرست همهٔ پرونده‌ها به‌همراه تکراری‌ها",
        "listduplicatedfiles-summary": "این فهرست پرونده‌هایی با نسخه‌های اخیر این پرونده تکراری است که نسخه‌های اخبر سایر پرونده‌ها است. فقط پرونده‌های محلی در نظر گرفته شده‌اند.",
        "listduplicatedfiles-entry": "[[:File:$1|$1]][[$3|{{PLURAL:$2|یک تکرار|$2 تکرار}}]] دارد.",
        "listusers-submit": "پیش دار",
        "listusers-noresult": "هچ کابری در گیزگ نه بوت.",
        "listusers-blocked": "(بند بیتگ)",
-       "activeusers": "لیست کاربران فعال",
+       "activeusers": "کنشدارݔں کارزورۏکانء لیست",
        "activeusers-count": "$1 {{PLURAL:$1|اصلاح|اصلاح}} نوکین",
        "activeusers-from": "پیشدار کاربرانی که شروع بنت گون :‌",
        "activeusers-noresult": "هچ کاربری درگیزگ نه بیت",
        "listgrouprights-group": "گروه",
        "listgrouprights-rights": "حقوق",
        "listgrouprights-helppage": "Help: حقوق گروه",
-       "listgrouprights-members": "(لیست اعضا)",
+       "listgrouprights-members": "(ھۏرݔنانء لیست)",
        "listgrouprights-addgroup": "تونیت اضافه کنت {{PLURAL:$2|گروه|گروهان}}: $1",
        "listgrouprights-removegroup": "تونیت بزوریت {{PLURAL:$2|گروهء|گروهانء}}: $1",
        "listgrouprights-addgroup-all": "تونیت کل گروهان اضافه کنت",
index d8e0cb1..831adfb 100644 (file)
@@ -75,7 +75,7 @@
        "sat": "Sap",
        "january": "Januari",
        "february": "Pibuari",
-       "march": "Marat",
+       "march": "Marit",
        "april": "April",
        "may_long": "Mai",
        "june": "Juni",
        "oct": "Ukt",
        "nov": "Nup",
        "dec": "Dis",
+       "january-date": "$1 Januari",
+       "february-date": "$1 Pibuari",
+       "march-date": "$1 Marat",
+       "april-date": "$1 April",
+       "may-date": "$1 Mai",
+       "june-date": "$1 Juni",
+       "july-date": "$1 Juli",
+       "august-date": "$1 Agustus",
+       "september-date": "$1 Siptimbir",
+       "october-date": "$1 Uktubir",
+       "november-date": "$1 Nupimbir",
+       "december-date": "$1 Disimbir",
+       "period-am": "AM",
+       "period-pm": "PM",
        "pagecategories": "{{PLURAL:$1|Tumbung}}",
-       "category_header": "Halaman dalam pilah \"$1\"",
+       "category_header": "Tungkaran dalam tumbung \"$1\"",
        "subcategories": "Sub-tumbung",
        "category-media-header": "Média dalam pilah \"$1\"",
        "category-empty": "\"Kada tahaga tulisan maupun média dalam pilah ngini.\"",
        "hidden-category-category": "Tumbung tasungkup",
        "category-subcat-count": "{{PLURAL:$2|Tumbung ngini baisi asa sub-tumbung nangkaya ngini.|Pilih ngini baisi {{PLURAL:$1|sub-tumbung|$1 sub-tumbung}}, matan sabarataan $2.}}",
        "category-subcat-count-limited": "Tumbung ini baisi {{PLURAL:$1|sub-tumbung|$1 sub-tutumbung}} barikut.",
-       "category-article-count": "{{PLURAL:$2|Pilah ngini baisi {{PLURAL:$1|$1 halaman}}, tumatan jumlah $2.}}",
+       "category-article-count": "{{PLURAL:$2|Tumbung ngini baisi {{PLURAL:$1|$1 tungkaran}}, tumatan jumlah $2.}}",
        "category-article-count-limited": "Tumbung ini baisi {{PLURAL:$1|asa tungkaran|$1 tutungkaran}} barikut.",
        "category-file-count": "{{PLURAL:$2|Pilah ngini baisi {{PLURAL:$1|$1 barakas}}, matan jumlah $2.}}",
        "category-file-count-limited": "Tumbung ngini baisi {{PLURAL:$1|barakas|$1 barakas}} barikut.",
        "returnto": "Bulik ka $1.",
        "tagline": "Matan {{SITENAME}}",
        "help": "Patulung",
-       "search": "Pangikihan",
-       "searchbutton": "Kikih",
+       "help-mediawiki": "Patulung parihal MediaWiki",
+       "search": "Panggagaian",
+       "searchbutton": "Gagai",
        "go": "Tulak",
        "searcharticle": "Tulak",
-       "history": "Riwayat halaman",
+       "history": "Sajarah tungkaran",
        "history_short": "Sajarah",
        "history_small": "riwayat",
        "updatedmarker": "dihanyari tumatan ilangan pauncitan pian",
        "protect": "Lindungi",
        "protect_change": "ubah",
        "unprotect": "Palindungan",
-       "newpage": "Halaman hanyar",
+       "newpage": "Tungkaran hanyar",
        "talkpagelinktext": "pandir",
        "specialpage": "Tungkaran istimiwa",
        "personaltools": "Pakakas saurang",
        "redirectedfrom": "(Diugahakan matan $1)",
        "redirectpagesub": "Tungkaran paugahan",
        "redirectto": "Maugahakan ka:",
-       "lastmodifiedat": "Halaman ngini pahabisan diubah wayah $1, pukul $2.",
+       "lastmodifiedat": "Tungkaran ngini pahabisan diubah wayah $1, pukul $2.",
        "viewcount": "Tungkaran ini sudah diungkai {{PLURAL:$1|kali|$1 kali}}.",
        "protectedpage": "Tungkaran nang dilindungi",
        "jumpto": "Malacung ka:",
        "edithelp": "Patulung mambabak",
        "helppage-top-gethelp": "Patulung",
        "mainpage": "Tungkaran Tatambaian",
-       "mainpage-description": "Halaman tatambaian",
+       "mainpage-description": "Tungkaran tatambaian",
        "policy-url": "Project:Kaaripan",
        "portal": "Lawang bubuhan",
        "portal-url": "Project:Lawang bubuhan",
        "hidetoc": "sungkupakan",
        "collapsible-collapse": "Siup",
        "collapsible-expand": "Kambangakan",
+       "confirmable-yes": "Inggih",
+       "confirmable-no": "Kada",
        "thisisdeleted": "Tiringi atawa mambulikakan $1?",
        "viewdeleted": "Tiringi $1?",
        "restorelink": "$1 {{PLURAL:$1|babakan|babakan}} nang sudah dihapus",
        "site-atom-feed": "Kitihan Atum $1",
        "page-rss-feed": "Kitihan RSS ''$1''",
        "page-atom-feed": "Kitihan Atum ''$1''",
-       "red-link-title": "$1 (halaman baluman ada)",
+       "red-link-title": "$1 (tungkaran baluman ada)",
        "sort-descending": "Surtir baturun",
        "sort-ascending": "Surtir banaik",
-       "nstab-main": "Halaman",
+       "nstab-main": "Tungkaran",
        "nstab-user": "Pamakai",
        "nstab-media": "Média",
-       "nstab-special": "Halaman istimiwa",
+       "nstab-special": "Tungkaran istimiwa",
        "nstab-project": "Halaman rangka gawian",
        "nstab-image": "Barakas",
        "nstab-mediawiki": "Pasan",
        "nstab-template": "Citakan",
        "nstab-help": "Patulung",
        "nstab-category": "Tumbung",
-       "mainpage-nstab": "Halaman tatambaian",
+       "mainpage-nstab": "Tungkaran tatambaian",
        "nosuchaction": "Kadada palakuan nangkaitu",
        "nosuchactiontext": "Tindakan nang diminta URL kada sah.\nPian tagasnya salah katik URL, atawa maumpati sabuting tautan nang kada bujur.\nNgini jua bisa ai ada bug di parangkat lunak nang dipuruk {{SITENAME}}.",
        "nosuchspecialpage": "Kadada halaman istimiwa nangitu",
        "cannotdelete-title": "Kada kawa mahapus tungkaran \"$1\"",
        "delete-hook-aborted": "Pahapusan diwalangakan ulih kait parser.\nKadada katarangan.",
        "badtitle": "Judul buruk",
-       "badtitletext": "Judul halaman nang diminta kada sah, puang, atawa judul antarbasa atawa antarwiki nang salah sambung.",
+       "badtitletext": "Judul tungkaran nang diminta kada sah, puang, atawa judul antarbasa atawa antarwiki nang salah sambung. Bisa baisi sabuting atawa labih karakter nang kada kawa dipakai di judul.",
        "perfcached": "Data barikut adalah timbuluk wan pina kada mutakhir. A maximum of {{PLURAL:$1|one result is|$1 results are}} available in the cache.",
        "perfcachedts": "Data nang dudi ni adalah timbuluk, wan tauncit dihahanyari pada $1. A maximum of {{PLURAL:$4|one result is|$4 results are}} available in the cache.",
        "querypage-no-updates": "Pamugaan matan tungkaran ngini rahat dipajahkan. Data nang ada di sia wayahini kada akan dimuat ulang.",
        "createacct-submit": "Ulah akun Pian",
        "createacct-benefit-heading": "{{SITENAME}} diulah ulih urang-urang nangkaya Pian.",
        "createacct-benefit-body1": "{{PLURAL:$1|babakan}}",
-       "createacct-benefit-body2": "{{PLURAL:$1|halaman}}",
+       "createacct-benefit-body2": "{{PLURAL:$1|tungkaran}}",
        "createacct-benefit-body3": "{{PLURAL:$1|sumbangan}} pahabisnya",
        "badretype": "Katasunduk nang Pian buati kada pas.",
        "userexists": "Ngaran pamakai nang dibuati hudah dipuruk urang lain.\nMuhun pilih sabuting ngaran lain.",
        "summary": "Kasimpulan:",
        "subject": "Parihal:",
        "minoredit": "Ngini adalah babakan sapalih",
-       "watchthis": "Itihi halaman ngini",
-       "savearticle": "Simpan halaman",
+       "watchthis": "Itihi tungkaran ini",
+       "savearticle": "Simpan tungkaran",
        "preview": "Tilik",
        "showpreview": "Tampaiakan titilikan",
        "showdiff": "Tampaiakan paubahan",
        "accmailtitle": "Katasunduk takirim.",
        "accmailtext": "Sabuting katasunduk babarang gasan [[User talk:$1|$1]] sudah dikirim ka $2.\n\nKatasunduk gasan pamakai hanyar nangini kawa diubah di halaman ''[[Special:ChangePassword|ubah katasunduk]]'' limbah babuat log.",
        "newarticle": "(Hanyar)",
-       "newarticletext": "Pian maumpati tautan ka halaman nang balum tasadia. Amun handak maulah halaman itu, katiklah isi halaman di kutak di bawah ngini (janaki [$1 halaman patulung] gasan maklumat labih lanjut). Amun pian kada bakurinah sampai ka halaman ngini, kalik picikan <strong>back</strong> di panjalajah wéb Pian.",
+       "newarticletext": "Pian maumpati tautan ka tungkaran nang balum tasadia. Amun handak maulah tungkaran itu, katiklah isi tungkaran di kutak di bawah ngini (itihi[$1 tungkaran patulung] gasan maklumat labih lanjut). Amun pian kada bakurinah sampai ka tungkaran ngini, kalik picikan <strong>back</strong> di panjalajah wéb Pian.",
        "anontalkpagetext": "----''Ngini adalah halaman pamandiran gasan pamakai kada bangaran nang baluman ma-ulah akun pulang, atawa  kada mamakainya. Kami tapaksa mamakai numurik alamat IP hagan maminanduinya.\nAlamat IP nangkaini kawaai dipuruk ulih babarapa pamakai.\nAmun Pian adalah pamakai kada bangaran wan marasa kumin nang kada pas ta ka Pian, muhun [[Special:CreateAccount|ulah sabuah akun]] atawa [[Special:UserLogin|babuat log]] gasan mahindari kabingungan awan pamakai kada bangaran lain kaina.",
-       "noarticletext": "Damini kadada naskah di halaman ngini.\nPian kawa [[Special:Search/{{PAGENAME}}|mangikihi gasan judul halaman ngini]] di halaman lain, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} mancari log tarait], atawa [{{fullurl:{{FULLPAGENAME}}|action=edit}} maulah halaman ngini]</span>.",
-       "noarticletext-nopermission": "Wayahini kadada naskah di halaman ngini.\nPian kawa [[Special:Search/{{PAGENAME}}|manggagai gasan judul halaman ngini]] di halaman lain, atawa <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} manggagai log tarait]</span>, tagal Pian kada baisi ijin gasan maulah halaman ngini.",
+       "noarticletext": "Damini kadada naskah di tungkaran ngini.\nPian kawa [[Special:Search/{{PAGENAME}}|mangikihi gasan judul tungkaran ngini]] di tungkaran lain, <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} mancari log tarait], atawa [{{fullurl:{{FULLPAGENAME}}|action=edit}} maulah tungkaran ngini]</span>.",
+       "noarticletext-nopermission": "Wayahini kadada naskah di tungkaran ngini.\nPian kawa [[Special:Search/{{PAGENAME}}|manggagai gasan judul tungkaran ngini]] di tungkaran lain, atawa <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} manggagai log tarait]</span>, tagal Pian kada baisi ijin gasan maulah tungkaran ngini.",
        "userpage-userdoesnotexist": "Akun pamakai \"<nowiki>$1</nowiki>\" kada tadaptar.\nMuhun pariksa/ditukui amun Pian handak maulah/mambabak tungkaran ngini.",
        "userpage-userdoesnotexist-view": "Akun pamakai \"$1\" kada tadaptar.",
        "blocked-notice-logextract": "Pamakai nangini parhatan diblukir.\nLog blukir pahabisannya tasadia di bawah ngini gasan rujukan:",
        "semiprotectedpagewarning": "'''Catatan:''' Tungkaran ngini sudah dilindungi nang akibatnya pamakai tadaptar haja nang kawa mambabak.\nLog masuk pauncitnya disadiakan di bawah gasan rujukan:",
        "cascadeprotectedwarning": "<strong>Paringatan:</strong> Halaman ngini dilindungi jadinya pamakai lawan [[Special:ListGroupRights|hak aksis batantu]] wara nang kawa mambabaknya maraga ditransklusiakan dalam {{PLURAL:$1|halaman}} nang dilindungi barinting.",
        "titleprotectedwarning": "'''Paringatan: Tungkaran ngini sudah dilindungi nang akibatnya [[Special:ListGroupRights|hak khas]] diparluakan hagan maulah ngini.'''\nLog masuk pauncitnya disadiakan di bawah gasan rujukan:",
-       "templatesused": "{{PLURAL:$1|Citakan}} nang dipakai di halaman ngini:",
+       "templatesused": "{{PLURAL:$1|Citakan}} nang dipakai di tungkaran ngini:",
        "templatesusedpreview": "{{PLURAL:$1|Citakan|Cicitakan}} nang dipakai di titilikan ngini:",
        "templatesusedsection": "{{PLURAL:$1|Citakan|Cicitakan}} nang diguna'akan di hagian ini:",
        "template-protected": "(dilindungi)",
        "template-semiprotected": "(semi-dilindungi)",
-       "hiddencategories": "Halaman ngini adalah angguta matan {{PLURAL:$1|1 pilah tatukup|$1 pilah tatukup}}:",
+       "hiddencategories": "Tungkaran ngini adalah angguta matan {{PLURAL:$1|1 tumbung tatukup|$1 tumbung tatukup}}:",
        "nocreatetext": "{{SITENAME}} lagi mambatasi kakawaan maulah tungkaran hanyar.\nPian kawa babulik wan mambabak sabuah tungkaran nag ada, atawa [[Special:UserLogin|lbabuat log atawa baulah sabuah akun]]",
        "nocreate-loggedin": "Pian kada baisi ijin hagan maulah tungkaran-tungkaran hanyar.",
        "sectioneditnotsupported-title": "Pambabakan hagian kada didukung",
        "nextn": "{{PLURAL:$1|$1}} imbahnya",
        "prevn-title": "Tadahulu $1 {{PLURAL:$1|kulihan|kulihan-kulihan}}",
        "nextn-title": "$1 {{PLURAL:$1|kulihan|kulihan-kulihan}} imbahnya",
-       "shown-title": "Tampaiakan $1 {{PLURAL:$1|kulihan}} par halaman",
+       "shown-title": "Tampaiakan $1 {{PLURAL:$1|kulihan}} par tungkaran",
        "viewprevnext": "Tiringi ($1 {{int:pipe-separator}} $2) ($3)",
        "searchmenu-exists": "'''Ada tungkaran bangaran \"[[:$1]]\" dalam wiki ini.'''",
-       "searchmenu-new": "<strong>Ulah halaman \"[[:$1]]\" di wiki ngini!</strong> {{PLURAL:$2|0=|Itihi jua halaman nang dihagaakan matan pangikihan Pian.|Itihi jua kulihan pangikihan nang dihagaakan.}}",
-       "searchprofile-articles": "Halaman isi",
+       "searchmenu-new": "<strong>Ulah halaman \"[[:$1]]\" di wiki ngini!</strong> {{PLURAL:$2|0=|Itihi jua tungkaran nang dihagaakan matan panggagaian Pian.|Itihi jua kulihan panggagaian nang dihagaakan.}}",
+       "searchprofile-articles": "Tungkaran isi",
        "searchprofile-images": "Multimadia",
        "searchprofile-everything": "Samunyaan",
        "searchprofile-advanced": "Haratan",
        "enhancedrc-history": "sajarah",
        "recentchanges": "Paubahan pahanyarnya",
        "recentchanges-legend": "Pilihan paubahan pahanyarnya",
-       "recentchanges-summary": "Jajak paubahan wiki pahanyarnya pada halaman ngini",
+       "recentchanges-summary": "Jajak paubahan wiki pahanyarnya pada tungkaran ngini",
        "recentchanges-noresult": "Kadada paubahan dalam rantang waktu ngini nang rasuk lawan syarat.",
        "recentchanges-feed-description": "Susuri paubahan pahanyarnya dalam wiki di kitihan ini",
-       "recentchanges-label-newpage": "Babakan ngini maulah sabuting halaman hanyar",
+       "recentchanges-label-newpage": "Babakan ngini maulah sabuting tungkaran hanyar",
        "recentchanges-label-minor": "Ngini babakan sapalih",
        "recentchanges-label-bot": "Babakan ngini digawi ulih bot",
        "recentchanges-label-unpatrolled": "Babakan ngini baluman ta'awasi",
-       "recentchanges-label-plusminus": "Paubahan ukuran halaman dalam bita",
+       "recentchanges-label-plusminus": "Paubahan ukuran tungkaran dalam bita",
        "recentchanges-legend-heading": "<strong>Katarangan:</strong>",
-       "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (janaki jua [[Special:NewPages|daptar halaman hanyar]])",
+       "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (itihi jua [[Special:NewPages|daptar tungkaran hanyar]])",
        "rcnotefrom": "Di bawah ngini adalah {{PLURAL:$5|paubahan}} tumatan <strong>$3, $4</strong> (ditampaiakan sampai <strong>$1</strong> paubahan).",
        "rclistfrom": "Tampaiakan paubahan pahanyarnya matan $3 $2",
        "rcshowhideminor": "$1 pambabakan sapalih",
        "recentchangeslinked-feed": "Paubahan tarait",
        "recentchangeslinked-toolbox": "Paubahan tarait",
        "recentchangeslinked-title": "Paubahan nang tarait lawan \"$1\"",
-       "recentchangeslinked-summary": "Masukakan ngaran halaman gasan malihat paubahan pada halaman tarait matan atawa ka halaman ngintu (amun handak malihat angguta sabuting pilah, masukakan {{ns:category}}). Paubahan pada [[Special:Watchlist|daptar itihan Pian]] talihat <strong>dicitak kandal</strong>.",
-       "recentchangeslinked-page": "Ngaran halaman:",
-       "recentchangeslinked-to": "Tampaiakan paubahan matan halaman nang barait lawan halaman nang disurungakan",
+       "recentchangeslinked-summary": "Masukakan ngaran tungkaran gasan malihat paubahan pada tungkaran tarait matan atawa ka halaman ngintu (amun handak malihat angguta sabuting pilah, masukakan {{ns:category}}). Paubahan pada [[Special:Watchlist|daptar itihan Pian]] talihat <strong>dicitak kandal</strong>.",
+       "recentchangeslinked-page": "Ngaran tungkaran:",
+       "recentchangeslinked-to": "Tampaiakan paubahan matan tungkaran nang barait lawan tungkaran nang disurungakan",
        "upload": "Unggah barakas",
        "uploadbtn": "Hunggahakan barakas",
        "reuploaddesc": "Babulik ka furmulir paunggahan",
        "filehist-filesize": "Ukuran barakas",
        "filehist-comment": "Ulasan",
        "imagelinks": "Tautan barakas",
-       "linkstoimage": "{{PLURAL:$1|Halaman|$1 halaman}} nangini mamakai barakas ngini:",
+       "linkstoimage": "{{PLURAL:$1|Tungkaran|$1 tungkaran}} nangini mamakai barakas ngini:",
        "linkstoimage-more": "Labih daripada $1 {{PLURAL:$1|pamakaian halaman}} ka barakas ngini.\nDaptar barikut manampaiakan {{PLURAL:$1|halaman panambaian|$1 halaman panambaian}} nang mamakai barakas ngini haja.\nSabuting [[Special:WhatLinksHere/$2|daptar hibak]] tasadia.",
        "nolinkstoimage": "Kadada tutungkaran nang mamakai barakas ngini.",
        "morelinkstoimage": "Tiringi [[Special:WhatLinksHere/$1|tautan lagi]] ka barakas ngini.",
        "duplicatesoffile": "Barikut {{PLURAL:$1|barakas panggandaan|$1 babarakas panggandaan}} matan barakas ngini ([[Special:FileDuplicateSearch/$2|rarincian labih]]):",
        "sharedupload": "Barakas ini matan $1 wan mungkin dipuruk rangka-rangka gawian lain.",
        "sharedupload-desc-there": "Barakas ngini matan $1 wan pina dipuruk ulih rarangka-gawi lain.\nMuhun janaki [$2 tungkaran diskripsi barakas] gasan panjalasan labih.",
-       "sharedupload-desc-here": "Barakas ngini matan $1 wan pinanya dipakai ulih rangka-gawi lain.\nPamaparan ngini [$2 halaman diskripsi barakas] ditampaiakan di bawah.",
+       "sharedupload-desc-here": "Barakas ngini matan $1 wan pinanya dipakai ulih rangka-gawi lain.\nPamaparan ngini [$2 tungkaran panjalas barakas] ditampaiakan di bawah.",
        "filepage-nofile": "Kadada barakas bangaran ngini.",
        "filepage-nofile-link": "Kadada barakas bangaran ngini tasadia, tagal Pian kawa [$1 mahunggah ngini].",
        "uploadnewversion-linktext": "Buatakan bantuk nang labih hanyar matan barakas ini",
        "unusedtemplates": "Citakan nang kada dipuruk",
        "unusedtemplatestext": "Daptar barikut adalah samua tungkaran pada ngaran kamar {{ns:template}} nang kada dipuruk di tungkaran manapun.\nPariksa 'hulu tautan lain ka citakan itu sabalum mahapusnya.",
        "unusedtemplateswlh": "tautan lain",
-       "randompage": "Halaman babarang",
+       "randompage": "Tungkaran babarang",
        "randompage-nopages": "Kadada tungkaran pada {{PLURAL:$2||}}kamar ngaran ini: $1.",
        "randomredirect": "Paugahan babarang",
        "randomredirect-nopages": "Kada tadapat paugahan pada ngaran kamar \"$1\".",
        "listusers-creationsort": "Susun ulih tanggal paulahan",
        "usereditcount": "$1 {{PLURAL:$1|babakan|bababakan}}",
        "usercreated": "{{GENDER:$3|Diulah}} pada $1 pukul $2",
-       "newpages": "Halaman hanyar",
+       "newpages": "Tungkaran hanyar",
        "newpages-username": "Ngaran pamakai:",
        "ancientpages": "Tutungkaran panuhanya",
        "move": "Pindahakan",
        "prevpage": "Tungkaran sabalumnya ($1)",
        "allpagesfrom": "Manampaiakan tungkaran mulai matan:",
        "allpagesto": "Manampaiakan ujung pahabisan tungkaran:",
-       "allarticles": "Samunyaan halaman",
+       "allarticles": "Samunyaan tungkaran",
        "allinnamespace": "Sabarataan tutungkaran (ngaran-kamar $1)",
        "allpagessubmit": "Tulak",
        "allpagesprefix": "Tampilakan tutungkaran bamula lawan:",
        "sp-contributions-newonly": "Hanya tampaiakan babakan nang barupa paulahan halaman",
        "sp-contributions-submit": "Kikih",
        "whatlinkshere": "Tautan balik",
-       "whatlinkshere-title": "Halaman nang batautan ka ''$1''",
-       "whatlinkshere-page": "Halaman:",
-       "linkshere": "Halaman nangini batautan ka <strong>$2</strong>:",
+       "whatlinkshere-title": "Tungkaran nang batautan ka ''$1''",
+       "whatlinkshere-page": "Tungkaran:",
+       "linkshere": "Tungkaran nangini batautan ka <strong>$2</strong>:",
        "nolinkshere": "Kadada tutungkaran tataut ka '''$2'''.",
        "nolinkshere-ns": "Kadada tutungkaran tataut ka '''$2''' dalam ruang-ngaran nang dipilih.",
-       "isredirect": "halaman paugahan",
+       "isredirect": "tungkaran paugahan",
        "istemplate": "transklusi",
        "isimage": "tautan barakas",
        "whatlinkshere-prev": "$1 {{PLURAL:$1|sabalumnya|sabalumnya}}",
        "semiprotectedpagemovewarning": "'''Catatan:''' Tungkaran ngini sudah dilindungi laluai pamuruk tadaptar haja nang kawa mamindahakan ngini.\nLog masuk pauncitan disadiakan di bawah gasan rujukan:",
        "move-over-sharedrepo": "==Barakas ada==\n[[:$1]] ada pintangan panyimpanan babagi. Mamindahakan sabuah barakas ka judul ngini akan manulis-tindih barakas babagi.",
        "file-exists-sharedrepo": "Ngaran barakas nang dipilih sudah dipuruk pintangan panyimpanan babagi.\nMuhun pilih ngaran lain.",
-       "export": "Kirimi halaman ka luar",
+       "export": "Kirim tungkaran ka luar",
        "exporttext": "Pian kawa ma-ikspur naskah wan halam babakan matan sabuah tungkaran tartantu atawa sarangkai tutungkaran tabungkus dalam bantuk XML.\nNgini kawa di-impur dalam wiki lain mamuruk MediaWiki lung [[Special:Import|tungkaran impur]].\n\nHagan ma-ikspur tutungkaran, buati judul dalam kutak naskah di bawah, asa judul par garis, wan pilihi nang mana Pian handak ralatan tadamini nangkaitu jua samunyaan raralatan lawas, awan garis tungkaran halam, atawa ralatan tadamini awan panjalasan pasal babakan ta-uncit.\n\nDalam kasus pahanyarnya Pian kawa jua mamuruk sabuah tautanm gasan cuntuh [[{{#Special:Export}}/{{MediaWiki:Mainpage}}]] gasan tungkaran \"[[{{MediaWiki:Mainpage}}]]\".",
        "exportall": "Ekspor samunyaan tungkaran.",
        "exportcuronly": "Tamasuk ralatan tadamini haja, kada sahibakan halam",
        "import-logentry-upload-detail": "$1 {{PLURAL:$1|ralatan|raralatan}}",
        "import-logentry-interwiki-detail": "$1 {{PLURAL:$1|ralatan|raralatan}} matan $2",
        "javascripttest": "Mantis JavaScript",
-       "tooltip-pt-userpage": "Halaman {{GENDER:|pamakai Pian}}",
+       "tooltip-pt-userpage": "Tungkaran {{GENDER:|pamakai Pian}}",
        "tooltip-pt-anonuserpage": "Halaman pamakai IP Pian",
-       "tooltip-pt-mytalk": "Halaman {{GENDER:|pamandiran Pian}}",
+       "tooltip-pt-mytalk": "Tungkaran {{GENDER:|pamandiran Pian}}",
        "tooltip-pt-anontalk": "Pamandiran pasal bababakan matan alamat IP ngini",
        "tooltip-pt-preferences": "Kakatujuan {{GENDER:|Pian}}",
-       "tooltip-pt-watchlist": "Daptar halaman nang Pian itihi paubahannya",
+       "tooltip-pt-watchlist": "Daptar tungkaran nang Pian itihi paubahannya",
        "tooltip-pt-mycontris": "Daptar sumbangan {{GENDER:|Pian}}",
        "tooltip-pt-login": "Pian sabaiknya babuat ka dalam log; tagal ngini kada kawajiban pang",
        "tooltip-pt-logout": "Kaluar log",
        "tooltip-pt-createaccount": "Pian dianjurakan gasan maulah akun wan babuat log; tagal, hal ngintu kada wajib",
-       "tooltip-ca-talk": "Pamandiran pasal isi halaman",
-       "tooltip-ca-edit": "Babak halaman ngini",
+       "tooltip-ca-talk": "Pamandiran pasal isi tungkaran",
+       "tooltip-ca-edit": "Babak tungkaran ini",
        "tooltip-ca-addsection": "Mulai hagian hanyar",
-       "tooltip-ca-viewsource": "Halaman ngini dilindungi. Pian kawa manjanaki asal mulanya.",
-       "tooltip-ca-history": "Ralatan bahari halaman ngini",
+       "tooltip-ca-viewsource": "Tungkaran ngini dilindungi. Pian kawa maitihi asal mulanya.",
+       "tooltip-ca-history": "Ralatan bahari tungkaran ngini",
        "tooltip-ca-protect": "Lindungi tungkaran ini",
        "tooltip-ca-unprotect": "Ganti parlindungan tungkaran ngini",
        "tooltip-ca-delete": "Hapus tungkaran ini",
        "tooltip-ca-undelete": "Bulikakan babakan ka tungkaran ini sabalum tungkaran ini dihapus",
-       "tooltip-ca-move": "Pindahakan halaman ngini",
-       "tooltip-ca-watch": "Tambahi halaman ngini ka daptar itihan Pian",
+       "tooltip-ca-move": "Ugahakan tungkaran ngini",
+       "tooltip-ca-watch": "Tambahakan tungkaran ini ka daptar itihan Pian",
        "tooltip-ca-unwatch": "Buang tungkaran ngini matan daptar itihan Pian",
        "tooltip-search": "Gagai di {{SITENAME}}",
-       "tooltip-search-go": "Tulak ka sabuting halaman bangaran sama amun sudah ada",
-       "tooltip-search-fulltext": "Gagai halaman nang baisi naskah nang kaya ngini",
-       "tooltip-p-logo": "Ilangi halaman tatambaian",
-       "tooltip-n-mainpage": "Ilangi halaman tatambaian",
-       "tooltip-n-mainpage-description": "Ilangi halaman tatambaian",
+       "tooltip-search-go": "Tulak ka sabuting tungkaran bangaran sama amun sudah ada",
+       "tooltip-search-fulltext": "Gagai tungkaran nang baisi naskah nang kaya ngini",
+       "tooltip-p-logo": "Ilangi tungkaran tatambaian",
+       "tooltip-n-mainpage": "Ilangi tungkaran tatambaian",
+       "tooltip-n-mainpage-description": "Ilangi tungkaran tatambaian",
        "tooltip-n-portal": "Pasal rangka-gawian, apa nang kawa pian gawi, di mana gasan manggagai sasuatu",
        "tooltip-n-currentevents": "Gagai panjalasan pasal garamaan",
        "tooltip-n-recentchanges": "Daptar paubahan pahanyarnya dalam wiki",
-       "tooltip-n-randompage": "Tampaiakan babarang halaman",
+       "tooltip-n-randompage": "Tampaiakan sabuting tungkaran babarang",
        "tooltip-n-help": "Wadah manggagai patulung",
-       "tooltip-t-whatlinkshere": "Daptar samunyaan halaman wiki nang ada tautan ka sini",
-       "tooltip-t-recentchangeslinked": "Paubahan pahanyarnya dalam halaman nang baisi tautan tumatan halaman ngini",
+       "tooltip-t-whatlinkshere": "Daptar samunyaan tungkaran wiki nang ada tautan ka sini",
+       "tooltip-t-recentchangeslinked": "Paubahan pahanyarnya dalam tungkaran nang baisi tautan matan tungkaran ngini",
        "tooltip-feed-rss": "Kitihan RSS gasan tungkaran ini",
        "tooltip-feed-atom": "Kitihan Atum gasan tungkaran ngini",
        "tooltip-t-contributions": "Daptar sumbangan {{GENDER:$1|pamakai ngini}}",
        "tooltip-t-emailuser": "Kirimi suril ka {{GENDER:$1|pamakai ngini}}",
        "tooltip-t-upload": "Unggah barakas",
-       "tooltip-t-specialpages": "Daptar samunyaan halaman istimiwa",
-       "tooltip-t-print": "Vérsi citak halaman ngini",
-       "tooltip-t-permalink": "Tautan tatap ka ralatan halaman ngini",
-       "tooltip-ca-nstab-main": "Janaki halaman isi",
-       "tooltip-ca-nstab-user": "Janaki halaman pamakai",
+       "tooltip-t-specialpages": "Daptar samunyaan tungkaran istimiwa",
+       "tooltip-t-print": "Vérsi citak tungkaran ngini",
+       "tooltip-t-permalink": "Tautan tatap ka ralatan tungkaran ngini",
+       "tooltip-ca-nstab-main": "Tiringi isi tungkaran",
+       "tooltip-ca-nstab-user": "Tiringi tungkaran pamakai",
        "tooltip-ca-nstab-media": "Tiringi tungkaran media",
-       "tooltip-ca-nstab-special": "Ngini halaman istimiwa, kada kawa dibabak.",
+       "tooltip-ca-nstab-special": "Ngini tungkaran istimiwa, kada kawa dibabak.",
        "tooltip-ca-nstab-project": "Janaki halaman rangka gawian",
-       "tooltip-ca-nstab-image": "Janaki halaman barakas",
+       "tooltip-ca-nstab-image": "Tiringi tungkaran barakas",
        "tooltip-ca-nstab-mediawiki": "Janaki pasan sistem",
        "tooltip-ca-nstab-template": "Janaki citakan",
        "tooltip-ca-nstab-help": "Tiringi tungkaran patulung",
-       "tooltip-ca-nstab-category": "Janaki halaman pilah",
+       "tooltip-ca-nstab-category": "Tiringi tungkaran tumbung",
        "tooltip-minoredit": "Tandai ngini sabagai sabutik pambabakan sapalih",
        "tooltip-save": "Simpan paubahan Pian",
        "tooltip-preview": "Tilik paubahan Pian. Muhun pakai ngini sabalum manyimpan.",
        "tooltip-watchlistedit-raw-submit": "Hanyari daptar itihan",
        "tooltip-recreate": "Ulah pulang tungkaran biar gin suah dihapus",
        "tooltip-upload": "Mulai pangunggahan",
-       "tooltip-rollback": "\"Pambulik\" mamasahakan babakan-babakan di halaman ngini ka panyumbang pahabisan dalam satu kali kalik.",
+       "tooltip-rollback": "\"Pambulik\" mamasahakan babakan-babakan di tungkaran ngini ka panyumbang pahabisan dalam sakali kalik.",
        "tooltip-undo": "\"Bulikakan\" mawalangi ralatan ngini wan mambuka kutak pambabakan lawan mode tilik. Alasan kawa ditambahakan di kutak kasimpulan.",
        "tooltip-preferences-save": "Simpan kakatujuan",
        "tooltip-summary": "Buati sabuting kasimpulan handap",
        "pageinfo-hidden-categories": "{{PLURAL:$1|Tumbung}} tatukup ($1)",
        "pageinfo-templates": "{{PLURAL:$1|Citakan|Cicitakan}} nang ditransklusi ($1)",
        "pageinfo-transclusions": "{{PLURAL:$1|Tungkaran|Tutungkaran}} ditransklusikan pada ( $1 )",
-       "pageinfo-toolboxlink": "Panjalasan halaman",
+       "pageinfo-toolboxlink": "Panjalasan tungkaran",
        "pageinfo-redirectsto": "Ba-ugah ka",
        "pageinfo-redirectsto-info": "Maklumat",
        "pageinfo-contentpage": "Dirikin sabagai tungkaran isi",
        "metadata-help": "Barakas ngini mangandung panjalasan tambahan, mungkin ditambahakan ulih kudakan atawa paundai nang dipakai gasan maulah atawa digitalisasi barakas. Amun barakas ngini sudah diubah, parincian nang ada mungkin kada sapanuhnya sasuai lawan barakas nang diubah.",
        "metadata-expand": "Tampaiakan tambahan rincian",
        "metadata-collapse": "Sungkupakan tambahan rincian",
-       "metadata-fields": "Pancitraan metadata tadaptar dalam pasan ngini akan masuk dalam halaman pancitraan wayah tabel metadata tatukup. Nang lainnya cagaran babaku tatukup.\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",
+       "metadata-fields": "Pancitraan metadata tadaptar dalam pasan ngini akan masuk dalam tungkaran pancitraan wayah tabel metadata tatukup. Nang lainnya cagaran babaku tatukup.\n* make\n* model\n* datetimeoriginal\n* exposuretime\n* fnumber\n* isospeedratings\n* focallength\n* artist\n* copyright\n* imagedescription\n* gpslatitude\n* gpslongitude\n* gpsaltitude",
        "namespacesall": "samunyaan",
        "monthsall": "samunyaan",
        "confirmemail": "Yakinakan alamat suril",
        "fileduplicatesearch-result-1": "Barakas ''$1'' kada baisi panggandaan parsis.",
        "fileduplicatesearch-result-n": "Barakas ''$1'' baisi {{PLURAL:$2|1 panggandaan parsis|$2 papanggandaan parsis}}.",
        "fileduplicatesearch-noresults": "Kadada barakas bangaran ''$1'' taugai.",
-       "specialpages": "Halaman istimiwa",
+       "specialpages": "Tungkaran istimiwa",
        "specialpages-note-restricted": "* Tutungkaran istimiwa normal\n* <span class=\"mw-specialpagerestricted\">Tutungkaran istimiwa tabatas.</span>\n* <span class=\"mw-specialpagecached\">Tutungkaran istimiwa timbuluk (pinanya bakulat).</span>",
        "specialpages-group-maintenance": "Lapuran pamaliharaan",
        "specialpages-group-other": "Tungkaran istimiwa lainnya",
        "htmlform-submit": "Kirim",
        "htmlform-reset": "Bulikakan paubahan",
        "htmlform-selectorother-other": "Lain-lain",
-       "logentry-delete-delete": "$1 {{GENDER:$2|mahapus}} halaman $3",
+       "logentry-delete-delete": "$1 {{GENDER:$2|mahapus}} tungkaran $3",
        "logentry-delete-restore": "$1 {{GENDER:$2|mambulikakan}} halaman $3 ($4)",
        "logentry-delete-event": "$1 mangganti kakawaan dijanaki {{PLURAL:$5|sabuah log kajadian|$5 log kajadian}} pintangan $3: $4",
        "logentry-delete-revision": "$1 {{GENDER:$2|maubah}} tampaian {{PLURAL:$5|$5 ralatan}} di halaman $3: $4",
        "revdelete-uname-unhid": "ngaran pamakai kada disungkupakan",
        "revdelete-restricted": "Talamar pambatasan hagan pambakal-pambakal",
        "revdelete-unrestricted": "Buang pambatasan gasan pambakal-pambakal",
-       "logentry-move-move": "$1 {{GENDER:$2|mamindahakan}} halaman $3 ka $4",
+       "logentry-move-move": "$1 {{GENDER:$2|maugahakan}} tungkaran $3 ka $4",
        "logentry-move-move-noredirect": "$1 {{GENDER:$2|mamindahakan}} halaman $3 ka $4 kada pakai maulah paugahan",
        "logentry-move-move_redir": "$1 {{GENDER:$2|mamindahakan}} halaman $3 ka $4 manimpa paugahan lawas",
        "logentry-move-move_redir-noredirect": "$1 diugah tungkaran $3 ka $4 lung sabuah paugahan awan-kada maninggalakan sabuah paugahan",
index a315d3f..c7d5987 100644 (file)
        "watcherrortext": "\"$1\" এর নজরতালিকা পরিবর্তনের সময় একটি ত্রুটি হয়েছে।",
        "enotif_reset": "সমস্ত পাতা দেখা হয়েছে হিসেবে চিহ্নিত করুন",
        "enotif_impersonal_salutation": "{{SITENAME}} ব্যবহারকারী",
-       "enotif_subject_deleted": "{{SITENAME}} এর $1 পাতাটি {{gender:$2|$2}} অপসারণ করেছেন",
+       "enotif_subject_deleted": "{{SITENAME}} এর $1 পাতাটি $2 {{GENDER:$2|অপসারণ করেছেন}}",
        "enotif_subject_created": "{{SITENAME}} এর $1 পাতাটি {{gender:$2|$2}} তৈরী করেছেন",
        "enotif_subject_moved": "{{SITENAME}} এর $1 পাতাটি {{gender:$2|$2}} স্থানান্তর করেছেন",
        "enotif_subject_restored": "{{SITENAME}} এর $1 পাতাটি {{gender:$2|$2}} পুনরায় ফিরিয়ে এনেছেন",
        "enotif_subject_changed": "{{SITENAME}} এর $1 পাতাটি {{gender:$2|$2}} পরিবর্তন করেছেন",
-       "enotif_body_intro_deleted": "{{SITENAME}} এর $1 পাতাটি {{gender:$2|$2}} $PAGEEDITDATE তারিখে অপসারণ করেছেন, বিস্তারিত $3।",
+       "enotif_body_intro_deleted": "{{SITENAME}} এর $1 পাতাটি $2 $PAGEEDITDATE তারিখে {{GENDER:$2|অপসারণ করেছেন}}, বিস্তারিত $3।",
        "enotif_body_intro_created": "{{SITENAME}} এর $1 পাতাটি {{gender:$2|$2}} $PAGEEDITDATE তারিখে তৈরী করেছেন, বর্তমান সংস্করণ দেখুন এখানে $3।",
        "enotif_body_intro_moved": "{{SITENAME}} এর $1 পাতাটি {{gender:$2|$2}} $PAGEEDITDATE তারিখে স্থানান্তর করেছেন, বর্তমান সংস্করণ দেখুন এখানে $3।",
        "enotif_body_intro_restored": "{{SITENAME}} এর $1 পাতাটি {{gender:$2|$2}} $PAGEEDITDATE আগের অবস্থায় ফিরিয়ে এনেছেন, বর্তমান সংস্করণ দেখুন এখানে $3।",
index df0716f..c08494d 100644 (file)
        "recentchangeslinked-feed": "Srodne izmjene",
        "recentchangeslinked-toolbox": "Srodne izmjene",
        "recentchangeslinked-title": "Srodne promjene sa \"$1\"",
-       "recentchangeslinked-summary": "Upišite naziv stranice da biste vidjeli promjene koje vode na ili sa te stranice. (Da biste vidjeli članove neke kategorije, upišite Kategorija:Naziv kategorije). Promjene na stranicama na [[Special:Watchlist|spisku praćenja]] istaknute su <strong>podebljanim slovima</strong>.",
+       "recentchangeslinked-summary": "Upišite naziv stranice da biste vidjeli izmjene koje vode na ili sa te stranice. (Da biste vidjeli članove neke kategorije, upišite {{ns:category}}:Naziv kategorije). Izmjene na stranicama na [[Special:Watchlist|spisku praćenja]] istaknute su <strong>podebljanim slovima</strong>.",
        "recentchangeslinked-page": "Naslov stranice:",
        "recentchangeslinked-to": "Prikaži izmjene stranica koji su povezane s datom stranicom",
        "recentchanges-page-added-to-category": "Stranica [[:$1]] dodana je u kategoriju",
index b9c8dc7..04f2722 100644 (file)
        "block-log-flags-angry-autoblock": "rozšířené automatické blokování zapnuto",
        "block-log-flags-hiddenname": "uživatelské jméno skryto",
        "range_block_disabled": "Blokování rozsahů IP adres je zakázáno.",
+       "ipb-prevent-user-talk-edit": "U částečných bloků musí být editace vlastní uživatelské diskuse povolena, pokud blok nezahrnuje omezení jmenného prostoru {{ns:3}}.",
        "ipb_expiry_invalid": "Neplatný čas vypršení.",
        "ipb_expiry_old": "Čas vypršení je v minulosti.",
        "ipb_expiry_temp": "Blokování skrytých uživatelských jmen by měla být trvalá.",
        "delete_and_move_reason": "Smazáno pro umožnění přesunu z „[[$1]]“",
        "selfmove": "Název je stejný; nelze stránku přesunout na sebe samu.",
        "immobile-source-namespace": "Stránky ve jmenném prostoru „$1“ nelze přesouvat",
+       "immobile-source-namespace-iw": "Z této wiki nelze přesouvat stránky na jiných wiki.",
        "immobile-target-namespace": "Stránky nelze přesouvat do jmenného prostoru „$1“",
        "immobile-target-namespace-iw": "Mezijazykový odkaz není validní cíl při přesouvání stránky.",
        "immobile-source-page": "Tuto stránku nelze přesouvat.",
        "permanentlink": "Trvalý odkaz",
        "permanentlink-revid": "ID revize",
        "permanentlink-submit": "Přejít na revizi",
+       "newsection": "Nová sekce",
+       "newsection-page": "Cílová stránka",
+       "newsection-submit": "Jít na stránku",
        "dberr-problems": "Promiňte! Tento server má v tuto chvíli technické problémy.",
        "dberr-again": "Zkuste několik minut počkat a poté znovu načíst stránku.",
        "dberr-info": "(Nelze se připojit k databázi: $1)",
        "restrictionsfield-help": "Jedna IP adresa nebo CIDR rozsah na řádek. Všechno povolíte pomocí:<pre>0.0.0.0/0\n::/0</pre>",
        "edit-error-short": "Chyba: $1",
        "edit-error-long": "Chyby:\n\n$1",
+       "specialmute": "Ztlumení",
+       "specialmute-success": "Požadované ztlumení bylo upraveno. Všechny ztlumené uživatele najdete ve [[Special:Preferences|svém nastavení]].",
+       "specialmute-submit": "Potvrdit",
+       "specialmute-label-mute-email": "Ignorovat e-maily od tohoto uživatele",
+       "specialmute-header": "Vyberte si prosím požadované ztlumení uživatele <b>{{BIDI:[[User:$1|$1]]}}</b>.",
+       "specialmute-error-invalid-user": "Požadované uživatelské jméno nebylo nalezeno.",
+       "specialmute-error-no-options": "Funkce ztlumení uživatele není dostupná. Důvodem může být: neověřili jste svou e-mailovou adresu nebo administrátor wiki na této wiki vypnul e-mailové funkce nebo listinu zakázaných e-mailů.",
+       "specialmute-email-footer": "Spravovat nastavení e-mailů od uživatele {{BIDI:$2}} můžete na <$1>.",
+       "specialmute-login-required": "Pro změnu ztlumení se musíte přihlásit.",
+       "mute-preferences": "Nastavení ztlumení",
        "revid": "revize $1",
        "pageid": "Stránka s ID $1",
        "interfaceadmin-info": "$1\n\nOprávnění editovat celoprojektové soubory s CSS/JS/JSON bylo nedávno odděleno z oprávnění <code>editinterface</code>. Pokud nerozumíte, proč se vám zobrazuje tato chyba, vizte [[mw:MediaWiki_1.32/interface-admin]].",
index 10d1709..b1fae68 100644 (file)
        "rcfilters-filter-showlinkedto-label": "Vis ændringer på sider der linker til",
        "rcfilters-filter-showlinkedto-option-label": "<strong>Sider som linker til</strong> den valgte side",
        "rcfilters-target-page-placeholder": "Indtast et sidenavn (eller en kategori)",
+       "rcfilters-allcontents-label": "Alt indhold",
        "rcnotefrom": "Nedenfor er op til '''$1''' {{PLURAL:$5|ændring|ændringer}} siden '''$2''' vist.",
        "rclistfromreset": "Nulstil datovalg",
        "rclistfrom": "Vis nye ændringer startende fra den $3 kl. $2",
index 8a99dd9..00e0413 100644 (file)
@@ -59,7 +59,8 @@
                        "Fitoschido",
                        "KATRINE1993",
                        "Vlad5250",
-                       "Sarri.greek"
+                       "Sarri.greek",
+                       "Kostajh"
                ]
        },
        "tog-underline": "Υπογράμμιση συνδέσμων:",
        "history": "Ιστορικό σελίδας",
        "history_short": "Ιστορικό",
        "history_small": "ιστορικό",
-       "updatedmarker": "ενημερώθηκαν από την τελευταία επίσκεψή μου",
+       "updatedmarker": "ενημερώθηκαν από την τελευταία επίσκεψή σας",
        "printableversion": "Έκδοση εκτύπωσης",
        "permalink": "Σταθερός σύνδεσμος",
        "print": "Εκτύπωση",
index 8988419..3220f7a 100644 (file)
        "apisandbox": "API sandbox",
        "apisandbox-summary": "",
        "apisandbox-jsonly": "JavaScript is required to use the API sandbox.",
-       "apisandbox-api-disabled": "The API is disabled on this site.",
        "apisandbox-intro": "Use this page to experiment with the <strong>MediaWiki web service API</strong>.\nRefer to [[mw:API:Main page|the API documentation]] for further details of API usage. Example: [https://www.mediawiki.org/wiki/API#A_simple_example get the content of a Main Page]. Select an action to see more examples.\n\nNote that, although this is a sandbox, actions you carry out on this page may modify the wiki.",
        "apisandbox-submit": "Make request",
        "apisandbox-reset": "Clear",
        "month": "From month (and earlier):",
        "year": "From year (and earlier):",
        "date": "From date (and earlier):",
-       "sp-contributions-newbies": "Show contributions of new accounts only",
-       "sp-contributions-newbies-sub": "For new accounts",
-       "sp-contributions-newbies-title": "User contributions for new accounts",
        "sp-contributions-blocklog": "block log",
        "sp-contributions-suppresslog": "suppressed {{GENDER:$1|user}} contributions",
        "sp-contributions-deleted": "deleted {{GENDER:$1|user}} contributions",
        "sp-contributions-footer": "-",
        "sp-contributions-footer-anon": "-",
        "sp-contributions-footer-anon-range": "-",
-       "sp-contributions-footer-newbies": "-",
        "sp-contributions-outofrange": "Unable to show any results. The requested IP range is larger than the CIDR limit of /$1.",
        "whatlinkshere": "What links here",
        "whatlinkshere-title": "Pages that link to \"$1\"",
        "newimages-legend": "Filter",
        "newimages-label": "Filename (or a part of it):",
        "newimages-user": "IP address or username",
-       "newimages-newbies": "Show contributions of new accounts only",
        "newimages-showbots": "Show uploads by bots",
        "newimages-hidepatrolled": "Hide patrolled uploads",
        "newimages-mediatype": "Media type:",
index 814e6d7..f5af730 100644 (file)
        "rcfilters-filter-showlinkedto-label": "Mostrar cambios en páginas que enlazan a",
        "rcfilters-filter-showlinkedto-option-label": "<strong>Páginas que enlazan hacia</strong> la página seleccionada",
        "rcfilters-target-page-placeholder": "Escribe un nombre de página (o de categoría)",
+       "rcfilters-alldiscussions-label": "Todas las discusiones",
        "rcnotefrom": "Debajo {{PLURAL:$5|aparece el cambio|aparecen los cambios}} desde <strong>$3, $4</strong> (se muestran hasta <strong>$1</strong>).",
        "rclistfromreset": "Restablecer selección de fecha",
        "rclistfrom": "Mostrar cambios nuevos desde las $2 del $3",
        "move-subpages": "Intentar trasladar las subpáginas (hasta $1)",
        "move-talk-subpages": "Intentar trasladar las subpáginas de discusión (hasta $1)",
        "movepage-page-exists": "La página $1 ya existe, por lo que no se puede cambiarle el nombre automáticamente.",
+       "movepage-source-doesnt-exist": "La página $1 no existe por lo que no puede ser trasladada.",
        "movepage-page-moved": "La página $1 ha sido trasladada a $2.",
        "movepage-page-unmoved": "La página $1 no se ha podido trasladar a $2.",
        "movepage-max-pages": "Se {{PLURAL:$1|ha trasladado un máximo de una página|han trasladado un máximo de $1 páginas}}, y no van a trasladarse más automáticamente.",
index eeeb77e..23d5755 100644 (file)
        "exif-scenetype-1": "A directly photographed image",
        "exif-customrendered-0": "Normal process",
        "exif-customrendered-1": "Custom process",
+       "exif-customrendered-2": "HDR (no original saved)",
+       "exif-customrendered-3": "HDR (original saved)",
+       "exif-customrendered-4": "Original (for HDR)",
+       "exif-customrendered-6": "Panorama",
+       "exif-customrendered-7": "Portrait HDR",
+       "exif-customrendered-8": "Portrait",
        "exif-exposuremode-0": "Auto exposure",
        "exif-exposuremode-1": "Manual exposure",
        "exif-exposuremode-2": "Auto bracket",
index 2a55eb3..10dbb80 100644 (file)
        "exif-scenetype-1": "See also:\n* {{msg-mw|Exif-scenetype}}\n* {{msg-mw|Exif-scenetype-1}}",
        "exif-customrendered-0": "{{exif-qqq}}\n\nSee also:\n* {{msg-mw|Exif-customrendered}}\n* {{msg-mw|Exif-customrendered-0}}\n* {{msg-mw|Exif-customrendered-1}}",
        "exif-customrendered-1": "{{exif-qqq}}\n\nSee also:\n* {{msg-mw|Exif-customrendered}}\n* {{msg-mw|Exif-customrendered-0}}\n* {{msg-mw|Exif-customrendered-1}}",
+       "exif-customrendered-2": "{{exif-qqq}}\n\nSee also:\n* {{msg-mw|Exif-customrendered}}",
+       "exif-customrendered-3": "{{exif-qqq}}\n\nSee also:\n* {{msg-mw|Exif-customrendered}}",
+       "exif-customrendered-4": "{{exif-qqq}}\n\nSee also:\n* {{msg-mw|Exif-customrendered}}",
+       "exif-customrendered-6": "{{exif-qqq}}\n\nSee also:\n* {{msg-mw|Exif-customrendered}}",
+       "exif-customrendered-7": "{{exif-qqq}}\n\nSee also:\n* {{msg-mw|Exif-customrendered}}",
+       "exif-customrendered-8": "{{exif-qqq}}\n\nSee also:\n* {{msg-mw|Exif-customrendered}}",
        "exif-exposuremode-0": "{{exif-qqq}}\n{{Related|Exif-exposuremode}}",
        "exif-exposuremode-1": "{{exif-qqq}}\n{{Related|Exif-exposuremode}}",
        "exif-exposuremode-2": "{{exif-qqq}}\n\nA type of exposure mode shown as part of the metadata on image description pages. The Wikipedia article on [[w:Bracketing#Exposure_bracketing|bracketing]] says that 'auto bracket' is a camera exposure setting which automatically takes a series of pictures at slightly different light exposures.\n\n{{Related|Exif-exposuremode}}",
index 821a48c..954ba46 100644 (file)
        "exif-photometricinterpretation-3": "Paletă",
        "exif-photometricinterpretation-4": "Mască de transparență",
        "exif-photometricinterpretation-5": "Separat (Probabil CMYK)",
+       "exif-photometricinterpretation-8": "CIE L*a*b*",
+       "exif-photometricinterpretation-9": "CIE L*a*b* (codare ICC)",
+       "exif-photometricinterpretation-10": "CIE L*a*b* (codare ITU)",
        "exif-unknowndate": "Dată necunoscută",
        "exif-orientation-1": "Normală",
        "exif-orientation-2": "Oglindită orizontal",
index e7608a6..473a03a 100644 (file)
        "exif-bitspersample": "Bitůw na průbka",
        "exif-compression": "Metoda kompresyji",
        "exif-photometricinterpretation": "Interpretacyjo fotůmetryčno",
-       "exif-orientation": "Uorjyntacyjo uobrozu",
+       "exif-orientation": "Ôriyntacyjŏ",
        "exif-samplesperpixel": "Průbek na piksel",
        "exif-planarconfiguration": "Rozkuod danych",
        "exif-ycbcrsubsampling": "Podprůbkowańe Y do C",
        "exif-ycbcrpositioning": "Rozmješčyńy Y i C",
-       "exif-xresolution": "Rozdźelčość w poźůmje",
-       "exif-yresolution": "Rozdźelčość w pjůńy",
+       "exif-xresolution": "Rozdzielczość we poziōmie",
+       "exif-yresolution": "Rodzielczość we piōnie",
        "exif-stripoffsets": "Přesůńjyńće pasůw uobrazu",
        "exif-rowsperstrip": "Ličba wjeršy na pas uobrazu",
        "exif-stripbytecounts": "Ličba bajtůw na pas uobrazu",
        "exif-primarychromaticities": "Kolory třech barw guůwnych",
        "exif-ycbcrcoefficients": "Maćeř wspůučynńikůw transformacyji barw ze RGB na YCbCr",
        "exif-referenceblackwhite": "Wartość půnktu uodńyśyńo čerńi i bjeli",
-       "exif-datetime": "Data i čas modyfikacyji plika",
+       "exif-datetime": "Data i czas modyfikacyje zbioru",
        "exif-imagedescription": "Titel uobrozka",
        "exif-make": "Producynt fotoaparatu",
        "exif-model": "Model fotoaparatu",
-       "exif-software": "Ůžyte uoprůgramowańy",
+       "exif-software": "Użyte ôprogramowanie",
        "exif-artist": "Autor",
        "exif-copyright": "Wuaśćićel praw autorskych",
-       "exif-exifversion": "Wersyja standardu Exif",
+       "exif-exifversion": "Wersyjŏ Exif",
        "exif-flashpixversion": "Uobsůgiwano wersyjo Flashpix",
-       "exif-colorspace": "Přestřyń kolorůw",
+       "exif-colorspace": "Przestrzyń farbōw",
        "exif-componentsconfiguration": "Značyńy skuadowych",
        "exif-compressedbitsperpixel": "Skůmpresowanych bitůw na piksel",
        "exif-pixelxdimension": "Prawidłowa szyrzka uobrozu",
        "exif-pixelydimension": "Prawidłowo wyżka uobrozu",
        "exif-usercomment": "Kůmyntoř užytkowńika",
        "exif-relatedsoundfile": "Powjůnzany plik audjo",
-       "exif-datetimeoriginal": "Data i čas utwořyńo uoryginouu",
-       "exif-datetimedigitized": "Data i čas zeskanowańo",
+       "exif-datetimeoriginal": "Data i czas stworzyniŏ ôryginału",
+       "exif-datetimedigitized": "Data i czas digitalizacyje",
        "exif-subsectime": "Data i čas modyfikacyji pliku – uuamki sekůnd",
        "exif-subsectimeoriginal": "Data i čas utwořyńo uoryginouu – uuamki sekůnd",
        "exif-subsectimedigitized": "Data i čas zeskanowańo – uuamki sekůnd",
        "exif-gpsdifferential": "Korekcyjo růžńicy GPS",
        "exif-compression-1": "ńyskůmpresowany",
        "exif-unknowndate": "ńyznano data",
-       "exif-orientation-1": "normalno",
+       "exif-orientation-1": "Normalno",
        "exif-orientation-2": "odbiće we źřadle w poźůmje",
        "exif-orientation-3": "uobroz uobrůcůny uo 180°",
        "exif-orientation-4": "uodbiće we źřadle w pjůńy",
index 2cc372a..5752bee 100644 (file)
@@ -16,6 +16,8 @@
        "exif-samplesperpixel": "Төс өлешләре саны",
        "exif-xresolution": "Ятма ачыклык",
        "exif-yresolution": "Асма ачыклык",
+       "exif-rowsperstrip": "Бер бүлемдә юллар саны",
+       "exif-stripbytecounts": "Кысылган бүлемдә байтлар саны",
        "exif-datetime": "Файл үзгәреше датасы һәм вакыты",
        "exif-imagedescription": "Сурәт атамасы",
        "exif-make": "Камера җитештерүчесе",
@@ -23,7 +25,7 @@
        "exif-software": "Кулланылган программа",
        "exif-artist": "Автор",
        "exif-copyright": "Авторлык хокукы иясе",
-       "exif-exifversion": "Exif Ñ\8eÑ\80амаÑ\81ы",
+       "exif-exifversion": "Exif Ñ\87Ñ\8bгаÑ\80Ñ\8bÑ\88ы",
        "exif-flashpixversion": "FlashPix ярашлы юрамасы",
        "exif-colorspace": "Төсләр киңлеге",
        "exif-componentsconfiguration": "Төсләр төзелешенең конфигурациясе",
        "exif-gpslongitude": "Озынлык",
        "exif-gpsaltituderef": "Югарылык индексы",
        "exif-gpsaltitude": "Югарылык",
-       "exif-gpstimestamp": "UTC буенча вакыт",
+       "exif-gpstimestamp": "GPS вакыты (атом сәгате)",
        "exif-gpssatellites": "Кулланылган иярченнәр тасвирламасы",
        "exif-gpsstatus": "Алгычның статусы һәм төшерү вакыты",
        "exif-gpsmeasuremode": "Урнашуны билгеләү ысулы",
        "exif-gpsdop": "Билгеләүнең дөреслеге",
        "exif-gpsspeedref": "Тизлекне исәпләү берәмлеге",
        "exif-gpsspeed": "Хәрәкәт тизлеге",
-       "exif-gpsdatestamp": "Дата",
+       "exif-gpsdatestamp": "GPS датасы",
        "exif-keywords": "Иң мөһиме",
+       "exif-headline": "Башисем",
        "exif-source": "Чыганак",
+       "exif-contact": "Элемтә өчен мәгълүмат",
        "exif-writer": "Язучы",
        "exif-languagecode": "Тел",
        "exif-iimversion": "IIM юрамасы",
        "exif-iimcategory": "Төркем",
        "exif-iimsupplementalcategory": "Өстәмә төркемнәр",
+       "exif-datetimereleased": "Чыгарылу вакыты",
        "exif-identifier": "Идентификатор",
        "exif-label": "Билгеләү",
        "exif-copyrighted": "Авторлык хокукы халәте",
        "exif-copyrightowner": "Авторлык хокукы иясе",
        "exif-usageterms": "Куллану шартлары",
+       "exif-photometricinterpretation-0": "Ак һәм кара (ак — 0)",
+       "exif-photometricinterpretation-1": "Ак һәм кара (кара — 0)",
+       "exif-unknowndate": "Билгесез вакыт",
        "exif-orientation-1": "Гадәти",
        "exif-orientation-3": "180° ка борылган",
+       "exif-planarconfiguration-1": "«chunky» форматы",
+       "exif-planarconfiguration-2": "«planar» форматы",
        "exif-componentsconfiguration-0": "барлыкта юк",
        "exif-exposureprogram-0": "Билгесез",
        "exif-exposureprogram-1": "Кулдан җайлау режимы",
        "exif-meteringmode-0": "Билгесез",
        "exif-meteringmode-1": "Уртача",
        "exif-meteringmode-3": "Нокталы",
-       "exif-meteringmode-4": "Ð\9cÑ\83лÑ\8cÑ\82инокталы",
+       "exif-meteringmode-4": "Ð\9aүп нокталы",
        "exif-meteringmode-5": "Паттернлы",
        "exif-meteringmode-6": "Өлешләтә",
        "exif-meteringmode-255": "Башка",
        "exif-gpsdop-moderate": "Уртача ($1)",
        "exif-gpsdop-fair": "Ярыйсы ($1)",
        "exif-gpsdop-poor": "Начар ($1)",
+       "exif-objectcycle-a": "Иртән генә",
+       "exif-objectcycle-p": "Кичен генә",
+       "exif-objectcycle-b": "Иртән һәм кичен",
        "exif-dc-date": "Дата(лар)",
        "exif-dc-publisher": "Нәшир",
        "exif-dc-relation": "Бәйле медиа",
        "exif-dc-type": "Медиа төре",
        "exif-rating-rejected": "Кире кагылды",
        "exif-isospeedratings-overflow": "65535 тән күбрәк",
+       "exif-iimcategory-fin": "Экономика һәм бизнес",
+       "exif-iimcategory-evn": "Әйләнә-тирәдәге мохит",
        "exif-iimcategory-hth": "Сәламәтлек",
        "exif-iimcategory-lab": "Хезмәт",
+       "exif-iimcategory-pol": "Сәясәт",
+       "exif-iimcategory-rel": "Дин һәм иман",
+       "exif-iimcategory-sci": "Фән һәм техника",
+       "exif-iimcategory-spo": "Спорт",
        "exif-iimcategory-wea": "Һава торышы",
        "exif-urgency-normal": "Гадәти ($1)",
        "exif-urgency-low": "Түбән ($1)",
index 2a07679..50d1db2 100644 (file)
@@ -9,7 +9,8 @@
                        "PhiLiP",
                        "Qiyue2001",
                        "Xiaomingyan",
-                       "神樂坂秀吉"
+                       "神樂坂秀吉",
+                       "予弦"
                ]
        },
        "exif-imagewidth": "宽度",
index 0a03da6..b0aced5 100644 (file)
        "revdelete-unsuppress": "حذف محدودیت‌ها در بازبینی‌های ترمیم‌شده",
        "revdelete-log": "دلیل:",
        "revdelete-submit": "اعمال بر {{PLURAL:$1|نسخهٔ|نسخه‌های}} انتخاب شده",
-       "revdelete-success": "Ù¾Û\8cداÛ\8cÛ\8c Ù\86سخÙ\87 Ø¨Ù\87â\80\8cرÙ\88ز شد.",
+       "revdelete-success": "Ù¾Û\8cداÛ\8cÛ\8c Ù\86سخÙ\87 Ø±Ù\88زآÙ\85د شد.",
        "revdelete-failure": "'''پیدایی نسخه‌ها قابل به روز کردن نیست:'''\n$1",
        "logdelete-success": "تغییر پیدایی مورد انجام شد.",
        "logdelete-failure": "'''پیدایی سیاهه‌ها قابل تنظیم نیست:'''\n$1",
index d8b028e..8299b19 100644 (file)
        "passwordreset-ignored": "Salasanan palauttamista ei käsitelty. Ehkä tarjoajaa ei ollut määritetty?",
        "passwordreset-invalidemail": "Virheellinen sähköpostiosoite",
        "passwordreset-nodata": "Käyttäjätunnusta ja salasanaa ei annettu",
-       "changeemail": "Muuta tai poista sähköpostiosoite",
+       "changeemail": "Muuta tai poista E-posti atressi",
        "changeemail-header": "Täydennä tämä lomake, jolla voit muuttaa sähköpostiosoitettasi. Jos haluat poistaa sähköpostiosoitteesi kokonaan tunnuksesi yhteydestä, älä kirjoita uudeksi osoitteeksi mitään vaan jätä se tyhjäksi.",
        "changeemail-no-info": "Tämän sivun käyttö edellyttää sisäänkirjautumista.",
        "changeemail-oldemail": "Nykyinen sähköpostiosoite:",
        "prefs-watchlist-managetokens": "Hallitse avaimia",
        "prefs-misc": "Muut",
        "prefs-resetpass": "Muuta salasana",
-       "prefs-changeemail": "Muuta tai poista sähköpostiosoite",
+       "prefs-changeemail": "Muuta tai poista E-posti atressi",
        "prefs-setemail": "Aseta sähköpostiosoite",
        "prefs-email": "Sähköpostiasetukset",
        "prefs-rendering": "Ulkoasu",
index 5cae831..1782812 100644 (file)
        "tag-filter": "Filtrer les [[Special:Tags|balises]] :",
        "tag-filter-submit": "Filtrer",
        "tag-list-wrapper": "[[Special:Tags|{{PLURAL:$1|Balise|Balises}}]] : $2",
-       "tag-mw-contentmodelchange": "modification du modèle de contenu",
+       "tag-mw-contentmodelchange": "Modification du modèle de contenu",
        "tag-mw-contentmodelchange-description": "Modifications qui [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel changent le modèle de contenu] d'une page",
        "tag-mw-new-redirect": "Nouvelle redirection",
        "tag-mw-new-redirect-description": "Modifications qui créent une nouvelle redirection ou transforment une page en redirection",
        "tag-mw-changed-redirect-target-description": "Modifications qui changent la cible d’une redirection",
        "tag-mw-blank": "Blanchiment",
        "tag-mw-blank-description": "Modifications qui suppriment le contenu des pages",
-       "tag-mw-replace": "Remplacé",
+       "tag-mw-replace": "Contenu remplacé",
        "tag-mw-replace-description": "Modifications qui enlèvent plus de 90% du contenu des pages",
        "tag-mw-rollback": "Révocation",
        "tag-mw-rollback-description": "Modifications qui annulent des modifications existantes en utilisant le lien de révocation (''rollback'')",
index ebca6ef..f95fc94 100644 (file)
        "rcfilters-filter-showlinkedto-label": "הצגת שינויים בדפים שמקשרים אל",
        "rcfilters-filter-showlinkedto-option-label": "<strong>דפים שמקשרים אל</strong> הדף שנבחר",
        "rcfilters-target-page-placeholder": "יש להקליד שם דף (או קטגוריה)",
+       "rcfilters-allcontents-label": "כל התכנים",
+       "rcfilters-alldiscussions-label": "כל הדיונים",
        "rcnotefrom": "להלן {{PLURAL:$5|השינוי שבוצע|השינויים שבוצעו}} מאז <strong>$3, $4</strong> (מוצגים עד <strong>$1</strong>).",
        "rclistfromreset": "איפוס בחירת התאריך",
        "rclistfrom": "הצגת שינויים חדשים החל מ־$2, $3",
        "move-subpages": "העברת דפי המשנה (עד $1)",
        "move-talk-subpages": "העברת דפי המשנה של דף השיחה (עד $1)",
        "movepage-page-exists": "הדף $1 קיים כבר ולא ניתן לדרוס אותו אוטומטית.",
+       "movepage-source-doesnt-exist": "הדף $1 אינו קיים ולא ניתן להעבירו.",
        "movepage-page-moved": "הדף $1 הועבר לשם $2.",
        "movepage-page-unmoved": "לא ניתן להעביר את הדף $1 לשם $2.",
        "movepage-max-pages": "{{PLURAL:$1|דף אחד כבר הועבר|$1 דפים כבר הועברו}}. זה המספר המרבי ולא ניתן להעביר דפים נוספים אוטומטית.",
        "delete_and_move_reason": "מחיקה כדי לאפשר העברה מהשם \"[[$1]]\"",
        "selfmove": "הכותרת זהה;\nלא ניתן להעביר דף לעצמו.",
        "immobile-source-namespace": "לא ניתן להעביר דפים במרחב השם \"$1\".",
+       "immobile-source-namespace-iw": "לא ניתן להעביר דפים באתרי ויקי אחרים מתוך אתר הוויקי הזה.",
        "immobile-target-namespace": "לא ניתן להעביר דפים למרחב השם \"$1\".",
        "immobile-target-namespace-iw": "קישור בינוויקי אינו יעד תקין להעברת דף.",
        "immobile-source-page": "דף זה אינו ניתן להעברה.",
        "immobile-target-page": "לא ניתן להעביר אל כותרת יעד זו.",
+       "movepage-invalid-target-title": "השם המבוקש אינו תקין.",
        "bad-target-model": "היעד המבוקש משתמש במודל תוכן שונה. לא ניתן להמיר $1 ל{{grammar:תחילית|$2}}.",
        "imagenocrossnamespace": "לא ניתן להעביר קובץ למרחב שם אחר.",
        "nonfile-cannot-move-to-file": "לא ניתן להעביר דף שאינו קובץ למרחב קובץ.",
index d2691d7..9baf870 100644 (file)
        "timezoneregion-indian": "Samudera Hindia",
        "timezoneregion-pacific": "Samudera Pasifik",
        "allowemail": "Izinkan pengguna lain mengirim surel kepada saya",
-       "email-allow-new-users-label": "Izinkan email dari pengguna baru",
+       "email-allow-new-users-label": "Izinkan surel dari pengguna baru",
        "email-blacklist-label": "Cegah para pengguna ini mengirim saya surel:",
        "prefs-searchoptions": "Cari",
        "prefs-namespaces": "Ruang nama",
index 3b78ced..1b56359 100644 (file)
@@ -24,6 +24,7 @@
        "tog-watchmoves": "Tinye ihu akwụkwọ na failụ niile mụ bugara n'ihe m ga na-elebara anya",
        "tog-watchdeletion": "Tinye ihu akwụkwọ na failụ niile m hichara n'ebe m ga na-elebara anya",
        "tog-watchuploads": "Tinye failụ ohụụ m mere ọpụload n'ihe mụ ga na -elebara anya",
+       "tog-watchrollback": "Gbakwụnye ihu akwụkwọgasị ebe m mere ndezigharị n'ịhe m ga na-elebara anya",
        "tog-minordefault": "Me ka nhoro da na orü ntakịrị níle",
        "tog-previewontop": "Zitú ntàkịrị mgbe opuzọr zi igbe orü",
        "tog-previewonfirst": "Zitú nke takírí orü mbu",
        "tog-norollbackdiff": "egosila ndíiche ma í gosipútacha otu ebe a di na mbú",
        "tog-useeditwarning": "gwam mgbe m hapụrụ ihu akwụkwọ nhaziri na echekwaghị ihe ndị m gbamworo",
        "tog-prefershttps": "gbaa mbọ na eji njikọta doro anya mgbe ọbụla ị chọrọ ibanye n'ịntanetị",
+       "tog-showrollbackconfirmation": "zipụta lịnkị na-egosi mgbe a na-eme ndezighari",
        "underline-always": "M̀gbèọbụlà",
        "underline-never": "Emelaème",
-       "underline-default": "Ndatụ ihü njikota",
+       "underline-default": "difọọltụ bụrawuza",
        "editfont-style": "Rüwa ámá udị mkpúrù èdè:",
        "editfont-monospace": "Otụ ihe ná kechí mkpúrù èdè",
        "editfont-sansserif": "Mkpúrù èdè sans-serif",
        "october-date": "Ọnwaìri $1",
        "november-date": "Ọnwaìrinàotù $1",
        "december-date": "Ọnwa Iri na abụọ $1",
+       "period-am": "oge ụtụtụ",
        "period-pm": "oge mgbede",
        "pagecategories": "{{PLURAL:$1|Ụdàkọ}}",
        "category_header": "Ihu nà ime ụdàkọ \"$1\"",
        "history": "Ịta ihüá",
        "history_short": "Ịta",
        "history_small": "akụkọ ihe mere eme",
-       "updatedmarker": "ihe gáráníru ké mgbe m byàrà nga mbu",
+       "updatedmarker": "ndezi emere kemgbe ị gara na site a",
        "printableversion": "Ùdì ǹke mbipụ̀",
        "permalink": "Jikodo ekechịrị",
        "print": "Dotié",
        "pool-timeout": "Ógè e zuole Í ché ncedọ",
        "pool-queuefull": "Pool kyu zùrù",
        "pool-errorunknown": "Nsogbu nke námaghi",
-       "pool-servererror": "pulu kauta sava adịghị ugbu a",
+       "pool-servererror": "puulu kaụnta savis adịghị ugbu a",
        "poolcounter-usage-error": "e nwere nsogbu: $1",
        "aboutsite": "Màkà {{SITENAME}}",
        "aboutpage": "Project:Màkà",
        "right-move": "Papụ̀ ihuâ",
        "right-movefile": "Papụ̀ àfabà",
        "right-upload": "Tịnyé ihe na nsónùsòrò",
+       "right-writeapi": "Iji ede API",
        "right-delete": "Kàchafu ihü",
        "right-bigdelete": "Kàcha ihü nwéré ákíkó mbu dí ógólógó",
        "right-undelete": "Ágbakashia ótù ihü",
        "recentchanges-label-bot": "Bot deziri ihe a",
        "recentchanges-label-unpatrolled": "ebugharịbegi ndezi a",
        "recentchanges-label-plusminus": "Pegi a agbanwela na otu ọha site na ọnu ọgụgụ bayits",
+       "recentchanges-legend-heading": "<strong>Isi-okwu</strong>",
        "recentchanges-legend-newpage": "$1 - ihü ohúrù",
        "rcfilters-savedqueries-cancel-label": "Hapụ̀",
        "rclistfrom": "Zìrí ihe gbanwere ọhúrù shí $3 $2",
        "filehist-filesize": "Ívù usòrò",
        "filehist-comment": "Nkwute",
        "imagelinks": "Mgbanwe usòrò",
-       "linkstoimage": "{{PLURAL:$1|Ihü nká|Ihü nke $1}} na jikodo gá usòrò nká:",
+       "linkstoimage": "Ihe ndị na-eso {{PLURAL:$1|ihe eji Ihu akwụkwọ eme|$1 ihe eji Ihu akwụkwọ eme}} na faịlụ a:",
        "nolinkstoimage": "Ọdighi ihuakwụkwọ nwere failụ a.",
        "sharedupload": "Ákwúkwó runotu nke shì $1 na ó nwèríkí di na orürü nke ndi ozor.",
        "sharedupload-desc-here": "Failụ a si na $1,enwekwara ike iji ya eme ihe na arụmarụ ọzọ. Nkọwa na [$2 ihuakwukwọ nkọwa failụ] eziri na okpuru.",
        "undelete-show-file-submit": "Eeh",
        "namespace": "Ahàm̀bara:",
        "invert": "Tụgha ǹke ǹhọ̀rọ",
+       "tooltip-invert": "Kachie igbe a izocha mgbanwe Ihu-akwụkwọ ndị nnọ na aha-ebe ahọpụtara(yana aha-ebe jikọtara ya m'obụrụ na akachiri ya)",
+       "namespace_association": "Nyìrí aha-ebe",
+       "tooltip-namespace_association": "Kachie igbea itinye kwa okwu ma ọbụ isi-okwu aha-ebe jikọtara aha ahọpụtara",
        "blanknamespace": "(Ḿkpà)",
        "contributions": "atụmatụ metụrara Jenda.{{GENDER:$1|User}}",
        "contributions-title": "Orü ọ'bànifé nà $1",
        "svg-long-desc": "usòrò SVG, nà áhà pixel $1 × $2, ívụ usòrò: $3",
        "show-big-image": "Failụ si na nke mbu",
        "show-big-image-preview": "Otu nyochaa a ha:$1",
+       "show-big-image-other": "Ndị ọzọ {{PLURAL:$2|mkpebi|mkpebi}}:$1.",
        "show-big-image-size": "$1 × $2 piksels",
        "file-info-gif-looped": "etemte",
        "newimages-legend": "Nzàtà",
index 8b8a973..aca6e7c 100644 (file)
        "tag-mw-contentmodelchange": "Modifiko di la kontenajo di ula modelo",
        "tag-mw-contentmodelchange-description": "Redakturi qui [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel modifikas la modelo di kontenajo] di ula pagino",
        "tag-mw-new-redirect": "Nova ridirekto",
+       "tag-mw-new-redirect-description": "Redakturi qui kreas nova ridirekto, o chanjas kontenajo di pagino a ridirekto",
+       "tag-mw-removed-redirect": "Ridirekto efacita",
+       "tag-mw-changed-redirect-target": "Emo di ridirekto modifikata",
+       "tag-mw-changed-redirect-target-description": "Redakturi qui modifikas la skopo di ula ridirekto",
        "tag-mw-blank-description": "Redakturi qui efacas pagini",
        "tag-mw-replace": "Remplasita",
        "tag-mw-replace-description": "Redakturi qui removas plua kam 90% de la kontenajo di ula pagino",
index 26532b6..bbd2dfa 100644 (file)
                        "Senpremì",
                        "Ignazio Cannata",
                        "Frubino",
-                       "TheRukk"
+                       "TheRukk",
+                       "Titore"
                ]
        },
        "tog-underline": "Sottolinea i collegamenti:",
        "blockedtitle": "Utente bloccato.",
        "blocked-email-user": "<strong>Alla tua utenza è stato vietato l'invio di email. Puoi ancora modificare altre pagine di questa wiki.</strong> Puoi vedere tutti i dettagli del blocco su [[Special:MyContributions|contributi dell'utenza]].\n\nIl blocco è stato effettuato da $1.\n\nLa ragione data è <em>$2</em>.\n\n* Inizio del blocco: $8\n* Scadenza del blocco: $6\n* Destinatario del blocco: $7\n* ID Blocco #$5",
        "blockedtext-partial": "<strong>Alla tua utenza o indirizzo IP è stato vietato di apportare modifiche a questa pagina. Puoi ancora modificare altre pagine di questa wiki.</strong> Puoi vedere tutti i dettagli del blocco su [[Special:MyContributions|contributi dell'utenza]].\n\nIl blocco è stato effettuato da $1.\n\nLa ragione data è <em>$2</em>.\n\n* Inizio del blocco: $8\n* Scadenza del blocco: $6\n* Destinatario del blocco: $7\n* ID Blocco #$5",
-       "blockedtext": "<strong>Il tuo nome utente o indirizzo IP è stato bloccato.</strong>\n\nIl blocco è stato imposto da $1. La motivazione del blocco è la seguente: <em>$2</em>.\n\n* Inizio del blocco: $8\n* Scadenza del blocco: $6\n* Intervallo di blocco: $7\n\nSe lo si desidera, è possibile contattare $1 o un altro [[{{MediaWiki:Grouppage-sysop}}|amministratore]] per discutere del blocco.\n\nSi noti che la funzione \"{{int:emailuser}}\" non è attiva se non è stato registrato un indirizzo email valido nelle proprie [[Special:Preferences|preferenze]] o se l'utilizzo di tale funzione è stato bloccato.\n\nL'indirizzo IP attuale è $3, il numero ID del blocco è #$5.\nSi prega di specificare tutti i dettagli precedenti in qualsiasi richiesta di chiarimenti.",
+       "blockedtext": "<strong>Il tuo nome utente o indirizzo IP è stato bloccato.</strong>\n\nIl blocco è stato imposto da $1. La motivazione del blocco è la seguente: <em>$2</em>.\n\n* Inizio del blocco: $8\n* Scadenza del blocco: $6\n* Destinatario del blocco: $7\n\nSe lo si desidera, è possibile contattare $1 o un altro [[{{MediaWiki:Grouppage-sysop}}|amministratore]] per discutere del blocco.\n\nSi noti che la funzione \"{{int:emailuser}}\" non è attiva se non è stato registrato un indirizzo email valido nelle proprie [[Special:Preferences|preferenze]] o se l'utilizzo di tale funzione è stato bloccato.\n\nL'indirizzo IP attuale è $3, il numero ID del blocco è #$5.\nSi prega di specificare tutti i dettagli precedenti in qualsiasi richiesta di chiarimenti.",
        "autoblockedtext": "Questo indirizzo IP è stato bloccato automaticamente perché condiviso con un altro utente, a sua volta bloccato da $1.\nLa motivazione del blocco è la seguente:\n\n:<em>$2</em>\n\n* Inizio del blocco: $8\n* Scadenza del blocco: $6\n* Intervallo di blocco: $7\n\nÈ possibile contattare $1 o un altro [[{{MediaWiki:Grouppage-sysop}}|amministratore]] per richiedere eventuali chiarimenti circa il blocco.\n\nSi noti che la funzione \"{{int:emailuser}}\" non è attiva se non è stato registrato un indirizzo e-mail valido nelle proprie [[Special:Preferences|preferenze]] e, comunque, se nell'applicare il blocco, tale funzione è stata disabilitata (per la durata del blocco).\n\nL'indirizzo IP attuale è $3, il numero ID del blocco è #$5\nSi prega di specificare tutti i dettagli qui inclusi nel compilare qualsiasi richiesta di chiarimenti.",
        "systemblockedtext": "Il tuo nome utente o l'indirizzo IP è stato bloccato automaticamente da MediaWiki.\nLa motivazione del blocco è la seguente:\n\n:''$2''\n\n* Inizio del blocco: $8\n* Scadenza del blocco: $6\n* Intervallo di blocco: $7\n\nL'indirizzo IP attuale è $3.\nSi prega di specificare tutti i dettagli qui inclusi nel compilare qualsiasi richiesta di chiarimenti.",
        "blockednoreason": "nessuna motivazione indicata",
        "prefs-timeoffset": "Ore di differenza",
        "prefs-advancedediting": "Opzioni generali",
        "prefs-developertools": "Strumenti per gli sviluppatori",
-       "prefs-editor": "Editore",
+       "prefs-editor": "Editor",
        "prefs-preview": "Anteprima",
        "prefs-advancedrc": "Opzioni avanzate",
        "prefs-advancedrendering": "Opzioni avanzate",
index 0f789a0..413c45a 100644 (file)
        "rcfilters-filter-showlinkedto-label": "다음 문서로 링크한 문서의 변경사항 보기",
        "rcfilters-filter-showlinkedto-option-label": "<strong>선택된 문서로 링크하는</strong> 문서들",
        "rcfilters-target-page-placeholder": "문서 이름(또는 분류)을 입력하세요",
+       "rcfilters-allcontents-label": "모든 내용",
+       "rcfilters-alldiscussions-label": "모든 토론",
        "rcnotefrom": "아래는 <strong>$3, $4</strong>부터 시작하는 {{PLURAL:$5|바뀜이 있습니다}}. (최대 <strong>$1</strong>개가 표시됨)",
        "rclistfromreset": "날짜 선택 초기화",
        "rclistfrom": "$3 $2부터 시작하는 새로 바뀐 문서 보기",
        "move-subpages": "하위 문서도 이동 ($1개까지)",
        "move-talk-subpages": "토론 문서의 하위 문서도 이동하기 ($1개까지)",
        "movepage-page-exists": "$1 문서가 이미 존재하므로 자동으로 덮어쓸 수 없습니다.",
+       "movepage-source-doesnt-exist": "$1 문서는 존재하지 않으며 이동할 수 없습니다.",
        "movepage-page-moved": "\"$1\" 문서를 \"$2\" 문서로 이동했습니다.",
        "movepage-page-unmoved": "$1 문서를 $2 문서로 이동할 수 없습니다.",
        "movepage-max-pages": "{{PLURAL:$1|문서}}를 최대 $1개 이동했으며 나머지 문서는 자동으로 이동하지 않습니다.",
        "delete_and_move_reason": "\"[[$1]]\"에서 문서를 이동하기 위해 삭제함",
        "selfmove": "제목이 동일합니다.\n같은 제목으로는 문서를 이동할 수 없습니다.",
        "immobile-source-namespace": "\"$1\" 이름공간에 속한 문서는 이동시킬 수 없습니다.",
+       "immobile-source-namespace-iw": "다른 위키의 문서는 이 위키로부터 이동할 수 없습니다.",
        "immobile-target-namespace": "\"$1\" 이름공간에 속한 문서는 이동시킬 수 없습니다.",
        "immobile-target-namespace-iw": "인터위키 링크를 넘어 문서를 이동할 수 없습니다.",
        "immobile-source-page": "이 문서는 이동할 수 없습니다.",
        "immobile-target-page": "목표 제목으로 이동할 수 없습니다.",
+       "movepage-invalid-target-title": "요청한 이름은 유효하지 않습니다.",
        "bad-target-model": "원하는 대상은 다른 내용 모델을 사용합니다. $1에서 $2로 변환할 수 없습니다.",
        "imagenocrossnamespace": "파일을 파일이 아닌 이름공간으로 이동할 수 없습니다.",
        "nonfile-cannot-move-to-file": "파일이 아닌 문서를 파일 이름공간으로 이동할 수 없습니다.",
index 110841f..152fecc 100644 (file)
        "continue-editing": "Pai ka kotak panyuntiangan",
        "previewconflict": "Pratayang iko mancaminan teks pado bagian ateh kotak suntiangan teks sabagaimano akan taliek bilo Sanak manyimpannyo.",
        "session_fail_preview": "Maaf, kami indak bisa mamproses suntiangan Sanak dek ilangnyo data sesi. \n\nSanak mungkin lah takalua dari log. <strong>Mohon pastikan baso Sanak masih masuak log. Cubo sajo sakali lai.</strong>.\nKok masih indak bisa, cubo [[Special:UserLogout|kalua]] dan masuak log sakali lai, dan pareso kok panjalajah web sanak mambuliahan panyimpanan ''cookies'' dari laman web ko.",
-       "session_fail_preview_html": "'''Kami indak dapek mamproses suntiangan Sanak karano hilangnyo sesi data.'''\n\n''Dek {{SITENAME}} mangizinan panggunoan HTML mantah, pratonton alah disuruakan sabagai pancagahan terhadok sarangan JavaScript.''\n\n'''Jikok iko marupoan suntiangan nan sah, silakan cubo lai.\nJikok masih jo indak barasil, cubolah [[Special:UserLogout|kalua log]] dan masuak baliak.'''",
+       "session_fail_preview_html": "Maaf, kami indak bisa mamproses suntiangan sanak dek ilangnyo data sesi. \n\n<em> Dek sebab teks HTML mantah pado {{SITENAME}} alah diaktifkan, pratinjau ko disuruakkan untuak mancagah sarangan JavaScript.</em>\n\n<strong>Kok iko satu pacuboan suntiangan nan sah, mohon cubo liak.</strong>\nKok indak juo bisa, cubo [[Special:UserLogout|kalua log]] dan masuak lai. Pareso kalau panjalajah web Sanak mampabuliahan cookie dari laman ko.",
        "token_suffix_mismatch": "'''Suntiangan Sanak ditolak karano aplikasi klien Sanak maubah karakter tando baco pado suntiangan.'''\nSuntiangan tasabuik ditolak untuak mancegah kasalahan pado teks laman.\nHal iko kadang tajadi jikok Sanak manggunokan layanan proxy anonim babasis web nan bamasalah.",
        "edit_form_incomplete": "'''Babarapo bagian dari formulir suntiangan indak mancapai server; pariso baliak apokah suntiangan Sanak tatap utuah dan cubo lai.'''",
        "editing": "Manyuntiang $1",
        "copyrightwarning2": "Parhatikan bahawa sadoalah kontribusi terhadap {{SITENAME}} dapek disuntiang, diubah, atau dihapuih oleh panyumbang lainnyo. Jikok Sanak indak ingin tulisan Sanak disuntiang urang lain, jan kiriman ka siko.<br />Sanak jua bajanji bahawa iko adolah hasil karyo Sanak surang, atau disalin dari sumber miliak umum atau sumber bebas nan lain (liek $1 untuak informasi labiah lanjuik). '''JAN KIRIMAN KARYO NAN DILINDUNGI HAK CIPTA TANPA IJIN!'''",
        "editpage-cannot-use-custom-model": "Model konten ko indak dapek diubah.",
        "longpageerror": "'''Kasalahan: Teks nan Sanak kiriman sagadang {{PLURAL:$1|$1 kilobita}}, barati labiah gadang dari jumlah maksimum {{PLURAL:$2|$2 kilobita}}. Teks indak dapek disimpan.'''",
-       "readonlywarning": "'''PARINGATAN: Basis data sadang dikunci untuak pamaliharaan, sahinggo saat iko Sanak indak dapek manyimpan hasil suntiangan.''' \nSanak mungkin paralu manyalin teks suntiangan Sanak ko dan simpankan ka sabuah berkas teks guno mamuekannyo baliak kundian.\n\nPanguruih nan mangunci basis data maagiahan panjalehan barikuik: $1",
+       "readonlywarning": "<strong>Paringatan: Basis data ko dikunci untuak karajo pambarasiahan. Sanak indak bisa manyimpan suntiangan kini ko.</strong>\nSanak disarankan untuak manyalin jo manyimpan suntiangan sanak ka file teks untuak diunggah kudian.\n\nManuruik panguruih sistem nan mangunci: $1",
        "protectedpagewarning": "'''Paringatan: Laman iko sadang dilinduangi sahinggo hanyo pangguno jo hak akses pangurus nan dapek manyuntiangnyo.'''\nEntri catatan tarakhir disadioan di bawah untuak referensi:",
-       "semiprotectedpagewarning": "'''Catatan:''' Laman ko sadang dilinduangi, jadi hanyo pangguno tadaftar nan dapek manyuntiangnyo.\nEntri log tarakhia disadioan di bawah untuak reperensi:",
-       "cascadeprotectedwarning": "'''Paringatan:''' Laman ko sadang dilinduangi jadi hanyo pangguno jo hak akses panguruih sajo nan dapek manyuntiangnyo karano disaratoan dalam {{PLURAL:$1|laman}} nan alah dilinduangi jo palinduangan batingkek:",
+       "semiprotectedpagewarning": "<strong>Catatan:</strong> Laman ko alah dilinduangi, hanyo pangguno nan alah takonfirmasi sacaro otomatis nan bisa manyuntiang.\nLog nan paliang akhia:",
+       "cascadeprotectedwarning": "<strong>Paringatan:</strong> Laman ko alah dilinduangi. Hanyo pangguno nan [[Special:ListGroupRights|punyo hak nan tatantu]] buliah untuak manyuntiang, dek karano laman ko ditransklusi pado {{PLURAL:$1|laman nan dilinduangi ko}}:",
        "titleprotectedwarning": "'''Paringatan: Laman iko alah dilinduangi sahinggo diparaluan [[Special:ListGroupRights|hak khusus]] untuak mambueknyo.'''\nEntri catatan tarakhir disadioan di bawah untuak referensi:",
        "templatesused": "{{PLURAL:$1|Templat}} nan digunoan di laman ko:",
        "templatesusedpreview": "{{PLURAL:$1|Templat}} nan digunoan dalam pratonton ko:",
        "defaultmessagetext": "Teks baku.",
        "content-failed-to-parse": "Gagal manjabarkan konten $2 untuak model $1: $3",
        "invalid-content-data": "Data kanduangan indak valid.",
-       "content-not-allowed-here": "Konten \"$1\" indak diizinan di laman [[:$2]]",
+       "content-not-allowed-here": "Isi \"$1\" indak diizinkan pado laman [[:$2]] di slot \"$3\"",
        "editwarning-warning": "Maninggakan laman ko dapek maakibaikan parubahan nan dibuek hilang. Jikok Sanak lah masuak log, dapek mamatian pasan ko malalui bagian \"Panyuntiangan\" pado laman pangaturan.",
        "slot-name-main": "Utamo",
        "content-model-wikitext": "Teks wiki",
        "prefs-personal": "Profil pangguno",
        "prefs-rc": "Parubahan baru",
        "prefs-watchlist": "Daftar pantau",
+       "prefs-editwatchlist": "Suntiang daftar pantauan",
+       "prefs-editwatchlist-label": "Suntiang entri daftar pantauan Sanak:",
        "prefs-watchlist-days": "Jumlah hari dalam daftar pantau:",
        "prefs-watchlist-days-max": "Maksimum $1 {{PLURAL:$1|hari}}",
        "prefs-watchlist-edits": "Jumlah suntiangan nan ditunjuakan pado daftar pantau:",
        "timezoneregion-indian": "Samudera Hindia",
        "timezoneregion-pacific": "Samudera Pasifik",
        "allowemail": "Izinkan pangguno lain mangirim surel",
+       "email-allow-new-users-label": "Izinkan surel dari pangguno baru",
+       "email-blacklist-label": "Panggono ko indak dapek kirim surel ka Ambo:",
        "prefs-searchoptions": "Cari",
        "prefs-namespaces": "Ruangnamo",
        "default": "baku",
        "prefs-files": "Berkas",
-       "prefs-custom-css": "CSS paribadi",
-       "prefs-custom-js": "JS paribadi",
+       "prefs-custom-css": "CSS surang",
+       "prefs-custom-js": "JS surang",
        "prefs-common-config": "CSS/JS untuak kasado kulik:",
        "prefs-reset-intro": "Angku dapek manggunokan laman ko untuak mangambalikan pangaturan ka setelan baku situs ko.\nPangambalian pangaturan indak dapek dibatalan.",
        "prefs-emailconfirm-label": "Surel konfirmasi:",
        "prefs-advancedwatchlist": "Piliahan lanjuik",
        "prefs-displayrc": "Piliahan tampilan",
        "prefs-displaywatchlist": "Piliahan tampilan",
+       "prefs-changesrc": "Parubahan ditampilkan",
        "prefs-diffs": "Pabedoan",
        "userrights": "Manajemen hak pangguno",
        "userrights-lookup-user": "Mangatua kalompok pangguno",
index 59b7e93..e377971 100644 (file)
        "rcfilters-filter-showlinkedto-label": "കണ്ണി ചേർക്കപ്പെട്ട താളുകളിലെ മാറ്റങ്ങൾ കാണിക്കുക",
        "rcfilters-filter-showlinkedto-option-label": "തിരഞ്ഞെടുത്ത താളിലേക്ക് <strong>കണ്ണി ചേർക്കപ്പെട്ട താളുകൾ</strong>",
        "rcfilters-target-page-placeholder": "താളിന്റെ (അല്ലെങ്കിൽ വർഗ്ഗത്തിന്റെ) പേര് നൽകുക",
+       "rcfilters-allcontents-label": "എല്ലാ ഉള്ളടക്കവും",
+       "rcfilters-alldiscussions-label": "എല്ലാ സംവാദങ്ങളും",
        "rcnotefrom": "<strong>$3, $4</strong> മുതലുള്ള {{PLURAL:$5|മാറ്റം|മാറ്റങ്ങൾ}} ആണ് താഴെയുള്ളത്  (<strong>$1</strong> എണ്ണം വരെ കൊടുക്കുന്നതാണ്).",
        "rclistfromreset": "തീയതി എടുത്തത് പുനഃസജ്ജീകരിക്കുക",
        "rclistfrom": "$3 $2 മുതലുള്ള മാറ്റങ്ങൾ പ്രദർശിപ്പിക്കുക",
        "specialmute": "നിശബ്ദമാക്കുക",
        "specialmute-submit": "സ്ഥിരീകരിക്കുക",
        "specialmute-label-mute-email": "ഈ ഉപയോക്താവിൽ നിന്നുമുള്ള ഇമെയിലുകൾ നിശബ്ദമാക്കുക",
+       "specialmute-login-required": "താങ്കളുടെ നിശബ്ദമാക്കൽ ഐച്ഛികങ്ങൾ മാറ്റുന്നതിനായി ദയവായി പ്രവേശിക്കുക.",
+       "mute-preferences": "നിശബ്ദമാക്കൽ ഐച്ഛികങ്ങൾ",
        "revid": "നാൾപ്പതിപ്പ് $1",
        "pageid": "താൾ ഐ.ഡി. $1",
        "interfaceadmin-info": "$1\n\nസൈറ്റ്‌വ്യാപക സി.എസ്.എസ്./ജെ.എസ്./ജെസൺ പ്രമാണങ്ങൾ തിരുത്താനുള്ള അവകാശം സമീപകാലത്ത് <code>editinterface</code> അവകാശത്തിൽനിന്നും വേർപെടുത്തിയതാണ്. ഈ പിഴവ് എന്തുകൊണ്ടാണ് പ്രദർശിക്കപ്പെടുന്നതെന്ന് താങ്കൾക്ക് മനസ്സിലാകുന്നില്ലെങ്കിൽ [[mw:MediaWiki_1.32/interface-admin]] കാണുക.",
index c75710f..250dc65 100644 (file)
        "mainpage": "Paggena prencepale",
        "mainpage-description": "Paggena prencepale",
        "policy-url": "Project:Policy",
-       "portal": "Porta d'<nowiki/>'a commonetà",
+       "portal": "Porta d'a commonetà",
        "portal-url": "Project:Porta d''a commonetà",
        "privacy": "'Nformazzione ppe a privacy",
        "privacypage": "Project:'Nfrummazione ncopp'â privacy",
        "virus-scanfailed": "scanziona fallita (codece $1)",
        "virus-unknownscanner": "antivirus scanusciuto:",
        "logouttext": "'''Site asciùte.'''\n\nNota ca arcune paggene putessero cuntinuà ad cumparì comme se 'o logout nun fosse affettuato fin quanno nun sarrà pulezzata 'a cache d\"o proprio browser.",
+       "logging-out-notify": "Staje ascenno, aspietta.",
+       "logout-failed": "Nun se può ascì mo: $1",
        "cannotlogoutnow-title": "Mo nun se pò ascì",
        "cannotlogoutnow-text": "'A disconessione nun è possibbele quanno s'ausa $1.",
        "welcomeuser": "Bemmenuto, $1!",
        "badretype": "'E passwords ch'è mis nun songe eguale.",
        "usernameinprogress": "Na criazione 'e cunto pe' st'utente è già nprugresso. Pe' piacere aspettate.",
        "userexists": "'O nomme utente ch'avete miso è già ausàto.\nPe' piacere sciglite n'atu nomme.",
+       "createacct-normalization": "'O nomme tuio sarrà cagnato a \"$2\" pe raggioni tecniche.",
        "loginerror": "Probblema 'e accièsso",
        "createacct-error": "Errore 'e criazione 'e cunto",
        "createaccounterror": "Nun se può crià nu cunto: $1",
        "resetpass-abort-generic": "'O cagnamiento d' 'a password s'è spezzato 'a na stensione.",
        "resetpass-expired": "'A pasword è ammaturata. Avite 'e ffà na password nova pe putè trasì.",
        "resetpass-expired-soft": "'A pasword vuost è ammaturata e s'adda cagnà. Avite 'e scegliere na password nova mò, o ffà click ncopp'a \"{{int:authprovider-resetpass-skip-label}}\" p' 'a cagnà aroppo.",
+       "resetpass-validity": "'A pasword toia nun è bbona: $1",
        "resetpass-validity-soft": "'A password toja nun è bbona: $1\n\nAvite 'e scegliere na password nova mò, o ffà click ncopp'a \"{{int:authprovider-resetpass-skip-label}}\" p' 'a cagnà aròppo.",
        "passwordreset": "Riabbìa 'a password",
        "passwordreset-text-one": "Ghienche stu modulo pe' ricevere na mmasciata e-mail c' 'a password temporanea.",
        "histfirst": "primma",
        "histlast": "urdema",
        "historysize": "({{PLURAL:$1|1 byte|$1 byte}})",
-       "historyempty": "(abbacante)",
+       "historyempty": "abbacante",
        "history-feed-title": "Cronologgia",
        "history-feed-description": "Cronologgia d' 'a paggena ncopp'a stu sito",
        "history-feed-item-nocomment": "$1 'o $2",
        "rcfilters-savedqueries-apply-label": "Crea filtro",
        "rcfilters-savedqueries-cancel-label": "Scancella",
        "rcfilters-clear-all-filters": "Pulezza tutt' 'e filtre",
-       "rcfilters-show-new-changes": "Vide 'e cagnamiente cchiù nnove",
+       "rcfilters-show-new-changes": "Vide 'e cagnamiente cchiù nnove 'e $1",
        "rcfilters-invalid-filter": "Filtro invalido",
        "rcfilters-filterlist-title": "Filtre",
        "rcfilters-filterlist-whatsthis": "Cumme funzionano?",
        "rcfilters-highlightmenu-help": "Piglia nu culore p'evidenzià sta proprietà",
        "rcfilters-filterlist-noresults": "Nisciuno filtro truvato",
        "rcfilters-noresults-conflict": "Nun s'hanno truvato risultati pecché 'a cerca tene nu cunflitto",
-       "rcfilters-state-message-subset": "Sto filtro nun tene effetti pecché 'e risultati suoi traseno 'int' {{{{PLURAL:$2|'e cerca|cerche}} cchiù gruosse (pruova 'a evidenzià pe verè): $1",
+       "rcfilters-state-message-subset": "Sto filtro nun tene effetti pecché 'e risultati suoi traseno 'int' {{{{PLURAL:$2|'a cerca|'e ccerche}} cchiù gruosse (pruova 'a evidenzià pe verè): $1",
        "rcfilters-filtergroup-authorship": "Autore d' 'o cuntribbuto",
        "rcfilters-filter-editsbyself-label": "Cagnamiénte d'ê tuoie",
        "rcfilters-filter-editsbyself-description": "Contribbute d'ê tuoie",
        "rcfilters-filter-watchlistactivity-seen-description": "Càgni a paggene ch'hê visto 'a cuanno facettero ll'urdimo cagnamiénto.",
        "rcfilters-filtergroup-lastrevision": "Ùrdeme verziune",
        "rcfilters-filter-lastrevision-label": "Verzione 'e mmo",
+       "rcfilters-tag-prefix-namespace-inverted": "<strong>:no</strong> $1",
        "rcfilters-watchlist-markseen-button": "Segna tutt'ê cagni comme visti",
        "rcfilters-watchlist-edit-watchlist-button": "Càgna 'e lista tuia d'ê paggene cuntrullate",
        "rcfilters-watchlist-showupdated": "'E càgne 'e ppaggene ca nun hê visto so' 'e <strong>niro</strong> e ch'ê ppalluccelle chiene.",
        "deadendpages": "Paggene ca nun spòntano",
        "deadendpagestext": "'E paggene ccà abbascio nun spontano a n'ati paggene ncopp'a {{SITENAME}}.",
        "protectedpages": "Paggene prutette",
+       "protectedpages-filters": "Filtri:",
        "protectedpages-indef": "Sulamente prutezziune a tiempo nun definito",
        "protectedpages-summary": "Sta paggena elenca 'e paggene ch'esisteno e ca mo stanne prutette. P'avé n'elenco 'e titule prutette â criazione, vedite [[{{#special:ProtectedTitles}}|{{int:protectedtitles}}]].",
        "protectedpages-cascade": "Sulamente prutezziune ricurzive",
        "deleting-backlinks-warning": "<strong>Attenzione:</strong>\n[[Special:WhatLinksHere/{{FULLPAGENAME}}|ati paggene]] cunteneno cullegamiente o paggene appennute â n'ata paggena ca state pe' scancellà.",
        "deleting-subpages-warning": "<strong>Accuorto:</strong> 'A paggena ca staie pe scancellà tene  [[Special:PrefixIndex/{{FULLPAGENAME}}/|{{PLURAL:$1|na sottopaggena|$1 sottopaggene|51=cchiù 'e 50 sottopaggene}}]].",
        "rollback": "Ausa na revizione 'e primma",
+       "rollback-confirmation-yes": "Sfàjere",
+       "rollback-confirmation-no": "Scancella",
        "rollbacklink": "sfàjere",
        "rollbacklinkcount": "sfàje {{PLURAL:$1|nu cagnamiento|$1 cagnamiente}}",
        "rollbacklinkcount-morethan": "sfàje cchiù 'e {{PLURAL:$1|nu cagnamiento|$1 cagnamiente}}",
        "mycontris": "'E ffatiche d''e mmeje",
        "anoncontribs": "Cuntribbute",
        "contribsub2": "Ppe {{GENDER:$3|$1}} ($2)",
+       "contributions-subtitle": "Pe {{GENDER:$3|$1}}",
        "contributions-userdoesnotexist": "'O cunto utente \"$1\" nun è riggistrato.",
        "nocontribs": "Nisciunu cagnamiento è stato truvato cu sti criterie.",
        "uctop": "attuale",
        "ipb-disableusertalk": "Nun permettere a st'utente edità 'a paggena 'e chiacchiera d' 'a soja pe' tramente ch'e bloccato",
        "ipb-change-block": "Fremma n'ata vota ll'utente cu ste mpustaziune",
        "ipb-confirm": "Cunferma 'o blocco",
+       "ipb-sitewide": "Pe tutte parte",
        "ipb-pages-label": "Paggene",
        "badipaddress": "Indirizzo IP nun valido",
        "blockipsuccesssub": "Blocco aseguito",
index db616d9..7e90572 100644 (file)
@@ -97,7 +97,8 @@
                        "KlaasZ4usV",
                        "Elroy",
                        "PiefPafPier",
-                       "Ecthelion3"
+                       "Ecthelion3",
+                       "RadioAzureus"
                ]
        },
        "tog-underline": "Verwijzingen onderstrepen:",
        "rcfilters-filter-showlinkedto-label": "Toon wijzigingen op pagina's gekoppeld naar",
        "rcfilters-filter-showlinkedto-option-label": "<strong>Pagina's gekoppeld naar</strong> de geselecteerde pagina",
        "rcfilters-target-page-placeholder": "Voer een paginanaam (of categorie) in",
+       "rcfilters-allcontents-label": "De volledige inhoud",
+       "rcfilters-alldiscussions-label": "Alle discussies",
        "rcnotefrom": "Wijzigingen sinds <strong>$3 om $4</strong> (maximaal <strong>$1</strong> {{PLURAL:$1|wijziging|wijzigingen}}).",
        "rclistfromreset": "Datum selectie opnieuw instellen",
        "rclistfrom": "Wijzigingen bekijken vanaf $3 $2",
        "immobile-target-namespace-iw": "Een interwikikoppeling is geen geldige bestemming voor het hernoemen van een pagina.",
        "immobile-source-page": "Deze pagina kan niet hernoemd worden.",
        "immobile-target-page": "Het is niet mogelijk te hernoemen naar die paginanaam.",
-       "movepage-invalid-target-title": "De opgevraagde naam is ongeldig.",
+       "movepage-invalid-target-title": "De gevraagde naam is ongeldig.",
        "bad-target-model": "De gewenste bestemming gebruikt een ander inhoudsmodel. Het is niet mogelijk om te zetten van $1 naar $2.",
        "imagenocrossnamespace": "Een mediabestand kan niet naar een andere naamruimte verplaatst worden",
        "nonfile-cannot-move-to-file": "Het is niet mogelijk te hernoemen van en naar de bestandsnaamruimte",
index 91239d3..6d8d318 100644 (file)
        "rcfilters-watchlist-markseen-button": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߓߍ߯ ߣߐ߬ߣߐ߬ ߦߋߣߍ߲ ߘߌ߫",
        "rcfilters-watchlist-edit-watchlist-button": "ߌ ߟߊ߫ ߞߐߜߍ߫ ߡߊߝߟߍߣߍ߲ ߠߎ߬ ߛߙߍߘߍ ߡߊߦߟߍ߬ߡߊ߲߫",
        "rcfilters-target-page-placeholder": "ߞߐߜߍ ߕߐ߮ ߟߊߘߏ߲߬ (ߥߟߊ߫ ߦߌߟߡߊ)",
+       "rcfilters-allcontents-label": "ߞߣߐߘߐ ߟߎ߬ ߓߍ߯",
+       "rcfilters-alldiscussions-label": "ߘߊߘߐߖߊߥߏ ߟߎ߬ ߓߍ߯",
        "rcnotefrom": "ߘߎ߰ߟߊ ߘߐ߫ {{PLURAL:$5|is the change|are the changes}} ߞߊ߬ߦߌ߯ <strong>$3, $4</strong> (up to <strong>$1</strong> shown).",
        "rclistfromreset": "ߞߐߜߍ ߓߊߕߐߡߐ߲ߠߌ߲ ߡߊߦߟߍ߬ߡߊ߲߫",
        "rclistfrom": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲߬ ߞߎߘߊ ߟߎ߫ ߦߌ߬ߘߊ ߘߊߡߌ߬ߣߊ߬ ߣߌ߲߭ ߡߊ߬ $2, $3",
        "rc-enhanced-hide": "ߝߊߙߊ߲ߝߊ߯ߛߌ ߟߎ߬ ߢߡߊߘߏ߲߰",
        "rc-old-title": "ߊ߬ ߓߊߞߘߐ ߟߊߘߊ߲߫ ߣߍ߲߫ ߦߋ߫ ߕߊ߲߬ ߠߋ߫ \"$1\"",
        "recentchangeslinked": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߜߋ߲߬ߞߘߎ߬ߢߐ߲߰ߡߊ ߟߎ߬",
+       "recentchangeslinked-feed": "ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߜߋ߲߬ߞߘߎ߬ߢߐ߲߰ߡߊ ߟߎ߬",
        "recentchangeslinked-toolbox": "ߢߟߊߞߎߘߦߊߟߌ߫ ߜߋ߲߬ߞߘߎ߬ߡߊ ߟߎ߬",
        "recentchangeslinked-title": "ߊ߬ ߟߌ߬ߤߟߊ ߡߊߦߟߍ߬ߡߊ߲߫ ߦߊ߲߬ \"$1\"",
        "recentchangeslinked-summary": "ߞߐߜߍ ߕߐ߮ ߟߊߘߏ߲߬߸ ߞߊ߬ ߞߐߜߍ ߛߘߌ߬ߜߋ߲ ߡߊ߬ߦߟߍ߬ߡߊ߲߬ߠߌ߲ ߦߋ߫߸ ߥߟߊ߫ \nߞߊ߬ ߝߘߊ߫ ߞߐߜߍ ߣߌ߲߬ ߠߊ߫. (ߖߐ߲߬ߛߊ߬ ߌ ߘߌ߫ ߦߌߟߡߊ ߛߌ߲߬ߝߏ߲ ߠߎ߬ ߦߋ߫߸ ߣߌ߲߬ ߠߊߘߏ߲߬ {{ns:category}}: ߦߌߟߡߊ ߕߐ߮). ߦߟߍ߬ߡߊ߲߬ ߡߍ߲ ߦߋ߫ ߞߐߜߍ ߣߌ߲߬ [[Special:Watchlist|your Watchlist]] ߘߐ߫߸ ߏ߬ ߦߋ߫ <strong>ߛߓߍߘߋ߲߫ ߞߎ߲ߓߊ</strong> ߟߋ߬ ߘߐ߫.",
        "recentchangeslinked-page": "ߞߐߜߍ ߕߐ߮:",
        "recentchangeslinked-to": "ߞߐߜߍ ߛߘߌ߬ߜߋ߲ ߠߎ߬ ߦߌ߬ߘߊ߬߸ ߞߊ߬ ߞߐߜߍ ߣߌ߬ ߞߋߟߋ߲ߘߌ߫",
        "recentchanges-page-added-to-category": "[[:$1]] ߓߘߊ߫ ߟߊߘߏ߲߬ ߦߌߟߡߊ ߘߐ߫",
+       "recentchanges-page-added-to-category-bundled": "[[:$1]] ߓߘߊ߫ ߟߊߘߏ߲߬ ߦߌߟߡߊ ߘߐ߫߸  [[Special:WhatLinksHere/$1|this page is included within other pages]]",
        "recentchanges-page-removed-from-category": "[[:$1]] ߛߋ߲߬ߓߐ߫ ߦߌߟߡߊ ߘߐ߫",
+       "recentchanges-page-removed-from-category-bundled": "[[:$1]] ߓߘߊ߫ ߛߋ߲߬ߓߐ߫ ߦߌߟߡߊ ߘߐ߫߸  [[Special:WhatLinksHere/$1|this page is included within other pages]]",
        "autochange-username": "ߡߋߘߌߦߊ߫-ߥߞߌ ߞߍߒߖߘߍߦߋ߫ ߡߊߦߟߍߡߊ߲ߠߌ߲",
        "upload": "ߞߐߕߐ߮ ߟߊߦߟߍ",
        "uploadbtn": "ߞߐߕߐ߮ ߟߊߦߟߍ߬",
        "uploadstash-bad-path-bad-format": "ߟߊߘߏ߲߬ߣߍ߲  \"$1\"  ߕߍ߫ ߖߙߎߡߎ߲߫ ߢߎߡߊ߫ ߘߌ߫.",
        "uploadstash-file-not-found": "ߟߊߘߏ߲߬ߣߍ߲  \"$1\" ߕߍ߫ ߥߊ߬ߣߊߙߌ ߟߎ߬ ߘߐ߫.",
        "uploadstash-file-not-found-no-thumb": "ߞߝߊ߬ߟߋ߲ߛߋ߲ ߕߍ߫ ߣߊ߬ ߟߊߛߐ߬ߘߐ߲߬ ߠߊ߫.",
+       "uploadstash-file-not-found-missing-content-type": "ߞߣߐߘߐ ߛߎ߯ߦߊ ߞߎ߲߬ߕߐ߮ ߞߐߢߌ߬ߣߊ߬ߣߍ߲߫.",
        "uploadstash-no-extension": "ߘߐ߬ߥߙߊ߬ߟߌ ߦߋ߫ ߝߏߦߊ߲ ߠߋ߬ ߘߌ߫.",
        "uploadstash-zero-length": "ߞߐߕߐ߮ ߦߋ߫ ߥߊ߲߬ߥߊ߲߬ ߘߐߞߏߟߏ߲ ߠߋ߬ ߘߌ߫.",
        "img-auth-nofile": "ߞߐߕߐ߮  \"$1\" ߕߍ߫ ߦߋ߲߬.",
index f199643..6e22ad4 100644 (file)
        "showdiff": "Yong-a wallak",
        "anoneditwarning": "<strong>Warning:</strong> You are not logged in. Noonook IP-karl-up will be publicly djinang il noonook wallak. Noonook-il <strong>[$1 log in]</strong> or <strong>[$2 create an gudak]</strong>, noonook wallak will be attributed to noonook niall-kwel-le, along with other benefits.",
        "blockedtitle": "Niall be nap-nap",
-       "blockedtext": "<strong>Your username or IP address has been blocked.</strong>\n\nThe block was made by $1.\nThe reason given is <em>$2</em>.\n\n* Start of block: $8\n* Expiration of block: $6\n* Intended blockee: $7\n\nYou can contact $1 or another [[{{MediaWiki:Grouppage-sysop}}|administrator]] to discuss the block.\nYou cannot use the \"email this user\" feature unless a valid email address is specified in your [[Special:Preferences|account preferences]] and you have not been blocked from using it.\nYour current IP address is $3, and the block ID is #$5.\nPlease include all above details in any queries you make.",
+       "blockedtext": "<strong>Your username or IP address has been blocked.</strong>\n\nThe block was made by $1.\nThe reason given is <em>$2</em>.\n\n* Start of block: $8\n* Expiration of block: $6\n* Intended blockee: $7\n\nYou can contact $1 or another [[{{MediaWiki:Grouppage-sysop}}|administrator]] to discuss the block.\nYou cannot use the \"{{int:emailuser}}\" feature unless a valid email address is specified in your [[Special:Preferences|account preferences]] and you have not been blocked from using it.\nYour current IP address is $3, and the block ID is #$5.\nPlease include all above details in any queries you make.",
        "loginreqlink": "yaarlkoorl",
        "newarticletext": "Noonook ngwaliny beda bibol uart-yogow yeye.\nWallak bibol qadgin mar waangkin ngardal (djinang [$1 mar yira bibol] ngatta katitjiny)\nWarra bainya noonook nidja, click noonook bowser's <strong>woort koorl</strong>button",
-       "anontalkpagetext": "----\n<em>Nidja waangkininy bibol for an anonymous niall uart-quadga gudak, or who does not use it.</em>\nWe therefore have to use the numerical IP-karl-up to identify him/her.\nSuch an IP-karl-up can be shared by several niall.\nIf noonook anonymous niall and feel that irrelevant waangkin have been directed at noonook, please [[Special:CreateAccount|quadga gudak]] or [[Special:UserLogin|log in]] to avoid future confusion with other anonymous niall.",
+       "anontalkpagetext": "----\n<em>Nidja waangkininy bibol for an anonymous niall uart-quadga gudak, or who does not use it.</em>\nWe therefore have to use the numerical IP-karl-up to identify balang.\nSuch an IP-karl-up can be shared by several niall.\nIf noonook anonymous niall and feel that irrelevant waangkin have been directed at noonook, please [[Special:CreateAccount|quadga gudak]] or [[Special:UserLogin|log in]] to avoid future confusion with other anonymous niall.",
        "noarticletext": "There is currently no text in this page.\nYou can [[Special:Search/{{PAGENAME}}|search for this page title]] in other pages,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} search the related logs],\nor [{{fullurl:{{FULLPAGENAME}}|action=edit}} create this page]</span>.",
        "noarticletext-nopermission": "Nidja yeye uart text il nidja bibol.\nNoonook [[Special:Search/{{PAGENAME}}|genuniny-ung nidja bibol katta wir-iny]] bura wam bibol, ka <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} genuniny boonadairn]</span>, noonook uart kaya ijow walbirniny nidja bibol.",
        "userpage-userdoesnotexist-view": "Niall gaduk $1 be uart yeye-quadga",
        "recentchangeslinked-feed": "Noyyang wallak",
        "recentchangeslinked-toolbox": "Noyyang wallak",
        "recentchangeslinked-title": "Wallak noyyanging $1",
-       "recentchangeslinked-summary": "Nidga list-ang wallak yeye bibol beda wer-ang ngela bibol (or il ngela warrangan)\n\nBibol il [[Special:Watchlist|noonook djinanglist]] be <strong>moorn</strong>",
+       "recentchangeslinked-summary": "Nidga list-ang wallak yeye bibol beda wer-ang ngela bibol ({{ns:category}} il ngela warrangan)\n\nBibol il [[Special:Watchlist|noonook djinanglist]] be <strong>moorn</strong>",
        "recentchangeslinked-page": "Bibol kwel-le:",
        "recentchangeslinked-to": "Yong-a wallak bibol beda nitja bibol",
        "upload": "Yirra file",
index 8a1f1fe..3d39fd7 100644 (file)
        "rcfilters-filter-showlinkedto-label": "Pokaż zmiany na stronach linkujących do",
        "rcfilters-filter-showlinkedto-option-label": "<strong>Strony linkujące do</strong> zaznaczonej strony",
        "rcfilters-target-page-placeholder": "Wprowadź nazwę strony (lub kategorii)",
-       "rcfilters-alldiscussions-label": "Wszystkie dyskusje",
+       "rcfilters-allcontents-label": "Wszystkie (treść)",
+       "rcfilters-alldiscussions-label": "Wszystkie (dyskusje)",
        "rcnotefrom": "Poniżej {{PLURAL:$5|pokazano zmianę|pokazano zmiany}} {{PLURAL:$5|wykonaną|wykonane}} po <strong>$3, $4</strong> (nie więcej niż '''$1''' pozycji).",
        "rclistfromreset": "Zresetuj wybór daty",
        "rclistfrom": "Pokaż nowe zmiany od $3 $2",
        "ipb-disableusertalk": "Edytowanie przez tego użytkownika swojej strony dyskusji",
        "ipb-change-block": "Zmień ustawienia blokady",
        "ipb-confirm": "Potwierdzam blokadę",
-       "ipb-sitewide": "Całkowita",
-       "ipb-partial": "Częściowa",
+       "ipb-sitewide": "Całkowicie",
+       "ipb-partial": "Częściowo",
        "ipb-sitewide-help": "Wszystkie strony na wiki i wszystkie akcje inne edycyjne.",
        "ipb-partial-help": "Konkretne strony lub przestrzenie nazw.",
        "ipb-pages-label": "Strony",
index 86933e3..aa33ee0 100644 (file)
        "mytalk": "In the personal URLs page section - right upper corner.\n\nUsed as link title in your personal toolbox.\n\nSee also:\n* {{msg-mw|Mytalk}}\n* {{msg-mw|Accesskey-pt-mytalk}}\n* {{msg-mw|Tooltip-pt-mytalk}}\n{{Identical|Talk}}",
        "anontalk": "Same as {{msg-mw|mytalk}} but used for non-logged-in users.\n{{Identical|Talk}}\n\nSee also:\n* {{msg-mw|Accesskey-pt-anontalk}}\n* {{msg-mw|Tooltip-pt-anontalk}}",
        "navigation": "This is shown as a section header in the sidebar of most skins.\n\n{{Identical|Navigation}}",
-       "and": "The translation for \"and\" appears in the [[Special:Version]] page, between the last two items of a list. If a comma is needed, add it at the beginning without a gap between it and the \"&\". &amp;#32; is a blank space, one character long. Please leave it as it is.\n\nThis can also appear in the credits page if the credits feature is enabled,for example [{{canonicalurl:Support|action=credits}} the credits of the support page]. (To view any credits page type <nowiki>&action=credits</nowiki> at the end of any URL in the address bar.)\n{{Identical|And}}",
+       "and": "The translation for \"and\" appears in the [[Special:Version]] page, between the last two items of a list. If a comma is needed, add it at the beginning without a gap between it and the '''<code>&amp;#32;</code>''' is a blank space, one character long, as a numeric character entity reference (in order to avoid its automatic removal at start of the wikipage). Please leave it as it is (this does '''not''' imply any semicolon punctuation), or remove the whole sequence completely in languages that don't use a leading space.\n\nThis can also appear in the credits page if the credits feature is enabled,for example [{{canonicalurl:Support|action=credits}} the credits of the support page]. (To view any credits page type <nowiki>&action=credits</nowiki> at the end of any URL in the address bar.)\n{{Identical|And}}",
        "faq": "FAQ is short for ''frequently asked questions''.\n{{Identical|FAQ}}",
        "sitetitle": "{{Ignore}}",
        "sitesubtitle": "{{Ignore}}",
        "versionrequired": "This message is not used in the MediaWiki core, but was introduced with the reason that it could be useful for extensions.\n\nParameters:\n* $1 - MediaWiki version number\nSee also:\n* {{msg-mw|Versionrequiredtext}}",
        "versionrequiredtext": "This message is not used in the MediaWiki core, but was introduced with the reason that it could be useful for extensions.\n\nParameters:\n* $1 - MediaWiki version number\nSee also:\n* {{msg-mw|Versionrequired}}",
        "ok": "{{Identical|OK}}",
-       "pagetitle": "{{Optional}}\n{{doc-important|You most probably do not need to translate this message.}}\nDo '''not''' replace SITENAME with a translation of Wikipedia or some encyclopedic additions. The message has to be neutral for all projects.\n\nParameters:\n* $1 - page title or any one of the following messages:\n** {{msg-mw|Contributions-title}}\n** {{msg-mw|Searchresults-title}}\n** {{msg-mw|Sp-contributions-newbies-title}}",
+       "pagetitle": "{{Optional}}\n{{doc-important|You most probably do not need to translate this message.}}\nDo '''not''' replace SITENAME with a translation of Wikipedia or some encyclopedic additions. The message has to be neutral for all projects.\n\nParameters:\n* $1 - page title or any one of the following messages:\n** {{msg-mw|Contributions-title}}\n** {{msg-mw|Searchresults-title}}",
        "pagetitle-view-mainpage": "{{optional}}",
        "backlinksubtitle": "{{optional}}\nAppears in subtitle. Parameters:\n* $1 - a link to the page (HTML)",
        "retrievedfrom": "Message which appears in the source of every page, but it is hidden. It is shown when printing.\n\nParameters:\n* $1 - a link back to the current page: {{FULLURL:{{FULLPAGENAME}}}}",
        "apisandbox": "{{doc-special|ApiSandbox}}",
        "apisandbox-summary": "{{ignored}}\n{{doc-specialpagesummary|ApiSandbox}}",
        "apisandbox-jsonly": "Displayed as an error message if the browser does not have JavaScript enabled.",
-       "apisandbox-api-disabled": "Displayed as an error message if the API is disabled on this site.",
        "apisandbox-intro": "Displayed (from JavaScript) as a header on [[Special:ApiSandbox]].",
        "apisandbox-submit": "JavaScript button label for submitting the request.",
        "apisandbox-reset": "JavaScript button label for clearing the form.\n{{Identical|Clear}}",
        "month": "Used in [[Special:Contributions]] and history pages ([{{fullurl:Sandbox|action=history}} example]), as label for a dropdown box to select a specific month to view the edits made in that month, and the earlier months. See also {{msg-mw|year}}.",
        "year": "Used in [[Special:Contributions]] and history pages ([{{fullurl:Sandbox|action=history}} example]), as label for an input box to select a specific year to view the edits made in that year, and the earlier years.\n\nSee also:\n* {{msg-mw|month}}",
        "date": "Used in [[Special:Contributions]] and history pages ([{{fullurl:Sandbox|action=history}} example]), as label for an input box to select a specific date to view the edits made on that date, and earlier.",
-       "sp-contributions-newbies": "Text of radio button on special page [[Special:Contributions]].",
-       "sp-contributions-newbies-sub": "Note at the top of the page of results for a search on [[Special:Contributions]] where 'Show contributions for new accounts only' has been selected.",
-       "sp-contributions-newbies-title": "The page title in your browser bar, but not the page title.\n\nSee also:\n* {{msg-mw|Sp-contributions-newbies-sub}}",
        "sp-contributions-blocklog": "Used as a display name for a link to the block log on for example [[Special:Contributions/Mediawiki default]]\n\nUsed as link title in [[Special:Contributions]] and in [[Special:DeletedContributions]].\n\nSee also:\n* {{msg-mw|Sp-contributions-talk}}\n* {{msg-mw|Change-blocklink}}\n* {{msg-mw|Unblocklink}}\n* {{msg-mw|Blocklink}}\n* {{msg-mw|Sp-contributions-uploads}}\n* {{msg-mw|Sp-contributions-logs}}\n* {{msg-mw|Sp-contributions-deleted}}\n* {{msg-mw|Sp-contributions-userrights}}\n{{Identical|Block log}}",
        "sp-contributions-suppresslog": "Used as a display name for a link to log entries of suppressed edits made by that user.\n\nUsed as link title in [[Special:Contributions]] and in [[Special:DeletedContributions]]. Parameters:\n* $1 is a plain text username used for GENDER.\nSee also {{msg-mw|sp-contributions-deleted}}, {{msg-mw|sp-deletedcontributions-contribs}}, {{msg-mw|contributions}}, {{msg-mw|deletedcontributions-title}}.",
        "sp-contributions-deleted": "This is a link anchor used in [[Special:Contributions]]/''name'', when user viewing the page has the right to delete pages, or to restore deleted pages.\n\nUsed as link title in [[Special:Contributions]]. Parameters:\n* $1 is a plain text username used for GENDER.\nSee also:\n* {{msg-mw|Sp-contributions-talk}}\n* {{msg-mw|Change-blocklink}}\n* {{msg-mw|Unblocklink}}\n* {{msg-mw|Blocklink}}\n* {{msg-mw|Sp-contributions-blocklog}}\n* {{msg-mw|Sp-contributions-uploads}}\n* {{msg-mw|Sp-contributions-logs}}\n* {{msg-mw|Sp-contributions-userrights}}",
        "sp-contributions-footer": "{{ignored}}This is the footer for users that are not anonymous or newbie on [[Special:Contributions]].",
        "sp-contributions-footer-anon": "{{ignored}}This is the footer for anonymous users on [[Special:Contributions]].",
        "sp-contributions-footer-anon-range": "{{ignored}}This is the footer for IP ranges on [[Special:Contributions]].",
-       "sp-contributions-footer-newbies": "{{ignored}}This is the footer for newbie users on [[Special:Contributions]].",
        "sp-contributions-outofrange": "Message shown when a user tries to view contributions of an IP range that's too large. $1 is the numerical limit imposed on the CIDR range.",
        "whatlinkshere": "The text of the link in the toolbox (on the left, below the search menu) going to [[Special:WhatLinksHere]].\n\nSee also:\n* {{msg-mw|Whatlinkshere}}\n* {{msg-mw|Accesskey-t-whatlinkshere}}\n* {{msg-mw|Tooltip-t-whatlinkshere}}",
        "whatlinkshere-title": "Title of the special page [[Special:WhatLinksHere]]. This page appears when you click on the 'What links here' button in the toolbox. $1 is the name of the page concerned.",
        "newimages-legend": "Caption of the fieldset for the filter on [[Special:NewImages]]\n\n{{Identical|Filter}}",
        "newimages-label": "Caption of the filter editbox on [[Special:NewImages]]",
        "newimages-user": "Caption of the username/IP address editbox on [[Special:NewImages]]",
-       "newimages-newbies": "Used as label for a checkbox. When checked, [[Special:NewImages]] will only display uploads by new users.",
        "newimages-showbots": "Used as label for a checkbox. When checked, [[Special:NewImages]] will also display uploads by users in the bots group.",
        "newimages-hidepatrolled": "Used as label for a checkbox. When checked, [[Special:NewImages]] will not display patrolled uploads.\n\nCf. {{msg-mw|tog-hidepatrolled}} and {{msg-mw|apihelp-feedrecentchanges-param-hidepatrolled}}.",
        "newimages-mediatype": "Used as label for a multiselect where users can select the media types to display.",
index d3f8b0a..e3dc8f5 100644 (file)
        "rcfilters-filter-showlinkedto-label": "Показать правки на ссылающихся страницах",
        "rcfilters-filter-showlinkedto-option-label": "<strong>Страницы, ссылающиеся</strong> на выбранную",
        "rcfilters-target-page-placeholder": "Введите имя страницы (или категории)",
+       "rcfilters-allcontents-label": "Все пространства имён",
+       "rcfilters-alldiscussions-label": "Все обсуждения",
        "rcnotefrom": "Ниже {{PLURAL:$5|указано изменение|перечислены изменения}} с <strong>$3, $4</strong> (показано не более <strong>$1</strong>).",
        "rclistfromreset": "Сбросить выбор даты",
        "rclistfrom": "Показать изменения с $3 $2.",
        "move-subpages": "Переименовать подстраницы (до $1)",
        "move-talk-subpages": "Переименовать подстраницы страницы обсуждения (до $1)",
        "movepage-page-exists": "Страница $1 уже существует и не может быть автоматически перезаписана.",
+       "movepage-source-doesnt-exist": "Страница $1 не существует, а потому не может быть переименована.",
        "movepage-page-moved": "Страница $1 была переименована в $2.",
        "movepage-page-unmoved": "Страница $1 не может быть переименована в $2.",
        "movepage-max-pages": "{{PLURAL:$1|Была переименована|Было переименовано|Были переименованы}} $1 {{PLURAL:$1|страница|страницы|страниц}} — это максимум; большее число страниц автоматически переименовать нельзя.",
        "delete_and_move_reason": "Удалено для возможности переименования «[[$1]]»",
        "selfmove": "Невозможно переименовать страницу: исходное и новое имя страницы совпадают.",
        "immobile-source-namespace": "Невозможно переименовывать страницы в пространстве имён «$1»",
+       "immobile-source-namespace-iw": "Страницы из других вики не могут быть переименованы в этой вики.",
        "immobile-target-namespace": "Невозможно переместить страницу в пространство имён «$1»",
        "immobile-target-namespace-iw": "Ссылка интервики не может быть использована для переименования.",
        "immobile-source-page": "Эту страницу нельзя переименовать.",
        "immobile-target-page": "Нельзя присвоить странице это имя.",
+       "movepage-invalid-target-title": "Запрошенное имя недопустимо.",
        "bad-target-model": "Невозможно преобразовать $1 в $2. У страниц несовместимые модели содержимого.",
        "imagenocrossnamespace": "Невозможно дать файлу имя из другого пространства имён",
        "nonfile-cannot-move-to-file": "Невозможно переименовывать не-файловые страницы в файлы",
        "specialmute-success": "Изменения по отключению уведомлений были сохранены. Просмотрите всех отключённых участников на [[Special:Preferences|ваших настройках]].",
        "specialmute-submit": "Подтвердить",
        "specialmute-label-mute-email": "Отключить эл. почту от этого участника",
-       "specialmute-header": "Пожалуйста, выберите настройки уведомлений для {{GENDER:$1|участника|участницы}} <b>{{BIDI:[[User:$1|$1]]}}</b>.",
+       "specialmute-header": "Пожалуйста, выберите настройки отключения уведомлений для {{GENDER:$1|участника|участницы}} <b>{{BIDI:[[User:$1|$1]]}}</b>.",
        "specialmute-error-invalid-user": "Указанное вами имя участника не может быть найдено.",
+       "specialmute-error-no-options": "Функции отключения уведомлений недоступны. Это вызвано либо тем, что вы не подтвердили электронную почту, либо тем, что администратор выключил в этой вики функции электронной почты и\\или функции чёрного списка.",
        "specialmute-email-footer": "Для управления настройками эл. почты {{GENDER:$2|участника|участницы}} {{BIDI:$2}}, пожалуйста, посетите <$1>.",
        "specialmute-login-required": "Пожалуйста авторизируйтесь, чтобы управлять отключением уведомлений.",
        "mute-preferences": "Настройки выключения",
index eef1d93..e68fde6 100644 (file)
        "returnto": "$1 ڏانھن وَرو.",
        "tagline": "{{SITENAME}} طرفان",
        "help": "مدد",
+       "help-mediawiki": "ميڊياوڪي بابت مدد",
        "search": "ڳولا",
        "searchbutton": "ڳوليو",
        "go": "هلو",
        "history": "صفحي جي سوانح",
        "history_short": "سوانح",
        "history_small": "سوانح",
+       "updatedmarker": "توھان جي آخري ڦيري کان جديديل",
        "printableversion": "ڇپائتو پرت",
        "permalink": "مسقتل ڳنڍڻو",
        "print": "ڇاپيو",
        "privacy": "ذاتيات پاليسي",
        "privacypage": "Project:ذاتيات پاليسي",
        "badaccess": "اجازتي چُڪَ",
-       "badaccess-groups": "هن عمل کي محدود ڪيو ويو آهي $1 {{PLURAL:$2|جو اختيار رکندڙ|جا اختيار رکندڙن}} لاءِ.",
+       "badaccess-groups": "توھان جنھن عمل لاءِ عرض ڪيو آھي اھو $1 {{PLURAL:$2|گروھ|گروھن}} جي واپرائيندڙن تائين محدود ٿيل آھي.",
        "versionrequired": "ذريعات‌وڪي جو ورزن $1 درڪار",
        "versionrequiredtext": "هيءُ صفحو استعمال ڪرڻ لاءِ ذريعات‌وڪي جو ورزن $1 درڪار آهي. وڌيڪ ڄاڻڻ لاءِ [[Special:Version|ورزن بابت صفحو]] ڏسو.",
        "ok": "ٺيڪ",
+       "backlinksubtitle": "→ $1",
        "retrievedfrom": "\"$1\" تان ورتل",
        "youhavenewmessages": "{{PLURAL:$3|توھان وٽ}} $1 ($2) آھن.",
+       "youhavenewmessagesfromusers": "{{PLURAL:$4|توھان کي}} {{PLURAL:$3|ٻي واپرائيندڙ|$3 واپرائيندڙن}} ($2) کان $1 آھن.",
        "youhavenewmessagesmanyusers": "توهان لاءِ ڪيترن ئي واپرائيندڙن ($2) طرفان $1 آهن.",
        "newmessageslinkplural": "{{PLURAL:$1|ھڪ نئون پيغام|999=نوان پيغام}}",
        "newmessagesdifflinkplural": "آخري {{PLURAL:$1|تبديلي|999=تبديليون}}",
        "site-atom-feed": "$1 اڻو روان رسد",
        "page-rss-feed": "\"$1\" RSS برق مواد",
        "page-atom-feed": "\"$1\" اڻو روان رسد",
+       "feed-atom": "ايٽم",
+       "feed-rss": "آر.ايس.ايس",
        "red-link-title": "$1 (صفحو وجود نٿو رکي)",
        "sort-descending": "لهندڙ ترتيب ڏيو",
        "sort-ascending": "چڙهندڙ ترتيب ڏيو",
        "badarticleerror": "هن صفحي تي اهڙو عمل ڪار نہ آهي.",
        "cannotdelete": "$1 نالي صفحو يا فائيل ڊهي نہ سگھيو. ٿي سگھي ٿو تہ ڪنهن ان کي اڳ ۾ ئي ڊاهي ڇڏيو هجي.",
        "cannotdelete-title": "$1 نالي صفحي کي ڊاهي نہ ٿا سگھون.",
+       "delete-scheduled": "صفحو \"$1\" ڊاھ لاءِ رٿيل آھي.\nمھرباني ڪري صابر رھو.",
        "badtitle": "خراب عنوان",
        "badtitletext": "صفحي جو گھربل عنوان ڪار ڪونهي، يا خالي آهي، يا وري غيردرست طريقي سان ڳنڍيل بين‌الزباني يا بين‌الوڪي عنوان آهي. \nان ۾ هڪ يا هڪ کان وڌيڪ اهڙا اکر موجود آهن، جيڪي عنوان ۾ استعمال ڪري نٿا سگھجن.",
        "title-invalid-utf8": "صفحي جي ڄاڻايل عنوان ۾ ناقابلِڪار يُو ٽِيئيف-8 ترتيب شامل آھي.",
        "title-invalid-interwiki": "ڄاڻايل عنوان ۾ اهڙو بين‌الوڪِي ڳنڍڻو شامل آهي، جيڪو عنوانن ۾ استعمال ڪري نٿو سگھجي.",
+       "title-invalid-talk-namespace": "گھربل پصفحي عنوان ڪنھن بحث صفحي ڏانھن اشارو ڪري ٿو جيڪو وجود نٿو رکي سگھي.",
        "title-invalid-characters": "صفحي جي ڄاڻايل عنوان ۾ ناقابلِڪار اکر شامل آهن: \"$1\".",
        "title-invalid-leading-colon": "صفحي جي ڄاڻايل عنوان جي ابتدا ۾ ناقابلِڪار ڪالن شامل آهي.",
        "viewsource": "ڪوڊ ڏسو",
        "viewsource-title": "$1 جو ڪوڊ ڏسو",
        "protectedpagetext": "هيءُ صفحو سوارڻ ۽ ٻين عملن کان بچائڻ لاءِ تحفظيو ويو آهي.",
        "viewsourcetext": "توهان هن صفحي جو ڪوڊ ڏسي ۽ نقل ڪري سگھو ٿا.",
+       "viewyourtext": "توھان ھاڻي ھن صفحي ۾ <strong>پنھنجي سنوارن</strong> جو ذريعو ڏسي ۽ نقل ڪري سگھو ٿا.",
        "protectedinterface": "هي صفحو سافٽ ويئر جو انٽرفيس متعين ڪري ٿو ۽ غلط استعال کان بچڻ لاءِ ان کي تحفظيو ويو آهي.\nتمام وڪي ۾ ترجمو شامل ڪرڻ لاءِ يا هن ۾ تبديلي ڪرڻ لاءِ ميڊياوڪي ترجمو [https://translatewiki.net/ translatewiki.net] استعمال ڪيو.",
        "namespaceprotected": "توهان کي نانءُپولار <strong>$1</strong> جا صفحا سنوارڻ جا اختيار ناهن.",
        "sitecssprotected": "اوهان وٽ ھن سيايسايس صفحي کي سنوارڻ جي اجازت ناھي، ڇو تہ ان سان سڀني گھمندڙ متاثر ٿي سگھن ٿا.",
        "mycustomcssprotected": "توهان کي هيءُ CSS صفحو سنوارڻ جي اجازت نہ آهي.",
-       "mycustomjsprotected": "توهان کي هيءُ جاوا اسڪرپٽ صفحو سنوارڻ جي اجازت حاصل ڪانهي.",
+       "mycustomjsprotected": "توهان کي هيءُ جاوا-اسڪرپٽ صفحو سنوارڻ جي اجازت ناھي.",
        "myprivateinfoprotected": "توهان کي پنهنجي ذاتي معلومات سنوارڻ جي اجازت حاصل نہ آهي.",
        "mypreferencesprotected": "توھان کي پنھنجون ترجيحون سنوارڻ جي اجات حاصل ڪانھي.",
        "ns-specialprotected": "خاص صفحا سنواري نٿا سگھجن.",
-       "titleprotected": "[[User:$1|$1]] اهڙي عنوان سان صفحو سرجڻ تي روڪ لڳائي ڇڏي آهي. سبب <em>$2</em> ڄاڻايو ويو آهي.",
-       "invalidtitle": "غلط عنوان",
+       "titleprotected": "[[User:$1|$1]] اهڙي عنوان سان صفحو سرجڻ تي روڪ لڳائي ڇڏي آهي.\nسبب <em>$2</em> ڄاڻايو ويو آهي.",
+       "invalidtitle": "ناقابلِڪار عنوان",
        "exception-nologin": "داخل ٿيل نہ آهيو",
-       "virus-unknownscanner": "اڻڄاتل اينٽي وائرس:",
+       "virus-unknownscanner": "اڻڄاتل اينٽي-وائرس:",
+       "logging-out-notify": "توھان کي خارج ڪيو پيو وڃي، مھرباني ڪري ترسو.",
+       "logout-failed": "ھاڻي خارج نٿو ٿي سگھجي: $1",
        "cannotlogoutnow-title": "ھاڻي خارج نٿو ٿي سگھجي",
        "cannotlogoutnow-text": "$1 استعمال ڪرڻ دوران خارج ٿيڻ ممڪن نہ آھي.",
        "welcomeuser": "ڀليڪار، $1!",
+       "welcomecreation-msg": "توھان جو کاتو کلي چڪو آھي.\nتوھان {{SITENAME}} لاءِ پنھنجون [[Special:Preferences|ترجيحون]] جيڪڏھن وڻي تہ بدلائي سگھو ٿا.",
        "yourname": "واپرائيندڙ-نانءُ:",
        "userlogin-yourname": "واپرائيندڙ-نانءُ",
        "userlogin-yourname-ph": "پنھنجو واپرائيندڙ-نانءُ ڄاڻايو",
        "createacct-realname": "اصل نالو (مرضيءَ موجب)",
        "createacct-reason": "سبب",
        "createacct-reason-ph": "توهان ٻيو کاتو ڇو کولي رهيا آهيو",
+       "createacct-reason-help": "کاتو سرجڻ لاگ ۾ ڏيکاريل پيغام",
        "createacct-submit": "پنھنجو کاتو کوليو",
        "createacct-another-submit": "کاتو کوليو",
        "createacct-continue-submit": "کاتو کولڻ جاري رکو",
        "changepassword-throttled": "توهان تازو ئي داخل ٿيڻ جون هيڪانديون گھڻيون ڪوششون ڪيون آهن. مهرباني ڪري $1 لاءِ ترسي پوءِ وري ڪوشش ڪريو.",
        "botpasswords": "بوٽ جو ڳجھولفظ",
        "botpasswords-disabled": "بوٽ ڳجھالفظ ناقابلِڪار ڪيل آھن.",
-       "botpasswords-existing": "باٽ جا هاڻوڪا پاسورڊَ",
-       "botpasswords-createnew": "باٽ جو نئون پاسورڊ ٺاهيو",
-       "botpasswords-editexisting": "باٽ جي هاڻوڪي پاسورڊ کي سنواريو",
+       "botpasswords-existing": "بوٽ جا موجودہ ڳجھالفظ",
+       "botpasswords-createnew": "بوٽ جو نئون ڳجھولفظ ٺاهيو",
+       "botpasswords-editexisting": "بوٽ جو موجودہ گجھولفظ سنواريو",
        "botpasswords-label-appid": "باٽ جو نالو:",
        "botpasswords-label-create": "سرجيو",
        "botpasswords-label-update": "تجديد",
        "botpasswords-label-resetpassword": "ڳجھولفظ ٻيھر مقرر ڪريو",
        "botpasswords-label-grants-column": "منظور",
        "botpasswords-bad-appid": "بوٽ نانءُ \"$1\" قابلِڪار ناھي.",
-       "botpasswords-created-title": "باٽ پاسورڊ ٺاهيو ويو آهي",
-       "botpasswords-deleted-title": "باٽ پاسورڊ ڊاٿو ويو",
+       "botpasswords-created-title": "بوٽ گجھولفظ ٺاھيو ويو",
+       "botpasswords-deleted-title": "بوٽ ڳجھولفظ ڊاٿو ويو",
        "resetpass_forbidden": "ڳجھالفظ بدلائي نٿا سگھجن",
        "resetpass_forbidden-reason": "ڳجھالفظ بدلائي نٿا سگھجن:$1",
        "resetpass-no-info": "هيءُ صفحو پڙهڻ لاءِ داخل ٿيڻ ضروري آهي.",
        "savechanges": "تبديليون سانڍيو",
        "publishpage": "صفحو ڇاپيو",
        "publishchanges": "تبديليون ڇاپيو",
-       "savearticle-start": "صفحو سانڍيو",
-       "savechanges-start": "تبديليون سانڍيو",
-       "publishpage-start": "صفحو ڇاپيو",
-       "publishchanges-start": "تبديليون ڇاپيو",
+       "savearticle-start": "صفحو سانڍيو",
+       "savechanges-start": "تبديليون سانڍيو",
+       "publishpage-start": "صفحو ڇاپيو",
+       "publishchanges-start": "تبديليون ڇاپيو",
        "preview": "پيش نگاھ",
        "showpreview": "پيش نگاھ",
        "showdiff": "تبديليون ڏيکاريو",
        "newarticletext": "توھان اھڙي صفحي جو ڳنڍڻو وٺي ھتي پھتا آھيو، جيڪو اڃا وجود نٿو رکي.\nاھڙو صفحو جوڙڻ لاءِ، ھيٺين دٻي ۾ لکڻ شروع ڪريو (وڌيڪ ڄاڻڻ لاءِ [$1 امدادي صفحو] ڏسندا).\nجي توھان ھتي غلطيءَ ۾ اچي ويا آهيو، تہ رڳو پنھنجي جھانگُوءَ جي <strong>back</strong> بٽڻ تي ٽڙڪ ڪريو.",
        "noarticletext": "في‌الوقت هن صفحي اندر ڪو بہ ٽيڪسٽ نہ آهي.\nتوهان ٻين صفحن ۾ [[Special:Search/{{PAGENAME}}|search ساڳي عنوان جي ڳولا]] ڪري سگھو ٿا،  \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>، پر توھان کي ان جي سرجڻ جي اجازت نہ آھي.",
-       "missing-revision": "صفحي \"{{FULLPAGENAME}}\" جو نمبر #$1 وجود نٿو رکي.\n\nاڪثر اهو تڏهن ٿيندو آهي، جڏهن اوهان ڪنهن پراڻي ڳنڍڻي تان اچو يا صفحو [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} ڊاٺو] ويو هجي.\n\n.",
+       "missing-revision": "صفحي \"{{FULLPAGENAME}}\" جو نمبر #$1 وجود نٿو رکي.\n\nاڪثر اهو تڏهن ٿيندو آهي، جڏهن اوهان ڪنهن پراڻي ڳنڍڻي تان اچو يا صفحو [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} ڊاٺو] ويو هجي.",
        "userpage-userdoesnotexist-view": "واپرائيندڙ کاتو $1 درج ٿيل نہ آهي.",
        "blocked-notice-logextract": "هيءُ واپرائيندڙ في‌الحال بندشيل آهي.\nتازو بندش لاگ حوالي طور پيش ڪجي ٿو:",
        "updated": "(تجديديل)",
        "yourtext": "توهان جو متن",
        "storedversion": "سانڍيل مسودو",
        "yourdiff": "تفاوت",
-       "copyrightwarning": "ياد رکندا تہ {{SITENAME}} لاءِ سموريون ڀاڱيداريون $2 تحت پڌريون ڪجن ٿيون (تفصيلن لاءِ $1 ڏسندا). اوهان جي تحرير کي {{SITENAME}} جي قائدن تحت سنواري سگهجي ٿو. جيڪڏهن اوهان نٿا چاهيو تہ اوهان جي لکڻين کي بي رحميءَ سان سنواريو وڃي يا ورهائي عام ڪيو وڃي تہ پوءِ پنهنجي لکڻي هتي جمع نہ ڪرايو. پنهنجو مواد هتي جمع ڪرڻ جو مطلب هوندو تہ توهان کي جمع ڪرايل مواد جي مفت فراهمي ۽ کُليل تبديليءَ تي ڪوبہ اعتراز ناهي.<br />\nتوهان اهڙي پڪ ڏيڻ جا پابند پڻ آهيو تہ توهان جو جمع ڪرايل مواد توهان جو پنهنجو لکيل آهي يا وري توهان ڪنهن مفت وسيلي تان ڪاپي ڪيو آهي.\n'''تحفظيل حق ۽ واسطا رکندڙ مواد واسطيدار مالڪ کان اڳواٽ اجازت وٺڻ کان سواءِ هتي جمع نہ ڪريو.'''",
+       "copyrightwarning": "ياد رکندا تہ {{SITENAME}} لاءِ سموريون ڀاڱيداريون $2 ھيٺ ڏنل ڄاتيون وڃن ٿيون (تفصيلن لاءِ $1 ڏسندا).\nجيڪڏهن اوهان نٿا چاهيو تہ اوهان جي لکڻيءَ کي بي رحميءَ سان سنواريو وڃي يا ورهائي عام ڪيو وڃي تہ پوءِ ان کي هتي اماڻيو.<br />\nتوهان اسان سان اھو بہ وچن ڪريو ٿا تہ ھي توهان پاڻ لکيو آھي يا وري ڪنھن مفت وسيلي يا عوامي ڊومين تان نقل ڪيو آهي.\n<strong>حق-۽-واسطا-رکندڙ ڪم کان اجازت سواءِ نہ اماڻيو.</strong>",
        "copyrightwarning2": "ياد رکندا تہ {{SITENAME}} لاءِ سموريون ڀاڱيدارين کي ٻيا ڀاڱيدار سنواري، بدلائي، يا ڊاهي سگھن ٿا. جيڪڏهن اوهان نہ ٿا چاهيو تہ اوهان جي لکڻين کي بي رحميءَ سان سنواريون وڃي يا ورهائي عام ڪيو وڃي تہ پوءِ پنهنجي لکڻي هتي جمع نہ ڪرايو.</br>\nتوهان اهڙي پڪ ڏيڻ جا پابند پڻ آهيو تہ توهان جو جمع ڪرايل مواد توهان جو پنهنجو لکيل آهي يا وري توهان ڪنهن اهڙي ئي مفت عوامي وسيلي تان ڪاپي ڪيو آهي. (تفصيلن لاءِ $1 ڏسندا).\n\n<strong>تحفظيل حق ۽ واسطا رکندڙ مواد واسطيدار مالڪ کان اڳواٽ اجازت وٺڻ بنان هتي جمع نہ ڪريو.</strong>",
-       "protectedpagewarning": "<strong>چتاءُ: هيءَ صفحو اهڙيءَ ريت تحفظيو ويو آهي جو فقط منتظمين ئي ان کي سنواري سگھن ٿا. </strong>\nتازه ترين لاگ حوالي طور پيش ڪجي ٿو:",
+       "protectedpagewarning": "<strong>چتاءُ: هيءَ صفحو اهڙيءَ ريت تحفظيو ويو آهي جو فقط منتظم ئي ان کي سنواري سگھن ٿا. </strong>\nتازي-ترين لاگ داخلا حوالي طور ھيٺ پيش ڪجي ٿي:",
        "semiprotectedpagewarning": "<strong>نوٽ:</strong> هيءَ صفحو اهڙيءَ ريت تحفظيو ويو آهي جو فقط خودڪار نموني پڪ ڪيل واپرائيندڙ ئي ان کي سنواري سگھن ٿا.\nتازه ترين لاگ حوالي طور پيش ڪجي ٿو:",
        "templatesused": "هن صفحي تي استعمال ٿيندڙ {{PLURAL:$1|سانچو|سانچا}}:",
        "templatesusedpreview": "هن پيش نگاھ ۾ استعمال ٿيل {{PLURAL:$1|سانچو|سانچا}}:",
        "templatesusedsection": "هن سيڪشن ۾ استعمال ٿيل {{PLURAL:$1|سانچو|سانچا}}:",
        "template-protected": "(تحفظيل)",
        "template-semiprotected": "(نيم-تحفظيل)",
-       "hiddencategories": "هيءُ صفحو  {{PLURAL:$1|1 لڪل زمري|$1 لڪل زمرن}}: جو رڪن آهي:",
+       "hiddencategories": "هيءُ صفحو {{PLURAL:$1|1 لڪل زمري|$1 لڪل زمرن}}: جو رڪن آهي:",
        "edittools-upload": "-",
        "nocreatetext": "{{SITENAME}} نوان صفحا سرجڻ جي روڪَ ڪئي آھي.\nتوھان اڳ ئي موجود صفحن کي سنواري سگھو ٿا، يا [[Special:UserLogin|داخل ٿي يا نئون کاتو کولي سگھو ٿا]].",
-       "nocreate-loggedin": "توهان کي نوان صفحا سرجڻ جي اجازت حاصل ڪانهي.",
-       "sectioneditnotsupported-title": "سيڪشن جي سنوار ممڪن نہ آهي",
-       "sectioneditnotsupported-text": "هن صفحي تي سيڪشن کي سنوارڻ ممڪن نہ آهي.",
-       "permissionserrors": "اجازتÙ\86اÙ\85Ù\8a Ø¬Ù\8a Ú\86Ù\8fÚªÙ\8e",
-       "permissionserrorstext": "هيٺين {{PLURAL:$1|سبب|سببن}} ڪري، توهان کي اهو ڪرڻ جي اجازت حاصل ڪانهي.",
+       "nocreate-loggedin": "توهان کي نوان صفحا سرجڻ جي اجازت ناھي.",
+       "sectioneditnotsupported-title": "ڀاڱي جي سنوار سپورٽڊ ناھي",
+       "sectioneditnotsupported-text": "هن صفحي تي ڀاڱي جي سنوار سپورٽڊ ناھي.",
+       "permissionserrors": "اجازتي چُڪَ",
+       "permissionserrorstext": "هيٺين {{PLURAL:$1|سبب|سببن}} ڪري، توهان کي اهو ڪرڻ جي اجازت ناھي.",
        "permissionserrorstext-withaction": "ھيٺين {{PLURAL:$1|سبب|سببن}} ڪري، توھان کي $2 جي اجازت ڪانھي.",
        "recreate-moveddeleted-warn": "'''خبردار: توھان اھڙو صفحو نئين سِر سرجي رھيا آھيو جيڪو اڳ ڊاٺو ويو آھي.'''\n\nبھتر ٿيندو تہ توھان سوچي وٺو تہ ڇا ان صفحي کي سنوارڻ چڱو ٿيندو.\nتوهان جي سھولت خاطر ھتي ان صفحي جو ڊاٺ لاگ ميسر ڪجي ٿو:",
        "moveddeleted-notice": "ھيءُ صفحو ڊھي چڪو آهي. \nحوالي طور صفحي جا ڊاھ، حفاظت ۽ چورڻ لاگ ھيٺ ڏنل آھن.",
        "moveddeleted-notice-recent": "معاف ڪجو، هيءُ صفحو تازو ئي ڊاٺو ويو ھو (پوين 24 ڪلاڪن اندر). حوالي طور صفحي جا ڊاھ، حفاظت ۽ چورڻ لاگ ھيٺ ڏنل آھن.",
        "log-fulllog": "پُورو لاگ ڏسو",
-       "edit-conflict": "سنوار تڪرار",
-       "postedit-confirmation-created": "هيءُ صفحو سرجي چڪو آهي.",
-       "postedit-confirmation-restored": "هيءُ صفحو بحالجي چڪو آهي.",
+       "edit-conflict": "سنوار تڪرار.",
+       "postedit-confirmation-created": "صفحو سرجي چڪو آهي.",
+       "postedit-confirmation-restored": "صفحو بحالجي چڪو آهي.",
        "postedit-confirmation-saved": "توھان جي سنوار سانڍجي وئي ھئي.",
-       "edit-already-exists": "نئون صفحو سرجي نہ سگھيو. اهو اڳ ۾ ئي وجود رکي ٿو.",
-       "invalid-content-data": "ناقابل ڪار موادي اعداد",
+       "postedit-confirmation-published": "توھان جي سنوار ڇاپجي وئي.",
+       "edit-already-exists": "نئون صفحو سرجي نہ سگھيو.\nاهو اڳ ئي وجود رکي ٿو.",
+       "defaultmessagetext": "پيغام جو ڏنل متن",
+       "invalid-content-data": "ناقابلِڪار موادي اعداد",
        "content-not-allowed-here": "\"$1\" مواد جي هن صفحي [[:$2]] جي جڳھ \"$3\" تي اجازت ناھي.",
+       "slot-name-main": "مُک",
        "content-model-wikitext": "وڪي‌ٽيڪسٽ",
-       "content-model-text": "سادو ٽيڪسٽ",
-       "content-model-javascript": "جاوا اسڪرپٽ",
+       "content-model-text": "سادو متن",
+       "content-model-javascript": "جاوا-اسڪرپٽ",
+       "content-model-css": "سي.ايس.ايس",
        "content-json-empty-object": "خالي آبجيڪٽ",
        "content-json-empty-array": "خالي اري",
        "duplicate-args-warning": "چتاءُ: [[:$2]] کي [[:$1]] ڪال ڪري رهيو آهي، جنهن منجھہ ’$3‘ نيم‌پيما لاءِ هڪ کان وڌيڪ قدر ڄاڻايل آهن. فقط آخري ڄاڻايل قدر استعمال ڪيو ويندو.",
        "parser-template-loop-warning": "سانچو چڪر لڌو ويو: [[$1]]",
+       "undo-nochange": "سنوار اڳ ئي اڻڪريل ظاھر پئي ٿي.",
+       "undo-summary": "$1 [[Special:Contributions/$2|$2]] ([[User talk:$2|بحث]]) پاران ورجاءُ اڻڪريو",
+       "undo-summary-username-hidden": "ڪنھن لڪيل واپرائيندڙ پاران $1 اڻڪريو",
        "cantcreateaccount-text": "هن آءِپي پتي تان کاتي جي کولڻ تي (<strong>$1</strong>)  [[User:$3|$3]] بندش وڌل آهي.\n\n$3 جو ڄاڻايل سبب ھي <em>$2</em> آهي.",
-       "cantcreateaccount-range-text": "آءÙ\90Ù¾Ù\8a Ù¾ØªÙ\86 Ø¬Ù\8a Ø­Ø¯ <strong>$1</strong> Û¾ [[User:$3|$3]] Ú©Ø§ØªÙ\88 Ú©Ù\88Ù\84Ú» ØªÙ\8a Ø±Ù\88Úª Ù\84ڳائÙ\8a Ù\88ئÙ\8a Ø¢Ù\87Ù\8aØ\8c$4 Ø¬Ù\86Ù\87Ù\86 Û¾ ØªÙ\88Ù\87اÙ\86 Ø¬Ù\88 Ø¢Ø¡Ù\90Ù¾Ù\8a Ù¾ØªÙ\88 Ø¨Û\81 (<strong>$4</strong>)Ø\8c  Ù¾Ú» Ø´Ø§Ù\85Ù\84 Ø¢Ù\87Ù\8a. \n\n$3 Ø§Ù\86 Ø±Ù\88ÚªÙ\8e Ø¬Ù\88 Ø³Ø¨Ø¨ \"$2\" ڄاڻايو آهي.",
+       "cantcreateaccount-range-text": "آئÙ\90Ù¾Ù\8a Ù¾ØªÙ\86 Ø¬Ù\8a Ø­Ø¯ <strong>$1</strong> Û¾ [[User:$3|$3]] Ú©Ø§ØªÙ\88 Ú©Ù\88Ù\84Ú» ØªÙ\8a Ø±Ù\88Úª Ù\84ڳائÙ\8a Ù\88ئÙ\8a Ø¢Ù\87Ù\8aØ\8c$4 Ø¬Ù\86Ú¾Ù\86 Û¾ ØªÙ\88Ù\87اÙ\86 Ø¬Ù\88 Ø¢Ø¦Ù\90Ù¾Ù\8a Ù¾ØªÙ\88 Ø¨Û\81 (<strong>$4</strong>)Ø\8c Ù¾Ú» Ø´Ø§Ù\85Ù\84 Ø¢Ù\87Ù\8a. \n\n$3 Ø§Ù\86 Ø±Ù\88ÚªÙ\8e Ø¬Ù\88 Ø³Ø¨Ø¨ <em>\"$2\"<em> ڄاڻايو آهي.",
        "viewpagelogs": "هن صفحي جا لاگس ڏسو",
        "nohistory": "هن صفحي جي ڪا بہ سوانح نہ آهي.",
-       "currentrev": "هاڻوڪو مسودو",
-       "currentrev-asof": "$1 جو تازو ترين مسودو",
+       "currentrev": "تازو-ترين ورجاءُ",
+       "currentrev-asof": "تازو-ترين ترين ورجاءُ بمطابق $1",
        "revisionasof": "$1 وارو پرت",
        "revision-info": "$1 جو {{GENDER:$6|$2}}$7 جي سنوار بعد مسودو",
        "previousrevision": "←اڃا پراڻو پرت",
        "cur": "ھاڻوڪو",
        "next": "اڳيون",
        "last": "پويون",
-       "page_first": "پهريون",
+       "page_first": "پھريون",
        "page_last": "آخري",
        "history-fieldset-title": "مسودا ڇاڻيو",
        "history-show-deleted": "رڳو ڊاٺل مسودا",
-       "histfirst": "اوائلي ترين",
-       "histlast": "تازه ترين",
+       "histfirst": "اوائلي-ترين",
+       "histlast": "نئون-ترين",
        "historysize": "({{PLURAL:$1|1 بائيٽ|$1 بائيٽون}})",
        "historyempty": "خالي",
        "history-feed-title": "ورجاءُ سوانح",
        "history-feed-description": "وڪي جي هن صفحي جي ورجاءُ سوانح",
        "history-feed-item-nocomment": "$2 تي $1",
+       "history-edit-tags": "چونڊيل ورجائن جا ٽيگز سنواريو",
        "rev-deleted-comment": "(سنوار جو تَتُ ھٽايل)",
        "rev-deleted-user": "(واپرائيندڙ-نانءُ ڊاٿو ويو)",
        "rev-deleted-event": "(لاگ تفصيل هٽايا ويا)",
        "rev-suppressed-no-diff": "توهان اهو تفاوت نٿا ڏسي سگھو، ڇاڪاڻ تہ مسودن مان ڪو ھڪ <strong>ڊاھيو ويو آھي</strong>.",
        "rev-delundel": "نمائش تبديل ڪريو",
        "rev-showdeleted": "ڏيکاريو",
-       "revisiondelete": "Ù\85سÙ\88ادا ڊاهيو/اڻ‌ڊاهيو",
-       "revdelete-no-file": "ڄاڻايل فائيل وجود نہ ٿو رکي.",
-       "revdelete-show-file-submit": "ها",
+       "revisiondelete": "Ù\88رجاءÙ\8e ڊاهيو/اڻ‌ڊاهيو",
+       "revdelete-no-file": "ڄاڻايل فائيل وجود نٿو رکي.",
+       "revdelete-show-file-submit": "ھا",
        "revdelete-legend": "نمائش جون پابنديون ترتيب ڪريو",
+       "revdelete-hide-text": "ورجاءَ جو متن",
        "revdelete-hide-image": "فائيل جو مواد لڪايو",
        "revdelete-hide-name": "هدف ۽ نيمپيما لڪايو",
        "revdelete-hide-comment": "سنوار جو تتُ",
-       "revdelete-hide-user": "اÙ\8aÚ\8aÙ\8aٽر Ø¬Ù\88 Ù\88اپرائÙ\8aÙ\86دÚ\99\86اÙ\86Ø¡Ù\8f/آءÙ\90Ù¾Ù\90ي پتو",
+       "revdelete-hide-user": "سÙ\86Ù\88ارÙ\8aÙ\86دÚ\99 Ø¬Ù\88 Ù\88اپرائÙ\8aÙ\86دÚ\99\86اÙ\86Ø¡Ù\8f/آئÙ\90Ù¾ي پتو",
        "revdelete-hide-restricted": "منتظمن توڙي ٻين کان مليل اعداد دٻايو",
        "revdelete-radio-same": "(نہ بدلايو)",
        "revdelete-radio-set": "لڪل",
        "revdelete-radio-unset": "ظاهر",
        "revdelete-suppress": "منتظمن توڙي ٻين کان مليل اعداد دٻايو",
+       "revdelete-unsuppress": "ورايل ورجائن تان پابنديون ھٽايو",
        "revdelete-log": "سبب:",
+       "revdelete-submit": "چونڊيل {{PLURAL:$1|ورجاءُ|ورجاءَ}} لاڳو ڪريو",
+       "revdelete-success": "ورجاءُ ظاھريت جديدي وئي.",
+       "revdelete-failure": "ورجاءُ ظاھريت نہ جديدي سگھجي:\n$1",
+       "logdelete-success": "لاگ ظاھريت مرتب ٿي.",
+       "logdelete-failure": "لاگ ظاھريت مرتب نہ ٿي سگھجي:\n$1",
        "revdel-restore": "نمائش تبديل ڪريو",
        "pagehist": "صفحي جي سوانح",
-       "deletedhist": "Ú\8aاٺل سوانح",
+       "deletedhist": "Ú\8aاٿل سوانح",
        "revdelete-otherreason": "ٻيا/اضافي ڪارڻ:",
        "revdelete-reasonotherlist": "ٻيو ڪارڻ",
-       "revdelete-edit-reasonlist": "ڊاٺ جا سبب سنواريو",
-       "revdelete-offender": "ڀيري جو ليکڪ:",
+       "revdelete-edit-reasonlist": "ڊاھ جا سبب سنواريو",
+       "revdelete-offender": "ورجاءَ جو ليکڪ:",
        "mergehistory": "صفحن جون سوانح ضم ڪريو",
-       "mergehistory-box": "ٻن صفحن جي ڀيرن کي ضم ڪريو:",
-       "mergehistory-from": "ذريعہ صفحو:",
+       "mergehistory-box": "ٻن صفحن جي ورجائن کي ضم ڪريو:",
+       "mergehistory-from": "مصدر صفحو:",
        "mergehistory-into": "مقصود صفحو:",
        "mergehistory-list": "ضمائتي سنوار سوانح",
        "mergehistory-go": "ضم ڪرڻ جوڳيون سنوارون ڏيکاريو",
-       "mergehistory-submit": "ڀيرن کي ضم ڪريو",
-       "mergehistory-empty": "ڪي بہ ڀيرا ضم ڪري نہ ٿا سگھجن.",
+       "mergehistory-submit": "ورجاءَ ضم ڪريو",
+       "mergehistory-empty": "ڪي بہ ورجاءَ ضم نٿا ڪري سگھجن.",
+       "mergehistory-fail-bad-timestamp": "وقت-ٺپو ناقابلِڪار آھي.",
+       "mergehistory-fail-invalid-source": "ذريعو صفحو ناقابلِڪار آھي.",
+       "mergehistory-fail-invalid-dest": "منزل صفحو ناقابلِڪار آھي.",
+       "mergehistory-fail-permission": "سوانح ضم ڪرڻ لاءِ اڻپوريون اجازتون.",
+       "mergehistory-fail-self-merge": "ذريعو ۽ منزل صفحا ساڳيا آھن.",
        "mergehistory-no-source": "مصدر صفحو $1 وجود نٿو رکي.",
        "mergehistory-no-destination": "مقصود صفحو $1 وجود نہ ٿو رکي.",
        "mergehistory-invalid-source": "مصدر صفحي جو عنوان قابل‌ڪار هجڻ لازمي آهي.",
        "mergehistory-comment": "[[:$1]]، [[:$2]] ۾ ضم ٿي ويو: $3",
        "mergehistory-same-destination": "مصدر ۽ مقصود صفحا ساڳيا نٿا ٿي سگھن",
        "mergehistory-reason": "سبب:",
+       "mergehistory-revisionrow": "$1 ($2) $3 . . $4 $5 $6",
        "mergelog": "ضم لاگ",
-       "revertmerge": "اڻ ضم",
+       "revertmerge": "اڻ ضم ڪريو",
        "history-title": "\"$1\" جي ورجاءُ سوانح",
-       "difference-title": "\"$1\" Ø¬Ù\8a Ù\85سÙ\88دن ۾ تفاوت",
+       "difference-title": "\"$1\" Ø¬Ù\8a Ù\88رجائن ۾ تفاوت",
        "difference-title-multipage": "صفحن \"$1\" ۽ \"$2\" ۾ تفاوت",
-       "difference-multipage": "(صفحن درميان تفاوت)",
+       "difference-multipage": "(صفحن وچ ۾ تفاوت)",
        "lineno": "سِٽَ $1:",
-       "compareselectedversions": "چونڊيل پرت ڀيٽيو",
+       "compareselectedversions": "چونڊيل ورجاءَ ڀيٽيو",
+       "showhideselectedversions": "چونڊيل ورجائن جي ظاھريت بدلايو",
        "editundo": "اڻڪريو",
        "diff-empty": "(ڪو بہ تفاوت ڪونھي)",
-       "diff-multi-sameuser": "({{PLURAL:$1|هڪ تڪڙو مسودو|$1 تڪڙا مسودا}} ساڳي واپرائيندڙ طرفان ظهار نه ٿيندا)",
+       "diff-multi-sameuser": "({{PLURAL:$1|هڪ وچولو ورجاءُ|$1 وچولا ورجاءَ}} ساڳي واپرائيندڙ طرفان ظاھر نہ ٿيندا)",
        "searchresults": "ڳولا نتيجا",
+       "search-filter-title-prefix": "صرف انھن صفحن ۾ ڳوليندي جن جو عنوان \"$1\" سان شروع ٿي ٿو.",
+       "search-filter-title-prefix-reset": "سڀ صفحا ڳوليو",
        "searchresults-title": "”$1“ لاءِ ڳولا نتيجا",
        "titlematches": "صفحي جو عنوان مشابھت رکي ٿو",
        "textmatches": "صفحي جو متن مشابھت رکي ٿو",
        "prev-page": "اڳوڻو صفحو",
        "next-page": "اڳيون صفحو",
        "prevn-title": "{{PLURAL:$1|پويون|پويان}} $1 {{PLURAL:$1|نتيجو|نتيجا}}",
-       "nextn-title": "{{PLURAL:$1|ٻيو|ٻيا}} $1 {{PLURAL:$1|نتيجو|نتيجا}}",
+       "nextn-title": "اڳيان/اڳيان $1 {{PLURAL:$1|نتيجو|نتيجا}}",
        "shown-title": "$1 {{PLURAL:$1|نتيجو|نتيجا}} في صفحو ڏيکاريو",
        "viewprevnext": "ڏسو ($1 {{int:pipe-separator}} $2) ($3)",
        "searchmenu-exists": "<strong>ھن وڪيءَ تي \"[[:$1]]\" نالي ھڪ صفحو آھي.</strong> {{PLURAL:$2|0=|ٻيا لڌل ڳولا نتيجا پڻ ڏسو.}}",
        "search-result-size": "$1 ({{PLURAL:$2|لفظُ|$2 لفظَ}})",
        "search-result-category-size": "{{PLURAL:$1|1 رڪن|$1 رڪنَ}} ({{PLURAL:$2|1 ذيلي زمرو|$2 ذيلي زمرا}}, {{PLURAL:$3|1 فائيل|$3 فائيلَ}})",
        "search-redirect": "($1 کان چوريو)",
-       "search-section": "(سيڪشن $1)",
+       "search-section": "(ڀاڱو $1)",
        "search-category": "(زمرو $1)",
        "search-file-match": "(فائيل جي مواد سان ملي ٿو)",
        "search-suggest": "ڇا توهان جو مطلب ھيو: $1",
-       "search-rewritten": "نتيجا براءِ $1. يا $2 بابت نتيجا ڏسو.",
+       "search-rewritten": "$1 لاءِ نتيجا ڏيکاريندي. ان بجاءِ $2 ڳوليو.",
        "search-interwiki-caption": "برادر رٿائن مان نتيجا",
        "search-interwiki-default": "$1 مان نتيجا",
        "search-interwiki-more": "(وڌيڪ)",
        "powersearch-ns": "نانءُپولارن ۾ ڳوليو:",
        "powersearch-togglelabel": "چڪاسيو:",
        "powersearch-toggleall": "سڀ",
-       "powersearch-togglenone": "ڪو بہ نہ",
+       "powersearch-togglenone": "ڪوبہ نہ",
        "search-external": "خارجي ڳولا",
-       "search-error": "$1 ۾ ڳولا ڪندي چُڪَ ٿي.",
+       "search-error": "$1: ڳوليندي چُڪَ ظاھر ٿي آھي",
        "preferences": "ترجيحون",
        "mypreferences": "ترجيحون",
        "prefs-edits": "سنوارن جو انگ:",
        "prefs-email": "برقٽپال چارا",
        "prefs-rendering": "حليو",
        "saveprefs": "سانڍيو",
-       "restoreprefs": "شروعاتي ترتيبون واپس ڪيو (سمورن خانن ۾)",
+       "restoreprefs": "(سمورن خانن ۾) سڀ ڏنل ترتيبون ورايو",
        "prefs-editing": "سنوارڻ",
        "searchresultshead": "ڳولا",
        "stub-threshold-sample-link": "نمونو",
-       "stub-threshold-disabled": "غÙ\8aرÙ\81عال",
+       "stub-threshold-disabled": "اڻ-Ù\81عاÙ\8aل",
        "recentchangesdays": "تازين تبديلين ۾ ڏيکارڻ جي لاءِ ڏينهن:",
        "recentchangesdays-max": "وڌ ۾ وڌ $1 {{PLURAL:$1|ڏينهن}}",
        "recentchangescount": "تازين تبديلين، صفحن جي سوانح، ۽ لاگس ۾ ڏيکارڻ لاءِ سنوارن جو خودڪار ڏنل انگ:",
        "prefs-help-recentchangescount": "وڌ ۾ وڌ انگ: 1000",
        "savedprefs": "توھان جون ترجيحون سانڍجي چڪيون آھن.",
        "savedrights": "{{GENDER:$1|$1}} جا واپرائيندڙ گروھ سانڍجي چڪا آھن.",
-       "timezonelegend": "ٽائÙ\8aÙ\85 Ø²Ù\88Ù\86:",
+       "timezonelegend": "Ù\88Ù\82ت Ù¾Ù½Ù\88:",
        "localtime": "مقامي وقت:",
        "timezoneuseserverdefault": "وڪي عدم پيروي استعمال ڪريو ($1)",
        "timezoneuseoffset": "ٻيو (ھيٺ ڄاڻايو)",
-       "servertime": "سَروَر پٽاندر وقت:",
+       "servertime": "سَروَر جو وقت:",
        "guesstimezone": "جھانگُوءَ مان ڀريو",
        "timezoneregion-africa": "آفريڪا",
        "timezoneregion-america": "آمريڪا",
        "timezoneregion-indian": "سنڌي ساگر",
        "timezoneregion-pacific": "ماٺو ساگر",
        "allowemail": "ٻين واپرائيندڙن کي مون ڏانھن برقٽپال ڪرڻ جي اجازت ڏيو",
-       "email-allow-new-users-label": "نوان واپرائيندڙ برق ٽپال موڪلين",
-       "email-blacklist-label": "هنن واپرائندڙن کي مون ڏانهن برقٽپال ڪرڻ جي اجازت نه ڏيو:",
+       "email-allow-new-users-label": "بلڪل-نون واپرائيندڙن کان برقٽپالن جي اجازت ڏيو",
+       "email-blacklist-label": "هنن واپرائندڙن کي مون ڏانھن برقٽپال ڪرڻ کان منع ڪريو:",
        "prefs-searchoptions": "ڳولا",
        "prefs-namespaces": "نانءُپولار",
        "default": "ڏنل",
-       "prefs-files": "فائيلس",
+       "prefs-files": "فائيلَ",
        "prefs-reset-intro": "اوهان هن صفحي کي ويب-سرزمين لاءِ ڏنل ترجيحن کي ٻيھر مرتب ڪرڻ لاءِ استعمال ڪري سگھو ٿا.\nهي عمل واپس نٿو ٿي سگھي.",
        "prefs-emailconfirm-label": "برقٽپال خاطري:",
        "youremail": "برقٽپال:",
        "prefs-registration-date-time": "$1",
        "yourrealname": "اصل نالو:",
        "yourlanguage": "ٻولي:",
-       "yournick": "Ù\86ئÙ\8aÙ\86 ØµØ­Ù\8aØ­:",
-       "prefs-help-signature": "بحث صفحي تي رايا ڏيڻ وقت هن نشانين ذريعي \"<nowiki>~~~~</nowiki>\" دستخط ڪيو، جيڪي پاڻ مرادو توهان جي دستخط ۽ وقت ۾ تبديل ٿي ويندا.",
-       "badsiglength": "اها صحيح هيڪاندي ڊگھي آهي.\nاها وڌ ۾ وڌ $1 {{PLURAL:$1|اکر|اکرن}} تي ٻڌل هوڻ گھرجي.",
-       "yourgender": "توهان ڪهڙو تعارف چاهيندا؟",
+       "yournick": "Ù\86ئÙ\88Ù\86 Ø¯Ø³ØªØ®Ø·",
+       "prefs-help-signature": "بحث صفحي تي رايا ڏيڻ وقت هن نشانين ذريعي \"<nowiki>~~~~</nowiki>\" دستخط ڪيو، جيڪي پاڻمرادو توهان جي دستخط ۽ وقت ۾ تبديل ٿي ويندا.",
+       "badsiglength": "اهو درتخط هيڪاندو ڊگھو آهي.\nاها وڌ ۾ وڌ $1 {{PLURAL:$1|اکر|اکرن}} تي ٻڌل هجڻ گھرجي.",
+       "yourgender": "توھان ڪيئن بيان ٿيڻ چاھيندا؟",
        "gender-unknown": "توهان جو ذڪر ڪندي، جيترو ٿي سگھيو، منطقگري بي جنس لفظن جو استعمال ڪندي.",
-       "gender-male": "هيءُ وڪي صفحا سنواريندو آهي",
-       "gender-female": "هيءَ وڪي صفحا سنواريندي آهي",
+       "gender-male": "هي وڪي صفحا سنواريندو آهي",
+       "gender-female": "ھوءَ وڪي صفحا سنواريندي آهي",
        "prefs-help-gender": "هن ترجيح جي تربيت اختياري آهي.\nاوهان يا ٻين واپرائيندڙن جو سافٽويئر ويليو ذريعي مناسب وياڪرڻي جنس مطابق ذڪر ڪندو.\nهي معلومات عام هوندي.",
        "email": "برقٽپال",
        "prefs-help-realname": "اصل نالو اختياري آهي.\nجيڪڏهن توهان اصل نالو ڄاڻائڻ جو فيصلو ٿا ڪريو، تہ اهو توهان کي توهان جي ڪم جي مڃتا ڏيڻ لاءِ ڪم آندو ويندو.",
        "prefs-help-email": "برقٽپال ڄاڻائڻ اختياري آهي، پر جڏهن توهان ڳجھولفظ وسري ويندا آهيو، تڏهن ان جو استعمال توهان کي نئون ڳجھولفظ ڏيڻ لاءِ استعمال ڪيو ويندو آهي.",
-       "prefs-help-email-others": "اوهان چونڊ ڪري سگھو ٿا ته اوهان جي ذاتي يا بحث صفحي تي موجود ڳنڍڻي ذريعي ٻيو ڪو واپرائيندڙ اوهان کي برقٽپال ڪري سگھي ٿو يا نه.\nاوهان جو برقٽپال پتو ٻين پاران رابطي ڪرڻ وقت ڳجهو رکيو ويندو.",
+       "prefs-help-email-others": "اوهان چونڊ ڪري سگھو ٿا تہ اوهان جي ذاتي يا بحث صفحي تي موجود ڳنڍڻي ذريعي ٻيو ڪو واپرائيندڙ اوهان کي برقٽپال ڪري سگھي ٿو يا نہ.\nاوهان جو برقٽپال پتو ٻين پاران رابطي ڪرڻ وقت ڳجھو رکيو ويندو.",
        "prefs-help-email-required": "برقٽپال پتو گھربل آهي.",
        "prefs-info": "بنيادي ڄاڻ",
        "prefs-i18n": "بين‌الاقوامڪاري",
-       "prefs-signature": "صحÙ\8aØ­",
+       "prefs-signature": "دستخط",
        "prefs-dateformat": "تاريخ جو طرز",
-       "prefs-advancedediting": "عمومي چارا",
-       "prefs-editor": "اÙ\8aÚ\8aÙ\8aٽر",
+       "prefs-advancedediting": "عام چارا",
+       "prefs-editor": "سÙ\86Ù\88ارگاھ",
        "prefs-preview": "پيش نگاھ",
        "prefs-advancedrc": "متقدم چارا",
        "prefs-advancedrendering": "متقدم چارا",
        "prefs-advancedwatchlist": "متقدم چارا",
        "prefs-displayrc": "نماڪار چارا",
        "prefs-displaywatchlist": "نماڪار چارا",
+       "prefs-changesrc": "تبديليون ڏيکاريل",
+       "prefs-changeswatchlist": "تبديليون ڏيکاريل",
+       "prefs-pageswatchlist": "نظر ۾ صفحا",
        "prefs-tokenwatchlist": "ٽوڪن",
        "prefs-diffs": "تفاوت",
        "prefs-help-prefershttps": "هيءَ ترجيح توهان جي ايند داخل ٿيڻ تي عمل ۾ ايندي.",
        "userrights-editusergroup": "{{GENDER:$1|واپرائيندڙ}} گروھ سنواريو",
        "userrights-viewusergroup": "{{GENDER:$1|واپرائيندڙ}} گروھ ڏيکاريو",
        "saveusergroups": "{{GENDER:$1|واپرائيندڙ}} گروھ سانڍيو",
-       "userrights-groupsmember": "برڪن:",
+       "userrights-groupsmember": "جÙ\88 رڪن:",
        "userrights-groupsmember-auto": "رڪن واجبي:",
        "userrights-groupsmember-type": "$1",
        "userrights-reason": "سبب:",
        "userrights-no-interwiki": "توهان کي ٻين وڪين تي واپرائيندڙ حق سنوارڻ جي اجازت ناھي.",
-       "userrights-nodatabase": "اعداخانو $1 يا تہ وجود نہ ٿو رکي يا تہ اهو مقامي اعدادخانو نہ آهي.",
+       "userrights-nodatabase": "اعداخانو $1 يا تہ وجود نٿو رکي يا تہ اهو مقامي اعدادخانو نہ آهي.",
        "userrights-changeable-col": "گروپَ جيڪي توهان تبديل ڪري سگھو ٿا",
        "userrights-unchangeable-col": "گروپَ جيڪي توهان تبديل نٿا ڪري سگھو",
        "userrights-irreversible-marker": "$1*",
        "userrights-no-shorten-expiry-marker": "$1#",
+       "userrights-expiry-current": "مدو پورو $1",
        "userrights-expiry-none": "مدي خارج نٿو ٿي",
+       "userrights-expiry": "مدو پورو:",
+       "userrights-expiry-existing": "موجودہ مدو پورو ٿيڻ جو وقت: $3، $2",
        "userrights-expiry-othertime": "ٻيو وقت:",
        "userrights-expiry-options": "1 ڏينھن:1 ڏينھن،1 ھفتو:1 ھفتا،1 مھينو:1 مھينو،3 مھينا:3 مھينا،6 مھينا:6 مھينا،1 سال:1 سال",
        "group": "گروھ:",
        "group-bureaucrat": "ڪامورا",
        "group-all": "(سڀ)",
        "group-user-member": "{{GENDER:$1|واپرائيندڙ}}",
+       "group-autoconfirmed-member": "{{GENDER:$1|پاڻمرادو-پڪ-ڪيل واپرائيندڙ}}",
        "group-bot-member": "{{GENDER:$1|بوٽ}}",
        "group-sysop-member": "{{GENDER:$1|منتظم}}",
        "group-interface-admin-member": "{{GENDER:$1|منتظم براءِ حليو}}",
        "grouppage-user": "{{ns:project}}:واپرائيندڙ",
        "grouppage-autoconfirmed": "{{ns:project}}:خودڪارنموني پڪ ڪيل رڪن",
        "grouppage-bot": "{{ns:project}}:بوٽس",
-       "grouppage-sysop": "{{ns:project}}:منتظمين",
+       "grouppage-sysop": "{{ns:project}}:منتظم",
        "grouppage-interface-admin": "{{ns:project}}:منتظم براءِ حليو",
        "grouppage-bureaucrat": "{{ns:project}}:ڪامورا",
        "grouppage-suppress": "{{ns:project}}:دٻايو",
        "right-read": "صفحا پڙهو",
        "right-edit": "صفحا سنواريو",
-       "right-createpage": "صفحا سنواريو (جيڪي مباحثي صفحا نہ آهن)",
-       "right-createtalk": "مباحثي صفحا سرجيو",
+       "right-createpage": "صفحا سرجيو (جيڪي گفتگوئي صفحا نہ آهن)",
+       "right-createtalk": "گفتگوئي صفحا سرجيو",
        "right-createaccount": "نوان واپرائيندڙ کاتا کوليو",
-       "right-minoredit": "ترÙ\85Ù\8aÙ\85Ù\8fÙ\86 Ú©Ù\8a Ù\85عÙ\85Ù\8fÙ\88Ù\84Ù\8a Ú\84اڻايو",
+       "right-minoredit": "سÙ\86Ù\88ارÙ\86 Ú©Ù\8a Ù\85عÙ\85Ù\88Ù\84Ù\8a Ø·Ù\88ر Ù\86شاÙ\86 Ù\84Ú³ايو",
        "right-move": "صفحا چوريو",
-       "right-move-subpages": "Ø°Ù\8aÙ\84Ù\8a ØµÙ\81Ø­Ù\86 Ø³Ù\85Ù\8aت ØµÙ\81حا چوريو",
+       "right-move-subpages": "صÙ\81Ø­Ù\86 Ú©Ù\8a Ø³Ù\86دÙ\86 Ø°Ù\8aÙ\84Ù\8a-صÙ\81Ø­Ù\86 Ø³Ù\85Ù\8aت چوريو",
        "right-move-categorypages": "زمراتي صفحا چوريو",
        "right-movefile": "فائيل چوريو",
        "right-upload": "فائيل چاڙهيو",
-       "right-reupload": "موجوده فائيلن مٿان",
+       "right-reupload": "موجوده فائيلن مٿان-لکو",
        "right-upload_by_url": "ڪنهن يُوآرايل تان فائيل چاڙهيو",
        "right-writeapi": "ايپيآءِ لکڻ جو استعمال",
        "right-delete": "صفحا ڊاهيو",
        "right-unblockself": "ڪنهن تان بندش ختم ڪريو",
        "right-editinterface": "واپرائيندڙ باهمرُو کي سنواريو",
        "right-viewmywatchlist": "پنهنجي نظر ۾ فھرست ڏسو",
-       "right-editmywatchlist": "پنهنجي نگھداشت واري فهرست کي سنواريو. ياد رکو ڪجهه ڪم هن اختيار کان سواءِ پڻ ممڪن آهن.",
-       "right-viewmyprivateinfo": "پنهنجي ذاتي معلومات ڏسو (جيئن: برق ٽپال پتو، اصل نالو وغيره)",
-       "right-editmyprivateinfo": "پنهنجي ذاتي معلومات سنواريو (جيئن برق ٽپال، اصل نالو)",
+       "right-editmywatchlist": "پنھنجي نظر ۾ فھرست کي سنواريو. ياد رکو ڪجھ ڪم هن اختيار کان سواءِ پڻ ممڪن آهن.",
+       "right-viewmyprivateinfo": "پنھنجي ذاتي ڊيٽا ڏسو (جيئن: برقٽپال پتو، اصل نالو وغيره)",
+       "right-editmyprivateinfo": "پنھنجي ذاتي ڊيٽا سنواريو (جيئن برقٽپال، اصل نالو)",
        "right-editmyoptions": "پنهنجون ترجيحون سنواريو",
-       "right-import": "ٻين وڪيز کان صفحا درآمديو",
+       "right-import": "ٻين وڪين کان صفحا درآمديو",
        "right-importupload": "ڪو فائيل چاڙهي صفحا درآمديو",
        "right-patrol": "ٻين جون سنوارون گشت-ڪيل طور نشان لڳايو",
        "right-autopatrol": "سندس سنوارون پاڻمرادو گشت ڪيل طور نشان لڳل آھن",
-       "right-mergehistory": "صÙ\81Ø­Ù\86 Ø¬Ù\8a Ø³Ù\88اÙ\86Ø­ Ø³Ù\86Ù\88اريو",
+       "right-mergehistory": "صÙ\81Ø­Ù\86 Ø¬Ù\8a Ø³Ù\88اÙ\86Ø­ Ø¶Ù\85 Úªريو",
        "right-userrights": "واپرائيندڙ جا سڀ حق سنواريو",
        "right-userrights-interwiki": "ٻين وڪين تي واپرائيندڙن جا حق سنواريو",
        "right-siteadmin": "اعدادخانو بنديو ۽ کوليو",
        "right-managechangetags": "[[Special:Tags|ٽيگس]] سرجيو ۽ ڊاهيو.",
        "grant-group-file-interaction": "ميڊيا سان لھ وچڙ ۾ اچو",
        "grant-group-email": "برقٽپال اماڻيو",
-       "grant-group-other": "گاڏڙ ساڏڙ سرگرمي",
+       "grant-group-administration": "انتظامي عمل سرانجام ڏيو",
+       "grant-group-private-information": "اوھان بابت خانگي ڊيٽا تائين رسائي ڪريو",
+       "grant-group-other": "گاڏڙ-ساڏڙ سرگرمي",
        "grant-blockusers": "واپرائيندڙن کي بندشيو ۽ اڻبندشيو",
        "grant-createaccount": "کاتا کوليو",
        "grant-createeditmovepage": "صفحا سرجيو، سنواريو، ۽ چوريو",
+       "grant-delete": "صفحا، ورجاءَ، ۽ لاگ داخلائون ڊاھيو",
        "grant-editmywatchlist": "پنھنجي نظر ۾ فھرست سنواريو",
-       "grant-editpage": "Ù\87اڻÙ\88ÚªÙ\86 ØµÙ\81Ø­Ù\86 Ú©Ù\8a سنواريو",
+       "grant-editpage": "Ù\85Ù\88جÙ\88دÛ\81 ØµÙ\81حا سنواريو",
        "grant-editprotected": "تحفظيل صفحا سنواريو",
+       "grant-patrol": "صفحن ۾ تبديلين جو گشت ڪريو",
+       "grant-privateinfo": "خانگي معلومات تي رسائي ڪريو",
+       "grant-protect": "صفحا تحفظيو ۽ اڻتحفظيو",
        "grant-rollback": "صفحن ۾ ڪيل تبديليون واپس ورايو",
        "grant-sendemail": "ٻين واپرائيندڙن ڏانھن برقٽپال موڪليو",
-       "grant-uploadeditmovefile": "Ù\81ائÙ\8aÙ\84 Ú\86اÚ\99Ù\87Ù\8aÙ\88Ø\8c Ù\85Ù\8eٽاÙ\8aÙ\88Ø\8c Û½ Ú\8aاÙ\87يو",
+       "grant-uploadeditmovefile": "Ù\81ائÙ\8aÙ\84 Ú\86اÚ\99Ù\87Ù\8aÙ\88Ø\8c Ù\85Ù\8eٽاÙ\8aÙ\88Ø\8c Û½ Ú\86Ù\88ريو",
        "grant-uploadfile": "نئون فائيل چاڙهيو",
        "grant-basic": "بنيادي حقَ",
-       "grant-viewdeleted": "Ú\8aÙ\8eÙºÙ\8eÙ\84Ù\8e فائيلَ ۽ صفحا ڏسو",
+       "grant-viewdeleted": "Ú\8aÙ\8eÙ¿Ù\84 فائيلَ ۽ صفحا ڏسو",
        "grant-viewmywatchlist": "پنھنجي نظر ۾ فھرست ڏسو",
+       "grant-viewrestrictedlogs": "پابند-ٿيل لاگ داخلائون ڏسو",
        "newuserlogpage": "واپرائيندڙ جو سرجڻ لاگ",
+       "newuserlogpagetext": "ھي واپرائيندڙ سرجاين جو لاگ آھي.",
        "rightslog": "واپرائيندڙ حق لاگ",
        "action-read": "هي صفحو پڙهو",
        "action-edit": "هن صفحي کي سسنواريو",
        "action-minoredit": "هن سنوار کي معمولي طور نشان لڳايو",
        "action-move": "هيءَُ صفحو چوريو",
        "action-move-subpages": "هيءُ صفحو، ۽ ان جا ذيلي صفحا چوريو",
-       "action-move-categorypages": "زمرن جا صفحا چوريو",
+       "action-move-categorypages": "زمراتي صفحا چوريو",
        "action-movefile": "هيءُ فائيل چوريو",
        "action-upload": "هيءُ فائيل چاڙهيو",
        "action-delete": "هيءُ صفحو ڊاهيو",
        "action-deleterevision": "ڀيرا ڊاھيو",
+       "action-deletelogentry": "لاگ داخلائون ڊاھيو",
        "action-deletedhistory": "ڪنھن صفحي جي ڊاھ سوانح ڏسو",
-       "action-browsearchive": "ڊاٺل صفحن ۾ ڳوليو",
+       "action-deletedtext": "ڊاھيل ورجاءَ جو متن ڏسو",
+       "action-browsearchive": "ڊاٿل صفحن ۾ ڳوليو",
        "action-undelete": "صفحا اڻڊاھيو",
        "action-suppressrevision": "لڪيل ڀيرن تي نظرثاني ڪريو ۽ بحاليو",
        "action-suppressionlog": "هيءُ ذاتي لاگ ڏسو",
        "action-import": "ٻي ڪنهن وڪي کان صفحا درآمد ڪريو",
        "action-importupload": "ڪو فائيل چاڙهي صفحا درآمديو",
        "action-patrol": "ٻين جون سنوارون گشت-ڪيل طور نشان لڳايو",
+       "action-autopatrol": "پنھنجي سنوار گشت-ڪيل طور نشان لڳرايو",
        "action-unwatchedpages": "اڻ ڏٺل صفحن جي فھرست ڏسو",
        "action-mergehistory": "هن صفحي جي سوانح ضم ڪريو",
        "action-userrights": "واپرائيندڙ جا سڀ حق سنواريو",
        "action-userrights-interwiki": "ٻين وڪين جي واپرائيندڙن جا حق سنواريو",
        "action-siteadmin": "اعدادخاني کي بند ڪريو يا کوليو",
        "action-sendemail": "برقٽپال اماڻيو",
+       "action-editmyoptions": "پنھنجون ترجيحون سنواريو",
        "action-editmywatchlist": "پنھنجي نظر ۾ فھرست سنواريو",
        "action-viewmywatchlist": "پنهنجي نظر ۾ فھرست ڏسو",
        "action-viewmyprivateinfo": "پنهنجي ذاتي معلومات ڏسو",
        "action-editmyprivateinfo": "پنهنجي ذاتي معلومات سنواريو",
        "action-purge": "هن صفحي جي صفائي ڪيو",
+       "action-editprotected": "\"{{int:protect-level-sysop}}\" طور تحفظيل صفحا سنواريو",
+       "action-editsemiprotected": "\"{{int:protect-level-autoconfirmed}}\" طور تحفظيل صفحا سنواريو",
+       "action-editinterface": "واپرائيندڙ انٽرفيس سنواريو",
+       "action-editusercss": "واپرائيندڙن جا سي.ايس.ايس فائيل سنواريو",
+       "action-unblockself": "ڪنھن جي بندش ختم ڪريو",
        "nchanges": "$1 {{PLURAL:$1|تبديلي|تبديليون}}",
+       "ntimes": "$1ڀيرا",
+       "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|آخري ڦيري کان}}",
        "enhancedrc-history": "سوانح",
        "recentchanges": "تازيون تبديليون",
        "recentchanges-legend": "تازين تبديلين جا چارا",
        "recentchanges-summary": "ھن صفحي تي وڪيءَ ۾ ڪيل تازيون ترين سنوارون ڏيکاريو.",
        "recentchanges-noresult": "ڏنل عرصي ۾ ڪي بہ تبديليون ھنن ڪسوٽين سان نٿيون ملن.",
-       "recentchanges-feed-description": "ۡهن روان رسد ۾ آيل تازيون تبديليون لهو",
+       "recentchanges-feed-description": "هن روان رسد ۾ آيل تازيون تبديليون لھو.",
        "recentchanges-label-newpage": "هن سنوار ھڪ نئون صفحو سرجيو",
        "recentchanges-label-minor": "ھيءَ ھڪ معمولي سنوار آھي",
        "recentchanges-label-bot": "ھيءَ سنوار بوٽ عمل ۾ آندي",
        "recentchanges-legend-heading": "<strong>ڪنجي:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (پڻ ڏسو [[Special:NewPages|نون صفحن جي فھرست]])",
        "recentchanges-submit": "ڏيکاريو",
+       "rcfilters-tag-remove": "'$1' ھٽايو",
        "rcfilters-legend-heading": "<strong>مخففن جي فھرست:</strong>",
        "rcfilters-other-review-tools": "نظرثانيءَ جا ٻيا اوزار",
        "rcfilters-group-results-by-page": "صفحي جي لحاظ سان گروھي نتيجا",
        "rcfilters-activefilters": "سرگرم ڇاڻيون",
        "rcfilters-activefilters-hide": "لڪايو",
        "rcfilters-activefilters-show": "ڏيکاريو",
+       "rcfilters-activefilters-hide-tooltip": "سرگرم ڇاڻين جي ايراضي لڪايو",
+       "rcfilters-activefilters-show-tooltip": "سرگرم ڇاڻين جي ايراضي ڏيکاريو",
        "rcfilters-advancedfilters": "متقدم ڇاڻيون",
        "rcfilters-limit-title": "ڏيکارڻ لاءِ نتيجا",
        "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|تبديلي|$1 تبديليون}}، $2",
        "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": "اڃان ڪا به ڇاڻي سانڍيل ناهي",
+       "rcfilters-quickfilters-placeholder-title": "اڃا ڪابہ ڇاڻي سانڍيل ناهي",
        "rcfilters-savedqueries-defaultlabel": "سانڍيل ڇاڻيون",
        "rcfilters-savedqueries-rename": "ٻيھر نالو ڏيو",
-       "rcfilters-savedqueries-setdefault": "Ú\8aÙ\8aÙ\81اÙ\84Ù½ Ø¬Ù\8a Ø·Ù\88ر ØªÙ\8a Ú\8fÙ\8aکارÙ\8aو",
+       "rcfilters-savedqueries-setdefault": "Ú\8fÙ\86Ù\84 Ø·Ù\88ر ØªÙ\8a Ø±Ú©و",
        "rcfilters-savedqueries-remove": "ڊاھيو",
        "rcfilters-savedqueries-new-name-label": "نالو",
        "rcfilters-savedqueries-apply-label": "ڇاڻي سرجيو",
        "rcfilters-savedqueries-cancel-label": "رد",
        "rcfilters-savedqueries-add-new-title": "ھاڻوڪيون ڇاڻين جون ترتيبون سانڍيو",
        "rcfilters-restore-default-filters": "ڏنل ڇاڻيون ريسٽور ڪريو",
-       "rcfilters-clear-all-filters": "سڀئي لڳل ڇاڻيو هٽايو",
+       "rcfilters-clear-all-filters": "سڀئي لڳل ڇاڻيون هٽايو",
        "rcfilters-show-new-changes": "$1 کان نيون تبديليون ڏسو",
        "rcfilters-search-placeholder": "تبديليون ڇاڻيو (مينيو استعمال ڪريو يا ڇاڻيءَ جي ڳولا ڪريو)",
        "rcfilters-invalid-filter": "ناقابلِڪار ڇاڻي",
        "rcfilters-filter-editsbyself-label": "مون پاران تبديليون",
        "rcfilters-filter-editsbyself-description": "توھان جون پنھنجون ڀاڱيداريون.",
        "rcfilters-filter-editsbyother-label": "ٻين پاران تبديليون",
-       "rcfilters-filtergroup-user-experience-level": "Ù\88اپرائÙ\8aÙ\86دÚ\99Ù\86 Ø¬Ù\8a Ø¯Ø§Ø®Ù\84ا ۽ تجربو",
+       "rcfilters-filtergroup-user-experience-level": "Ù\88اپرائÙ\8aÙ\86دÚ\99Ù\86 Ø¬Ù\8a Ø±Ø¬Ø³Ù½Ø±Ù\8aØ´Ù\86 ۽ تجربو",
        "rcfilters-filter-user-experience-level-registered-label": "رجسٽر ٿيل",
-       "rcfilters-filter-user-experience-level-registered-description": "داخÙ\84 Ù¿Ù\8aÙ\84 Ø§Ù\8aÚ\8aÙ\8aٽر.",
+       "rcfilters-filter-user-experience-level-registered-description": "داخÙ\84 Ù¿Ù\8aÙ\84 Ø³Ù\86Ù\88ارÙ\8aÙ\86دÚ\99.",
        "rcfilters-filter-user-experience-level-unregistered-label": "اڻرجسٽر ٿيل",
        "rcfilters-filter-user-experience-level-unregistered-description": "سنواريندڙ جيڪي داخل ٿيل ناھن.",
        "rcfilters-filter-user-experience-level-newcomer-label": "نوان ايندڙ",
        "rcfilters-filter-minor-label": "معمولي سنوارون",
        "rcfilters-filter-major-label": "غير-معمولي سنوارون",
        "rcfilters-filter-major-description": "معمولي طور نشان نہ لڳل سنوارون.",
+       "rcfilters-filtergroup-watchlist": "نظر ۾ فھرستيل صفحا",
        "rcfilters-filter-watchlist-watched-label": "نظر ۾ فھڙست تي",
        "rcfilters-filter-watchlist-watched-description": "توھان جي نظر ۾ فھرست ۾ صفحن ۾ تبديليون.",
        "rcfilters-filter-watchlist-watchednew-label": "نيون نظر ۾ فھرست ۾ تبديليون",
        "rcfilters-filter-newpages-description": "نوان صفحا ٺاھيندڙ سنوارون.",
        "rcfilters-filter-categorization-label": "زمري ۾ تبديليون",
        "rcfilters-filter-logactions-label": "لاگڊ عمل",
+       "rcfilters-filtergroup-lastrevision": "تازا-ترين ورجاءَ",
+       "rcfilters-filter-lastrevision-label": "تازو-ترين ورجاءُ",
+       "rcfilters-filter-lastrevision-description": "ڪنھن صفحي ۾ صرف تازي ترين تبديلي.",
+       "rcfilters-filter-previousrevision-label": "تازو-ترين ورجاءُ نہ",
+       "rcfilters-filter-previousrevision-description": "سڀ تبديليون جيڪي \"تازو-ترين ورجاءُ\" ناھن.",
+       "rcfilters-tag-prefix-namespace-inverted": "<strong>:نہ</strong> $1",
        "rcfilters-view-tags": "ٽيگ-ٿيل سنوارون",
        "rcfilters-liveupdates-button": "سڌي-سنئين تجديد",
+       "rcfilters-liveupdates-button-title-on": "سڌيون-سنيون جدتون بند ڪريو",
+       "rcfilters-liveupdates-button-title-off": "نئون تبديليون جيئن ئي ٿين ڏيکاريو",
+       "rcfilters-watchlist-markseen-button": "سڀ تبديلين کي ڏٺل طور نشان لڳايو",
+       "rcfilters-watchlist-edit-watchlist-button": "پنھنجي نظر ۾ صفحن جي فھرست سنواريو",
+       "rcfilters-alldiscussions-label": "سڀ گفتگوئون",
        "rcnotefrom": "هيٺ {{PLURAL:$5|تبديلي آهي|تبديليون آهن}} کان <strong>$3, $4</strong> (تائين <strong>$1</strong> ) ڏيکاريل آهن.",
+       "rclistfromreset": "تاريخ چونڊڻ ٻيھر مرتب ڪريو",
        "rclistfrom": "$2، $3 کان شروع ٿيندڙ نيون تبديليون ڏيکاريو",
        "rcshowhideminor": "$1 معمولي سنوارون",
        "rcshowhideminor-show": "ڏيکاريو",
        "newpageletter": "نئون",
        "boteditletter": "گ",
        "unpatrolledletter": "!",
+       "rc-change-size": "$1",
        "rc-change-size-new": "$1 {{PLURAL:$1|بائيٽ|بائيٽون}} تبديليءَ کانپوءِ",
-       "newsectionsummary": "/* $1 */ نئون سيڪشن",
+       "newsectionsummary": "/* $1 */ نئون ڀاڱو",
        "rc-enhanced-expand": "تفصيل ڏيکاريو",
        "rc-enhanced-hide": "تفصيل لڪايو",
        "rc-old-title": "اصل ۾ \"$1\" طور سرجيل",
        "recentchangeslinked-summary": "تبديليون ڏسڻ لاءِ صفحي جو نالو هڻو پوءِ اها هن صفحي تي هجن يا ڳنڍيل صفحي تي. (زمري جارُڪن ڏسڻ لاءِ، {{ns:زمرو}}:زمري جو نالو)هڻو. [[Special:Watchlist|your Watchlist]] صفحي تي تبديليون <strong>bold</strong> ۾ آهن.",
        "recentchangeslinked-page": "صفحي جو نالو:",
        "recentchangeslinked-to": "رڳو ڄاڻايل صفحي سان ڳانڍيل صفحن ۾ ٿيل تبديليون نمايو",
+       "recentchanges-page-added-to-category": "[[:$1]] کي زمري ۾ وڌو ويو",
+       "recentchanges-page-removed-from-category": "[[:$1]] کي زمري مان ھٽايو ويو",
        "upload": "فائيل چاڙھيو",
        "uploadbtn": "فائيل چاڙهيو",
        "uploadnologin": "داخل ٿيل ناھيو",
        "uploadnologintext": "فائيل چاڙهڻ لاءِ $1.",
        "uploaderror": "چاڙھ چُڪَ",
-       "uploadtext": "Ù\81ائÙ\84 Ú\86اÚ\99Ù\87Ú» Ù\84اءÙ\90 Ù\87Ù\8aÙºÙ\8aÙ\88Ù\86 Ù\81ارÙ\85 Ø§Ø³ØªØ¹Ù\85اÙ\84 ÚªÙ\8aÙ\88.\nپراڻا Ú\86اÚ\99Ù\87Ù\8aÙ\84 Ù\81ائÙ\84 Ú\8fسڻ Ù\8aا Ú³Ù\88Ù\84Ú» Ù\84اءÙ\90 [[Special:FileList|Ú\86اÚ\99Ù\87Ù\8aÙ\84 Ù\81ائÙ\84Ù\86 Ø¬Ù\8a Ù\81Ù\87رست]] ØªÙ\8a Ù\88Ú\83Ù\88Ø\8c Ù»Ù\87Ù\8aر Ú\86اÚ\99Ù\87Ù\8aÙ\84 Ù\81ائÙ\84 [[Special:Log/upload|Ú\86اÚ\99Ù\87Ù\8aÙ\84 Ù\84اگ]] Û½ Ø®ØªÙ\85 ÚªÙ\8aÙ\84 [[Special:Log/delete|Ú\8aاٺ Ù\84اگ]] ØªÙ\8a Ú\8fسÙ\8a Ø³Ú¯Ú¾Ø¬Ù\86 Ù¿Ø§.\n\nÙ\81ائÙ\84 Ø¬Ù\8a Ø§Ø³ØªØ¹Ù\85اÙ\84 Ù\84اءÙ\90 Ù\87Ù\8aÙº Ú\8fÙ\8aکارÙ\8aÙ\84 Ø·Ø±Ù\8aÙ\82Ù\88 Ø§Ø³ØªØ¹Ù\85اÙ\84 ÚªØ±Ù\8a Ø³Ú¯Ú¾Ø¬Ù\8a Ù¿Ù\88:\n* <strong><code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:Ù\81ائÙ\84 Ø¬Ù\88 Ù\86اÙ\84Ù\88.jpg]]</nowiki></code></strong> Ù\81ائÙ\84 Ø¬Ù\8a Ù\85ÚªÙ\85Ù\84 Ø§Ø³ØªØ¹Ù\85اÙ\84 Ù\84اءÙ\90\n* <strong><code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:Ù\81ائل جو نالو.png|200px|thumb|left|متبادل اکر]]</nowiki></code></strong> هن جي مدد سان تصوير جي سائيز ڏئي سگھجي ٿي جيئن 200 پگزل\n* <strong><code><nowiki>[[</nowiki>{{ns:media}}<nowiki>:File.ogg]]</nowiki></code></strong> فائل کي ڏيکارڻ کان بغير شامل ڪرڻ",
+       "uploadtext": "Ù\81ائÙ\8aÙ\84 Ú\86اÚ\99Ù\87Ú» Ù\84اءÙ\90 Ù\87Ù\8aÙºÙ\8aÙ\88Ù\86 Ù\81ارÙ\85 Ø§Ø³ØªØ¹Ù\85اÙ\84 ÚªÙ\8aÙ\88.\nپراڻا Ú\86اÚ\99Ù\87Ù\8aÙ\84 Ù\81ائÙ\8aÙ\84 Ú\8fسڻ Ù\8aا Ú³Ù\88Ù\84Ú» Ù\84اءÙ\90 [[Special:FileList|Ú\86اÚ\99Ù\87Ù\8aÙ\84 Ù\81ائÙ\8aÙ\84Ù\86 Ø¬Ù\8a Ù\81ھرست]] ØªÙ\8a Ù\88Ú\83Ù\88Ø\8c Ù»Ù\8aھر Ú\86اÚ\99Ù\87Ù\8aÙ\84 Ù\81ائÙ\8aÙ\84 [[Special:Log/upload|Ú\86اÚ\99Ù\87Ù\8aÙ\84 Ù\84اگ]] Û½ Ø®ØªÙ\85 ÚªÙ\8aÙ\84 [[Special:Log/delete|Ú\8aاٺ Ù\84اگ]] ØªÙ\8a Ú\8fسÙ\8a Ø³Ú¯Ú¾Ø¬Ù\86 Ù¿Ø§.\n\nÙ\81ائÙ\8aÙ\84 Ø¬Ù\8a Ø§Ø³ØªØ¹Ù\85اÙ\84 Ù\84اءÙ\90 Ù\87Ù\8aÙº Ú\8fÙ\8aکارÙ\8aÙ\84 Ø·Ø±Ù\8aÙ\82Ù\88 Ø§Ø³ØªØ¹Ù\85اÙ\84 ÚªØ±Ù\8a Ø³Ú¯Ú¾Ø¬Ù\8a Ù¿Ù\88:\n* <strong><code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:Ù\81ائÙ\84 Ø¬Ù\88 Ù\86اÙ\84Ù\88.jpg]]</nowiki></code></strong> Ù\81ائÙ\8aÙ\84 Ø¬Ù\8a Ù\85ÚªÙ\85Ù\84 Ø§Ø³ØªØ¹Ù\85اÙ\84 Ù\84اءÙ\90\n* <strong><code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:Ù\81ائÙ\8aل جو نالو.png|200px|thumb|left|متبادل اکر]]</nowiki></code></strong> هن جي مدد سان تصوير جي سائيز ڏئي سگھجي ٿي جيئن 200 پگزل\n* <strong><code><nowiki>[[</nowiki>{{ns:media}}<nowiki>:File.ogg]]</nowiki></code></strong> فائل کي ڏيکارڻ کان بغير شامل ڪرڻ",
        "uploadlogpage": "چاڙھ لاگ",
-       "filename": "فائيل نانءُ",
+       "filename": "فائيل-نانءُ",
        "filedesc": "تَتُ",
        "fileuploadsummary": "تَتُ:",
        "filereuploadsummary": "فائيل تبديليون:",
        "filesource": "ذريعو:",
        "ignorewarnings": "چتائن کي نظرانداز ڪريو",
-       "badfilename": "فائيل‌نانءُ بدلائي \"$1\" رکيو ويو آهي.",
+       "badfilename": "فائيل‌-نانءُ بدلائي \"$1\" رکيو ويو آهي.",
        "empty-file": "توهان جو جمع ڪرايل فائيل خالي آهي.",
-       "filename-tooshort": "فائيل نانءَُ هيڪاندو ننڍو آهي.",
+       "filename-tooshort": "فائيل-نانءُ هيڪاندو ننڍو آهي.",
        "filetype-banned": "فائيل جو هيءُ قسم بندشيل آهي.",
-       "verification-error": "Ù\87Ù\86 Ù\81ائÙ\8aÙ\84 Ø¬Ù\8a ØªØµØ¯Ù\8aÙ\82 Ù¿Ù\8a Ù\86Û\81 سگھي.",
-       "illegal-filename": "اهو فائيل‌نانءُ ناقابل قبول آهي.",
-       "unknown-error": "ڪا اڻجاتل چُڪَ ٿي.",
+       "verification-error": "Ù\87Ù\86 Ù\81ائÙ\8aÙ\84 ØªØµØ¯Ù\8aÙ\82 Ù¾Ø§Ø³ Ù\86Û\81 ÚªØ±Ù\8a سگھي.",
+       "illegal-filename": "اھو فائيل‌-نانءُ ڪار ناھي آهي.",
+       "unknown-error": "ڪا اڻڄاتل چُڪَ پيش آئي.",
        "tmp-create-error": "عارضي فائيل سرجي نہ سگھيو.",
        "uploadwarning": "چاڙھ جو چتاءُ",
        "savefile": "فائيل سانڍيو",
        "uploaddisabled": "چاڙھ ناقابلِ ڪار بڻيل.",
-       "uploaddisabledtext": "فائيل چاڙهڻ بند ڪيل آهن.",
+       "uploaddisabledtext": "فائيل چاڙهڻ بند ناقابلِڪار بڻيل آهن.",
        "upload-scripted-pi-callback": "ن فائيل کي اپلوڊ نه ٿو ڪري سگهي جنهن ۾ ايڪس ايم ايل اسٽائيل شيٽ جون پراسيسنگ هدايتون شامل هجن.",
-       "uploaded-script-svg": "اسڪرپٽ جوڳو ايليمينٽ ”$1” مليو آهي، اپلوڊ ٿيل ايس وي جي فائيل ۾.",
-       "uploaded-hostile-svg": "اپلوڊ ٿيل ايس وي جي فائيل جو غير محفوظ سي ايس ايس ۾ اسٽائيل ايلمينٽ مليو",
+       "uploaded-script-svg": "چاڙھيل ايس.وي.جي فائيل ۾ اسڪرپٽ-جوڳو ايليمينٽ ”$1” مليو آهي.",
+       "uploaded-hostile-svg": "چاڙھيل ايس.وي.جي فائيل جو غير محفوظ سي.ايس.ايس اسٽائيل ايلمينٽ ۾ مليو.",
        "uploaded-event-handler-on-svg": "ايس وي جي فائيل ۾ ايوينٽ هينڊلر خصوصيتون <code>$1=\"$2\"</code> مقرر ڪرڻ جي اجازت نہ آهي.",
        "uploaded-href-unsafe-target-svg": "href جو غير محفوظ ڊيٽا: يوآرآءِ نشانو مليو آهي <code>&lt;$1 $2=\"$3\"&gt;</code> چاڙھيل اَيسوِيجِي فائيل ۾",
-       "uploaded-animate-svg": "”اينيميٽ“ ٽيگ ڳوليو  جيڪا ٿي سگهي ٿو href کي تبديل ڪري رهي هجي. \"form\" وصف استعمال ڪندي <code>&lt;$1 $2=\"$3\"&gt;</code> اپلوڊ ٿيل ايس وي جي فائيل ۾",
-       "uploaded-setting-event-handler-svg": "Ù\88اÙ\82عÙ\8a Ú©Ù\8a Ù\87Ù\8aÙ\86Ú\8aÙ\84 ÚªÙ\86دÚ\99 Ø¬Ù\8a Ø³Ù\8aÙ½Ù\86Ú¯ Ø¬Ù\88Ù\86 Ù\88صÙ\81Ù\88Ù\86 Ø¨Ù\84اڪ Ù¿Ù\8aÙ\84 Ø¢Ù\87Ù\86. \n<code>&lt;$1 $2=\"$3\"&gt;</code> Ø§Ù¾Ù\84Ù\88Ú\8a Ù¿Ù\8aÙ\84 Ø§Ù\8aس Ù\88Ù\8a جي فائيل ۾ مليو",
-       "uploaded-setting-href-svg": "\"set\"  Ù½Ù\8aÚ¯ Ú©Ù\8a \"href\" Ù\88صÙ\81 Ø§Ø³ØªØ¹Ù\85اÙ\84 ÚªÙ\86دÙ\8a Ø¨Ù\86Ù\8aادÙ\8a Ø¹Ù\86صر Ú©Ù\8a Ø¨Ù\84اڪ ÚªÙ\8aÙ\88 Ù\88Ù\8aÙ\88 Ø¢Ù\87Ù\8a",
-       "uploaded-wrong-setting-svg": "\"set\" ٽيگ کي استعمال ڪندي رموٽ/ڊيٽا/اسڪرپٽ ٽارگيٽ کي ڪنهن وصف سان جوڙڻ کي بلاڪ ڪيو ويو آهي. \n<code>&lt;set to=\"$1\"&gt;</code>اپلوڊ ٿيل ايس وي جي فائيل ۾ مليو آهي.",
-       "uploaded-setting-handler-svg": "اÙ\87Ù\8a Ø§Ù\8aس Ù\88Ù\8a Ø¬Ù\8a Ø¬Ù\8aÚªÙ\8a â\80\9dÙ\87Ù\8aÙ\86Ú\8aÙ\84 ÚªÙ\86دÚ\99â\80\9c Ù\88صÙ\81Ù\86 Ú©Ù\8a Ø±Ù\85Ù\88Ù½/Ú\8aÙ\8aٽا/اسڪرپٽ Ú©Ù\8a Ø³Ù\8aÙ½ Ù¿Ø§ ÚªÙ\86Ø\8c Ú©Ù\8a Ø¨Ù\84اڪ ÚªÙ\8aÙ\88 Ù\88Ù\8aÙ\88 Ø¢Ù\87Ù\8a.<code>$1=\"$2\"</code> Ù\85Ù\84Ù\8aÙ\88 Ø¢Ù\87Ù\8a Ø§Ù¾Ù\84Ù\88Ú\8a Ù¿Ù\8aÙ\84 Ø§Ù\8aس Ù\88Ù\8a Ø¬Ù\8a Ù\81ائÙ\8aÙ\84 Û¾.",
-       "uploaded-remote-url-svg": "ايس وي جي جيڪا سيٽ ڪري ٿي ڪنهن اسٽائيل وصف  رموٽ يو آر ايل سان  بلاڪ ٿيل آهي.\n <code>$1=\"$2\"</code> اپلوڊ ٿيل ايس وي جي فائيل ۾ مليو",
-       "uploaded-image-filter-svg": "هن يو آر ايل سان <code>&lt;$1 $2=\"$3\"&gt;</code> اميج فلٽر مليو آهي، اپلوڊ ٿيل ايس وي جي فائيل ۾،",
+       "uploaded-animate-svg": "”اينيميٽ“ ٽيگ ڳوليو جيڪو ٿي سگھي ٿو href کي تبديل ڪري رهي هجي، چاڙھيل ايس.وي.جي فائيل ۾ \"form\" وصف استعمال ڪندي <code>&lt;$1 $2=\"$3\"&gt;</code>",
+       "uploaded-setting-event-handler-svg": "Ù\85Ù\88Ù\82عÙ\88-سÙ\86Ú\80اÙ\84Ù\8aÙ\86دÚ\99 Ø¬Ø§ Ø§Ù\86تساب ØªØ±ØªÙ\8aبڻ Ø¨Ù\86دشÙ\8aÙ\84 Ø¢Ù\87Ù\86Ø\8c <code>&lt;$1 $2=\"$3\"&gt;</code> Ú\86اÚ\99Ú¾Ù\8aÙ\84 Ø§Ù\8aس.Ù\88Ù\8a.جي فائيل ۾ مليو",
+       "uploaded-setting-href-svg": "\"set\"  Ù½Ù\8aÚ¯ Ú©Ù\8a \"href\" Ù\88صÙ\81 Ø§Ø³ØªØ¹Ù\85اÙ\84 ÚªÙ\86دÙ\8a Ø¨Ù\86Ù\8aادÙ\8a Ø¹Ù\86صر Ú©Ù\8a Ø¨Ù\86دشÙ\8aÙ\88 Ù\88Ù\8aÙ\88 Ø¢Ú¾Ù\8a.",
+       "uploaded-wrong-setting-svg": "\"set\" ٽيگ کي استعمال ڪندي رموٽ/ڊيٽا/اسڪرپٽ ٽارگيٽ کي ڪنھڻ وصف سان جوڙڻ کي بلاڪ ڪيو ويو آهي. \n<code>&lt;set to=\"$1\"&gt;</code> چاڙھيل ايس.وي.جي فائيل ۾ مليو آهي.",
+       "uploaded-setting-handler-svg": "اÙ\8aس.Ù\88Ù\8a.جÙ\8a Ø¬Ù\8aÚªÙ\8a \"سÙ\86Ú\80اÙ\84Ù\8aÙ\86دÚ\99\" Ù\88صÙ\81Ù\86 Ú©Ù\8a Ø±Ù\85Ù\88Ù½/Ú\8aÙ\8aٽا/اسڪرپٽ Ú©Ù\8a Ø³Ù\8aÙ½ ÚªØ±Ù\8a Ù¿Ù\88Ø\8c Ú©Ù\8a Ø¨Ù\84اڪ ÚªÙ\8aÙ\88 Ù\88Ù\8aÙ\88 Ø¢Ù\87Ù\8a.<code>$1=\"$2\"</code> Ú\86اÚ\99Ú¾Ù\8aÙ\84 Ø§Ù\8aس.Ù\88Ù\8a.جÙ\8a Ù\81ائÙ\8aÙ\84 Û¾ Ù\85Ù\84Ù\8aÙ\88 Ø¢Ú¾Ù\8a.",
+       "uploaded-remote-url-svg": "ايس.وي.جي جيڪو سيٽ ڪري ٿو ڪنهن اسٽائيل وصف رموٽ يوآرايل سان بندشيل آهي. <code>$1=\"$2\"</code> چاڙھيل ايس.وي.جي فائيل ۾ مليو.",
+       "uploaded-image-filter-svg": "چاڙھيل ايس.وي.جي فائيل ۾ يوآرايل:<code>&lt;$1 $2=\"$3\"&gt;</code> سان عڪس ڇاڻي ملي آهي.",
        "uploadvirus": "هن فائيل ۾ وائرس آهي! \nتفصيل: $1",
        "upload-source": "ذريعي جو فائيل",
        "sourcefilename": "ذريعي جي فائيل جو نالو:",
        "upload-options": "چاڙھ جا چارا",
        "watchthisupload": "هيءُ فائيل نظر ۾ رکو",
        "upload-file-error": "اندروني چُڪَ",
-       "upload-misc-error": "چارهڻ مهل اَڻڄاتل چُڪ ٿي آهي",
-       "upload-http-error": "ايڇ ٽي ٽي پي جي چُڪَ ٿي آهي: $1",
+       "upload-misc-error": "چاڙھ جي اَڻڄاتل چُڪَ",
+       "upload-http-error": "ڪا ايڇ.ٽي.ٽي.پي چُڪَ پيش آئي آهي: $1",
        "upload-dialog-title": "فائيل چاڙهيو",
        "upload-dialog-button-cancel": "رد",
        "upload-dialog-button-back": "واپس",
        "upload-form-label-infoform-description": "تشريح",
        "upload-form-label-usage-title": "استعمال",
        "upload-form-label-usage-filename": "فائيل نانءُ",
-       "upload-form-label-own-work": "هيءُ منهنجو پنهنجو ڪم آهي.",
+       "upload-form-label-own-work": "هيءُ منھنجو پنھنجو ڪم آهي.",
        "upload-form-label-infoform-categories": "زمرا",
        "upload-form-label-infoform-date": "تاريخ",
        "backend-fail-notexists": "فائيل ''$1'' وجود نٿو رکي.",
        "img-auth-accessdenied": "دسترس کان جواب",
        "license": "لائيسنسڪاري:",
        "license-header": "لائيسنسڪاري",
-       "nolicense": "Ú\86Ù\88Ù\86Ú\8a Ø§Ú»Ù\85Ù\88جÙ\88د",
+       "nolicense": "ÚªÙ\88بÛ\81 Ù\86Û\81 Ú\86Ù\88Ù\86Ú\8aÙ\8aÙ\84",
        "listfiles-delete": "ڊاهيو",
        "imgfile": "فائيل",
-       "listfiles": "فائيل فهرست",
+       "listfiles": "فائيل فھرست",
        "listfiles_thumb": "ٽِڪِلِي",
        "listfiles_date": "تاريخ",
        "listfiles_name": "نالو",
        "filehist-datetime": "تاريخ/وقت",
        "filehist-thumb": "آڱوٺي ننھن",
        "filehist-thumbtext": "$1 جي نظرثاني لاءِ تصويري نشان",
-       "filehist-nothumb": "ٽِڪِلِي اڻموجود",
+       "filehist-nothumb": "ٽِڪِلِي ڪونھي",
        "filehist-user": "واپرائيندڙ",
        "filehist-dimensions": "ماپَ",
        "filehist-filesize": "فائيل ماپ",
        "imagelinks": "فائيل جو استعمال",
        "linkstoimage": "ھن فائيل کي {{PLURAL:$1|ھيٺيون صفحو استعمال ڪري ٿو|$1 ھيٺيان صفحا استعمال ڪن ٿا}}:",
        "nolinkstoimage": "ڪي بہ صفحا ناھن جيڪي ھن فائيل کي استعمال ڪندا ھجن.",
+       "linkstoimage-redirect": "$1 (فائيل چورڻو) $2",
        "sharedupload": "هيءَ فائيل $1 کان آهي ۽ ان کي ٻيون رٿائون به استعمال ڪري سگھن ٿيون.",
        "sharedupload-desc-here": "ھي فائيل $1 مان آھي ۽ ٻين رٿائن پاران پڻ استعمال ٿي سگھي ٿو. تشريح انجي [[$2 جو تشريحي صفحو]] ھيٺان ڏنل آھي.",
        "filepage-nofile": "ھن نالي سان ڪوبہ  فائيل وجود نٿو رکي.",
-       "uploadnewversion-linktext": "Ù\87Ù\86 Ù\81ائÙ\8aÙ\84 Ø¬Ù\88 Ù\86ئÙ\88Ù\86 Ù¾Ø±Øª چاڙهيو",
+       "uploadnewversion-linktext": "Ù\87Ù\86 Ù\81ائÙ\8aÙ\84 Ø¬Ù\88 Ù\86ئÙ\88Ù\86 Ù\88رجاءÙ\8f چاڙهيو",
        "shared-repo-from": "$1 کان",
        "shared-repo-name-wikimediacommons": "وڪيميڊيا ڪامنز",
        "upload-disallowed-here": "توھان ھن فائيل مٿان لکي نہ ٿا سگھو.",
        "filerevert-comment": "سبب:",
-       "filerevert-submit": "Ù\88اپس Ù\88راÙ\8aÙ\88",
+       "filerevert-submit": "ورايو",
        "filedelete": "$1 کي ڊاهيو",
        "filedelete-legend": "فائيل ڊاهيو",
        "filedelete-comment": "سبب:",
        "filedelete-submit": "ڊاهيو",
        "filedelete-reason-otherlist": "ٻيو سبب",
-       "filedelete-edit-reasonlist": "ڊاٺ جا سبب سنواريو",
-       "filedelete-maintenance-title": "فائيل ڊهي نہ سگھيو",
+       "filedelete-edit-reasonlist": "ڊاھ جا سبب سنواريو",
+       "filedelete-maintenance-title": "فائيل ڊاھجي نہ سگھيو",
        "mimesearch": "مائيم ڳولا",
        "download": "اتاريو",
        "unwatchedpages": "اڻ ڏٺل صفحا",
-       "listredirects": "چورڻن جي فهرست",
-       "unusedtemplates": "اڻ استعماليل سانچا",
+       "listredirects": "چورڻن جي فھرست",
+       "unusedtemplates": "اڻ-استعماليل سانچا",
        "unusedtemplateswlh": "ٻيا ڳنڍڻا",
        "randompage": "بلاترتيب صفحو",
        "randomincategory": "زمري مان ڪو بلاترتيب صفحو",
        "randomincategory-category": "زمرو:",
        "randomincategory-legend": "زمري مان ڪو بلاترتيب صفحو",
        "randomincategory-submit": "هلو",
-       "randomredirect": "بلا ترتيب چورڻو",
-       "statistics": "انگ اکر",
+       "randomredirect": "بلاترتيب چورڻو",
+       "statistics": "انگ-اکر",
        "statistics-header-pages": "صفحي انگ اکر",
        "statistics-header-edits": "سنوار جا انگ-اکر",
        "statistics-header-users": "واپرائيندڙن جا انگ اکر",
-       "statistics-header-hooks": "ٻيا انگ اکر",
+       "statistics-header-hooks": "ٻيا انگ-اکر",
        "statistics-articles": "موادي صفحا",
        "statistics-pages": "صفحا",
        "statistics-pages-desc": "وڪيءَ ۾ سڀ صفحا ٻشمول بحث صفحا، ڇوريل، وغيره.",
        "pageswithprop-prop": "خصوصيت نانءُ:",
        "pageswithprop-submit": "ھلو",
        "doubleredirects": "ٻٽا چورڻا",
-       "double-redirect-fixed-move": "[[$1]] چورجي چڪو آهي. ان کي خودڪاراً تجديديو ويو ۽ هاڻي اهو [[$2]] ڏانهن وٺي وڃي ٿو.",
+       "double-redirect-fixed-move": "[[$1]] چورجي چڪو آهي.\nان کي خودڪاراً تجديديو ويو ۽ هاڻي اهو [[$2]] ڏانھن وٺي وڃي ٿو.",
        "double-redirect-fixer": "ريڊائرڪٽ فڪس-ڪندڙ",
        "brokenredirects": "ٽٽل چورڻا",
        "brokenredirects-edit": "سنواريو",
        "brokenredirects-delete": "ڊاهيو",
-       "withoutinterwiki": "ڪنهن بہ ٻي ٻوليءَ سان نہ ڳنڍيل صفحا",
-       "withoutinterwiki-summary": "هيٺيان صفحا ڪنهن بہ ٻي ٻوليءَ ۾ ساڳي صفحي سان ڳنڍيل نہ آهن.",
+       "withoutinterwiki": "ٻولين جي ڳنڍڻن سواءِ صفحا",
+       "withoutinterwiki-summary": "ھيٺيان صفحا ڪنھن بہ ٻي ٻوليءَ ۾ ساڳي صفحي سان ڳنڍيل نہ آھن.",
        "withoutinterwiki-legend": "اڳياڙي",
        "withoutinterwiki-submit": "ڏيکاريو",
-       "fewestrevisions": "گھٽانگھٽ ترميميل صفحا",
+       "fewestrevisions": "گھٽ-ترين ورجاءَ رکندڙ صفحا",
        "nbytes": "$1 {{PLURAL:$1|بائيٽ|بائيٽون}}",
        "ncategories": "$1 {{PLURAL:$1|زمرو|زمرا}}",
        "ninterwikis": "$1 {{PLURAL:$1|بين‌الوڪي}}",
        "nimagelinks": "$1 {{PLURAL:$1|صفحي|صفحن}} ۾ استعمال ٿيل",
        "ntransclusions": "$1 {{PLURAL:$1|صفحي|صفحن}} ۾ استعمال ٿيل",
        "specialpage-empty": "ھن رپورٽ لاءِ ڪي بہ نتيجا ناھن.",
-       "lonelypages": "يتيم صفحا",
-       "uncategorizedpages": "اڻ زمريل صفحا",
+       "lonelypages": "يتيم-ٿيل صفحا",
+       "uncategorizedpages": "اڻزمرايل صفحا",
        "uncategorizedcategories": "اڻزمرايل زمرا",
        "uncategorizedimages": "اڻزمرايل فائيل",
        "uncategorizedtemplates": "اڻزمرايل سانچا",
-       "unusedcategories": "اڻ استعماليل زمرا",
-       "unusedimages": "اڻ استعماليل فائيلس",
+       "unusedcategories": "اڻ-استعماليل زمرا",
+       "unusedimages": "اڻ-استعماليل فائيلَ",
        "wantedcategories": "گھربل زمرا",
        "wantedpages": "گھربل صفحا",
        "wantedtemplates": "گھربل سانچا",
        "mostlinkedtemplates": "گھڻي کان گھڻا سانچا رکندڙ صفحا",
        "mostcategories": "گھڻي کان گھڻا زمرا رکندڙ صفحا",
        "mostimages": "وڌانوڌ ڳنڍيندڙ فائيل",
-       "mostrevisions": "وڌانوڌ ترميميل صفحا",
+       "mostrevisions": "وڌانوڌ ورجاءَ رکندڙ صفحا",
        "prefixindex": "هيءَ اڳياڙي رکندڙ سمورا صفحا",
        "prefixindex-namespace": "سمورا صفحا جن کي هيءَ اڳياڙي آهي ($1 نانءُپولار)",
        "prefixindex-submit": "ڏيکاريو",
        "protectedpages": "تحفظيل صفحا",
        "protectedpages-filters": "ڇاڻيون:",
        "protectedpages-noredirect": "چورڻا لڪايو",
-       "protectedpages-timestamp": "اوقاتي مُهُرَ",
+       "protectedpages-timestamp": "وقت-ٺپو",
        "protectedpages-page": "صفحو",
-       "protectedpages-params": "تحÙ\81ظ Ø¬Ø§ Ù\86Ù\85Ù\8aپيما",
+       "protectedpages-params": "تحÙ\81ظ Ø¬Ø§ Ù\86Ù\8aÙ\85پيما",
        "protectedpages-reason": "سبب",
        "protectedpages-submit": "صفحا ڏيکاريو",
        "protectedpages-unknown-timestamp": "اڻڄاتل",
        "newpages": "نوان صفحا",
        "newpages-submit": "ڏيکاريو",
        "newpages-username": "واپرائيندڙ-نانءُ:",
-       "ancientpages": "قديم ترين صفحا",
+       "ancientpages": "قديم-ترين صفحا",
        "move": "چوريو",
        "movethispage": "هيءُ صفحو چوريو",
        "notargettitle": "بنان هدف",
        "nopagetitle": "اهدافي صفحو اڻموجود",
        "pager-newer-n": "{{PLURAL:$1|نئون تر 1|نوان تر $1}}",
        "pager-older-n": "{{PLURAL:$1|پراڻو 1|پراڻا $1}}",
-       "apisandbox-retry": "ٻيهر ڪوشش ڪريو",
-       "apisandbox-helpurls": "امدادي ڳنڍڻا",
+       "apisandbox-retry": "ٻيھر ڪوشش ڪريو",
+       "apisandbox-helpurls": "مددي ڳنڍڻا",
        "apisandbox-examples": "مثال",
-       "apisandbox-dynamic-parameters-add-label": "نيمپيما شامل ڪريو",
+       "apisandbox-dynamic-parameters-add-label": "نيمپيما وجھو:",
        "apisandbox-dynamic-parameters-add-placeholder": "نيمپيما نانءُ",
-       "apisandbox-add-multi": "شامل ڪيو",
+       "apisandbox-add-multi": "وجھو",
        "apisandbox-results": "نتيجا",
        "apisandbox-continue": "جاري رکو",
        "booksources": "ڪتابي وسيلا",
        "actioncomplete": "ڪم پُورو",
        "actionfailed": "عمل ناڪام",
        "deletedtext": "\"$1\" ڊهي چڪو آهي.\nتازو ڊاٺل صفحن جي فهرست لاءِ $2 ڏسندا.",
-       "dellogpage": "ڊاٺ لاگ",
+       "dellogpage": "ڊاھ لاگ",
        "deletionlog": "ڊاٺ لاگ",
        "deletecomment": "سبب:",
        "deleteotherreason": "اڃا ڪو ٻيو سبب:",
        "revertpage": "[[Special:Contributions/$2|$2]] ([[User talk:$2|بحث]]) پاران سنوارون واپس [[User:$1|$1]] جي آخري مسودي ڏانھن ڪيون ويون",
        "changecontentmodel-title-label": "صفحي جو عنوان",
        "changecontentmodel-reason-label": "سبب:",
+       "changecontentmodel-submit": "بدلايو",
        "logentry-contentmodel-change-revertlink": "واپس ورايو",
        "logentry-contentmodel-change-revert": "واپس ورايو",
        "protectlogpage": "تحفظ لاگ",
        "protectedarticle": "محفوظ ٿيل \"[[$1]]\"",
        "modifiedarticleprotection": "\"[[$1]]\" جي تحفظ جي سطح تبديل ڪئي",
+       "unprotectedarticle": "\"[[$1]]\" تان تحفظ ھٽايو ويو",
        "movedarticleprotection": "\"[[$2]]\" جو حفاظت درجو \"[[$1]]\" جي طرف منتقل ڪيو",
+       "unprotectedarticle-comment": "\"[[$1]]\" تان {{GENDER:$2|تحفظ ھٽايو}}",
        "prot_1movedto2": "[[$1]] کي چوري [[$2]] تي رکيو ويو",
        "protect-legend": "تحفظڻ جي پڪ ڪريو",
        "protectcomment": "سبب:",
        "whatlinkshere-title": "\"$1\" سان ڳنڍيندڙ صفحا",
        "whatlinkshere-page": "صفحو:",
        "linkshere": "هيٺيان صفحا <strong>$2</strong> سان ڳنڍيل آهن:",
-       "nolinkshere": "'''$2''' سان ڪو بہ صفحو ڳنڍيل ناهي.",
+       "nolinkshere": "<strong>$2</strong> سان ڪو بہ صفحو ڳنڍيل ناهي.",
        "isredirect": "چورڻو صفحو",
        "istemplate": "شموليت",
        "isimage": "فائيل جو ڳنڍڻو",
        "whatlinkshere-prev": "{{PLURAL:$1|پويون|پويون $1}}",
        "whatlinkshere-next": "{{PLURAL:$1|اڳيون|اڳيان $1}}",
-       "whatlinkshere-links": "â\86\90 ڳنڍڻا",
+       "whatlinkshere-links": "â\86\92 ڳنڍڻا",
        "whatlinkshere-hideredirs": "$1 چوري ٿو",
        "whatlinkshere-hidetrans": "$1 شموليت",
        "whatlinkshere-hidelinks": "$1 ڳنڍڻا",
        "contribslink": "ڀاڱيداريون",
        "emaillink": "برقٽپال اماڻيو",
        "blocklogpage": "بندش لاگ",
-       "blocklogentry": "\"[[$1]]\" کي بندشيو ويو $2 $3 جي عرصي لاء",
+       "blocklogentry": "$2 $3 جي عرصي لاءِ [[$1]] کي بندشيو وي",
        "unblocklogentry": "$1 تان بندش هٽائي وئي",
        "block-log-flags-anononly": "فقط نامعلوم واپرائيندڙَ",
        "block-log-flags-nocreate": "کاتو کولڻ کان روڪ ٿيل",
        "tooltip-publish": "پنهنجيون تبديليون ڇاپيو",
        "tooltip-preview": "پنھنجي تبديلين تي نگاھ وجھو. براءِ مھرباني اھو سانڍڻ کان اڳ ڪندا.",
        "tooltip-diff": "لکت ۾ ڪيل پنھنجون تبديليون ڏسو",
-       "tooltip-compareselectedversions": "Ù\87Ù\86 ØµÙ\81Ø­Ù\8a Ø¬Ù\86 Ù»Ù\86 Ú\86Ù\88Ù\86Ú\8aÙ\8aÙ\84 Ù¾Ø±ØªÙ\86 Ø¯Ø±Ù\85Ù\8aاÙ\86 ØªÙ\81اÙ\88ت Ú\8fسÙ\88.",
+       "tooltip-compareselectedversions": "Ù\87Ù\86 ØµÙ\81Ø­Ù\8a Ø¬Ù\86 Ù\88Ú\86 Û¾ Ú\86Ù\88Ù\86Ú\8aÙ\8aÙ\84 Ù\88رجائÙ\86 Ù\88Ú\86 Û¾ ØªÙ\81اÙ\88ت Ú\8fسÙ\88",
        "tooltip-watch": "هيءُ صفحو پنهنجي نظر ۾ فھرست ۾ شامل ڪريو",
        "tooltip-watchlistedit-normal-submit": "فائيل ھٽايو",
        "tooltip-watchlistedit-raw-submit": "واچ لسٽ کي اَپڊيٽ ڪيو",
        "pageinfo-language": "صفحي جي مواد جي ٻولي",
        "pageinfo-content-model": "صفحي جي مواد جو ماڊل",
        "pageinfo-robot-index": "اجازت ڏنل",
-       "pageinfo-robot-noindex": "اجازت ناهي",
+       "pageinfo-robot-noindex": "اجازت-ناهي",
        "pageinfo-watchers": "صفحا ڏسندڙن جو انگ",
        "pageinfo-few-watchers": "$1 کان گھٽ {{PLURAL:$1|ڏسندڙ}}",
        "pageinfo-redirects-name": "ھن صفحي ڏانھن ڇوريل صفحن جو انگ",
        "confirm-unwatch-button": "ٺيڪ",
        "confirm-unwatch-top": "هيءُ صفحو پنهنجي نظر ۾ فهرست مان هٽائيندا؟",
        "confirm-rollback-top": "ھن صفحي ۾ ڪيل سنوارون واپس ورايون؟",
+       "semicolon-separator": "؛&#32;",
+       "comma-separator": "،&#32;",
        "quotation-marks": "\"$1\"",
        "imgmultipageprev": "← اڳوڻو صفحو",
-       "imgmultipagenext": "ايندڙ صفحو →",
+       "imgmultipagenext": "اڳيون صفحو ←",
        "imgmultigo": "هلو!",
-       "imgmultigoto": "$1 صفحي تي هلو",
+       "imgmultigoto": "$1 صفحي ڏانھن هلو",
        "img-lang-go": "ھلو",
        "table_pager_next": "مٿيون صفحو",
        "table_pager_prev": "پويون صفحو",
        "table_pager_limit_submit": "ھلو",
        "table_pager_empty": "ڪو بہ نتيجو نہ مليو",
        "autoredircomment": "صفحي کي [[$1]] ڏانھن چوريو",
+       "autosumm-removed-redirect": "[[$1]] ڏانھن چورڻو ھٽايو",
        "autosumm-newblank": "خالي صفحو سرجيو ويو",
        "watchlistedit-normal-title": "نظر ۾ فھرست کي سنواريو",
        "watchlistedit-raw-titles": "عنوانَ:",
        "version-specialpages": "خاص صفحا",
        "version-variables": "ڦِرڻا",
        "version-other": "ٻيو",
-       "version-license": "ذريعات‌وڪي لائيسنس",
-       "version-ext-license": "لائيسنس",
+       "version-license": "ذريعات‌وڪي اجازتنامو",
+       "version-ext-license": "اجازتنامو",
        "version-ext-colheader-name": "توسيع",
        "version-skin-colheader-name": "چَمَ",
        "version-ext-colheader-version": "ڀيرو",
-       "version-ext-colheader-license": "لائيسنس",
+       "version-ext-colheader-license": "اجازتنامو",
        "version-ext-colheader-description": "تشريح",
        "version-ext-colheader-credits": "ليکڪ",
-       "version-license-title": "لائيسنس براءِ $1",
+       "version-license-title": "$1 لاءِ اجازتنامو",
        "version-poweredby-others": "ٻيا",
        "version-poweredby-translators": "translatewiki.net جا ترجميڪار",
        "version-software": "تنصيب شده منطقگري",
        "version-software-version": "ڀيرو",
        "version-libraries-library": "لائبريري",
        "version-libraries-version": "ڀيرو",
-       "version-libraries-license": "لائيسنس",
+       "version-libraries-license": "اجازتنامو",
        "version-libraries-description": "تشريح",
        "version-libraries-authors": "ليکڪ",
        "redirect-submit": "ھلو",
        "tag-filter-submit": "ڇاڻي",
        "tag-list-wrapper": "[[Special:Tags|{{PLURAL:$1|ٽيگ|ٽيگز}}]]: $2",
        "tag-mw-new-redirect": "نئون چوريل",
+       "tag-mw-removed-redirect": "چورڻو ھٽايو",
        "tag-mw-blank": "خالي",
+       "tag-mw-rollback": "واپس-ورايو",
        "tag-mw-rollback-description": "واپس-ورايو ڳنڍڻي کي استعمال ڪندي پوين سنوارن کي واپس ورائيندڙ سنوارون",
        "tags-title": "ٽيگس",
        "tags-tag": "ٽيگ نانءُ",
        "htmlform-cloner-delete": "هٽايو",
        "htmlform-title-not-exists": "$1 وجود نٿو رکي.",
        "logentry-delete-delete": "$1 {{GENDER:$2|ڊاٿو}} صفحو $3",
+       "logentry-delete-restore": "$1 {{GENDER:$2|بحاليو}} صفحو $3 ($4)",
        "logentry-delete-revision": "$1 $3: $4 صفحي تي {{PLURAL:$5|ھڪ مسودي|$5 مسودن}} جي ظاھريت {{GENDER:$2|تبديل ڪئي}}",
        "revdelete-content-hid": "مواد لڪيل",
        "revdelete-uname-hid": "واپرائيندڙ-نانءُ لڪل",
+       "revdelete-unrestricted": "منتظمن تان پابنديون ھٽايون ويون",
        "logentry-block-block": "$1، {{GENDER:$4|$3}} تي $5 وقت جي خاتمي تائين {{GENDER:$2|بندش هئي آهي}} $6",
        "logentry-move-move": "$1 {{GENDER:$2|چوريو}} صفحو $3 ڏانهن $4",
        "logentry-move-move-noredirect": "$1 $3 صفحي کي $4 ڏانھن {{GENDER:$2|چوريو}} سواءِ ڪو ريڊائريڪٽ ڇڏيندي",
        "logentry-patrol-patrol-auto": "$1 پاڻمرادو صفحي $3 جي $4 مسودي تي گشت ڪيل طور {{GENDER:$2|نشان لڳايو}}",
        "logentry-newusers-create": "واپرائيندڙ کاتو $1 {{GENDER:$2|سرجيو ويو}}",
        "logentry-newusers-autocreate": "واپرائيندڙ کاتو $1 پاڻمرادو {{GENDER:$2|کوليو ويو}}",
+       "logentry-protect-unprotect": "$1 $3 تان تحفظ {{GENDER:$2|ھٽايو}}",
        "logentry-protect-protect": "$1 {{GENDER:$2|محفوظ ڪيو}} $3 $4",
        "logentry-upload-upload": "$1 {{GENDER:$2|چاڙهيو}} $3",
        "logentry-upload-overwrite": "$1 $3 جو ھڪ نئون ورزن {{GENDER:$2|چاڙھيو}}",
        "mw-widgets-usersmultiselect-placeholder": "وڌيڪ شامل ڪيو...",
        "date-range-from": "تاريخ کان:",
        "date-range-to": "تاريخ تائين:",
+       "randomrootpage": "بلاترتيب پاڙ صفحو",
        "log-action-filter-all": "سڀ"
 }
index e0f4f5c..049bb3b 100644 (file)
        "rcfilters-filter-showlinkedto-label": "Prikaži promjene na stranicama ka kojima vode veze",
        "rcfilters-filter-showlinkedto-option-label": "<strong>Stranice ka kojima vode veze sa</strong> izabrane stranice",
        "rcfilters-target-page-placeholder": "Unesite ime stranice (ili kategorije)",
+       "rcfilters-allcontents-label": "Cijeli sadržaj",
+       "rcfilters-alldiscussions-label": "Svi razgovori",
        "rcnotefrom": "Ispod {{PLURAL:$5|je izmjena|su izmjene}} od <strong>$3, $4</strong> (do <strong>$1</strong> prikazano).",
        "rclistfromreset": "Resetiraj izbor datuma",
        "rclistfrom": "Prikaži nove poruke od / Прикажи нове поруке од $3 $2",
        "move-subpages": "Premjesti podstranice (sve do $1)",
        "move-talk-subpages": "Premjesti podstranice stranice za razgovor (sve do $1)",
        "movepage-page-exists": "Stranica $1 već postoji i ne može biti automatski zamijenjena.",
+       "movepage-source-doesnt-exist": "Stranica $1 ne postoji i zbog toga ne može se premjestiti.",
        "movepage-page-moved": "Stranica $1 je premještena na $2.",
        "movepage-page-unmoved": "Stranica $1 ne može biti premještena u $2.",
        "movepage-max-pages": "Maksimum od $1 {{PLURAL:$1|stranice|stranice|stranica}} je premješteno i više nije moguće premjestiti automatski.",
        "delete_and_move_reason": "Obrisano da se oslobodi mjesto za premještanje iz „[[$1]]“",
        "selfmove": "Naslov je istovetan;\nne mogu ga premjestiti preko same sebe.",
        "immobile-source-namespace": "Ne mogu premjestiti stranice u imenski prostor \"$1\"",
+       "immobile-source-namespace-iw": "S ovog wikija ne mogu se premjestiti stranice na drugim wikijima.",
        "immobile-target-namespace": "Ne mogu se premjestiti stranice u imenski prostor \"$1\"",
        "immobile-target-namespace-iw": "Međuwiki link nije valjano odredište premještanja stranice.",
        "immobile-source-page": "Ova stranica se ne može premještati.",
        "immobile-target-page": "Ne može se preusmjeriti na taj odredišni naslov.",
+       "movepage-invalid-target-title": "Zatraženo ime nije valjano.",
        "bad-target-model": "Željeno odredište koristi drugačiji model sadržaja. Ne mogu da pretvorim iz $1 u $2.",
        "imagenocrossnamespace": "Ne može se premjestiti datoteka u nedatotečni imenski prostor",
        "nonfile-cannot-move-to-file": "Ne mogu se premjestiti podaci u datotečni imenski prostor",
index 5129716..fa0a999 100644 (file)
        "rcfilters-filter-showlinkedto-label": "Visa ändringar på sidor som länkar till",
        "rcfilters-filter-showlinkedto-option-label": "<strong>Sidor som länkar till</strong> den valda sidan",
        "rcfilters-target-page-placeholder": "Ange namnet på en sida (eller kategori)",
+       "rcfilters-allcontents-label": "Allt innehåll",
+       "rcfilters-alldiscussions-label": "Alla diskussioner",
        "rcnotefrom": "Nedan visas {{PLURAL:$5|ändringen|ändringar}} sedan <strong>$3, $4</strong> (upp till <strong>$1</strong> ändringar visas).",
        "rclistfromreset": "Återställ datumval",
        "rclistfrom": "Visa nya ändringar från och med $2 $3",
        "move-subpages": "Flytta undersidor (upp till $1)",
        "move-talk-subpages": "Flytta undersidor av diskussionssidan (upp till $1)",
        "movepage-page-exists": "Sidan $1 finns redan och kan inte skrivas över automatiskt.",
+       "movepage-source-doesnt-exist": "Sidan $1 finns inte och kan inte flyttas.",
        "movepage-page-moved": "Sidan $1 har flyttats till $2.",
        "movepage-page-unmoved": "Sidan $1 kunde inte flyttas till $2.",
        "movepage-max-pages": "Gränsen på $1 {{PLURAL:$1|flyttad sida|flyttade sidor}} har uppnåtts och inga fler sidor kommer att flyttas automatiskt.",
        "delete_and_move_reason": "Raderad för att göra plats till flyttning av \"[[$1]]\"",
        "selfmove": "Titeln är densamma;\nkan inte flytta en sida till sig själv.",
        "immobile-source-namespace": "Kan inte flytta sidor i namnrymden \"$1\"",
+       "immobile-source-namespace-iw": "Sidor på andra wikis kan inte flyttas från denna wiki.",
        "immobile-target-namespace": "Kan inte flytta sidor till namnrymden \"$1\"",
        "immobile-target-namespace-iw": "Interwikilänk är inte ett giltigt mål för sidflyttar.",
        "immobile-source-page": "Denna sida är inte flyttbar.",
        "immobile-target-page": "Kan inte flytta till det målnamnet.",
+       "movepage-invalid-target-title": "Det begärda namnet är inte giltigt.",
        "bad-target-model": "Den önskade destinationen använder en annan innehållsmodell. Kan inte konvertera från $1 till $2.",
        "imagenocrossnamespace": "Kan inte flytta filer till andra namnrymder än filnamnrymden.",
        "nonfile-cannot-move-to-file": "Kan inte flytta icke-fil till filnamnrymden.",
index f50c18c..c639a58 100644 (file)
@@ -73,6 +73,7 @@
        "tog-norollbackdiff": "రోల్‌బ్యాక్ చేసాక తేడాలు చూపించవద్దు",
        "tog-useeditwarning": "ఏదైనా పేజీని నేను వదిలివెళ్తున్నప్పుడు దానిలో భద్రపరచని మార్పులు ఉంటే నన్ను హెచ్చరించు",
        "tog-prefershttps": "లాగిన్ అయి ఉన్నప్పుడెల్లా భద్ర కనెక్షనునే వాడు",
+       "tog-showrollbackconfirmation": "రోల్‌బ్యాక్ లింకును నొక్కినపుడు నిర్ధారించుకునే సందేశాన్ని చూపించు",
        "underline-always": "ఎల్లప్పుడూ",
        "underline-never": "ఎప్పటికీ వద్దు",
        "underline-default": "అలంకారపు లేదా విహారిణి అప్రమేయం",
        "index-category": "సూచీకరించిన పేజీలు",
        "noindex-category": "సూచీకరించని పేజీలు",
        "broken-file-category": "తెగిపోయిన ఫైలులింకులు గల పేజీలు",
+       "categoryviewer-pagedlinks": "($1) ($2)",
+       "category-header-numerals": "$1–$2",
        "about": "గురించి",
        "article": "విషయపు పేజీ",
        "newwindow": "(కొత్త విండోలో వస్తుంది)",
        "versionrequired": "మీడియావికీ సాఫ్టువేరు వెర్షను $1 కావాలి",
        "versionrequiredtext": "ఈ పేజీని వాడటానికి మీకు మీడియావికీ సాఫ్టువేరు వెర్షను $1 కావాలి. [[Special:Version|వెర్షను పేజీ]]ని చూడండి.",
        "ok": "సరే",
+       "pagetitle": "$1 - {{SITENAME}}",
+       "pagetitle-view-mainpage": "{{SITENAME}}",
+       "backlinksubtitle": "← $1",
        "retrievedfrom": "\"$1\" నుండి వెలికితీశారు",
        "youhavenewmessages": "మీకు $1 ఉన్నాయి ($2).",
        "youhavenewmessagesfromusers": "{{PLURAL:$4|మీకు}} {{PLURAL:$3|మరో వాడుకరి|$3 వాడుకరుల}} నుండి  $1 ($2).",
        "page-rss-feed": "\"$1\" RSS ఫీడు",
        "page-atom-feed": "\"$1\" ఆటమ్ ఫీడు",
        "feed-atom": "యాటమ్",
+       "feed-rss": "RSS",
        "red-link-title": "$1 (పుట లేదు)",
        "sort-descending": "అవరోహణ క్రమంలో అమర్చు",
        "sort-ascending": "ఆరోహణ క్రమంలో అమర్చు",
        "virus-scanfailed": "స్కాన్ విఫలమైంది (సంకేతం $1)",
        "virus-unknownscanner": "అజ్ఞాత యాంటీవైరస్:",
        "logouttext": "<strong>ఇప్పుడు మీరు లాగౌటయ్యారు.</strong>\n\nఅయితే, ఓ గమనిక.. మీ విహారిణిలోని కోశాన్ని ఖాళీ చేసేవరకూ కొన్ని పేజీలు మీరింకా లాగినై ఉన్నట్లుగానే చూపించవచ్చు.",
+       "logging-out-notify": "మిమ్మల్ని లాగౌటు చేస్తున్నాం, ఆగండి.",
        "logout-failed": "ఇప్పుడు లాగౌట్ అవలేరు: $1",
        "cannotlogoutnow-title": "ఇప్పుడు లాగౌట్ అవలేరు",
        "cannotlogoutnow-text": "$1 ను వాడుతూండగా లాగౌట్ అవలేరు.",
        "nocookiesnew": "ఖాతాని సృష్టించాం, కానీ మీరు ఇంకా లోనికి ప్రవేశించలేదు.\nవాడుకరుల ప్రవేశానికి {{SITENAME}} కూకీలను వాడుతుంది.\nమీరు కూకీలని అచేతనం చేసివున్నారు.\nదయచేసి వాటిని చేతనంచేసి, మీ కొత్త వాడుకరి పేరు, సంకేతపదాలతో లోనికి ప్రవేశించండి.",
        "nocookieslogin": "వాడుకరుల ప్రవేశానికై {{SITENAME}} కూకీలను వాడుతుంది.\nమీరు కుకీలని అచేతనం చేసివున్నారు.\nవాటిని చేతనంచేసి ప్రయత్నించండి.",
        "nocookiesfornew": "మూలాన్ని కనుక్కోలేకపోయాం కాబట్టి, ఈ వాడుకరి ఖాతాను సృష్టించలేకపోయాం.\nమీ కంప్యూటర్లో కూకీలు చేతనమై ఉన్నాయని నిశ్చయించుకొని, ఈ పేజీని తిరిగి లోడు చేసి, మళ్ళీ ప్రయత్నించండి.",
+       "nocookiesforlogin": "{{int:nocookieslogin}}",
        "createacct-loginerror": "ఖాతా విజయవంతంగా సృష్టించబడింది, కానీ ఆటోమాటిగ్గా లాగిన్ అవలేరు.  స్వయంగా మీరే [[Special:UserLogin|లాగినవండి]].",
        "noname": "మీరు సరైన వాడుకరి పేరు ఇవ్వలేదు.",
        "loginsuccesstitle": "లాగినయ్యారు",
        "user-mail-no-body": "ఈమెయిలును ఖాళీగానో, మరీ తక్కువ విషయంతోనో పంపేందుకు ప్రయత్నించారు.",
        "changepassword": "సంకేతపదాన్ని మార్చండి",
        "resetpass_announce": "లాగిన్ను పూర్తిచేసేందుకు, తప్పనిసరిగా కొత్త సంకేతపదాన్ని ఇవ్వాలి:",
+       "resetpass_text": "<!-- ఇక్కడ పాఠ్యం చేర్చండి  -->",
        "resetpass_header": "ఖాతా సంకేతపదం మార్పు",
        "oldpassword": "పాత సంకేతపదం:",
        "newpassword": "కొత్త సంకేతపదం:",
        "headline_tip": "2వ స్థాయి శీర్షిక",
        "nowiki_sample": "ఫార్మాటు చేయని పాఠ్యాన్ని ఇక్కడ చేర్చండి",
        "nowiki_tip": "వికీ ఫార్మాటును పట్టించుకోవద్దు",
+       "image_sample": "Example.jpg",
        "image_tip": "ఇమిడ్చిన ఫైలు",
+       "media_sample": "Example.ogg",
        "media_tip": "దస్త్రపు లంకె",
        "sig_tip": "సమయంతో సహా మీ సంతకం",
        "hr_tip": "అడ్డగీత (అరుదుగా వాడండి)",
        "template-protected": "(సంరక్షితం)",
        "template-semiprotected": "(సెమీ-రక్షణలో ఉంది)",
        "hiddencategories": "ఈ పేజీ {{PLURAL:$1|ఒక దాచిన వర్గంలో|$1 దాచిన వర్గాల్లో}} ఉంది:",
+       "edittools-upload": "-",
        "nocreatetext": "{{SITENAME}}లో కొత్త పేజీలు సృష్టించడాన్ని నియంత్రించారు.\nమీరు వెనక్కి వెళ్ళి వేరే పేజీలు మార్చవచ్చు, లేదా [[Special:UserLogin|లోనికి ప్రవేశించండి లేదా ఖాతా సృష్టించుకోండి]].",
        "nocreate-loggedin": "కొత్త పేజీలను సృష్టించేందుకు మీకు అనుమతి లేదు.",
        "sectioneditnotsupported-title": "విభాగపు దిద్దుబాట్లకు తోడ్పాటు లేదు",
        "content-not-allowed-here": "స్లాట్ \"$3\" లో [[:$2]] పేజీలో పాఠ్యం \"$1\" కి అనుమతి లేదు",
        "editwarning-warning": "ఈ పేజీని వదిలివెళ్ళడం వల్ల మీరు చేసిన మార్పులను కోల్పోయే అవకాశం ఉంది.\nమీరు లాగిన్ అయివుంటే, ఈ హెచ్చరికని మీ అభిరుచులలోని \"{{int:prefs-editing}}\"  విభాగంలో అచేతనం చేసుకోవచ్చు.",
        "editpage-invalidcontentmodel-title": "ఈ కంటెంటు మోడలుకు మద్దతు లేదు",
+       "editpage-invalidcontentmodel-text": "\"$1\" అనే కంటెంటు మోడలుకు మద్దతు లేదు.",
        "editpage-notsupportedcontentformat-title": "పాఠ్యపు ఆకృతికి మద్దతు లేదు",
        "editpage-notsupportedcontentformat-text": "$2 పాఠ్యపు మోడల్, పాఠ్యపు ఆకృతి $1 కి మద్దతు ఇవ్వదు",
        "slot-name-main": "ప్రధాన",
        "content-model-text": "సాదా పాఠ్యం",
        "content-model-javascript": "జావాస్క్రిప్ట్",
        "content-model-css": "CSS",
+       "content-model-json": "JSON",
        "content-json-empty-object": "ఖాళీ అంశం",
        "content-json-empty-array": "ఖాళీ అరే",
        "duplicate-args-warning": "<strong>హెచ్చరిక:</strong> [[:$1]], \"$3\" పరామితికి ఒకటి కంటే ఎక్కువ విలువలు ఇచ్చి [[:$2]] ను పిలుస్తోంది. చిట్టచివరిగా ఇచ్చిన విలువను మాత్రమే వాడుతాం.",
        "mergehistory-comment": "[[:$1]]ని [[:$2]] లోనికి విలీనం చేసారు: $3",
        "mergehistory-same-destination": "మూల, గమ్యస్థాన పేజీలు ఒకటే కాకూడదు",
        "mergehistory-reason": "కారణం:",
+       "mergehistory-revisionrow": "$1 ($2) $3 . . $4 $5 $6",
        "mergelog": "విలీనాల చిట్టా",
        "revertmerge": "విలీనాన్ని రద్దుచెయ్యి",
        "mergelogpagetext": "ఒక పేజీ చరితాన్ని మరో పేజీ చరితం లోకి ఇటీవల చేసిన విలీనాల జాబితా ఇది.",
        "youremail": "ఈమెయిలు:",
        "username": "{{GENDER:$1|వాడుకరి పేరు}}:",
        "prefs-memberingroups": "ఈ {{PLURAL:$1|గుంపులో|గుంపులలో}} {{GENDER:$2|సభ్యుడు|సభ్యురాలు}}:",
+       "prefs-memberingroups-type": "$1",
        "group-membership-link-with-expiry": "$1 ($2 వరకు)",
        "prefs-registration": "నమోదైన సమయం:",
+       "prefs-registration-date-time": "$1",
        "yourrealname": "అసలు పేరు:",
        "yourlanguage": "భాష:",
        "yourvariant": "విషయపు భాషా వైవిధ్యం:",
        "prefs-advancedwatchlist": "ఉన్నత ఎంపికలు",
        "prefs-displayrc": "ప్రదర్శన ఎంపికలు",
        "prefs-displaywatchlist": "ప్రదర్శన ఎంపికలు",
+       "prefs-changesrc": "చూపించే మార్పులు",
+       "prefs-changeswatchlist": "చూపించే మార్పులు",
+       "prefs-pageswatchlist": "వీక్షించే పేజీలు",
        "prefs-tokenwatchlist": "టోకెన్",
        "prefs-diffs": "తేడాలు",
        "prefs-help-prefershttps": "ఈ అభిరుచి మీరు పైసారి లాగినైనపుడు అమలౌతుంది.",
        "saveusergroups": "{{GENDER:$1|వాడుకరి}} గుంపులను భద్రపరచు",
        "userrights-groupsmember": "సభ్యులు:",
        "userrights-groupsmember-auto": "సంభావిత సభ్యులు:",
+       "userrights-groupsmember-type": "$1",
        "userrights-groups-help": "ఈ వాడుకరి ఏయే గుంపులలో ఉండాలో మీరు మార్చవచ్చు.\n* టిక్కు పెట్టివుంటే సదరు గుంపులో ఈ వాడుకరి ఉన్నట్టు.\n* టిక్కు లేకుంటే సదరు గుంపులో ఈ వాడుకరి లేనట్టు.\n* * గుర్తు ఉంటే ఒకసారి ఆ గుంపుకు చేర్చాక మీరు తీసివేయలేరు, లేదా తీసివేసాక తిరిగి చేర్చలేరు.\n* ఈ # గుర్తు ఉంటే ఆ గుంపు కాలం తీరిపోయే సమయాన్ని పెంచగలరు; దాన్ని తగ్గించలేరు.",
        "userrights-reason": "కారణం:",
        "userrights-no-interwiki": "ఇతర వికీలలో వాడుకరి హక్కులను మార్చడానికి మీకు అనుమతి లేదు.",
        "userrights-nodatabase": "$1 అనే డేటాబేసు లేదు లేదా అది స్థానికం కాదు.",
        "userrights-changeable-col": "మీరు మార్చదగిన గుంపులు",
        "userrights-unchangeable-col": "మీరు మార్చలేని గుంపులు",
+       "userrights-irreversible-marker": "$1*",
+       "userrights-no-shorten-expiry-marker": "$1#",
        "userrights-expiry-current": "కాలంతీరే వ్యవధి $1",
        "userrights-expiry-none": "ఎన్నటికీ కాలం తీరిపోదు",
        "userrights-expiry": "కాలం తీరిపోయే వ్యవధి",
        "action-applychangetags": "మీ మార్పులతో ట్యాగులను ఆపాదించే",
        "action-deletechangetags": "డేటాబేసు నుండి ట్యాగులను తొలగించే",
        "action-purge": "ఈ పేజీని పర్జ్ చేసే",
+       "action-bigdelete": "పెద్ద చరితం ఉన్న పేజీలను తొలగించు",
        "action-blockemail": "ఈమెయిలు పంపకుండా వాడుకరిని నిరోధించే",
        "action-bot": "ఆటోమాటిక్ ప్రాసెస్ లాగా భావించే",
        "action-editsemiprotected": "\"{{int:protect-level-autoconfirmed}}\" గా సంరక్షించబడ్డ పేజీలను మార్చే",
        "action-override-export-depth": "5 లింకుల లోతు వరకు ఉన్న పేజీలతో సహా, పేజీలను ఎగుమతి చేసే",
        "action-suppressredirect": "పేజీని తరలించేటపుడు పాత పేరు నుండి దారిమార్పును సృష్టించకుండా చేసే",
        "nchanges": "{{PLURAL:$1|ఒక మార్పు|$1 మార్పులు}}",
+       "ntimes": "$1×",
        "enhancedrc-since-last-visit": "{{PLURAL:$1|చివరి సందర్శన తరువాత}}, $1",
        "enhancedrc-history": "చరిత్ర",
        "recentchanges": "ఇటీవలి మార్పులు",
        "recentchanges-label-plusminus": "ఈ పేజి పరిమాణంలో  జరిగిన మార్పుల  బైట్ల సంఖ్య",
        "recentchanges-legend-heading": "<strong>సూచిక :</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} ([[Special:NewPages|కొత్త పేజీల జాబితా]]ను కూడా చూడండి)",
+       "recentchanges-legend-plusminus": "(<em>±123</em>)",
        "recentchanges-submit": "చూపించు",
        "rcfilters-tag-remove": "'$1'ను తీసివెయ్యి",
        "rcfilters-legend-heading": "<strong>సంక్షేపాల (ఎబ్రీవియేషన్లు) జాబితా:</strong>",
        "rcfilters-clear-all-filters": "వడపోతకాలన్నింటినీ తుడిచెయ్యి",
        "rcfilters-show-new-changes": "$1 నుండి జరిగిన సరికొత్త మార్పులను చూడండి",
        "rcfilters-search-placeholder": "మార్పులను వడకట్టండి (మెనూను వాడండి లేదా వడపోత పేరు కోసం వెతకండి)",
+       "rcfilters-search-placeholder-mobile": "వడపోతలు",
        "rcfilters-invalid-filter": "తప్పు వడపోతకం",
        "rcfilters-empty-filter": "చేతనంగా ఉన్న వడపోతకాలేమీ లేవు. మార్పుచేర్పు లన్నిటినీ చూపించాం.",
        "rcfilters-filterlist-title": "వడపోతలు",
        "rcfilters-filter-logactions-label": "చిట్టాల్లోకి చేరిన కార్యకలాపాలు",
        "rcfilters-filter-logactions-description": "నిర్వాహక పనులు, ఖాతాల సృష్టి, పేజీ తొలగింపులు, ఎక్కింపులు...",
        "rcfilters-hideminor-conflicts-typeofchange": "కొన్ని రకాల మార్పులను \"చిన్న\" మార్పులుగా సూచించ జాలరు. అంచేత ఈ వడపోత కింది మార్పు రకాల వడపోతలతో ఘర్షిస్తోంది: $1",
+       "rcfilters-typeofchange-conflicts-hideminor": "ఈ రకపు వడపోత \"చిన్న మార్పుల\" వడపోతతో ఘర్షణ పడుతుంది. కొన్ని రకాల మార్పులను \"చిన్న\" అని సూచించలేం.",
        "rcfilters-filtergroup-lastrevision": "ఇటీవలి కూర్పులు",
        "rcfilters-filter-lastrevision-label": "ఇటీవలి కూర్పు",
        "rcfilters-filter-lastrevision-description": "పేజీలో ఇటీవల జరిగిన చిట్టచివరి మార్పు.",
        "rcfilters-filter-showlinkedto-label": "ఓ పేజీ నుండి లింకై ఉన్న పేజీల్లో జరిగిన మార్పులను చూపించు",
        "rcfilters-filter-showlinkedto-option-label": "ఎంచుకున్న పేజీకి <strong>లింకైన పేజీలు</strong>",
        "rcfilters-target-page-placeholder": "పేజీ (లేదా వర్గం) పేరు ఇవ్వండి",
+       "rcfilters-allcontents-label": "కంటెంటులన్నీ",
+       "rcfilters-alldiscussions-label": "చర్చలన్నీ",
        "rcnotefrom": "<strong>$3, $4</strong> తరువాత జరిగిన {{PLURAL:$5|మార్పు|మార్పులు}} కింద ఇచ్చాం (<strong>$1</strong> దాకా చూపించాం).",
        "rclistfromreset": "తేదీ ఎంపికను రీసెట్ చెయ్యి",
        "rclistfrom": "$3, $2 తో మొదలుపెట్టి ఆ తరువాత జరిగిన మార్పులను చూపించు",
        "minoreditletter": "చి",
        "newpageletter": "కొ",
        "boteditletter": "బా",
+       "unpatrolledletter": "!",
+       "rc-change-size": "$1",
        "rc-change-size-new": "మార్పు తర్వాత $1 {{PLURAL:$1|బైటు|బైట్లు}}",
        "newsectionsummary": "/* $1 */ కొత్త విభాగం",
        "rc-enhanced-expand": "వివరాలను చూపించు",
        "recentchangeslinked-page": "పేజీ పేరు:",
        "recentchangeslinked-to": "లేదంటే, ఇచ్చిన పేజీకి లింకయివున్న పేజీలలో జరిగిన మార్పులను చూపించు",
        "recentchanges-page-added-to-category": "[[:$1]] ను వర్గానికి చేర్చాం",
-       "recentchanges-page-added-to-category-bundled": "[[:$1]] à°µà°°à±\8dà°\97ానిà°\95à°¿ à°\9aà±\87à°°à±\8dà°\9aబడిà°\82ది, [[Special:WhatLinksHere/$1|à°\88 à°ªà±\87à°\9cà±\80 à°\87తర à°ªà±\87à°\9cà±\80à°²à±\8dà°²à±\8b à°\9aà±\87à°°à±\8dà°\9aబడింది]]",
+       "recentchanges-page-added-to-category-bundled": "[[:$1]] à°µà°°à±\8dà°\97ానిà°\95à°¿ à°\9aà±\87à°°à±\8dà°\9aారà±\81, [[Special:WhatLinksHere/$1|à°\88 à°ªà±\87à°\9cà±\80 à°\87తర à°ªà±\87à°\9cà±\80à°²à±\8dà°²à±\8b à°\9aà±\87à°°à°¿ à°\89ంది]]",
        "recentchanges-page-removed-from-category": "[[:$1]] వర్గం నుండి తీసివేయబడింది",
-       "recentchanges-page-removed-from-category-bundled": "[[:$1]] à°µà°°à±\8dà°\97à°\82 à°¨à±\81à°\82à°¡à°¿ à°¤à±\80సివà±\87యబడిà°\82ది, [[Special:WhatLinksHere/$1|ఈ పేజీ ఇతర పేజీల్లో చేర్చబడింది]]",
+       "recentchanges-page-removed-from-category-bundled": "[[:$1]] à°µà°°à±\8dà°\97à°\82 à°¨à±\81à°\82à°¡à°¿ à°¤à±\80సివà±\87సారà±\81, [[Special:WhatLinksHere/$1|ఈ పేజీ ఇతర పేజీల్లో చేర్చబడింది]]",
        "autochange-username": "MediaWiki ఆటోమాటిక్ మార్పు",
        "upload": "దస్త్రపు ఎక్కింపు",
        "uploadbtn": "దస్త్రాన్ని ఎక్కించు",
        "uploaddisabledtext": "ఫైళ్ళ ఎక్కింపులను అచేతనం చేసారు.",
        "php-uploaddisabledtext": "PHPలో ఫైలు ఎక్కింపులు అచేతనమై ఉన్నాయి.\nదయచేసి file_uploads అమరికని చూడండి.",
        "uploadscripted": "ఈ ఫైల్లో HTML కోడు గానీ స్క్రిప్టు కోడు గానీ ఉంది. వెబ్ బ్రౌజరు దాన్ని పొరపాటుగా అనువదించే అవకాశం ఉంది.",
+       "upload-scripted-dtd": "అప్రామాణిక DTD డిక్లరేషన్ను కలిగి ఉన్న SVG ఫైళ్ళను అప్‌లోడు చెయ్యలేరు.",
        "uploadscriptednamespace": "ఈ SVG ఫైలులోని పేరుబరి \"<nowiki>$1</nowiki>\" చెల్లనిది",
        "uploadinvalidxml": "ఎక్కించిన ఫైలులోని XML ను పార్సు చెయ్యలేకపోయాం.",
        "uploadvirus": "ఈ ఫైలులో వైరస్‌ ఉంది! వివరాలు: $1",
        "apisandbox-add-multi": "చేర్చు",
        "apisandbox-results": "ఫలితాలు",
        "apisandbox-request-url-label": "అభ్యర్థన URL:",
+       "apisandbox-request-format-json-label": "JSON",
        "apisandbox-request-time": "అభ్యర్ధన సమయం: {{PLURAL:$1|$1 మి.సె.}}",
        "apisandbox-continue": "కొనసాగించు",
        "apisandbox-continue-clear": "తుడిచివేయి",
        "apisandbox-multivalue-all-values": "$1 (అన్ని విలువలు)",
        "booksources": "పుస్తక మూలాలు",
        "booksources-search-legend": "పుస్తక మూలాల కోసం వెతుకు",
+       "booksources-isbn": "ISBN:",
        "booksources-search": "వెతుకు",
        "booksources-text": "కొత్త, పాత పుస్తకాలు అమ్మే ఇతర సైట్లకు లింకులు కింద ఇచ్చాం. మీరు వెతికే పుస్తకాలకు సంబంధించిన మరింత సమాచారం కూడా అక్కడ దొరకొచ్చు:",
        "booksources-invalid-isbn": "మీరిచ్చిన ISBN సరైనదిగా అనిపించుటలేదు; అసలు మూలాన్నుండి కాపీ చేయడంలో పొరపాట్లున్నాయేమో చూసుకోండి.",
        "listgrouprights-rights": "హక్కులు",
        "listgrouprights-helppage": "Help:గుంపు హక్కులు",
        "listgrouprights-members": "(సభ్యుల జాబితా)",
+       "listgrouprights-right-display": "<span class=\"listgrouprights-granted\">$1 <code>($2)</code></span>",
+       "listgrouprights-right-revoked": "<span class=\"listgrouprights-revoked\">$1 <code>($2)</code></span>",
        "listgrouprights-addgroup": "{{PLURAL:$2|గుంపుని|గుంపులను}} చేర్చగలరు: $1",
        "listgrouprights-removegroup": "{{PLURAL:$2|గుంపుని|గుంపులను}} తొలగించగలరు: $1",
        "listgrouprights-addgroup-all": "అన్ని గుంపులను చేర్చగలరు",
        "listgrants": "గ్రాంట్లు",
        "listgrants-grant": "గ్రాంటు",
        "listgrants-rights": "హక్కులు",
+       "listgrants-grant-display": "$1 <code>($2)</code>",
        "trackingcategories": "పహారా కాయు వర్గాలు",
        "trackingcategories-msg": "పహారా కాయు వర్గము",
        "trackingcategories-name": "సందేశం పేరు",
        "emailuserfooter": "ఈ ఈమెయిలును  $1, {{GENDER:$2|$2}} కు {{SITENAME}} లోని \"{{int:emailuser}}\" ఫంక్షను ద్వారా {{GENDER:$1|పంపించారు}}. {{GENDER:$2|మీరు}} ఈ ఈమెయిలుకు జవాబు పంపిస్తే, {{GENDER:$2|మీ}} మెయిలును నేరుగా {{GENDER:$1|ఒరిజినల్ సెండరుకు}} పంపిస్తాం. దీనితో, {{GENDER:$2|మీ}} ఈమెయిలు అడ్రసు {{GENDER:$1|వారికి}} తెలిసిపోతుంది.",
        "usermessage-summary": "వ్యవస్థ సందేశాన్ని వదిలివేస్తున్నాం.",
        "usermessage-editor": "వ్యవస్థ సందేశకులు",
+       "usermessage-template": "MediaWiki:UserMessage",
        "watchlist": "వీక్షణ జాబితా",
        "mywatchlist": "వీక్షణ జాబితా",
        "watchlistfor2": "$1 కొరకు $2",
        "deleting-backlinks-warning": "<strong>హెచ్చరిక:</strong> మీరు తొలగించబోతున్న పేజీకి [[Special:WhatLinksHere/{{FULLPAGENAME}}|ఇతర పేజీల]] నుండి లింకులు ఉన్నాయి. లేదా ఇతర పేజీల్లో అది ట్రాన్స్‍క్లూడు అవుతోంది.",
        "deleting-subpages-warning": "<strong>హెచ్చరిక:</strong> మీరు తొలగించబోతున్న పేజీకి [[Special:PrefixIndex/{{FULLPAGENAME}}/|{{PLURAL:$1|ఒక ఉపపేజీ ఉంది|$1 ఉపపేజీలున్నాయి|51=50 కి పైగా ఉపపేజీలున్నాయి}}]].",
        "rollback": "దిద్దుబాట్లను రద్దుచేయి",
+       "rollback-confirmation-confirm": "నిర్ధారించండి:",
+       "rollback-confirmation-yes": "రోల్‌బ్యాక్ చెయ్యి",
        "rollback-confirmation-no": "రద్దుచేయి",
        "rollbacklink": "రద్దుచేయి",
        "rollbacklinkcount": "$1 {{PLURAL:$1|మార్పును|మార్పులను}} రద్దుచేయి",
        "protect-fallback": "\"$1\" అనుమతి ఉన్న వాడుకరులను మాత్రమే అనుమతించు",
        "protect-level-autoconfirmed": "స్వయన్నిర్ధారిత వాడుకరులను మాత్రమే అనుమతించు",
        "protect-level-sysop": "నిర్వాహకులను మాత్రమే అనుమతించు",
+       "protect-summary-desc": "[$1=$2] ($3)",
        "protect-summary-cascade": "కాస్కేడింగు",
        "protect-expiring": "$1 (UTC)న కాలం చెల్లుతుంది",
        "protect-expiring-local": "$1న కాలం చెల్లుతుంది",
        "undelete-error-long": "ఫైలు $1 తొలగింపును రద్దు పరచడంలో లోపాలు దొర్లాయి",
        "undelete-show-file-confirm": "$2 నాడు $3 సమయాన ఉన్న \"<nowiki>$1</nowiki>\" ఫైలు యొక్క తొలగించిన కూర్పుని మీరు నిజంగానే చూడాలనుకుంటున్నారా?",
        "undelete-show-file-submit": "అవును",
+       "undelete-revision-row2": "$1 ($2) $3 . . $4 $5 $6 $7 $8",
        "namespace": "పేరుబరి:",
        "invert": "ఎంపికను తిరగవెయ్యి",
        "tooltip-invert": "ఎంచుకున్న పేరుబరి (చెక్ చేసి ఉంటే అనుబంధ పేరుబరి కూడా) లోని పేజీల్లో జరిగిన మార్పులను దాచేందుకు ఈ పెట్టెను చెక్ చెయ్యండి",
        "mycontris": "నా మార్పులు",
        "anoncontribs": "మార్పుచేర్పులు",
        "contribsub2": "{{GENDER:$3|$1}} ($2) కొరకు",
+       "contributions-subtitle": "{{GENDER:$3|$1}} కొరకు",
        "contributions-userdoesnotexist": "వాడుకరి ఖాతా \"$1\" నమోదుకాలేదు.",
+       "negative-namespace-not-supported": "నెగటివ్ విలువలున్న పేరుబరులకు మద్దతు లేదు.",
        "nocontribs": "ఈ విధమైన మార్పులేమీ దొరకలేదు.",
        "uctop": "ప్రస్తుత",
        "month": "ఈ నెల నుండి (అంతకు ముందువి):",
        "ipb-confirm": "నిరోధాన్ని ధృవపరచండి",
        "ipb-sitewide": "సైట్ వ్యాప్తంగా",
        "ipb-partial": "పాక్షికం",
+       "ipb-partial-help": "ప్రత్యేకించిన పేజీలు లేదా పేరుబరులు.",
        "ipb-pages-label": "పేజీలు",
+       "ipb-namespaces-label": "పేరుబరులు",
        "badipaddress": "సరైన ఐ.పి. అడ్రసు కాదు",
        "blockipsuccesssub": "నిరోధం విజయవంతం అయింది",
        "blockipsuccesstext": "[[Special:Contributions/$1|$1]] నిరోధించబడింది.<br />\nనిరోధాల సమీక్ష కొరకు [[Special:BlockList|నిరోధాల జాబితా]] చూడండి.",
index c9db9bc..4ada642 100644 (file)
                        "Fitoschido",
                        "TmY e12",
                        "Dual",
-                       "ToprakM"
+                       "ToprakM",
+                       "Suvarioglu"
                ]
        },
        "tog-underline": "Bağlantıların altını çizme:",
        "rcfilters-watchlist-preference-label": "JavaScript olmayan bir arayüz kullanın",
        "rcfilters-watchlist-preference-help": "Filtre Listesini arama olmadan veya işlevselliği vurgulayarak İzleme Listesi'ni yükler.",
        "rcfilters-target-page-placeholder": "Bir sayfa (ya da kategori) adı girin",
+       "rcfilters-allcontents-label": "Tüm içerikler",
        "rcnotefrom": "<strong>$3, $4</strong> tarihinden itibaren yapılan {{PLURAL:$5|değişiklik|değişiklik}} aşağıdadır (<strong>$1</strong> tarhine kadar olanlar gösterilmektedir).",
        "rclistfromreset": "Tarih seçimini sıfırla",
        "rclistfrom": "$3 $2 tarihinden itibaren yeni değişiklikleri göster",
index 9fd4840..5a73826 100644 (file)
        "rcfilters-filter-showlinkedto-label": "Показати зміни на сторінках, що посилаються сюди",
        "rcfilters-filter-showlinkedto-option-label": "<strong>Сторінки, що посилаються на</strong> обрану сторінку",
        "rcfilters-target-page-placeholder": "Уведіть назву сторінки (чи категорії)",
+       "rcfilters-allcontents-label": "Весь вміст",
+       "rcfilters-alldiscussions-label": "Всі обговорення",
        "rcnotefrom": "Нижче знаходяться {{PLURAL:$5|редагування}} з <strong>$3, $4</strong> (відображено до <strong>$1</strong>).",
        "rclistfromreset": "Скинути вибір дати",
        "rclistfrom": "Показати редагування починаючи з $3 $2.",
        "move-subpages": "Перейменувати підсторінки (до $1)",
        "move-talk-subpages": "Перейменувати підсторінки сторінки обговорення (до $1)",
        "movepage-page-exists": "Сторінка $1 вже існує і не може бути автоматично перезаписана.",
+       "movepage-source-doesnt-exist": "Сторінка $1 не існує та не може бути перейменована.",
        "movepage-page-moved": "Сторінка $1 перейменована на $2.",
        "movepage-page-unmoved": "Сторінка $1 не може бути перейменована на $2.",
        "movepage-max-pages": "$1 {{PLURAL:$1|сторінка була перейменована|сторінки були перейменовані|сторінок були перейменовані}} — це максимум, більше сторінок не можна перейменувати автоматично.",
        "delete_and_move_reason": "Вилучена для можливості перейменування сторінки «[[$1]]»",
        "selfmove": "Ця назва є ідентичною з поточною;\nнеможливо перейменувати сторінку на поточну назву.",
        "immobile-source-namespace": "Не можна перейменовувати сторінки з простору назв «$1»",
+       "immobile-source-namespace-iw": "Сторінки з інших вікі не можуть бути перейменовані у цій вікі.",
        "immobile-target-namespace": "Не можна перейменовувати сторінки до простору назв «$1»",
        "immobile-target-namespace-iw": "Інтервікі-посилання не підходить для перейменування сторінки.",
        "immobile-source-page": "Цю сторінку не можна перейменувати.",
        "immobile-target-page": "Не можна присвоїти сторінці цю назву.",
+       "movepage-invalid-target-title": "Запитуване ім'я недопустиме.",
        "bad-target-model": "Неможливо перетворити $1 на $2: несумісні моделі даних.",
        "imagenocrossnamespace": "Неможливо дати файлові назву з іншого простору назв",
        "nonfile-cannot-move-to-file": "Не можна перейменовувати сторінки з інших просторів назв на файли",
index f9ea792..9214243 100644 (file)
                        "Suchichi02",
                        "神樂坂秀吉",
                        "WQL",
-                       "Looong"
+                       "Looong",
+                       "予弦"
                ]
        },
        "tog-underline": "链接下划线:",
        "redirectedfrom": "(重定向自$1)",
        "redirectpagesub": "重定向页面",
        "redirectto": "重定向至:",
-       "lastmodifiedat": "在$2,此页面最后编辑于$1。",
+       "lastmodifiedat": "此页面最后编辑于$1 $2。",
        "viewcount": "此页面已经被访问过$1次。",
        "protectedpage": "受保护页面",
        "jumpto": "跳转至:",
        "group-sysop": "管理员",
        "group-interface-admin": "界面管理员",
        "group-bureaucrat": "行政员",
-       "group-suppress": "Flow监督员",
+       "group-suppress": "结构式讨论监督员",
        "group-all": "(所有)",
        "group-user-member": "{{GENDER:$1|用户}}",
        "group-autoconfirmed-member": "{{GENDER:$1|自动确认用户}}",
        "move-page-legend": "移动页面",
        "movepagetext": "您可以使用下面的表单来重命名一个页面,同时将其所有版本历史移动到新页面。旧标题将会被重定向到新标题。您可以自动更新链接至原标题的重定向。如果您不选择这样做的话,请检查[[Special:DoubleRedirects|双重]]或[[Special:BrokenRedirects|损坏重定向]]链接。您有责任确保链接会被正确指向他们应该被指向的地方。\n\n注意:如果已存在使用新标题的页面,此页面将<strong>不会</strong>被移动,除非新页面是重定向,并且没有过去的编辑历史。这意味着您可在误操作后将页面移回原处,同时,您无法覆盖现有页面。\n\n<strong>注意:</strong>对这样一个经常被访问的页面而言这可能是一个重大且唐突的更改;请在行动前先了解您的修改可能带来的一切后果。",
        "movepagetext-noredirectfixer": "用下面的表单来重命名一个页面,并将其版本历史同时移动到新页面。老的页面将成为新页面的重定向页。请检查[[Special:DoubleRedirects|双重重定向]]或[[Special:BrokenRedirects|损坏重定向]]链接。您应当负责确定所有链接依然会链到指定的页面。\n\n注意如果新页面已经有内容的话,页面将<strong>不会</strong>被移动,除非新页面无内容或是重定向页,而且没有版本历史。这意味着您可在误操作后将页面移回原处,同时,您无法覆盖现有页面。\n\n<strong>注意:</strong>对一个经常被访问的页面而言这可能是一个重大且唐突的更改;请在行动前先确定您了解其所可能带来的后果。",
+       "movepagetext-noredirectsupport": "使用下方表单来重命名页面,其所有历史记录也会被移动到新名称下。\n你需要确保相关链接指向正确。\n\n注意,如果新名称的页面已经存在,则此页面<strong>不会</strong>被移动。\n这意味着,如果重命名出错,你可以将页面重命名回原名称,但不能覆盖已存在页面。\n\n<strong>注意:</strong>\n对于人气较高的页面而已,此操作可能导致剧烈和意想不到的变化;\n请确保你了解此行为可能导致的后果,然后再执行操作。",
        "movepagetalktext": "如果您勾选此框,相关联的讨论页将被自动移动到新的标题,除非这里已经有了一个非空讨论页。\n\n在这种情况下,如有需要,您将不得不手动移动或合并页面。",
        "moveuserpage-warning": "'''警告:'''你将移动一个用户页面。请注意,只有该页面会被移动,该用户'''不会'''被更名。",
        "movecategorypage-warning": "<strong>警告:</strong>您将移动分类页面。请注意只有此页面将会移动,旧有分类的任何页面将<em>不会</em>同步移动。",
        "move-subpages": "移动子页面(最多$1页)",
        "move-talk-subpages": "如果可能,移动子对话页面(上至$1页)",
        "movepage-page-exists": "页面$1已存在,无法自动覆盖。",
+       "movepage-source-doesnt-exist": "页面 $1 不存在且无法被移动。",
        "movepage-page-moved": "页面$1已经移动到$2。",
        "movepage-page-unmoved": "页面$1无法移动到$2。",
        "movepage-max-pages": "所移动$1个页面的数量已达最大限额,无法同时自动移动更多页面。",
        "delete_and_move_reason": "删除以便移动[[$1]]",
        "selfmove": "标题相同;无法对页面进行自我移动。",
        "immobile-source-namespace": "无法移动名字空间为“$1”的页面",
+       "immobile-source-namespace-iw": "无法从此维基项目将页面移动至其他维基项目。",
        "immobile-target-namespace": "无法将页面移动到“$1”名字空间",
        "immobile-target-namespace-iw": "在移动页面时,跨wiki链接不是有效的目标。",
        "immobile-source-page": "此页面不能移动。",
        "immobile-target-page": "无法移动至该目标标题。",
+       "movepage-invalid-target-title": "请求的名称无效。",
        "bad-target-model": "要求的目标使用不同的内容模式。无法从$1转换到$2。",
        "imagenocrossnamespace": "无法将文件移动到非文件名字空间",
        "nonfile-cannot-move-to-file": "无法将非文件移动到文件名字空间",
index 9d3ef54..3b6f771 100644 (file)
        "group-sysop": "管理員",
        "group-interface-admin": "介面管理員",
        "group-bureaucrat": "行政員",
-       "group-suppress": "監督員",
+       "group-suppress": "çµ\90æ§\8bå¼\8fè¨\8eè«\96ç\9b£ç\9d£å\93¡",
        "group-all": "(全部)",
        "group-user-member": "{{GENDER:$1|使用者}}",
        "group-autoconfirmed-member": "自動確認使用者",
index fcf42ea..cd77468 100644 (file)
        "newpages-username": "用戶名稱:",
        "speciallogtitlelabel": "目標 (標題或用戶):",
        "checkbox-select": "選擇: $1",
+       "emailuser": "Email 聯絡此用戶",
        "emailusername": "用戶名稱:",
        "wlshowhidebots": "機械人",
        "blanknamespace": "(主要)",
index 5024395..ea12e42 100644 (file)
@@ -28,7 +28,6 @@ require_once __DIR__ . '/Maintenance.php';
 
 use MediaWiki\Linker\LinkTarget;
 use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionRecord;
 use Wikimedia\Rdbms\IResultWrapper;
 use Wikimedia\Rdbms\IMaintainableDatabase;
 
@@ -73,8 +72,6 @@ class NamespaceDupes extends Maintenance {
        }
 
        public function execute() {
-               $this->db = $this->getDB( DB_MASTER );
-
                $options = [
                        'fix' => $this->hasOption( 'fix' ),
                        'merge' => $this->hasOption( 'merge' ),
@@ -254,8 +251,8 @@ class NamespaceDupes extends Maintenance {
                foreach ( $targets as $row ) {
                        // Find the new title and determine the action to take
 
-                       $newTitle = $this->getDestinationTitle( $ns, $name,
-                               $row->page_namespace, $row->page_title, $options );
+                       $newTitle = $this->getDestinationTitle(
+                               $ns, $name, $row->page_namespace, $row->page_title );
                        $logStatus = false;
                        if ( !$newTitle ) {
                                $logStatus = 'invalid title';
@@ -338,18 +335,20 @@ class NamespaceDupes extends Maintenance {
        private function checkLinkTable( $table, $fieldPrefix, $ns, $name, $options,
                $extraConds = []
        ) {
+               $dbw = $this->getDB( DB_MASTER );
+
                $batchConds = [];
                $fromField = "{$fieldPrefix}_from";
                $namespaceField = "{$fieldPrefix}_namespace";
                $titleField = "{$fieldPrefix}_title";
                $batchSize = 500;
                while ( true ) {
-                       $res = $this->db->select(
+                       $res = $dbw->select(
                                $table,
                                [ $fromField, $namespaceField, $titleField ],
                                array_merge( $batchConds, $extraConds, [
                                        $namespaceField => 0,
-                                       $titleField . $this->db->buildLike( "$name:", $this->db->anyString() )
+                                       $titleField . $dbw->buildLike( "$name:", $dbw->anyString() )
                                ] ),
                                __METHOD__,
                                [
@@ -364,8 +363,8 @@ class NamespaceDupes extends Maintenance {
                        foreach ( $res as $row ) {
                                $logTitle = "from={$row->$fromField} ns={$row->$namespaceField} " .
                                        "dbk={$row->$titleField}";
-                               $destTitle = $this->getDestinationTitle( $ns, $name,
-                                       $row->$namespaceField, $row->$titleField, $options );
+                               $destTitle = $this->getDestinationTitle(
+                                       $ns, $name, $row->$namespaceField, $row->$titleField );
                                $this->totalLinks++;
                                if ( !$destTitle ) {
                                        $this->output( "$table $logTitle *** INVALID\n" );
@@ -378,7 +377,7 @@ class NamespaceDupes extends Maintenance {
                                        continue;
                                }
 
-                               $this->db->update( $table,
+                               $dbw->update( $table,
                                        // SET
                                        [
                                                $namespaceField => $destTitle->getNamespace(),
@@ -396,8 +395,8 @@ class NamespaceDupes extends Maintenance {
                                $this->output( "$table $logTitle -> " .
                                        $destTitle->getPrefixedDBkey() . "\n" );
                        }
-                       $encLastTitle = $this->db->addQuotes( $row->$titleField );
-                       $encLastFrom = $this->db->addQuotes( $row->$fromField );
+                       $encLastTitle = $dbw->addQuotes( $row->$titleField );
+                       $encLastFrom = $dbw->addQuotes( $row->$fromField );
 
                        $batchConds = [
                                "$titleField > $encLastTitle " .
@@ -433,6 +432,8 @@ class NamespaceDupes extends Maintenance {
         * @return IResultWrapper
         */
        private function getTargetList( $ns, $name, $options ) {
+               $dbw = $this->getDB( DB_MASTER );
+
                if (
                        $options['move-talk'] &&
                        MediaWikiServices::getInstance()->getNamespaceInfo()->isSubject( $ns )
@@ -442,7 +443,7 @@ class NamespaceDupes extends Maintenance {
                        $checkNamespaces = NS_MAIN;
                }
 
-               return $this->db->select( 'page',
+               return $dbw->select( 'page',
                        [
                                'page_id',
                                'page_title',
@@ -450,7 +451,7 @@ class NamespaceDupes extends Maintenance {
                        ],
                        [
                                'page_namespace' => $checkNamespaces,
-                               'page_title' . $this->db->buildLike( "$name:", $this->db->anyString() ),
+                               'page_title' . $dbw->buildLike( "$name:", $dbw->anyString() ),
                        ],
                        __METHOD__
                );
@@ -462,10 +463,9 @@ class NamespaceDupes extends Maintenance {
         * @param string $name The conflicting prefix
         * @param int $sourceNs The source namespace
         * @param int $sourceDbk The source DB key (i.e. page_title)
-        * @param array $options Associative array of validated command-line options
         * @return Title|false
         */
-       private function getDestinationTitle( $ns, $name, $sourceNs, $sourceDbk, $options ) {
+       private function getDestinationTitle( $ns, $name, $sourceNs, $sourceDbk ) {
                $dbk = substr( $sourceDbk, strlen( "$name:" ) );
                if ( $ns == 0 ) {
                        // An interwiki; try an alternate encoding with '-' for ':'
@@ -518,7 +518,9 @@ class NamespaceDupes extends Maintenance {
         * @return bool
         */
        private function movePage( $id, LinkTarget $newLinkTarget ) {
-               $this->db->update( 'page',
+               $dbw = $this->getDB( DB_MASTER );
+
+               $dbw->update( 'page',
                        [
                                "page_namespace" => $newLinkTarget->getNamespace(),
                                "page_title" => $newLinkTarget->getDBkey(),
@@ -535,7 +537,7 @@ class NamespaceDupes extends Maintenance {
                        [ 'imagelinks', 'il' ] ];
                foreach ( $fromNamespaceTables as $tableInfo ) {
                        list( $table, $fieldPrefix ) = $tableInfo;
-                       $this->db->update( $table,
+                       $dbw->update( $table,
                                // SET
                                [ "{$fieldPrefix}_from_namespace" => $newLinkTarget->getNamespace() ],
                                // WHERE
@@ -559,12 +561,8 @@ class NamespaceDupes extends Maintenance {
         * @return bool
         */
        private function canMerge( $id, LinkTarget $linkTarget, &$logStatus ) {
-               $latestDest = Revision::newFromTitle(
-                       $linkTarget, 0, RevisionRecord::READ_LATEST
-               );
-               $latestSource = Revision::newFromPageId(
-                       $id, 0, RevisionRecord::READ_LATEST
-               );
+               $latestDest = Revision::newFromTitle( $linkTarget, 0, Revision::READ_LATEST );
+               $latestSource = Revision::newFromPageId( $id, 0, Revision::READ_LATEST );
                if ( $latestSource->getTimestamp() > $latestDest->getTimestamp() ) {
                        $logStatus = 'cannot merge since source is later';
                        return false;
@@ -581,6 +579,8 @@ class NamespaceDupes extends Maintenance {
         * @return bool
         */
        private function mergePage( $row, Title $newTitle ) {
+               $dbw = $this->getDB( DB_MASTER );
+
                $id = $row->page_id;
 
                // Construct the WikiPage object we will need later, while the
@@ -592,17 +592,17 @@ class NamespaceDupes extends Maintenance {
                $wikiPage->loadPageData( 'fromdbmaster' );
 
                $destId = $newTitle->getArticleID();
-               $this->beginTransaction( $this->db, __METHOD__ );
-               $this->db->update( 'revision',
+               $this->beginTransaction( $dbw, __METHOD__ );
+               $dbw->update( 'revision',
                        // SET
                        [ 'rev_page' => $destId ],
                        // WHERE
                        [ 'rev_page' => $id ],
                        __METHOD__ );
 
-               $this->db->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
+               $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
 
-               $this->commitTransaction( $this->db, __METHOD__ );
+               $this->commitTransaction( $dbw, __METHOD__ );
 
                /* Call LinksDeletionUpdate to delete outgoing links from the old title,
                 * and update category counts.
index e80b6f6..bb48151 100644 (file)
@@ -53,8 +53,6 @@ class PopulateLogSearch extends LoggedUpdateMaintenance {
        }
 
        protected function doDBUpdates() {
-               global $wgActorTableSchemaMigrationStage;
-
                $batchSize = $this->getBatchSize();
                $db = $this->getDB( DB_MASTER );
                if ( !$db->tableExists( 'log_search' ) ) {
@@ -70,6 +68,19 @@ class PopulateLogSearch extends LoggedUpdateMaintenance {
                }
                $end = $db->selectField( 'logging', 'MAX(log_id)', '', __FUNCTION__ );
 
+               // This maintenance script is for updating pre-1.16 to 1.16. The target_author_id and
+               // target_author_ip relations it adds will later be migrated to target_author_actor by
+               // migrateActors.php. If the schema is already 1.34, we should have nothing to do.
+               if ( !$db->fieldExists( 'logging', 'log_user' ) ) {
+                       $this->output(
+                               "This does not appear to be an upgrade from MediaWiki pre-1.16 "
+                               . "(logging.log_user does not exist).\n"
+                       );
+                       $this->output( "Nothing to do.\n" );
+
+                       return true;
+               }
+
                # Do remaining chunk
                $end += $batchSize - 1;
                $blockStart = $start;
@@ -83,8 +94,8 @@ class PopulateLogSearch extends LoggedUpdateMaintenance {
                                'logging', [ 'log_id', 'log_type', 'log_action', 'log_params' ], $cond, __FUNCTION__
                        );
                        foreach ( $res as $row ) {
+                               // RevisionDelete logs - revisions
                                if ( LogEventsList::typeAction( $row, $delTypes, 'revision' ) ) {
-                                       // RevisionDelete logs - revisions
                                        $params = LogPage::extractParams( $row->log_params );
                                        // Param format: <urlparam> <item CSV> [<ofield> <nfield>]
                                        if ( count( $params ) < 2 ) {
@@ -109,33 +120,30 @@ class PopulateLogSearch extends LoggedUpdateMaintenance {
                                        $log = new LogPage( $row->log_type );
                                        // Add item relations...
                                        $log->addRelations( $field, $items, $row->log_id );
-                                       // Query item author relations...
+                                       // Determine what table to query...
                                        $prefix = substr( $field, 0, strpos( $field, '_' ) ); // db prefix
                                        if ( !isset( self::$tableMap[$prefix] ) ) {
                                                continue; // bad row?
                                        }
-                                       $tables = [ self::$tableMap[$prefix] ];
-                                       $fields = [];
-                                       $joins = [];
-                                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
-                                               // Read the old fields if we're still writing them regardless of read mode, to handle upgrades
-                                               $fields['userid'] = $prefix . '_user';
-                                               $fields['username'] = $prefix . '_user_text';
-                                       }
-                                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
-                                               // Read the new fields if we're writing them regardless of read mode, to handle upgrades
-                                               if ( $prefix === 'rev' ) {
-                                                       $tables[] = 'revision_actor_temp';
-                                                       $joins['revision_actor_temp'] = [
-                                                               ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) ? 'LEFT JOIN' : 'JOIN',
-                                                               'rev_id = revactor_rev',
-                                                       ];
-                                                       $fields['actorid'] = 'revactor_actor';
-                                               } else {
-                                                       $fields['actorid'] = $prefix . '_actor';
+                                       $table = self::$tableMap[$prefix];
+                                       $userField = $prefix . '_user';
+                                       $userTextField = $prefix . '_user_text';
+                                       // Add item author relations...
+                                       $userIds = $userIPs = [];
+                                       $sres = $db->select( $table,
+                                               [ $userField, $userTextField ],
+                                               [ $field => $items ]
+                                       );
+                                       foreach ( $sres as $srow ) {
+                                               if ( $srow->$userField > 0 ) {
+                                                       $userIds[] = intval( $srow->$userField );
+                                               } elseif ( $srow->$userTextField != '' ) {
+                                                       $userIPs[] = $srow->$userTextField;
                                                }
                                        }
-                                       $sres = $db->select( $tables, $fields, [ $field => $items ], __METHOD__, [], $joins );
+                                       // Add item author relations...
+                                       $log->addRelations( 'target_author_id', $userIds, $row->log_id );
+                                       $log->addRelations( 'target_author_ip', $userIPs, $row->log_id );
                                } elseif ( LogEventsList::typeAction( $row, $delTypes, 'event' ) ) {
                                        // RevisionDelete logs - log events
                                        $params = LogPage::extractParams( $row->log_params );
@@ -147,51 +155,22 @@ class PopulateLogSearch extends LoggedUpdateMaintenance {
                                        $log = new LogPage( $row->log_type );
                                        // Add item relations...
                                        $log->addRelations( 'log_id', $items, $row->log_id );
-                                       // Query item author relations...
-                                       $fields = [];
-                                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
-                                               // Read the old fields if we're still writing them regardless of read mode, to handle upgrades
-                                               $fields['userid'] = 'log_user';
-                                               $fields['username'] = 'log_user_text';
-                                       }
-                                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
-                                               // Read the new fields if we're writing them regardless of read mode, to handle upgrades
-                                               $fields['actorid'] = 'log_actor';
-                                       }
-
-                                       $sres = $db->select( 'logging', $fields, [ 'log_id' => $items ], __METHOD__ );
-                               } else {
-                                       continue;
-                               }
-
-                               // Add item author relations...
-                               $userIds = $userIPs = $userActors = [];
-                               foreach ( $sres as $srow ) {
-                                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
-                                               if ( $srow->userid > 0 ) {
-                                                       $userIds[] = intval( $srow->userid );
-                                               } elseif ( $srow->username != '' ) {
-                                                       $userIPs[] = $srow->username;
+                                       // Add item author relations...
+                                       $userIds = $userIPs = [];
+                                       $sres = $db->select( 'logging',
+                                               [ 'log_user', 'log_user_text' ],
+                                               [ 'log_id' => $items ]
+                                       );
+                                       foreach ( $sres as $srow ) {
+                                               if ( $srow->log_user > 0 ) {
+                                                       $userIds[] = intval( $srow->log_user );
+                                               } elseif ( IP::isIPAddress( $srow->log_user_text ) ) {
+                                                       $userIPs[] = $srow->log_user_text;
                                                }
                                        }
-                                       if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
-                                               if ( $srow->actorid ) {
-                                                       $userActors[] = intval( $srow->actorid );
-                                               } elseif ( $srow->userid > 0 ) {
-                                                       $userActors[] = User::newFromId( $srow->userid )->getActorId( $db );
-                                               } else {
-                                                       $userActors[] = User::newFromName( $srow->username, false )->getActorId( $db );
-                                               }
-                                       }
-                               }
-                               // Add item author relations...
-                               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
                                        $log->addRelations( 'target_author_id', $userIds, $row->log_id );
                                        $log->addRelations( 'target_author_ip', $userIPs, $row->log_id );
                                }
-                               if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
-                                       $log->addRelations( 'target_author_actor', $userActors, $row->log_id );
-                               }
                        }
                        $blockStart += $batchSize;
                        $blockEnd += $batchSize;
index a239fa0..4213d5f 100644 (file)
  * @ingroup Maintenance
  */
 
-use MediaWiki\Config\ServiceOptions;
-use MediaWiki\Logger\LoggerFactory;
-use MediaWiki\MediaWikiServices;
-
 require_once __DIR__ . '/Maintenance.php';
 
 /**
@@ -81,25 +77,13 @@ class RebuildLocalisationCache extends Maintenance {
 
                $conf = $wgLocalisationCacheConf;
                $conf['manualRecache'] = false; // Allow fallbacks to create CDB files
-               $conf['forceRecache'] = $force || !empty( $conf['forceRecache'] );
+               if ( $force ) {
+                       $conf['forceRecache'] = true;
+               }
                if ( $this->hasOption( 'outdir' ) ) {
                        $conf['storeDirectory'] = $this->getOption( 'outdir' );
                }
-               // XXX Copy-pasted from ServiceWiring.php. Do we need a factory for this one caller?
-               $lc = new LocalisationCacheBulkLoad(
-                       new ServiceOptions(
-                               LocalisationCache::$constructorOptions,
-                               $conf,
-                               MediaWikiServices::getInstance()->getMainConfig()
-                       ),
-                       new LCStoreDB( [] ),
-                       LoggerFactory::getInstance( 'localisation' ),
-                       [ function () {
-                               MediaWikiServices::getInstance()->getResourceLoader()
-                                       ->getMessageBlobStore()->clear();
-                       } ],
-                       MediaWikiServices::getInstance()->getLanguageNameUtils()
-               );
+               $lc = new LocalisationCacheBulkLoad( $conf );
 
                $allCodes = array_keys( Language::fetchLanguageNames( null, 'mwfile' ) );
                if ( $this->hasOption( 'lang' ) ) {
index 2e4cc88..7cc7575 100644 (file)
@@ -27,7 +27,6 @@
 
 require_once __DIR__ . '/Maintenance.php';
 
-use Wikimedia\Rdbms\IMaintainableDatabase;
 use Wikimedia\Rdbms\DatabaseSqlite;
 
 /**
@@ -38,11 +37,6 @@ use Wikimedia\Rdbms\DatabaseSqlite;
 class RebuildTextIndex extends Maintenance {
        const RTI_CHUNK_SIZE = 500;
 
-       /**
-        * @var IMaintainableDatabase
-        */
-       private $db;
-
        public function __construct() {
                parent::__construct();
                $this->addDescription( 'Rebuild search index table from scratch' );
@@ -54,23 +48,19 @@ class RebuildTextIndex extends Maintenance {
 
        public function execute() {
                // Shouldn't be needed for Postgres
-               $this->db = $this->getDB( DB_MASTER );
-               if ( $this->db->getType() == 'postgres' ) {
+               $dbw = $this->getDB( DB_MASTER );
+               if ( $dbw->getType() == 'postgres' ) {
                        $this->fatalError( "This script is not needed when using Postgres.\n" );
                }
 
-               if ( $this->db->getType() == 'sqlite' ) {
+               if ( $dbw->getType() == 'sqlite' ) {
                        if ( !DatabaseSqlite::getFulltextSearchModule() ) {
                                $this->fatalError( "Your version of SQLite module for PHP doesn't "
                                        . "support full-text search (FTS3).\n" );
                        }
-                       if ( !$this->db->checkForEnabledSearch() ) {
-                               $this->fatalError( "Your database schema is not configured for "
-                                       . "full-text search support. Run update.php.\n" );
-                       }
                }
 
-               if ( $this->db->getType() == 'mysql' ) {
+               if ( $dbw->getType() == 'mysql' ) {
                        $this->dropMysqlTextIndex();
                        $this->clearSearchIndex();
                        $this->populateSearchIndex();
@@ -87,8 +77,9 @@ class RebuildTextIndex extends Maintenance {
         * Populates the search index with content from all pages
         */
        protected function populateSearchIndex() {
-               $res = $this->db->select( 'page', 'MAX(page_id) AS count' );
-               $s = $this->db->fetchObject( $res );
+               $dbw = $this->getDB( DB_MASTER );
+               $res = $dbw->select( 'page', 'MAX(page_id) AS count' );
+               $s = $dbw->fetchObject( $res );
                $count = $s->count;
                $this->output( "Rebuilding index fields for {$count} pages...\n" );
                $n = 0;
@@ -101,7 +92,7 @@ class RebuildTextIndex extends Maintenance {
                        }
                        $end = $n + self::RTI_CHUNK_SIZE - 1;
 
-                       $res = $this->db->select(
+                       $res = $dbw->select(
                                $revQuery['tables'],
                                $revQuery['fields'],
                                [ "page_id BETWEEN $n AND $end", 'page_latest = rev_id' ],
@@ -131,11 +122,12 @@ class RebuildTextIndex extends Maintenance {
         * (MySQL only) Drops fulltext index before populating the table.
         */
        private function dropMysqlTextIndex() {
-               $searchindex = $this->db->tableName( 'searchindex' );
-               if ( $this->db->indexExists( 'searchindex', 'si_title', __METHOD__ ) ) {
+               $dbw = $this->getDB( DB_MASTER );
+               $searchindex = $dbw->tableName( 'searchindex' );
+               if ( $dbw->indexExists( 'searchindex', 'si_title', __METHOD__ ) ) {
                        $this->output( "Dropping index...\n" );
                        $sql = "ALTER TABLE $searchindex DROP INDEX si_title, DROP INDEX si_text";
-                       $this->db->query( $sql, __METHOD__ );
+                       $dbw->query( $sql, __METHOD__ );
                }
        }
 
@@ -143,11 +135,12 @@ class RebuildTextIndex extends Maintenance {
         * (MySQL only) Adds back fulltext index after populating the table.
         */
        private function createMysqlTextIndex() {
-               $searchindex = $this->db->tableName( 'searchindex' );
+               $dbw = $this->getDB( DB_MASTER );
+               $searchindex = $dbw->tableName( 'searchindex' );
                $this->output( "\nRebuild the index...\n" );
                foreach ( [ 'si_title', 'si_text' ] as $field ) {
                        $sql = "ALTER TABLE $searchindex ADD FULLTEXT $field ($field)";
-                       $this->db->query( $sql, __METHOD__ );
+                       $dbw->query( $sql, __METHOD__ );
                }
        }
 
@@ -155,8 +148,9 @@ class RebuildTextIndex extends Maintenance {
         * Deletes everything from search index.
         */
        private function clearSearchIndex() {
+               $dbw = $this->getDB( DB_MASTER );
                $this->output( 'Clearing searchindex table...' );
-               $this->db->delete( 'searchindex', '*', __METHOD__ );
+               $dbw->delete( 'searchindex', '*', __METHOD__ );
                $this->output( "Done\n" );
        }
 }
index 21d8b2d..612c092 100644 (file)
@@ -67,7 +67,7 @@ class MwSql extends Maintenance {
                $replicaDB = $this->getOption( 'replicadb', $this->getOption( 'slave', '' ) );
                if ( $replicaDB === 'any' ) {
                        $index = DB_REPLICA;
-               } elseif ( $replicaDB != '' ) {
+               } elseif ( $replicaDB !== '' ) {
                        $index = null;
                        $serverCount = $lb->getServerCount();
                        for ( $i = 0; $i < $serverCount; ++$i ) {
@@ -76,7 +76,7 @@ class MwSql extends Maintenance {
                                        break;
                                }
                        }
-                       if ( $index === null ) {
+                       if ( $index === null || $index === $lb->getWriterIndex() ) {
                                $this->fatalError( "No replica DB server configured with the name '$replicaDB'." );
                        }
                } else {
index bfd4d97..b9d5792 100644 (file)
  * @ingroup Maintenance
  */
 
+use MediaWiki\MediaWikiServices;
+
+use Wikimedia\Rdbms\DatabaseSqlite;
+
 require_once __DIR__ . '/Maintenance.php';
 
 /**
@@ -59,37 +63,37 @@ class SqliteMaintenance extends Maintenance {
                        return;
                }
 
-               $this->db = $this->getDB( DB_MASTER );
-
-               if ( $this->db->getType() != 'sqlite' ) {
+               $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
+               $dbw = $lb->getConnection( DB_MASTER );
+               if ( !( $dbw instanceof DatabaseSqlite ) ) {
                        $this->error( "This maintenance script requires a SQLite database.\n" );
 
                        return;
                }
 
                if ( $this->hasOption( 'vacuum' ) ) {
-                       $this->vacuum();
+                       $this->vacuum( $dbw );
                }
 
                if ( $this->hasOption( 'integrity' ) ) {
-                       $this->integrityCheck();
+                       $this->integrityCheck( $dbw );
                }
 
                if ( $this->hasOption( 'backup-to' ) ) {
-                       $this->backup( $this->getOption( 'backup-to' ) );
+                       $this->backup( $dbw, $this->getOption( 'backup-to' ) );
                }
        }
 
-       private function vacuum() {
-               $prevSize = filesize( $this->db->getDbFilePath() );
+       private function vacuum( DatabaseSqlite $dbw ) {
+               $prevSize = filesize( $dbw->getDbFilePath() );
                if ( $prevSize == 0 ) {
                        $this->fatalError( "Can't vacuum an empty database.\n" );
                }
 
                $this->output( 'VACUUM: ' );
-               if ( $this->db->query( 'VACUUM' ) ) {
+               if ( $dbw->query( 'VACUUM' ) ) {
                        clearstatcache();
-                       $newSize = filesize( $this->db->getDbFilePath() );
+                       $newSize = filesize( $dbw->getDbFilePath() );
                        $this->output( sprintf( "Database size was %d, now %d (%.1f%% reduction).\n",
                                $prevSize, $newSize, ( $prevSize - $newSize ) * 100.0 / $prevSize ) );
                } else {
@@ -97,9 +101,9 @@ class SqliteMaintenance extends Maintenance {
                }
        }
 
-       private function integrityCheck() {
+       private function integrityCheck( DatabaseSqlite $dbw ) {
                $this->output( "Performing database integrity checks:\n" );
-               $res = $this->db->query( 'PRAGMA integrity_check' );
+               $res = $dbw->query( 'PRAGMA integrity_check' );
 
                if ( !$res || $res->numRows() == 0 ) {
                        $this->error( "Error: integrity check query returned nothing.\n" );
@@ -112,10 +116,10 @@ class SqliteMaintenance extends Maintenance {
                }
        }
 
-       private function backup( $fileName ) {
+       private function backup( DatabaseSqlite $dbw, $fileName ) {
                $this->output( "Backing up database:\n   Locking..." );
-               $this->db->query( 'BEGIN IMMEDIATE TRANSACTION', __METHOD__ );
-               $ourFile = $this->db->getDbFilePath();
+               $dbw->query( 'BEGIN IMMEDIATE TRANSACTION', __METHOD__ );
+               $ourFile = $dbw->getDbFilePath();
                $this->output( "   Copying database file $ourFile to $fileName... " );
                Wikimedia\suppressWarnings();
                if ( !copy( $ourFile, $fileName ) ) {
@@ -124,7 +128,7 @@ class SqliteMaintenance extends Maintenance {
                }
                Wikimedia\restoreWarnings();
                $this->output( "   Releasing lock...\n" );
-               $this->db->query( 'COMMIT TRANSACTION', __METHOD__ );
+               $dbw->query( 'COMMIT TRANSACTION', __METHOD__ );
        }
 
        private function checkSyntax() {
index 254ed38..df15abf 100644 (file)
@@ -1173,7 +1173,7 @@ CREATE TABLE /*_*/image (
   -- see https://www.iana.org/assignments/media-types/
   img_minor_mime varbinary(100) NOT NULL default "unknown",
 
-  -- Description field as entered by the uploader.
+  -- Foreign key to comment table, which contains the description field as entered by the uploader.
   -- This is displayed in image upload history and logs.
   img_description_id bigint unsigned NOT NULL,
 
index d84ec5c..a3534f8 100755 (executable)
@@ -124,10 +124,6 @@ class UpdateMediaWiki extends Maintenance {
 
                $this->output( "MediaWiki {$wgVersion} Updater\n\n" );
 
-               foreach ( SpecialVersion::getSoftwareInformation() as $name => $version ) {
-                       $this->output( "{$name}: {$version}\n" );
-               }
-
                wfWaitForSlaves();
 
                if ( !$this->hasOption( 'skip-compat-checks' ) ) {
index 044b9be..1def4bd 100644 (file)
           "requires": {
             "lodash": "^4.17.11"
           }
+        },
+        "lodash": {
+          "version": "4.17.15",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+          "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
+          "dev": true
         }
       }
     },
         }
       }
     },
-    "humanize-duration": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.15.3.tgz",
-      "integrity": "sha512-BMz6w8p3NVa6QP9wDtqUkXfwgBqDaZ5z/np0EYdoWrLqL849Onp6JWMXMhbHtuvO9jUThLN5H1ThRQ8dUWnYkA==",
-      "dev": true
-    },
     "iconv-lite": {
       "version": "0.4.24",
       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
       }
     },
     "lodash": {
-      "version": "4.17.11",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
-      "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
+      "version": "4.17.15",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+      "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
       "dev": true
     },
     "lodash.get": {
       }
     },
     "mixin-deep": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz",
-      "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==",
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
+      "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==",
       "dev": true,
       "requires": {
         "for-in": "^1.0.2",
       "dev": true
     },
     "set-value": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz",
-      "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==",
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
+      "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
       "dev": true,
       "requires": {
         "extend-shallow": "^2.0.1",
       }
     },
     "union-value": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
-      "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
+      "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
       "dev": true,
       "requires": {
         "arr-union": "^3.1.0",
         "get-value": "^2.0.6",
         "is-extendable": "^0.1.1",
-        "set-value": "^0.4.3"
-      },
-      "dependencies": {
-        "extend-shallow": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
-          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
-          "dev": true,
-          "requires": {
-            "is-extendable": "^0.1.0"
-          }
-        },
-        "set-value": {
-          "version": "0.4.3",
-          "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz",
-          "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=",
-          "dev": true,
-          "requires": {
-            "extend-shallow": "^2.0.1",
-            "is-extendable": "^0.1.1",
-            "is-plain-object": "^2.0.1",
-            "to-object-path": "^0.3.0"
-          }
-        }
+        "set-value": "^2.0.1"
       }
     },
     "uniq": {
         "sauce-connect-launcher": "~1.2.3"
       }
     },
-    "wdio-spec-reporter": {
-      "version": "0.1.5",
-      "resolved": "https://registry.npmjs.org/wdio-spec-reporter/-/wdio-spec-reporter-0.1.5.tgz",
-      "integrity": "sha512-MqvgTow8hFwhFT47q67JwyJyeynKodGRQCxF7ijKPGfsaG1NLssbXYc0JhiL7SiAyxnQxII0UxzTCd3I6sEdkg==",
-      "dev": true,
-      "requires": {
-        "babel-runtime": "~6.26.0",
-        "chalk": "^2.3.0",
-        "humanize-duration": "~3.15.0"
-      }
-    },
     "wdio-sync": {
       "version": "0.7.3",
       "resolved": "https://registry.npmjs.org/wdio-sync/-/wdio-sync-0.7.3.tgz",
index f16b605..2b88303 100644 (file)
     "postcss-less": "2.0.0",
     "qunit": "2.9.1",
     "stylelint-config-wikimedia": "0.6.0",
+    "wdio-dot-reporter": "0.0.10",
     "wdio-junit-reporter": "0.4.4",
     "wdio-mediawiki": "file:tests/selenium/wdio-mediawiki",
     "wdio-mocha-framework": "0.6.4",
     "wdio-sauce-service": "0.4.14",
-    "wdio-spec-reporter": "0.1.5",
     "webdriverio": "4.14.4"
   }
 }
index 09998da..c47a3f4 100644 (file)
@@ -115,11 +115,15 @@ html5shiv:
   integrity: sha384-RPXhaTf22QktT8KTwZ6bUz/C+7CnccaIw5W/y/t0FW5WSDGj3wc3YtRIJC0w47in
 
 jquery:
-  type: file
-  src: https://code.jquery.com/jquery-3.3.1.js
-  # Integrity from link modals https://code.jquery.com/jquery/
-  integrity: sha256-2Kok7MbOyxpgUVvAk/HJ2jigOSYS2auK4Pfzbm7uH60=
-  dest: jquery.js
+  type: multi-file
+  files:
+    # Integrities from link modals https://code.jquery.com/jquery/
+    jquery.migrate.js:
+      src: https://code.jquery.com/jquery-migrate-3.0.1.js
+      integrity: sha256-VvnF+Zgpd00LL73P2XULYXEn6ROvoFaa/vbfoiFlZZ4=
+    jquery.js:
+      src: https://code.jquery.com/jquery-3.3.1.js
+      integrity: sha256-2Kok7MbOyxpgUVvAk/HJ2jigOSYS2auK4Pfzbm7uH60=
 
 jquery.chosen:
   type: multi-file
index 68a4326..0681831 100644 (file)
@@ -1,8 +1,9 @@
---- jquery-3.3.1.js    2019-04-01 08:39:29.000000000 +0200
-+++ jquery-3.3.1.js    2019-04-01 09:02:39.000000000 +0200
-@@ -260,8 +260,9 @@ jQuery.extend = jQuery.fn.extend = function() {
-                       for ( name in options ) {
-                               src = target[ name ];
+diff --git a/resources/lib/jquery/jquery.js b/resources/lib/jquery/jquery.js
+index 9b5206bcc6..34a5703d80 100644
+--- a/resources/lib/jquery/jquery.js
++++ b/resources/lib/jquery/jquery.js
+@@ -261,8 +261,9 @@ jQuery.extend = jQuery.fn.extend = function() {
+                               src = target[ name ];
                                copy = options[ name ];
  
 +                              // Prevent Object.prototype pollution
diff --git a/resources/lib/jquery/jquery.migrate-3.0.1.patch b/resources/lib/jquery/jquery.migrate-3.0.1.patch
new file mode 100644 (file)
index 0000000..6036cc9
--- /dev/null
@@ -0,0 +1,71 @@
+diff --git a/resources/lib/jquery/jquery.migrate.js b/resources/lib/jquery/jquery.migrate.js
+index 6ba8af4a42..711e424a39 100644
+--- a/resources/lib/jquery/jquery.migrate.js
++++ b/resources/lib/jquery/jquery.migrate.js
+@@ -1,6 +1,14 @@
+ /*!
+  * jQuery Migrate - v3.0.1 - 2017-09-26
+  * Copyright jQuery Foundation and other contributors
++ *
++ * Patched for MediaWiki:
++ * - Qualify the global lookup for 'jQuery' as 'window.jQuery',
++ *   because within mw.loader.implement() for 'jquery', the closure
++ *   specifies '$' and 'jQuery', which are undefined.
++ * - Add mw.track instrumentation for statistics.
++ * - Disable jQuery.migrateTrace by default. They are slow and
++ *   redundant given console.warn() already provides a trace.
+  */
+ ;( function( factory ) {
+       if ( typeof define === "function" && define.amd ) {
+@@ -15,7 +23,8 @@
+       } else {
+               // Browser globals
+-              factory( jQuery, window );
++              // PATCH: Qualify jQuery lookup as window.jQuery. --Krinkle
++              factory( window.jQuery, window );
+       }
+ } )( function( jQuery, window ) {
+ "use strict";
+@@ -58,7 +67,8 @@ jQuery.migrateWarnings = [];
+ // Set to false to disable traces that appear with warnings
+ if ( jQuery.migrateTrace === undefined ) {
+-      jQuery.migrateTrace = true;
++      // PATCH: Disable extra console.trace() call --Krinkle
++      jQuery.migrateTrace = false;
+ }
+ // Forget any warnings we've already given; public
+@@ -72,6 +82,10 @@ function migrateWarn( msg ) {
+       if ( !warnedAbout[ msg ] ) {
+               warnedAbout[ msg ] = true;
+               jQuery.migrateWarnings.push( msg );
++              // PATCH: Add instrumentation for statistics --Krinkle
++              if ( window.mw && window.mw.track ) {
++                      window.mw.track( "mw.deprecate", "jquery-migrate" );
++              }
+               if ( console && console.warn && !jQuery.migrateMute ) {
+                       console.warn( "JQMIGRATE: " + msg );
+                       if ( jQuery.migrateTrace && console.trace ) {
+@@ -466,20 +480,6 @@ jQuery.each( [ "load", "unload", "error" ], function( _, name ) {
+ } );
+-jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " +
+-      "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
+-      "change select submit keydown keypress keyup contextmenu" ).split( " " ),
+-      function( i, name ) {
+-
+-      // Handle event binding
+-      jQuery.fn[ name ] = function( data, fn ) {
+-              migrateWarn( "jQuery.fn." + name + "() event shorthand is deprecated" );
+-              return arguments.length > 0 ?
+-                      this.on( name, null, data, fn ) :
+-                      this.trigger( name );
+-      };
+-} );
+-
+ // Trigger "ready" event only once, on document ready
+ jQuery( function() {
+       jQuery( window.document ).triggerHandler( "ready" );
index 4343ecc..a91e57a 100644 (file)
                 * @param {boolean} [options.strictMode=false] Trigger strict mode parsing of the url.
                 * @param {boolean} [options.overrideKeys=false] Whether to let duplicate query parameters
                 *  override each other (`true`) or automagically convert them to an array (`false`).
+                * @param {boolean} [options.arrayParams=false] Whether to parse array query parameters (e.g.
+                *  `&foo[0]=a&foo[1]=b` or `&foo[]=a&foo[]=b`) or leave them alone. Currently this does not
+                *  handle associative or multi-dimensional arrays, but that may be improved in the future.
+                *  Implies `overrideKeys: true` (query parameters without `[...]` are not parsed as arrays).
                 * @throws {Error} when the query string or fragment contains an unknown % sequence
                 */
                function Uri( uri, options ) {
                        options = typeof options === 'object' ? options : { strictMode: !!options };
                        options = $.extend( {
                                strictMode: false,
-                               overrideKeys: false
+                               overrideKeys: false,
+                               arrayParams: false
                        }, options );
 
+                       this.arrayParams = options.arrayParams;
+
                        if ( uri !== undefined && uri !== null && uri !== '' ) {
                                if ( typeof uri === 'string' ) {
                                        this.parse( uri, options );
                                // using replace to iterate over a string
                                if ( uri.query ) {
                                        uri.query.replace( /(?:^|&)([^&=]*)(?:(=)([^&]*))?/g, function ( match, k, eq, v ) {
+                                               var arrayKeyMatch, i;
                                                if ( k ) {
                                                        k = Uri.decode( k );
                                                        v = ( eq === '' || eq === undefined ) ? null : Uri.decode( v );
+                                                       arrayKeyMatch = k.match( /^([^[]+)\[(\d*)\]$/ );
+
+                                                       // If arrayParams and this parameter name contains an array index...
+                                                       if ( options.arrayParams && arrayKeyMatch ) {
+                                                               // Remove the index from parameter name
+                                                               k = arrayKeyMatch[ 1 ];
+
+                                                               // Turn the parameter value into an array (throw away anything else)
+                                                               if ( !Array.isArray( q[ k ] ) ) {
+                                                                       q[ k ] = [];
+                                                               }
+
+                                                               i = arrayKeyMatch[ 2 ];
+                                                               if ( i === '' ) {
+                                                                       // If no explicit index, append at the end
+                                                                       i = q[ k ].length;
+                                                               }
+
+                                                               q[ k ][ i ] = v;
 
                                                        // If overrideKeys, always (re)set top level value.
                                                        // If not overrideKeys but this key wasn't set before, then we set it as well.
-                                                       if ( options.overrideKeys || !hasOwn.call( q, k ) ) {
+                                                       // arrayParams implies overrideKeys (no array handling for non-array params).
+                                                       } else if ( options.arrayParams || options.overrideKeys || !hasOwn.call( q, k ) ) {
                                                                q[ k ] = v;
 
                                                        // Use arrays if overrideKeys is false and key was already seen before
                         * @return {string}
                         */
                        getQueryString: function () {
-                               var args = [];
+                               var args = [],
+                                       arrayParams = this.arrayParams;
                                // eslint-disable-next-line no-jquery/no-each-util
                                $.each( this.query, function ( key, val ) {
                                        var k = Uri.encode( key ),
-                                               vals = Array.isArray( val ) ? val : [ val ];
-                                       vals.forEach( function ( v ) {
+                                               isArrayParam = Array.isArray( val ),
+                                               vals = isArrayParam ? val : [ val ];
+                                       vals.forEach( function ( v, i ) {
+                                               var ki = k;
+                                               if ( arrayParams && isArrayParam ) {
+                                                       ki += Uri.encode( '[' + i + ']' );
+                                               }
                                                if ( v === null ) {
-                                                       args.push( k );
+                                                       args.push( ki );
                                                } else if ( k === 'title' ) {
-                                                       args.push( k + '=' + mw.util.wikiUrlencode( v ) );
+                                                       args.push( ki + '=' + mw.util.wikiUrlencode( v ) );
                                                } else {
-                                                       args.push( k + '=' + Uri.encode( v ) );
+                                                       args.push( ki + '=' + Uri.encode( v ) );
                                                }
                                        } );
                                } );
index 22bac08..b129303 100644 (file)
                        )
                );
 
-               if ( this.cache ) {
-                       this.cache.set( pageData );
-               }
-
                // Offer the exact text as a suggestion if the page exists
                if ( this.addQueryInput && pageExists && !pageExistsExact ) {
                        titles.unshift( this.getQueryValue() );
+                       // Ensure correct page metadata gets used
+                       pageData[ this.getQueryValue() ] = pageData[ titleObj.getPrefixedText() ];
+               }
+
+               if ( this.cache ) {
+                       this.cache.set( pageData );
                }
 
                for ( i = 0, len = titles.length; i < len; i++ ) {
index ad05c6f..a3249de 100644 (file)
                         *             // From mw.loader.register()
                         *             'version': '########' (hash)
                         *             'dependencies': ['required.foo', 'bar.also', ...]
-                        *             'group': 'somegroup', (or) null
+                        *             'group': string, integer, (or) null
                         *             'source': 'local', (or) 'anotherwiki'
                         *             'skip': 'return !!window.Example', (or) null, (or) boolean result of skip
                         *             'module': export Object
 
                                dependencies.forEach( function ( module ) {
                                        // Only queue modules that are still in the initial 'registered' state
-                                       // (not ones already loading, ready or error).
+                                       // (e.g. not ones already loading or loaded etc.).
                                        if ( registry[ module ].state === 'registered' && queue.indexOf( module ) === -1 ) {
-                                               // Private modules must be embedded in the page. Don't bother queuing
-                                               // these as the server will deny them anyway (T101806).
-                                               if ( registry[ module ].group === 'private' ) {
-                                                       setAndPropagate( module, 'error' );
-                                               } else {
-                                                       queue.push( module );
-                                               }
+                                               queue.push( module );
                                        }
                                } );
 
                                                // Optimisation: Inherit (Object.create), not copy ($.extend)
                                                currReqBase = Object.create( reqBase );
                                                // User modules require a user name in the query string.
-                                               if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) {
+                                               if ( group === $VARS.groupUser && mw.config.get( 'wgUserName' ) !== null ) {
                                                        currReqBase.user = mw.config.get( 'wgUserName' );
                                                }
 
                                        packageExports: {},
                                        version: String( version || '' ),
                                        dependencies: dependencies || [],
-                                       group: typeof group === 'string' ? group : null,
+                                       group: typeof group === 'undefined' ? null : group,
                                        source: typeof source === 'string' ? source : 'local',
                                        state: 'registered',
                                        skip: typeof skip === 'string' ? skip : null
                                                        descriptor.state !== 'ready' ||
                                                        // Unversioned, private, or site-/user-specific
                                                        !descriptor.version ||
-                                                       descriptor.group === 'private' ||
-                                                       descriptor.group === 'user' ||
+                                                       descriptor.group === $VARS.groupPrivate ||
+                                                       descriptor.group === $VARS.groupUser ||
                                                        // Partial descriptor
                                                        // (e.g. skipped module, or style module with state=ready)
                                                        [ descriptor.script, descriptor.style, descriptor.messages,
index 99b548e..7c8df1a 100644 (file)
@@ -221,9 +221,6 @@ $wgAutoloadClasses += [
        # tests/phpunit/unit/includes
        'BadFileLookupTest' => "$testDir/phpunit/unit/includes/BadFileLookupTest.php",
 
-       # tests/phpunit/unit/includes/language
-       'LanguageNameUtilsTestTrait' => "$testDir/phpunit/unit/includes/language/LanguageNameUtilsTestTrait.php",
-
        # tests/phpunit/unit/includes/libs/filebackend/fsfile
        'TempFSFileTestTrait' => "$testDir/phpunit/unit/includes/libs/filebackend/fsfile/TempFSFileTestTrait.php",
 
index 36c3fe2..c8b8ef9 100644 (file)
@@ -313,7 +313,7 @@ class ParserTestRunner {
                        'class' => NullLockManager::class,
                ] ];
                $reset = function () {
-                       LockManagerGroup::destroySingletons();
+                       MediaWikiServices::getInstance()->resetServiceForTesting( 'LockManagerGroupFactory' );
                };
                $setup[] = $reset;
                $teardown[] = $reset;
index 6599041..0fa91d4 100644 (file)
@@ -8073,12 +8073,23 @@ File containing double quotes and spaces
 !! wikitext
 [[File:Cool "Gator".png]]
 !! html/php+tidy
-<p><a href="/index.php?title=Special:Upload&amp;wpDestFile=Cool_%22Gator%22.png" class="new" title="File:Cool &quot;Gator&quot;.png">File:Cool &quot;Gator&quot;.png</a>
+<p><a href="/index.php?title=Special:Upload&amp;wpDestFile=Cool_%22Gator%22.png" class="new" title="File:Cool &quot;Gator&quot;.png">File:Cool "Gator".png</a>
 </p>
 !! html/parsoid
 <p><figure-inline class="mw-default-size" typeof="mw:Error mw:Image" data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}]}'><a href="./Special:FilePath/Cool_%22Gator%22.png"><span resource='./File:Cool_"Gator".png' data-parsoid='{"a":{"resource":"./File:Cool_\"Gator\".png"},"sa":{"resource":"File:Cool \"Gator\".png"}}'>File:Cool "Gator".png</span></a></figure-inline></p>
 !! end
 
+!! test
+File containing single quotes
+!! wikitext
+[[File:Foo's ''italic'' bar.jpg]]
+[[File:Foo's ''italic'' bar.jpg|Foo's ''italic'' bar]]
+!! html/php+tidy
+<p><a href="/index.php?title=Special:Upload&amp;wpDestFile=Foo%27s_%27%27italic%27%27_bar.jpg" class="new" title="File:Foo&#39;s &#39;&#39;italic&#39;&#39; bar.jpg">File:Foo's <i>italic</i> bar.jpg</a>
+<a href="/index.php?title=Special:Upload&amp;wpDestFile=Foo%27s_%27%27italic%27%27_bar.jpg" class="new" title="File:Foo&#39;s &#39;&#39;italic&#39;&#39; bar.jpg">Foo's italic bar</a>
+</p>
+!! end
+
 !! test
 Redirect containing double quotes and spaces
 !! wikitext
@@ -8139,8 +8150,8 @@ Broken image links with HTML captions (T41700)
 [[File:Nonexistent|&lt;]]
 [[File:Nonexistent|a<i>b</i>c]]
 !! html/php
-<p><a href="/index.php?title=Special:Upload&amp;wpDestFile=Nonexistent" class="new" title="File:Nonexistent">&lt;script&gt;&lt;/script&gt;</a>
-<a href="/index.php?title=Special:Upload&amp;wpDestFile=Nonexistent" class="new" title="File:Nonexistent">&lt;script&gt;&lt;/script&gt;</a>
+<p><a href="/index.php?title=Special:Upload&amp;wpDestFile=Nonexistent" class="new" title="File:Nonexistent">&lt;script>&lt;/script></a>
+<a href="/index.php?title=Special:Upload&amp;wpDestFile=Nonexistent" class="new" title="File:Nonexistent">&lt;script>&lt;/script></a>
 <a href="/index.php?title=Special:Upload&amp;wpDestFile=Nonexistent" class="new" title="File:Nonexistent">&lt;</a>
 <a href="/index.php?title=Special:Upload&amp;wpDestFile=Nonexistent" class="new" title="File:Nonexistent">abc</a>
 </p>
index af5d88b..33518ef 100644 (file)
@@ -404,7 +404,6 @@ abstract class MediaWikiIntegrationTestCase extends PHPUnit\Framework\TestCase {
 
                $wgRequest = new FauxRequest();
                MediaWiki\Session\SessionManager::resetCache();
-               Language::clearCaches();
        }
 
        public function run( PHPUnit_Framework_TestResult $result = null ) {
@@ -1503,7 +1502,6 @@ abstract class MediaWikiIntegrationTestCase extends PHPUnit\Framework\TestCase {
 
                if ( !isset( $db->_originalTablePrefix ) ) {
                        $oldPrefix = $db->tablePrefix();
-
                        if ( $oldPrefix === $prefix ) {
                                // table already has the correct prefix, but presumably no cloned tables
                                $oldPrefix = self::$oldTablePrefix;
@@ -1513,11 +1511,13 @@ abstract class MediaWikiIntegrationTestCase extends PHPUnit\Framework\TestCase {
                        $tablesCloned = self::listTables( $db );
                        $dbClone = new CloneDatabase( $db, $tablesCloned, $prefix, $oldPrefix );
                        $dbClone->useTemporaryTables( self::$useTemporaryTables );
-
                        $dbClone->cloneTableStructure();
 
                        $db->tablePrefix( $prefix );
                        $db->_originalTablePrefix = $oldPrefix;
+
+                       $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
+                       $lb->setTempTablesOnlyMode( self::$useTemporaryTables, $lb->getLocalDomainID() );
                }
 
                return true;
@@ -1864,8 +1864,10 @@ abstract class MediaWikiIntegrationTestCase extends PHPUnit\Framework\TestCase {
 
                $dbClone = new CloneDatabase( $db, $tables, $db->tablePrefix(), $db->_originalTablePrefix );
                $dbClone->useTemporaryTables( self::$useTemporaryTables );
-
                $dbClone->cloneTableStructure();
+
+               $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
+               $lb->setTempTablesOnlyMode( self::$useTemporaryTables, $lb->getLocalDomainID() );
        }
 
        /**
index 00b8d18..6520fc5 100644 (file)
@@ -3029,6 +3029,35 @@ class OutputPageTest extends MediaWikiTestCase {
                ];
        }
 
+       /**
+        * @param int $titleLastRevision Last Title revision to set
+        * @param int $outputRevision Revision stored in OutputPage
+        * @param bool $expectedResult Expected result of $output->isRevisionCurrent call
+        * @covers OutputPage::isRevisionCurrent
+        * @dataProvider provideIsRevisionCurrent
+        */
+       public function testIsRevisionCurrent( $titleLastRevision, $outputRevision, $expectedResult ) {
+               $titleMock = $this->getMock( Title::class, [], [], '', false );
+               $titleMock->expects( $this->any() )
+                       ->method( 'getLatestRevID' )
+                       ->willReturn( $titleLastRevision );
+
+               $output = $this->newInstance( [], null, [ 'notitle' => true ] );
+               $output->setTitle( $titleMock );
+               $output->setRevisionId( $outputRevision );
+               $this->assertEquals( $expectedResult, $output->isRevisionCurrent() );
+       }
+
+       public function provideIsRevisionCurrent() {
+               return [
+                       [ 10, null, true ],
+                       [ 42, 42, true ],
+                       [ null, 0, true ],
+                       [ 42, 47, false ],
+                       [ 47, 42, false ]
+               ];
+       }
+
        /**
         * @return OutputPage
         */
diff --git a/tests/phpunit/includes/Rest/EntryPointTest.php b/tests/phpunit/includes/Rest/EntryPointTest.php
new file mode 100644 (file)
index 0000000..b599e9d
--- /dev/null
@@ -0,0 +1,96 @@
+<?php
+
+namespace MediaWiki\Tests\Rest;
+
+use EmptyBagOStuff;
+use GuzzleHttp\Psr7\Uri;
+use GuzzleHttp\Psr7\Stream;
+use MediaWiki\Rest\BasicAccess\StaticBasicAuthorizer;
+use MediaWiki\Rest\Handler;
+use MediaWiki\Rest\EntryPoint;
+use MediaWiki\Rest\RequestData;
+use MediaWiki\Rest\ResponseFactory;
+use MediaWiki\Rest\Router;
+use RequestContext;
+use WebResponse;
+
+/**
+ * @covers \MediaWiki\Rest\EntryPoint
+ * @covers \MediaWiki\Rest\Router
+ */
+class EntryPointTest extends \MediaWikiTestCase {
+       private static $mockHandler;
+
+       private function createRouter() {
+               global $IP;
+
+               return new Router(
+                       [ "$IP/tests/phpunit/unit/includes/Rest/testRoutes.json" ],
+                       [],
+                       '/rest',
+                       new EmptyBagOStuff(),
+                       new ResponseFactory(),
+                       new StaticBasicAuthorizer() );
+       }
+
+       private function createWebResponse() {
+               return $this->getMockBuilder( WebResponse::class )
+                       ->setMethods( [ 'header' ] )
+                       ->getMock();
+       }
+
+       public static function mockHandlerHeader() {
+               return new class extends Handler {
+                       public function execute() {
+                               $response = $this->getResponseFactory()->create();
+                               $response->setHeader( 'Foo', 'Bar' );
+                               return $response;
+                       }
+               };
+       }
+
+       public function testHeader() {
+               $webResponse = $this->createWebResponse();
+               $webResponse->expects( $this->any() )
+                       ->method( 'header' )
+                       ->withConsecutive(
+                               [ 'HTTP/1.1 200 OK', true, null ],
+                               [ 'Foo: Bar', true, null ]
+                       );
+
+               $entryPoint = new EntryPoint(
+                       RequestContext::getMain(),
+                       new RequestData( [ 'uri' => new Uri( '/rest/mock/EntryPoint/header' ) ] ),
+                       $webResponse,
+                       $this->createRouter() );
+               $entryPoint->execute();
+               $this->assertTrue( true );
+       }
+
+       public static function mockHandlerBodyRewind() {
+               return new class extends Handler {
+                       public function execute() {
+                               $response = $this->getResponseFactory()->create();
+                               $stream = new Stream( fopen( 'php://memory', 'w+' ) );
+                               $stream->write( 'hello' );
+                               $response->setBody( $stream );
+                               return $response;
+                       }
+               };
+       }
+
+       /**
+        * Make sure EntryPoint rewinds a seekable body stream before reading.
+        */
+       public function testBodyRewind() {
+               $entryPoint = new EntryPoint(
+                       RequestContext::getMain(),
+                       new RequestData( [ 'uri' => new Uri( '/rest/mock/EntryPoint/bodyRewind' ) ] ),
+                       $this->createWebResponse(),
+                       $this->createRouter() );
+               ob_start();
+               $entryPoint->execute();
+               $this->assertSame( 'hello', ob_get_clean() );
+       }
+
+}
index 47d3b92..7a4ea2d 100644 (file)
@@ -12,6 +12,7 @@ use Psr\Log\NullLogger;
 use WANObjectCache;
 use Wikimedia\Rdbms\IDatabase;
 use Wikimedia\Rdbms\LoadBalancer;
+use Wikimedia\Rdbms\MaintainableDBConnRef;
 use Wikimedia\TestingAccessWrapper;
 
 /**
@@ -51,8 +52,10 @@ class NameTableStoreTest extends MediaWikiTestCase {
                        ->disableOriginalConstructor()
                        ->getMock();
                $mock->expects( $this->any() )
-                       ->method( 'getConnection' )
-                       ->willReturn( $db );
+                       ->method( 'getConnectionRef' )
+                       ->willReturnCallback( function ( $i ) use ( $mock, $db ) {
+                               return new MaintainableDBConnRef( $mock, $db, $i );
+                       } );
                return $mock;
        }
 
index c5baeed..91c4a13 100644 (file)
@@ -943,11 +943,10 @@ class TitlePermissionTest extends MediaWikiLangTestCase {
                        'address' => '127.0.8.1',
                        'by' => $this->user->getId(),
                        'reason' => 'no reason given',
-                       'timestamp' => $prev + 3600,
+                       'timestamp' => $prev,
                        'auto' => true,
                        'expiry' => 0
                ] );
-               $this->user->mBlock->setTimestamp( 0 );
                $this->assertEquals( [ [ 'autoblockedtext',
                                "[[User:Useruser|\u{202A}Useruser\u{202C}]]", 'no reason given', '127.0.0.1',
                                "\u{202A}Useruser\u{202C}", null, 'infinite', '127.0.8.1',
index 93c5345..cf835ce 100644 (file)
@@ -119,7 +119,7 @@ class ApiQuerySearchTest extends ApiTestCase {
         */
        private function mockResultClosure( $title, $setters = [] ) {
                return function () use ( $title, $setters ){
-                       $result = MockSearchResult::newFromTitle( Title::newFromText( $title ) );
+                       $result = new MockSearchResult( Title::newFromText( $title ) );
 
                        foreach ( $setters as $method => $param ) {
                                $result->$method( $param );
index 6308b82..282188d 100644 (file)
@@ -160,7 +160,6 @@ class ApiQuerySiteinfoTest extends ApiTestCase {
                        'wgExtraInterlanguageLinkPrefixes' => [ 'self' ],
                        'wgExtraLanguageNames' => [ 'self' => 'Recursion' ],
                ] );
-               $this->resetServices();
 
                MessageCache::singleton()->enable();
 
index 39526fb..42957b6 100644 (file)
@@ -1,9 +1,4 @@
 <?php
-
-use MediaWiki\Config\ServiceOptions;
-use MediaWiki\Languages\LanguageNameUtils;
-use Psr\Log\NullLogger;
-
 /**
  * @group Database
  * @group Cache
@@ -24,51 +19,8 @@ class LocalisationCacheTest extends MediaWikiTestCase {
         */
        protected function getMockLocalisationCache() {
                global $IP;
-
-               $mockLangNameUtils = $this->createMock( LanguageNameUtils::class );
-               $mockLangNameUtils->method( 'isValidBuiltInCode' )->will( $this->returnCallback(
-                       function ( $code ) {
-                               // Copy-paste, but it's only one line
-                               return (bool)preg_match( '/^[a-z0-9-]{2,}$/', $code );
-                       }
-               ) );
-               $mockLangNameUtils->method( 'isSupportedLanguage' )->will( $this->returnCallback(
-                       function ( $code ) {
-                               return in_array( $code, [
-                                       'ar',
-                                       'arz',
-                                       'ba',
-                                       'de',
-                                       'en',
-                                       'ksh',
-                                       'ru',
-                               ] );
-                       }
-               ) );
-               $mockLangNameUtils->method( 'getMessagesFileName' )->will( $this->returnCallback(
-                       function ( $code ) {
-                               global $IP;
-                               $code = str_replace( '-', '_', ucfirst( $code ) );
-                               return "$IP/languages/messages/Messages$code.php";
-                       }
-               ) );
-               $mockLangNameUtils->expects( $this->never() )->method( $this->anythingBut(
-                       'isValidBuiltInCode', 'isSupportedLanguage', 'getMessagesFileName'
-               ) );
-
-               $lc = $this->getMockBuilder( LocalisationCache::class )
-                       ->setConstructorArgs( [
-                               new ServiceOptions( LocalisationCache::$constructorOptions, [
-                                       'forceRecache' => false,
-                                       'manualRecache' => false,
-                                       'ExtensionMessagesFiles' => [],
-                                       'MessagesDirs' => [],
-                               ] ),
-                               new LCStoreDB( [] ),
-                               new NullLogger,
-                               [],
-                               $mockLangNameUtils
-                       ] )
+               $lc = $this->getMockBuilder( \LocalisationCache::class )
+                       ->setConstructorArgs( [ [ 'store' => 'detect' ] ] )
                        ->setMethods( [ 'getMessagesDirs' ] )
                        ->getMock();
                $lc->expects( $this->any() )->method( 'getMessagesDirs' )
@@ -79,7 +31,7 @@ class LocalisationCacheTest extends MediaWikiTestCase {
                return $lc;
        }
 
-       public function testPluralRulesFallback() {
+       public function testPuralRulesFallback() {
                $cache = $this->getMockLocalisationCache();
 
                $this->assertEquals(
index 43e7075..f037a8c 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 
+use Psr\Log\NullLogger;
 use Wikimedia\Rdbms\TransactionProfiler;
 use Wikimedia\Rdbms\DatabaseDomain;
 use Wikimedia\Rdbms\Database;
@@ -43,19 +44,31 @@ class DatabaseTestHelper extends Database {
        protected $unionSupportsOrderAndLimit = true;
 
        public function __construct( $testName, array $opts = [] ) {
+               parent::__construct( $opts + [
+                       'host' => null,
+                       'user' => null,
+                       'password' => null,
+                       'dbname' => null,
+                       'schema' => null,
+                       'tablePrefix' => '',
+                       'flags' => 0,
+                       'cliMode' => $opts['cliMode'] ?? true,
+                       'agent' => '',
+                       'srvCache' => new HashBagOStuff(),
+                       'profiler' => null,
+                       'trxProfiler' => new TransactionProfiler(),
+                       'connLogger' => new NullLogger(),
+                       'queryLogger' => new NullLogger(),
+                       'errorLogger' => function ( Exception $e ) {
+                               wfWarn( get_class( $e ) . ": {$e->getMessage()}" );
+                       },
+                       'deprecationLogger' => function ( $msg ) {
+                               wfWarn( $msg );
+                       }
+               ] );
+
                $this->testName = $testName;
 
-               $this->profiler = null;
-               $this->trxProfiler = new TransactionProfiler();
-               $this->cliMode = $opts['cliMode'] ?? true;
-               $this->connLogger = new \Psr\Log\NullLogger();
-               $this->queryLogger = new \Psr\Log\NullLogger();
-               $this->errorLogger = function ( Exception $e ) {
-                       wfWarn( get_class( $e ) . ": {$e->getMessage()}" );
-               };
-               $this->deprecationLogger = function ( $msg ) {
-                       wfWarn( $msg );
-               };
                $this->currentDomain = DatabaseDomain::newUnspecified();
                $this->open( 'localhost', 'testuser', 'password', 'testdb', null, '' );
        }
index 0615e95..45ad180 100644 (file)
@@ -41,7 +41,7 @@ class LockManagerGroupIntegrationTest extends MediaWikiIntegrationTestCase {
                LockManagerGroup::destroySingletons();
 
                $this->assertSame(
-                       null,
+                       WikiMap::getCurrentWikiDbDomain()->getId(),
                        LockManagerGroup::singleton( null )->config( 'a' )['domain']
                );
        }
diff --git a/tests/phpunit/includes/language/ConverterRuleTest.php b/tests/phpunit/includes/language/ConverterRuleTest.php
new file mode 100644 (file)
index 0000000..1e06142
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * @covers ConverterRule
+ */
+class ConverterRuleTest extends MediaWikiTestCase {
+
+       public function setUp() {
+               parent::setUp();
+               $this->setMwGlobals( 'wgUser', new User );
+       }
+
+       public function testParseEmpty() {
+               $converter = new LanguageConverter( new Language(), 'en' );
+               $rule = new ConverterRule( '', $converter );
+               $rule->parse();
+
+               $this->assertSame( false, $rule->getTitle(), 'title' );
+               $this->assertSame( [], $rule->getConvTable(), 'conversion table' );
+               $this->assertSame( 'none', $rule->getRulesAction(), 'rules action' );
+       }
+
+}
index f496fa3..d239ac1 100644 (file)
@@ -290,6 +290,10 @@ class BagOStuffTest extends MediaWikiTestCase {
 
                $val = $this->cache->incrWithInit( $key, 0, 1, 3 );
                $this->assertEquals( 4, $val, "Correct init value" );
+               $this->cache->delete( $key );
+
+               $val = $this->cache->incrWithInit( $key, 0, 5 );
+               $this->assertEquals( 5, $val, "Correct init value" );
        }
 
        /**
index 329c642..ac988e6 100644 (file)
@@ -1348,6 +1348,35 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                }
        }
 
+       /**
+        * @covers WANObjectCache::get()
+        * @covers WANObjectCache::processCheckKeys()
+        */
+       public function testCheckKeyHoldoff() {
+               $cache = $this->cache;
+               $key = wfRandomString();
+               $checkKey = wfRandomString();
+
+               $mockWallClock = 1549343530.2053;
+               $cache->setMockTime( $mockWallClock );
+               $cache->touchCheckKey( $checkKey, 8 );
+
+               $mockWallClock += 1;
+               $cache->set( $key, 1, 60 );
+               $this->assertEquals( 1, $cache->get( $key, $curTTL, [ $checkKey ] ) );
+               $this->assertLessThan( 0, $curTTL, "Key in hold-off due to check key" );
+
+               $mockWallClock += 3;
+               $cache->set( $key, 1, 60 );
+               $this->assertEquals( 1, $cache->get( $key, $curTTL, [ $checkKey ] ) );
+               $this->assertLessThan( 0, $curTTL, "Key in hold-off due to check key" );
+
+               $mockWallClock += 10;
+               $cache->set( $key, 1, 60 );
+               $this->assertEquals( 1, $cache->get( $key, $curTTL, [ $checkKey ] ) );
+               $this->assertGreaterThan( 0, $curTTL, "Key not in hold-off due to check key" );
+       }
+
        /**
         * @covers WANObjectCache::delete
         * @covers WANObjectCache::relayDelete
index 839272f..4bb9d5a 100644 (file)
@@ -39,13 +39,13 @@ class LogFormatterTest extends MediaWikiLangTestCase {
                global $wgExtensionMessagesFiles;
                self::$oldExtMsgFiles = $wgExtensionMessagesFiles;
                $wgExtensionMessagesFiles['LogTests'] = __DIR__ . '/LogTests.i18n.php';
-               Language::clearCaches();
+               Language::getLocalisationCache()->recache( 'en' );
        }
 
        public static function tearDownAfterClass() {
                global $wgExtensionMessagesFiles;
                $wgExtensionMessagesFiles = self::$oldExtMsgFiles;
-               Language::clearCaches();
+               Language::getLocalisationCache()->recache( 'en' );
 
                parent::tearDownAfterClass();
        }
index 457030f..7ee1ec9 100644 (file)
@@ -148,7 +148,6 @@ class PasswordPolicyChecksTest extends MediaWikiTestCase {
         */
        public function testCheckPopularPasswordBlacklist( $expected, $password ) {
                global $IP;
-               $this->hideDeprecated( 'PasswordPolicyChecks::checkPopularPasswordBlacklist' );
                $this->setMwGlobals( [
                        'wgSitename' => 'sitename',
                        'wgPopularPasswordFile' => "$IP/includes/password/commonpasswords.cdb"
index f6fd824..c748e2c 100644 (file)
@@ -35,6 +35,7 @@ class ResourceLoaderContextTest extends PHPUnit\Framework\TestCase {
                // Misc
                $this->assertEquals( 'ltr', $ctx->getDirection() );
                $this->assertEquals( 'qqx|fallback||||||||', $ctx->getHash() );
+               $this->assertSame( [], $ctx->getReqBase() );
                $this->assertInstanceOf( User::class, $ctx->getUserObj() );
        }
 
@@ -75,6 +76,7 @@ class ResourceLoaderContextTest extends PHPUnit\Framework\TestCase {
                // Misc
                $this->assertEquals( 'ltr', $ctx->getDirection() );
                $this->assertEquals( 'zh|fallback|||styles|||||', $ctx->getHash() );
+               $this->assertSame( [ 'lang' => 'zh' ], $ctx->getReqBase() );
        }
 
        public static function provideDirection() {
index 213eed2..d4462e9 100644 (file)
@@ -326,13 +326,13 @@ mw.loader.register([
         "test.group.foo",
         "{blankVer}",
         [],
-        "x-foo"
+        2
     ],
     [
         "test.group.bar",
         "{blankVer}",
         [],
-        "x-bar"
+        3
     ]
 ]);'
                        ] ],
@@ -640,25 +640,25 @@ mw.loader.register([
         "test.group.foo.1",
         "{blankVer}",
         [],
-        "x-foo"
+        2
     ],
     [
         "test.group.foo.2",
         "{blankVer}",
         [],
-        "x-foo"
+        2
     ],
     [
         "test.group.bar.1",
         "{blankVer}",
         [],
-        "x-bar"
+        3
     ],
     [
         "test.group.bar.2",
         "{blankVer}",
         [],
-        "x-bar",
+        3,
         "example"
     ]
 ]);'
index 86c2e9f..ac4a1ca 100644 (file)
@@ -1095,6 +1095,32 @@ END
                $rl->respond( $context );
        }
 
+       /**
+        * Refuse requests for private modules.
+        *
+        * @covers ResourceLoader::respond
+        */
+       public function testRespondErrorPrivate() {
+               $rl = $this->getMockBuilder( EmptyResourceLoader::class )
+                       ->setMethods( [
+                               'measureResponseTime',
+                               'tryRespondNotModified',
+                               'sendResponseHeaders',
+                       ] )
+                       ->getMock();
+               $rl->register( [
+                       'foo' => [ 'class' => ResourceLoaderTestModule::class ],
+                       'bar' => [ 'class' => ResourceLoaderTestModule::class, 'group' => 'private' ],
+               ] );
+               $context = $this->getResourceLoaderContext(
+                       [ 'modules' => 'foo|bar', 'only' => null ],
+                       $rl
+               );
+
+               $this->expectOutputRegex( '/^\/\*.+Cannot build private module/s' );
+               $rl->respond( $context );
+       }
+
        /**
         * @covers ResourceLoader::respond
         */
diff --git a/tests/phpunit/includes/search/SearchResultTest.php b/tests/phpunit/includes/search/SearchResultTest.php
deleted file mode 100644 (file)
index 0e1e24c..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-
-class SearchResultTest extends MediawikiTestCase {
-       /**
-        * @covers SearchResult::getExtensionData
-        * @covers SearchResult::setExtensionData
-        */
-       public function testExtensionData() {
-               $result = SearchResult::newFromTitle( Title::newMainPage() );
-               $this->assertEquals( [], $result->getExtensionData(), 'starts empty' );
-
-               $data = [ 'hello' => 'world' ];
-               $result->setExtensionData( function () use ( &$data ) {
-                       return $data;
-               } );
-               $this->assertEquals( $data, $result->getExtensionData(), 'can set extension data' );
-               $data['this'] = 'that';
-               $this->assertEquals( $data, $result->getExtensionData(), 'refetches from callback' );
-       }
-
-       /**
-        * @covers SearchResult::getExtensionData
-        * @covers SearchResult::setExtensionData
-        */
-       public function testExtensionDataArrayBC() {
-               $result = SearchResult::newFromTitle( Title::newMainPage() );
-               $data = [ 'hello' => 'world' ];
-               $this->hideDeprecated( 'SearchResult::setExtensionData with array argument' );
-               $this->assertEquals( [], $result->getExtensionData(), 'starts empty' );
-               $result->setExtensionData( $data );
-               $this->assertEquals( $data, $result->getExtensionData(), 'can set extension data' );
-               $data['this'] = 'that';
-               $this->assertNotEquals( $data, $result->getExtensionData(), 'shouldnt hold any reference' );
-
-               $result->setExtensionData( $data );
-               $this->assertEquals( $data, $result->getExtensionData(), 'can replace extension data' );
-       }
-}
diff --git a/tests/phpunit/includes/search/SearchResultTraitTest.php b/tests/phpunit/includes/search/SearchResultTraitTest.php
new file mode 100644 (file)
index 0000000..6700c56
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+
+class SearchResultTraitTest extends MediawikiTestCase {
+       /**
+        * @covers SearchResultTrait::getExtensionData
+        * @covers SearchResultTrait::setExtensionData
+        */
+       public function testExtensionData() {
+               $result = new class() {
+                       use SearchResultTrait;
+               };
+               $this->assertEquals( [], $result->getExtensionData(), 'starts empty' );
+
+               $data = [ 'hello' => 'world' ];
+               $result->setExtensionData( function () use ( &$data ) {
+                       return $data;
+               } );
+               $this->assertEquals( $data, $result->getExtensionData(), 'can set extension data' );
+               $data['this'] = 'that';
+               $this->assertEquals( $data, $result->getExtensionData(), 'refetches from callback' );
+       }
+
+       /**
+        * @covers SearchResultTrait::getExtensionData
+        * @covers SearchResultTrait::setExtensionData
+        */
+       public function testExtensionDataArrayBC() {
+               $result = new class() {
+                       use SearchResultTrait;
+               };
+               $data = [ 'hello' => 'world' ];
+               $this->hideDeprecated( 'SearchResultTrait::setExtensionData with array argument' );
+               $this->assertEquals( [], $result->getExtensionData(), 'starts empty' );
+               $result->setExtensionData( $data );
+               $this->assertEquals( $data, $result->getExtensionData(), 'can set extension data' );
+               $data['this'] = 'that';
+               $this->assertNotEquals( $data, $result->getExtensionData(), 'shouldnt hold any reference' );
+
+               $result->setExtensionData( $data );
+               $this->assertEquals( $data, $result->getExtensionData(), 'can replace extension data' );
+       }
+}
index 6f618a2..2f6fa39 100644 (file)
@@ -3,24 +3,6 @@
 use Wikimedia\TestingAccessWrapper;
 
 class LanguageTest extends LanguageClassesTestCase {
-       use LanguageNameUtilsTestTrait;
-
-       /** @var array Copy of $wgHooks from before we unset LanguageGetTranslatedLanguageNames */
-       private $origHooks;
-
-       public function setUp() {
-               global $wgHooks;
-
-               parent::setUp();
-
-               // Don't allow installed hooks to run, except if a test restores them via origHooks (needed
-               // for testIsKnownLanguageTag_cldr)
-               $this->origHooks = $wgHooks;
-               $newHooks = $wgHooks;
-               unset( $newHooks['LanguageGetTranslatedLanguageNames'] );
-               $this->setMwGlobals( 'wgHooks', $newHooks );
-       }
-
        /**
         * @covers Language::convertDoubleWidth
         * @covers Language::normalizeForSearch
@@ -528,6 +510,84 @@ class LanguageTest extends LanguageClassesTestCase {
                );
        }
 
+       /**
+        * Test Language::isValidBuiltInCode()
+        * @dataProvider provideLanguageCodes
+        * @covers Language::isValidBuiltInCode
+        */
+       public function testBuiltInCodeValidation( $code, $expected, $message = '' ) {
+               $this->assertEquals( $expected,
+                       (bool)Language::isValidBuiltInCode( $code ),
+                       "validating code $code $message"
+               );
+       }
+
+       public static function provideLanguageCodes() {
+               return [
+                       [ 'fr', true, 'Two letters, minor case' ],
+                       [ 'EN', false, 'Two letters, upper case' ],
+                       [ 'tyv', true, 'Three letters' ],
+                       [ 'be-tarask', true, 'With dash' ],
+                       [ 'be-x-old', true, 'With extension (two dashes)' ],
+                       [ 'be_tarask', false, 'Reject underscores' ],
+               ];
+       }
+
+       /**
+        * Test Language::isKnownLanguageTag()
+        * @dataProvider provideKnownLanguageTags
+        * @covers Language::isKnownLanguageTag
+        */
+       public function testKnownLanguageTag( $code, $message = '' ) {
+               $this->assertTrue(
+                       (bool)Language::isKnownLanguageTag( $code ),
+                       "validating code $code - $message"
+               );
+       }
+
+       public static function provideKnownLanguageTags() {
+               return [
+                       [ 'fr', 'simple code' ],
+                       [ 'bat-smg', 'an MW legacy tag' ],
+                       [ 'sgs', 'an internal standard MW name, for which a legacy tag is used externally' ],
+               ];
+       }
+
+       /**
+        * @covers Language::isKnownLanguageTag
+        */
+       public function testKnownCldrLanguageTag() {
+               if ( !class_exists( 'LanguageNames' ) ) {
+                       $this->markTestSkipped( 'The LanguageNames class is not available. '
+                               . 'The CLDR extension is probably not installed.' );
+               }
+
+               $this->assertTrue(
+                       (bool)Language::isKnownLanguageTag( 'pal' ),
+                       'validating code "pal" an ancient language, which probably will '
+                               . 'not appear in Names.php, but appears in CLDR in English'
+               );
+       }
+
+       /**
+        * Negative tests for Language::isKnownLanguageTag()
+        * @dataProvider provideUnKnownLanguageTags
+        * @covers Language::isKnownLanguageTag
+        */
+       public function testUnknownLanguageTag( $code, $message = '' ) {
+               $this->assertFalse(
+                       (bool)Language::isKnownLanguageTag( $code ),
+                       "checking that code $code is invalid - $message"
+               );
+       }
+
+       public static function provideUnknownLanguageTags() {
+               return [
+                       [ 'mw', 'non-existent two-letter code' ],
+                       [ 'foo"<bar', 'very invalid language code' ],
+               ];
+       }
+
        /**
         * Test too short timestamp
         * @expectedException MWException
@@ -1752,6 +1812,12 @@ class LanguageTest extends LanguageClassesTestCase {
        public function testClearCaches() {
                $languageClass = TestingAccessWrapper::newFromClass( Language::class );
 
+               // Populate $dataCache
+               Language::getLocalisationCache()->getItem( 'zh', 'mainpage' );
+               $oldCacheObj = Language::$dataCache;
+               $this->assertNotCount( 0,
+                       TestingAccessWrapper::newFromObject( Language::$dataCache )->loadedItems );
+
                // Populate $mLangObjCache
                $lang = Language::factory( 'en' );
                $this->assertNotCount( 0, Language::$mLangObjCache );
@@ -1764,11 +1830,36 @@ class LanguageTest extends LanguageClassesTestCase {
                $lang->getGrammarTransformations();
                $this->assertNotNull( $languageClass->grammarTransformations );
 
+               // Populate $languageNameCache
+               Language::fetchLanguageNames();
+               $this->assertNotNull( $languageClass->languageNameCache );
+
                Language::clearCaches();
 
+               $this->assertNotSame( $oldCacheObj, Language::$dataCache );
+               $this->assertCount( 0,
+                       TestingAccessWrapper::newFromObject( Language::$dataCache )->loadedItems );
                $this->assertCount( 0, Language::$mLangObjCache );
                $this->assertCount( 0, $languageClass->fallbackLanguageCache );
                $this->assertNull( $languageClass->grammarTransformations );
+               $this->assertNull( $languageClass->languageNameCache );
+       }
+
+       /**
+        * @dataProvider provideIsSupportedLanguage
+        * @covers Language::isSupportedLanguage
+        */
+       public function testIsSupportedLanguage( $code, $expected, $comment ) {
+               $this->assertEquals( $expected, Language::isSupportedLanguage( $code ), $comment );
+       }
+
+       public static function provideIsSupportedLanguage() {
+               return [
+                       [ 'en', true, 'is supported language' ],
+                       [ 'fi', true, 'is supported language' ],
+                       [ 'bunny', false, 'is not supported language' ],
+                       [ 'FI', false, 'is not supported language, input should be in lower case' ],
+               ];
        }
 
        /**
@@ -1874,82 +1965,4 @@ class LanguageTest extends LanguageClassesTestCase {
                        [ 'èl', 'Ll' , 'Non-ASCII is overridden', [ 'è' => 'L' ] ],
                ];
        }
-
-       // The following methods are for LanguageNameUtilsTestTrait
-
-       private function isSupportedLanguage( $code ) {
-               return Language::isSupportedLanguage( $code );
-       }
-
-       private function isValidCode( $code ) {
-               return Language::isValidCode( $code );
-       }
-
-       private function isValidBuiltInCode( $code ) {
-               return Language::isValidBuiltInCode( $code );
-       }
-
-       private function isKnownLanguageTag( $code ) {
-               return Language::isKnownLanguageTag( $code );
-       }
-
-       /**
-        * Call getLanguageName() and getLanguageNames() using the Language static methods.
-        *
-        * @param array $options To set globals for testing Language
-        * @param string $expected
-        * @param string $code
-        * @param mixed ...$otherArgs Optionally, pass $inLanguage and/or $include.
-        */
-       private function assertGetLanguageNames( array $options, $expected, $code, ...$otherArgs ) {
-               if ( $options ) {
-                       foreach ( $options as $key => $val ) {
-                               $this->setMwGlobals( "wg$key", $val );
-                       }
-                       $this->resetServices();
-               }
-               $this->assertSame( $expected,
-                       Language::fetchLanguageNames( ...$otherArgs )[strtolower( $code )] ?? '' );
-               $this->assertSame( $expected, Language::fetchLanguageName( $code, ...$otherArgs ) );
-       }
-
-       private function getLanguageNames( ...$args ) {
-               return Language::fetchLanguageNames( ...$args );
-       }
-
-       private function getLanguageName( ...$args ) {
-               return Language::fetchLanguageName( ...$args );
-       }
-
-       private static function getFileName( ...$args ) {
-               return Language::getFileName( ...$args );
-       }
-
-       private static function getMessagesFileName( $code ) {
-               return Language::getMessagesFileName( $code );
-       }
-
-       private static function getJsonMessagesFileName( $code ) {
-               return Language::getJsonMessagesFileName( $code );
-       }
-
-       /**
-        * @todo This really belongs in the cldr extension's tests.
-        *
-        * @covers MediaWiki\Languages\LanguageNameUtils::isKnownLanguageTag
-        * @covers Language::isKnownLanguageTag
-        */
-       public function testIsKnownLanguageTag_cldr() {
-               if ( !class_exists( 'LanguageNames' ) ) {
-                       $this->markTestSkipped( 'The LanguageNames class is not available. '
-                               . 'The CLDR extension is probably not installed.' );
-               }
-
-               // We need to restore the extension's hook that we removed.
-               $this->setMwGlobals( 'wgHooks', $this->origHooks );
-
-               // "pal" is an ancient language, which probably will not appear in Names.php, but appears in
-               // CLDR in English
-               $this->assertTrue( Language::isKnownLanguageTag( 'pal' ) );
-       }
 }
index e92eb56..418832e 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 
-class MockSearchResult extends SearchResult {
+class MockSearchResult extends RevisionSearchResult {
        private $isMissingRevision = false;
        private $isBrokenTitle = false;
 
diff --git a/tests/phpunit/unit/includes/Rest/EntryPointTest.php b/tests/phpunit/unit/includes/Rest/EntryPointTest.php
deleted file mode 100644 (file)
index a74c0cb..0000000
+++ /dev/null
@@ -1,91 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Rest;
-
-use EmptyBagOStuff;
-use GuzzleHttp\Psr7\Uri;
-use GuzzleHttp\Psr7\Stream;
-use MediaWiki\Rest\BasicAccess\StaticBasicAuthorizer;
-use MediaWiki\Rest\Handler;
-use MediaWiki\Rest\EntryPoint;
-use MediaWiki\Rest\RequestData;
-use MediaWiki\Rest\ResponseFactory;
-use MediaWiki\Rest\Router;
-use WebResponse;
-
-/**
- * @covers \MediaWiki\Rest\EntryPoint
- * @covers \MediaWiki\Rest\Router
- */
-class EntryPointTest extends \MediaWikiUnitTestCase {
-       private static $mockHandler;
-
-       private function createRouter() {
-               return new Router(
-                       [ __DIR__ . '/testRoutes.json' ],
-                       [],
-                       '/rest',
-                       new EmptyBagOStuff(),
-                       new ResponseFactory(),
-                       new StaticBasicAuthorizer() );
-       }
-
-       private function createWebResponse() {
-               return $this->getMockBuilder( WebResponse::class )
-                       ->setMethods( [ 'header' ] )
-                       ->getMock();
-       }
-
-       public static function mockHandlerHeader() {
-               return new class extends Handler {
-                       public function execute() {
-                               $response = $this->getResponseFactory()->create();
-                               $response->setHeader( 'Foo', 'Bar' );
-                               return $response;
-                       }
-               };
-       }
-
-       public function testHeader() {
-               $webResponse = $this->createWebResponse();
-               $webResponse->expects( $this->any() )
-                       ->method( 'header' )
-                       ->withConsecutive(
-                               [ 'HTTP/1.1 200 OK', true, null ],
-                               [ 'Foo: Bar', true, null ]
-                       );
-
-               $entryPoint = new EntryPoint(
-                       new RequestData( [ 'uri' => new Uri( '/rest/mock/EntryPoint/header' ) ] ),
-                       $webResponse,
-                       $this->createRouter() );
-               $entryPoint->execute();
-               $this->assertTrue( true );
-       }
-
-       public static function mockHandlerBodyRewind() {
-               return new class extends Handler {
-                       public function execute() {
-                               $response = $this->getResponseFactory()->create();
-                               $stream = new Stream( fopen( 'php://memory', 'w+' ) );
-                               $stream->write( 'hello' );
-                               $response->setBody( $stream );
-                               return $response;
-                       }
-               };
-       }
-
-       /**
-        * Make sure EntryPoint rewinds a seekable body stream before reading.
-        */
-       public function testBodyRewind() {
-               $entryPoint = new EntryPoint(
-                       new RequestData( [ 'uri' => new Uri( '/rest/mock/EntryPoint/bodyRewind' ) ] ),
-                       $this->createWebResponse(),
-                       $this->createRouter() );
-               ob_start();
-               $entryPoint->execute();
-               $this->assertSame( 'hello', ob_get_clean() );
-       }
-
-}
diff --git a/tests/phpunit/unit/includes/filebackend/lockmanager/LockManagerGroupFactoryTest.php b/tests/phpunit/unit/includes/filebackend/lockmanager/LockManagerGroupFactoryTest.php
new file mode 100644 (file)
index 0000000..38fcf29
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+use MediaWiki\FileBackend\LockManager\LockManagerGroupFactory;
+use Wikimedia\Rdbms\LBFactory;
+
+/**
+ * @covers MediaWiki\FileBackend\LockManager\LockManagerGroupFactory
+ * @todo Should we somehow test that the LockManagerGroup objects are as we expect? How do we do
+ *   that without getting into testing LockManagerGroup itself?
+ */
+class LockManagerGroupFactoryTest extends MediaWikiUnitTestCase {
+       public function testGetLockManagerGroup() {
+               $mockLbFactory = $this->createMock( LBFactory::class );
+               $mockLbFactory->expects( $this->never() )->method( $this->anything() );
+
+               $factory = new LockManagerGroupFactory( 'defaultDomain', [], $mockLbFactory );
+               $lbmUnspecified = $factory->getLockManagerGroup();
+               $lbmFalse = $factory->getLockManagerGroup( false );
+               $lbmDefault = $factory->getLockManagerGroup( 'defaultDomain' );
+               $lbmOther = $factory->getLockManagerGroup( 'otherDomain' );
+
+               $this->assertSame( $lbmUnspecified, $lbmFalse );
+               $this->assertSame( $lbmFalse, $lbmDefault );
+               $this->assertSame( $lbmDefault, $lbmUnspecified );
+               $this->assertNotEquals( $lbmUnspecified, $lbmOther );
+               $this->assertNotEquals( $lbmFalse, $lbmOther );
+               $this->assertNotEquals( $lbmDefault, $lbmOther );
+
+               $this->assertSame( $lbmUnspecified, $factory->getLockManagerGroup() );
+               $this->assertSame( $lbmFalse, $factory->getLockManagerGroup( false ) );
+               $this->assertSame( $lbmDefault, $factory->getLockManagerGroup( 'defaultDomain' ) );
+               $this->assertSame( $lbmOther, $factory->getLockManagerGroup( 'otherDomain' ) );
+       }
+}
diff --git a/tests/phpunit/unit/includes/filebackend/lockmanager/LockManagerGroupTest.php b/tests/phpunit/unit/includes/filebackend/lockmanager/LockManagerGroupTest.php
new file mode 100644 (file)
index 0000000..79baac9
--- /dev/null
@@ -0,0 +1,88 @@
+<?php
+
+use Wikimedia\Rdbms\LBFactory;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * Since this is a unit test, we don't test the singleton() or destroySingletons() methods. We also
+ * can't test get() with a valid argument, because that winds up calling static methods of
+ * ObjectCache and LoggerFactory that aren't yet compatible with proper unit tests. Those will be
+ * tested in the integration test for now.
+ *
+ * @covers LockManagerGroup
+ */
+class LockManagerGroupTest extends MediaWikiUnitTestCase {
+       private function getMockLBFactory() {
+               $mock = $this->createMock( LBFactory::class );
+               $mock->expects( $this->never() )->method( $this->anythingBut( '__destruct' ) );
+               return $mock;
+       }
+
+       public function testConstructorNoConfigs() {
+               new LockManagerGroup( 'domain', [], $this->getMockLBFactory() );
+               $this->assertTrue( true, 'No exception thrown' );
+       }
+
+       public function testConstructorConfigWithNoName() {
+               $this->setExpectedException( Exception::class,
+                       'Cannot register a lock manager with no name.' );
+
+               new LockManagerGroup( 'domain',
+                       [ [ 'name' => 'a', 'class' => 'b' ], [ 'class' => 'c' ] ], $this->getMockLBFactory() );
+       }
+
+       public function testConstructorConfigWithNoClass() {
+               $this->setExpectedException( Exception::class,
+                       'Cannot register lock manager `c` with no class.' );
+
+               new LockManagerGroup( 'domain',
+                       [ [ 'name' => 'a', 'class' => 'b' ], [ 'name' => 'c' ] ], $this->getMockLBFactory() );
+       }
+
+       public function testGetUndefined() {
+               $this->setExpectedException( Exception::class,
+                       'No lock manager defined with the name `c`.' );
+
+               $lmg = new LockManagerGroup( 'domain', [ [ 'name' => 'a', 'class' => 'b' ] ],
+                       $this->getMockLBFactory() );
+               $lmg->get( 'c' );
+       }
+
+       public function testConfigUndefined() {
+               $this->setExpectedException( Exception::class,
+                       'No lock manager defined with the name `c`.' );
+
+               $lmg = new LockManagerGroup( 'domain', [ [ 'name' => 'a', 'class' => 'b' ] ],
+                       $this->getMockLBFactory() );
+               $lmg->config( 'c' );
+       }
+
+       public function testConfig() {
+               $lmg = new LockManagerGroup( 'domain', [ [ 'name' => 'a', 'class' => 'b', 'foo' => 'c' ] ],
+                       $this->getMockLBFactory() );
+               $this->assertSame(
+                       [ 'class' => 'b', 'name' => 'a', 'foo' => 'c', 'domain' => 'domain' ],
+                       $lmg->config( 'a' )
+               );
+       }
+
+       public function testGetDefaultNull() {
+               $lmg = new LockManagerGroup( 'domain', [], $this->getMockLBFactory() );
+               $expected = new NullLockManager( [] );
+               $actual = $lmg->getDefault();
+               // Have to get rid of the $sessions for equality check to work
+               TestingAccessWrapper::newFromObject( $actual )->session = null;
+               TestingAccessWrapper::newFromObject( $expected )->session = null;
+               $this->assertEquals( $expected, $actual );
+       }
+
+       public function testGetAnyException() {
+               // XXX Isn't the name 'getAny' misleading if we don't get whatever's available?
+               $this->setExpectedException( Exception::class,
+                       'No lock manager defined with the name `fsLockManager`.' );
+
+               $lmg = new LockManagerGroup( 'domain', [ [ 'name' => 'a', 'class' => 'b' ] ],
+                       $this->getMockLBFactory() );
+               $lmg->getAny();
+       }
+}
diff --git a/tests/phpunit/unit/includes/language/LanguageNameUtilsTest.php b/tests/phpunit/unit/includes/language/LanguageNameUtilsTest.php
deleted file mode 100644 (file)
index 6fbd4a2..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-<?php
-
-use MediaWiki\Config\ServiceOptions;
-use MediaWiki\Languages\LanguageNameUtils;
-
-class LanguageNameUtilsTest extends MediaWikiUnitTestCase {
-       /**
-        * @param array $optionsArray
-        */
-       private static function newObj( array $optionsArray = [] ) : LanguageNameUtils {
-               return new LanguageNameUtils( new ServiceOptions(
-                       LanguageNameUtils::$constructorOptions,
-                       $optionsArray,
-                       [
-                               'ExtraLanguageNames' => [],
-                               'LanguageCode' => 'en',
-                               'UsePigLatinVariant' => false,
-                       ]
-               ) );
-       }
-
-       use LanguageNameUtilsTestTrait;
-
-       private function isSupportedLanguage( $code ) {
-               return $this->newObj()->isSupportedLanguage( $code );
-       }
-
-       private function isValidCode( $code ) {
-               return $this->newObj()->isValidCode( $code );
-       }
-
-       private function isValidBuiltInCode( $code ) {
-               return $this->newObj()->isValidBuiltInCode( $code );
-       }
-
-       private function isKnownLanguageTag( $code ) {
-               return $this->newObj()->isKnownLanguageTag( $code );
-       }
-
-       private function assertGetLanguageNames( array $options, $expected, $code, ...$otherArgs ) {
-               $this->assertSame( $expected, $this->newObj( $options )
-                       ->getLanguageNames( ...$otherArgs )[strtolower( $code )] ?? '' );
-               $this->assertSame( $expected,
-                       $this->newObj( $options )->getLanguageName( $code, ...$otherArgs ) );
-       }
-
-       private function getLanguageNames( ...$args ) {
-               return $this->newObj()->getLanguageNames( ...$args );
-       }
-
-       private function getLanguageName( ...$args ) {
-               return $this->newObj()->getLanguageName( ...$args );
-       }
-
-       private static function getFileName( ...$args ) {
-               return self::newObj()->getFileName( ...$args );
-       }
-
-       private static function getMessagesFileName( $code ) {
-               return self::newObj()->getMessagesFileName( $code );
-       }
-
-       private static function getJsonMessagesFileName( $code ) {
-               return self::newObj()->getJsonMessagesFileName( $code );
-       }
-}
diff --git a/tests/phpunit/unit/includes/language/LanguageNameUtilsTestTrait.php b/tests/phpunit/unit/includes/language/LanguageNameUtilsTestTrait.php
deleted file mode 100644 (file)
index bd777e9..0000000
+++ /dev/null
@@ -1,555 +0,0 @@
-<?php
-
-use MediaWiki\Languages\LanguageNameUtils;
-
-const AUTONYMS = LanguageNameUtils::AUTONYMS;
-const ALL = LanguageNameUtils::ALL;
-const DEFINED = LanguageNameUtils::DEFINED;
-const SUPPORTED = LanguageNameUtils::SUPPORTED;
-
-/**
- * For code shared between LanguageNameUtilsTest and LanguageTest.
- */
-trait LanguageNameUtilsTestTrait {
-       abstract protected function isSupportedLanguage( $code );
-
-       /**
-        * @dataProvider provideIsSupportedLanguage
-        * @covers MediaWiki\Languages\LanguageNameUtils::__construct
-        * @covers MediaWiki\Languages\LanguageNameUtils::isSupportedLanguage
-        * @covers Language::isSupportedLanguage
-        */
-       public function testIsSupportedLanguage( $code, $expected ) {
-               $this->assertSame( $expected, $this->isSupportedLanguage( $code ) );
-       }
-
-       public static function provideIsSupportedLanguage() {
-               return [
-                       'en' => [ 'en', true ],
-                       'fi' => [ 'fi', true ],
-                       'bunny' => [ 'bunny', false ],
-                       'qqq' => [ 'qqq', false ],
-                       'uppercase is not considered supported' => [ 'FI', false ],
-               ];
-       }
-
-       abstract protected function isValidCode( $code );
-
-       /**
-        * We don't test that the result is cached, because that should only be noticeable if the
-        * configuration changes in between calls, and 1) that should never happen in normal operation,
-        * 2) if you do it you deserve whatever you get, and 3) once the static Language method is
-        * dropped and the invalid title regex is moved to something injected instead of a static call,
-        * the cache will be undetectable.
-        *
-        * @todo Should we test changes to $wgLegalTitleChars here? Does anybody actually change that?
-        * Is it possible to change it usefully without breaking everything?
-        *
-        * @dataProvider provideIsValidCode
-        * @covers MediaWiki\Languages\LanguageNameUtils::isValidCode
-        * @covers Language::isValidCode
-        *
-        * @param string $code
-        * @param bool $expected
-        */
-       public function testIsValidCode( $code, $expected ) {
-               $this->assertSame( $expected, $this->isValidCode( $code ) );
-       }
-
-       public static function provideIsValidCode() {
-               $ret = [
-                       'en' => [ 'en', true ],
-                       'en-GB' => [ 'en-GB', true ],
-                       'Funny chars' => [ "%!$()*,-.;=?@^_`~\x80\xA2\xFF+", true ],
-                       'Percent escape not allowed' => [ 'a%aF', false ],
-                       'Percent with only one following char is okay' => [ '%a', true ],
-                       'Percent with non-hex following chars is okay' => [ '%AG', true ],
-                       'Named char reference "a"' => [ 'a&a', false ],
-                       'Named char reference "A"' => [ 'a&A', false ],
-                       'Named char reference "0"' => [ 'a&0', false ],
-                       'Named char reference non-ASCII' => [ "a&\x92", false ],
-                       'Numeric char reference' => [ "a&#0", false ],
-                       'Hex char reference 0' => [ "a&#x0", false ],
-                       'Hex char reference A' => [ "a&#xA", false ],
-                       'Lone ampersand is valid for title but not lang code' => [ '&', false ],
-                       'Ampersand followed by just # is valid for title but not lang code' => [ '&#', false ],
-                       'Ampersand followed by # and non-x/digit is valid for title but not lang code' =>
-                               [ '&#a', false ],
-               ];
-               $disallowedChars = ":/\\\000&<>'\"";
-               foreach ( str_split( $disallowedChars ) as $char ) {
-                       $ret["Disallowed character $char"] = [ "a{$char}a", false ];
-               }
-               return $ret;
-       }
-
-       abstract protected function isValidBuiltInCode( $code );
-
-       /**
-        * @dataProvider provideIsValidBuiltInCode
-        * @covers MediaWiki\Languages\LanguageNameUtils::isValidBuiltInCode
-        * @covers Language::isValidBuiltInCode
-        *
-        * @param string $code
-        * @param bool $expected
-        */
-       public function testIsValidBuiltInCode( $code, $expected ) {
-               $this->assertSame( $expected, $this->isValidBuiltInCode( $code ) );
-       }
-
-       public static function provideIsValidBuiltInCode() {
-               return [
-                       'Two letters, lowercase' => [ 'fr', true ],
-                       'Two letters, uppercase' => [ 'EN', false ],
-                       'Three letters' => [ 'tyv', true ],
-                       'With dash' => [ 'be-tarask', true ],
-                       'With extension (two dashes)' => [ 'be-x-old', true ],
-                       'Reject underscores' => [ 'be_tarask', false ],
-                       'One letter' => [ 'a', false ],
-                       'Only digits' => [ '00', true ],
-                       'Only dashes' => [ '--', true ],
-                       'Unreasonably long' => [ str_repeat( 'x', 100 ), true ],
-                       'qqq' => [ 'qqq', true ],
-               ];
-       }
-
-       abstract protected function isKnownLanguageTag( $code );
-
-       /**
-        * @dataProvider provideIsKnownLanguageTag
-        * @covers MediaWiki\Languages\LanguageNameUtils::isKnownLanguageTag
-        * @covers Language::isKnownLanguageTag
-        *
-        * @param string $code
-        * @param bool $expected
-        */
-       public function testIsKnownLanguageTag( $code, $expected ) {
-               $this->assertSame( $expected, $this->isKnownLanguageTag( $code ) );
-       }
-
-       public static function provideIsKnownLanguageTag() {
-               $invalidBuiltInCodes = array_filter( static::provideIsValidBuiltInCode(),
-                       function ( $arr ) {
-                               // If isValidBuiltInCode() returns false, we want to also, but if it returns true,
-                               // we could still return false from isKnownLanguageTag(), so skip those.
-                               return !$arr[1];
-                       }
-               );
-               return array_merge( $invalidBuiltInCodes, [
-                       'Simple code' => [ 'fr', true ],
-                       'An MW legacy tag' => [ 'bat-smg', true ],
-                       'An internal standard MW name, for which a legacy tag is used externally' =>
-                               [ 'sgs', true ],
-                       'Non-existent two-letter code' => [ 'mw', false ],
-                       'Very invalid language code' => [ 'foo"<bar', false ],
-               ] );
-       }
-
-       abstract protected function assertGetLanguageNames(
-               array $options, $expected, $code, ...$otherArgs
-       );
-
-       abstract protected function getLanguageNames( ...$args );
-
-       abstract protected function getLanguageName( ...$args );
-
-       /**
-        * @dataProvider provideGetLanguageNames
-        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNames
-        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNamesUncached
-        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageName
-        * @covers Language::fetchLanguageNames
-        * @covers Language::fetchLanguageName
-        *
-        * @param string $expected
-        * @param string $code
-        * @param mixed ...$otherArgs Optionally, pass $inLanguage and/or $include.
-        */
-       public function testGetLanguageNames( $expected, $code, ...$otherArgs ) {
-               $this->assertGetLanguageNames( [], $expected, $code, ...$otherArgs );
-       }
-
-       public static function provideGetLanguageNames() {
-               // @todo There are probably lots of interesting tests to add here.
-               return [
-                       'Simple code' => [ 'Deutsch', 'de' ],
-                       'Simple code in a different language (doesn\'t work without hook)' =>
-                               [ 'Deutsch', 'de', 'fr' ],
-                       'Invalid code' => [ '', '&' ],
-                       'Pig Latin not enabled' => [ '', 'en-x-piglatin', AUTONYMS, ALL ],
-                       'qqq doesn\'t have a name' => [ '', 'qqq', AUTONYMS, ALL ],
-                       'An MW legacy tag is recognized' => [ 'žemaitėška', 'bat-smg' ],
-                       // @todo Is the next test's result desired?
-                       'An MW legacy tag is not supported' => [ '', 'bat-smg', AUTONYMS, SUPPORTED ],
-                       'An internal standard name, for which a legacy tag is used externally, is supported' =>
-                               [ 'žemaitėška', 'sgs', AUTONYMS, SUPPORTED ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideGetLanguageNames_withHook
-        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNames
-        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNamesUncached
-        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageName
-        * @covers Language::fetchLanguageNames
-        * @covers Language::fetchLanguageName
-        *
-        * @param string $expected Expected return value of getLanguageName()
-        * @param string $code
-        * @param mixed ...$otherArgs Optionally, pass $inLanguage and/or $include.
-        */
-       public function testGetLanguageNames_withHook( $expected, $code, ...$otherArgs ) {
-               $this->setTemporaryHook( 'LanguageGetTranslatedLanguageNames',
-                       function ( &$names, $inLanguage ) {
-                               switch ( $inLanguage ) {
-                               case 'de':
-                                       $names = [
-                                               'de' => 'Deutsch',
-                                               'en' => 'Englisch',
-                                               'fr' => 'Französisch',
-                                       ];
-                                       break;
-
-                               case 'en':
-                                       $names = [
-                                               'de' => 'German',
-                                               'en' => 'English',
-                                               'fr' => 'French',
-                                               'sqsqsqsq' => '!!?!',
-                                               'bat-smg' => 'Samogitian',
-                                       ];
-                                       break;
-
-                               case 'fr':
-                                       $names = [
-                                               'de' => 'allemand',
-                                               'en' => 'anglais',
-                                               // Deliberate mistake (no cedilla)
-                                               'fr' => 'francais',
-                                       ];
-                                       break;
-                               }
-                       }
-               );
-
-               // Really we could dispense with assertGetLanguageNames() and just call
-               // testGetLanguageNames() here, but it looks weird to call a test method from another test
-               // method.
-               $this->assertGetLanguageNames( [], $expected, $code, ...$otherArgs );
-       }
-
-       public static function provideGetLanguageNames_withHook() {
-               return [
-                       'Simple code in a different language' => [ 'allemand', 'de', 'fr' ],
-                       'Invalid inLanguage defaults to English' => [ 'German', 'de', '&' ],
-                       'If inLanguage not provided, default to autonym' => [ 'Deutsch', 'de' ],
-                       'Hooks ignored for explicitly-requested autonym' => [ 'français', 'fr', 'fr' ],
-                       'Hooks don\'t make a language supported' => [ '', 'bat-smg', 'en', SUPPORTED ],
-                       'Hooks don\'t make a language defined' => [ '', 'sqsqsqsq', 'en', DEFINED ],
-                       'Hooks do make a language name returned with ALL' => [ '!!?!', 'sqsqsqsq', 'en', ALL ],
-               ];
-       }
-
-       /**
-        * @dataProvider provideGetLanguageNames_ExtraLanguageNames
-        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNames
-        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNamesUncached
-        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageName
-        * @covers Language::fetchLanguageNames
-        * @covers Language::fetchLanguageName
-        *
-        * @param string $expected Expected return value of getLanguageName()
-        * @param string $code
-        * @param mixed ...$otherArgs Optionally, pass $inLanguage and/or $include.
-        */
-       public function testGetLanguageNames_ExtraLanguageNames( $expected, $code, ...$otherArgs ) {
-               $this->setTemporaryHook( 'LanguageGetTranslatedLanguageNames',
-                       function ( &$names ) {
-                               $names['de'] = 'die deutsche Sprache';
-                       }
-               );
-               $this->assertGetLanguageNames(
-                       [ 'ExtraLanguageNames' => [ 'de' => 'deutsche Sprache', 'sqsqsqsq' => '!!?!' ] ],
-                       $expected, $code, ...$otherArgs
-               );
-       }
-
-       public static function provideGetLanguageNames_ExtraLanguageNames() {
-               return [
-                       'Simple extra language name' => [ '!!?!', 'sqsqsqsq' ],
-                       'Extra language is defined' => [ '!!?!', 'sqsqsqsq', AUTONYMS, DEFINED ],
-                       'Extra language is not supported' => [ '', 'sqsqsqsq', AUTONYMS, SUPPORTED ],
-                       'Extra language overrides default' => [ 'deutsche Sprache', 'de' ],
-                       'Extra language overrides hook for explicitly requested autonym' =>
-                               [ 'deutsche Sprache', 'de', 'de' ],
-                       'Hook overrides extra language for non-autonym' =>
-                               [ 'die deutsche Sprache', 'de', 'fr' ],
-               ];
-       }
-
-       /**
-        * Test that getLanguageNames() defaults to DEFINED, and getLanguageName() defaults to ALL.
-        *
-        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNames
-        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNamesUncached
-        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageName
-        * @covers Language::fetchLanguageNames
-        * @covers Language::fetchLanguageName
-        */
-       public function testGetLanguageNames_parameterDefault() {
-               $this->setTemporaryHook( 'LanguageGetTranslatedLanguageNames',
-                       function ( &$names ) {
-                               $names = [ 'sqsqsqsq' => '!!?!' ];
-                       }
-               );
-
-               // We use 'en' here because the hook is not run if we're requesting autonyms, although in
-               // this case (language that isn't defined by MediaWiki itself) that behavior seems wrong.
-               $this->assertArrayNotHasKey( 'sqsqsqsq', $this->getLanguageNames(), 'en' );
-
-               $this->assertSame( '!!?!', $this->getLanguageName( 'sqsqsqsq', 'en' ) );
-       }
-
-       /**
-        * @dataProvider provideGetLanguageNames_sorted
-        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNames
-        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNamesUncached
-        * @covers Language::fetchLanguageNames
-        *
-        * @param mixed ...$args To pass to method
-        */
-       public function testGetLanguageNames_sorted( ...$args ) {
-               $names = $this->getLanguageNames( ...$args );
-               $sortedNames = $names;
-               ksort( $sortedNames );
-               $this->assertSame( $sortedNames, $names );
-       }
-
-       public static function provideGetLanguageNames_sorted() {
-               return [
-                       [],
-                       [ AUTONYMS ],
-                       [ AUTONYMS, 'mw' ],
-                       [ AUTONYMS, ALL ],
-                       [ AUTONYMS, SUPPORTED ],
-                       [ 'he', 'mw' ],
-                       [ 'he', ALL ],
-                       [ 'he', SUPPORTED ],
-               ];
-       }
-
-       /**
-        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNames
-        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNamesUncached
-        * @covers Language::fetchLanguageNames
-        */
-       public function testGetLanguageNames_hookNotCalledForAutonyms() {
-               $count = 0;
-               $this->setTemporaryHook( 'LanguageGetTranslatedLanguageNames',
-                       function () use ( &$count ) {
-                               $count++;
-                       }
-               );
-
-               $this->getLanguageNames();
-               $this->assertSame( 0, $count, 'Hook must not be called for autonyms' );
-
-               // We test elsewhere that the hook works, but the following verifies that our test is
-               // working and $count isn't being incremented above only because we're checking autonyms.
-               $this->getLanguageNames( 'fr' );
-               $this->assertSame( 1, $count, 'Hook must be called for non-autonyms' );
-       }
-
-       /**
-        * @dataProvider provideGetLanguageNames_pigLatin
-        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNames
-        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNamesUncached
-        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageName
-        * @covers Language::fetchLanguageNames
-        * @covers Language::fetchLanguageName
-        *
-        * @param string $expected
-        * @param mixed ...$otherArgs Optionally, pass $inLanguage and/or $include.
-        */
-       public function testGetLanguageNames_pigLatin( $expected, ...$otherArgs ) {
-               $this->setTemporaryHook( 'LanguageGetTranslatedLanguageNames',
-                       function ( &$names, $inLanguage ) {
-                               switch ( $inLanguage ) {
-                               case 'fr':
-                                       $names = [ 'en-x-piglatin' => 'latin de cochons' ];
-                                       break;
-
-                               case 'en-x-piglatin':
-                                       // Deliberately lowercase
-                                       $names = [ 'en-x-piglatin' => 'igpay atinlay' ];
-                                       break;
-                               }
-                       }
-               );
-
-               $this->assertGetLanguageNames(
-                       [ 'UsePigLatinVariant' => true ], $expected, 'en-x-piglatin', ...$otherArgs );
-       }
-
-       public static function provideGetLanguageNames_pigLatin() {
-               return [
-                       'Simple test' => [ 'Igpay Atinlay' ],
-                       'Not supported' => [ '', AUTONYMS, SUPPORTED ],
-                       'Foreign language' => [ 'latin de cochons', 'fr' ],
-                       'Hook doesn\'t override explicit autonym' =>
-                               [ 'Igpay Atinlay', 'en-x-piglatin', 'en-x-piglatin' ],
-               ];
-       }
-
-       /**
-        * Just for the sake of completeness, test that ExtraLanguageNames will not override the name
-        * for pig Latin. Nobody actually cares about this and if anything current behavior is probably
-        * wrong, but once we're testing the whole file we may as well be comprehensive.
-        *
-        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNames
-        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageNamesUncached
-        * @covers MediaWiki\Languages\LanguageNameUtils::getLanguageName
-        * @covers Language::fetchLanguageNames
-        * @covers Language::fetchLanguageName
-        */
-       public function testGetLanguageNames_pigLatinAndExtraLanguageNames() {
-               $this->assertGetLanguageNames(
-                       [
-                               'UsePigLatinVariant' => true,
-                               'ExtraLanguageNames' => [ 'en-x-piglatin' => 'igpay atinlay' ]
-                       ],
-                       'Igpay Atinlay',
-                       'en-x-piglatin'
-               );
-       }
-
-       abstract protected static function getFileName( ...$args );
-
-       /**
-        * @dataProvider provideGetFileName
-        * @covers MediaWiki\Languages\LanguageNameUtils::getFileName
-        * @covers Language::getFileName
-        *
-        * @param string $expected
-        * @param mixed ...$args To pass to method
-        */
-       public function testGetFileName( $expected, ...$args ) {
-               $this->assertSame( $expected, $this->getFileName( ...$args ) );
-       }
-
-       public static function provideGetFileName() {
-               return [
-                       'Simple case' => [ 'MessagesXx.php', 'Messages', 'xx' ],
-                       'With extension' => [ 'MessagesXx.ext', 'Messages', 'xx', '.ext' ],
-                       'Replacing dashes' => [ '!__?', '!', '--', '?' ],
-                       'Empty prefix and extension' => [ 'Xx', '', 'xx', '' ],
-                       'Uppercase only first letter' => [ 'Messages_a.php', 'Messages', '-a' ],
-               ];
-       }
-
-       abstract protected function getMessagesFileName( $code );
-
-       /**
-        * @dataProvider provideGetMessagesFileName
-        * @covers MediaWiki\Languages\LanguageNameUtils::getMessagesFileName
-        * @covers Language::getMessagesFileName
-        *
-        * @param string $code
-        * @param string $expected
-        */
-       public function testGetMessagesFileName( $code, $expected ) {
-               $this->assertSame( $expected, $this->getMessagesFileName( $code ) );
-       }
-
-       public static function provideGetMessagesFileName() {
-               global $IP;
-               return [
-                       'Simple case' => [ 'en', "$IP/languages/messages/MessagesEn.php" ],
-                       'Replacing dashes' => [ '--', "$IP/languages/messages/Messages__.php" ],
-                       'Uppercase only first letter' => [ '-a', "$IP/languages/messages/Messages_a.php" ],
-               ];
-       }
-
-       /**
-        * @covers MediaWiki\Languages\LanguageNameUtils::getMessagesFileName
-        * @covers Language::getMessagesFileName
-        */
-       public function testGetMessagesFileName_withHook() {
-               $called = 0;
-
-               $this->setTemporaryHook( 'Language::getMessagesFileName',
-                       function ( $code, &$file ) use ( &$called ) {
-                               global $IP;
-
-                               $called++;
-
-                               $this->assertSame( 'ab-cd', $code );
-                               $this->assertSame( "$IP/languages/messages/MessagesAb_cd.php", $file );
-                               $file = 'bye-bye';
-                       }
-               );
-
-               $this->assertSame( 'bye-bye', $this->getMessagesFileName( 'ab-cd' ) );
-               $this->assertSame( 1, $called );
-       }
-
-       abstract protected function getJsonMessagesFileName( $code );
-
-       /**
-        * @covers MediaWiki\Languages\LanguageNameUtils::getJsonMessagesFileName
-        * @covers Language::getJsonMessagesFileName
-        */
-       public function testGetJsonMessagesFileName() {
-               global $IP;
-
-               // Not so much to test here, one test seems to be enough
-               $expected = "$IP/languages/i18n/en--123.json";
-               $this->assertSame( $expected, $this->getJsonMessagesFileName( 'en--123' ) );
-       }
-
-       /**
-        * getFileName, getMessagesFileName, and getJsonMessagesFileName all throw if they get an
-        * invalid code. To save boilerplate, test them all in one method.
-        *
-        * @dataProvider provideExceptionFromInvalidCode
-        * @covers MediaWiki\Languages\LanguageNameUtils::getFileName
-        * @covers MediaWiki\Languages\LanguageNameUtils::getMessagesFileName
-        * @covers MediaWiki\Languages\LanguageNameUtils::getJsonMessagesFileName
-        * @covers Language::getFileName
-        * @covers Language::getMessagesFileName
-        * @covers Language::getJsonMessagesFileName
-        *
-        * @param callable $callback Will throw when passed $code
-        * @param string $code
-        */
-       public function testExceptionFromInvalidCode( $callback, $code ) {
-               $this->setExpectedException( MWException::class, "Invalid language code \"$code\"" );
-
-               $callback( $code );
-       }
-
-       public static function provideExceptionFromInvalidCode() {
-               $ret = [];
-               foreach ( static::provideIsValidBuiltInCode() as $desc => list( $code, $valid ) ) {
-                       if ( $valid ) {
-                               // Won't get an exception from this one
-                               continue;
-                       }
-
-                       // For getFileName, we define an anonymous function because of the extra first param
-                       $ret["getFileName: $desc"] = [
-                               function ( $code ) {
-                                       return static::getFileName( 'Messages', $code );
-                               },
-                               $code
-                       ];
-
-                       $ret["getMessagesFileName: $desc"] =
-                               [ [ static::class, 'getMessagesFileName' ], $code ];
-
-                       $ret["getJsonMessagesFileName: $desc"] =
-                               [ [ static::class, 'getJsonMessagesFileName' ], $code ];
-               }
-               return $ret;
-       }
-}
index 5eb5e05..013fb0d 100644 (file)
 
        } );
 
+       QUnit.test( 'arrayParams', function ( assert ) {
+               var uri1, uri2, uri3, expectedQ, expectedS,
+                       uriMissing, expectedMissingQ, expectedMissingS,
+                       uriWeird, expectedWeirdQ, expectedWeirdS;
+
+               uri1 = new mw.Uri( 'http://example.com/?foo[]=a&foo[]=b&foo[]=c', { arrayParams: true } );
+               uri2 = new mw.Uri( 'http://example.com/?foo[0]=a&foo[1]=b&foo[2]=c', { arrayParams: true } );
+               uri3 = new mw.Uri( 'http://example.com/?foo[1]=b&foo[0]=a&foo[]=c', { arrayParams: true } );
+               expectedQ = { foo: [ 'a', 'b', 'c' ] };
+               expectedS = 'foo%5B0%5D=a&foo%5B1%5D=b&foo%5B2%5D=c';
+
+               assert.deepEqual( uri1.query, expectedQ,
+                       'array query parameters are parsed (implicit indexes)' );
+               assert.deepEqual( uri1.getQueryString(), expectedS,
+                       'array query parameters are encoded (always with explicit indexes)' );
+               assert.deepEqual( uri2.query, expectedQ,
+                       'array query parameters are parsed (explicit indexes)' );
+               assert.deepEqual( uri2.getQueryString(), expectedS,
+                       'array query parameters are encoded (always with explicit indexes)' );
+               assert.deepEqual( uri3.query, expectedQ,
+                       'array query parameters are parsed (mixed indexes, out of order)' );
+               assert.deepEqual( uri3.getQueryString(), expectedS,
+                       'array query parameters are encoded (always with explicit indexes)' );
+
+               uriMissing = new mw.Uri( 'http://example.com/?foo[0]=a&foo[2]=c', { arrayParams: true } );
+               // eslint-disable-next-line no-sparse-arrays
+               expectedMissingQ = { foo: [ 'a', , 'c' ] };
+               expectedMissingS = 'foo%5B0%5D=a&foo%5B2%5D=c';
+
+               assert.deepEqual( uriMissing.query, expectedMissingQ,
+                       'array query parameters are parsed (missing array item)' );
+               assert.deepEqual( uriMissing.getQueryString(), expectedMissingS,
+                       'array query parameters are encoded (missing array item)' );
+
+               uriWeird = new mw.Uri( 'http://example.com/?foo[0]=a&foo[1][1]=b&foo[x]=c', { arrayParams: true } );
+               expectedWeirdQ = { foo: [ 'a' ], 'foo[1][1]': 'b', 'foo[x]': 'c' };
+               expectedWeirdS = 'foo%5B0%5D=a&foo%5B1%5D%5B1%5D=b&foo%5Bx%5D=c';
+
+               assert.deepEqual( uriWeird.query, expectedWeirdQ,
+                       'array query parameters are parsed (multi-dimensional or associative arrays are ignored)' );
+               assert.deepEqual( uriWeird.getQueryString(), expectedWeirdS,
+                       'array query parameters are encoded (multi-dimensional or associative arrays are ignored)' );
+       } );
+
        QUnit.test( '.clone()', function ( assert ) {
                var original, clone;
 
index ed1288b..894dd19 100644 (file)
                        } );
        } );
 
+       QUnit.test( 'No storing of group=private responses', function ( assert ) {
+               var name = 'test.group.priv';
+
+               // Enable store and stub timeout/idle scheduling
+               this.sandbox.stub( mw.loader.store, 'enabled', true );
+               this.sandbox.stub( window, 'setTimeout', function ( fn ) {
+                       fn();
+               } );
+               this.sandbox.stub( mw, 'requestIdleCallback', function ( fn ) {
+                       fn();
+               } );
+
+               // See ResourceLoaderStartUpModule::$groupIds
+               mw.loader.register( name, 'x', [], 1 );
+               assert.strictEqual( mw.loader.store.get( name ), false, 'Not in store' );
+
+               mw.loader.implement( name, function () {} );
+               return mw.loader.using( name ).then( function () {
+                       assert.strictEqual( mw.loader.getState( name ), 'ready' );
+                       assert.strictEqual( mw.loader.store.get( name ), false, 'Still not in store' );
+               } );
+       } );
+
+       QUnit.test( 'No storing of group=user responses', function ( assert ) {
+               var name = 'test.group.user';
+
+               // Enable store and stub timeout/idle scheduling
+               this.sandbox.stub( mw.loader.store, 'enabled', true );
+               this.sandbox.stub( window, 'setTimeout', function ( fn ) {
+                       fn();
+               } );
+               this.sandbox.stub( mw, 'requestIdleCallback', function ( fn ) {
+                       fn();
+               } );
+
+               // See ResourceLoaderStartUpModule::$groupIds
+               mw.loader.register( name, 'y', [], 0 );
+               assert.strictEqual( mw.loader.store.get( name ), false, 'Not in store' );
+
+               mw.loader.implement( name, function () {} );
+               return mw.loader.using( name ).then( function () {
+                       assert.strictEqual( mw.loader.getState( name ), 'ready' );
+                       assert.strictEqual( mw.loader.store.get( name ), false, 'Still not in store' );
+               } );
+       } );
+
        QUnit.test( 'require()', function ( assert ) {
                mw.loader.register( [
                        [ 'test.require1', '0' ],
index 662224c..6512e7d 100644 (file)
@@ -121,7 +121,7 @@ exports.config = {
 
        // Test reporter for stdout.
        // See also: http://webdriver.io/guide/testrunner/reporters.html
-       reporters: [ 'spec', 'junit' ],
+       reporters: [ 'dot', 'junit' ],
        reporterOptions: {
                junit: {
                        outputDir: logPath